diff --git a/.eslintignore b/.eslintignore
index e8709dbf89..7fb6873ae0 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -14,5 +14,7 @@ jest.*.js
# sample apps
**sample-app**/
**playground**/
+samples/apps/passport/
+samples/**/*
-# put module specific ignore paths here
\ No newline at end of file
+# put module specific ignore paths here
diff --git a/.eslintrc b/.eslintrc
index 82374ca28a..ca5be797a9 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -6,6 +6,7 @@
"rollup.config.*",
"node_modules/",
"dist/",
+ "samples/",
"**sample-app**/",
"**playground**/"
],
diff --git a/.github/workflows/detect-node-version-change.yaml b/.github/workflows/detect-node-version-change.yaml
new file mode 100644
index 0000000000..f90d2d89bb
--- /dev/null
+++ b/.github/workflows/detect-node-version-change.yaml
@@ -0,0 +1,38 @@
+name: Detect Node Engine Version Change
+
+on:
+ pull_request:
+ branches:
+ - main
+ merge_group:
+ branches:
+ - main
+
+jobs:
+ detect:
+ name: Detect Node engine version change
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Get Node engine version from package.json
+ id: get_package_json_node_engine_version
+ run: |
+ node_engine_major_version=$(jq -r '.engines.node' ./sdk/package.json | sed 's/^>=//' | cut -d. -f1)
+ echo "::set-output name=node_engine_major_version::$node_engine_major_version"
+
+ - name: Get Node.js version from .nvmrc
+ id: get_nvmrc_node_version
+ run: echo "::set-output name=nvmrc_node_version::$(head -n 1 .nvmrc | cut -d. -f1)"
+
+ - name: Check Node.js engine version change
+ run: |
+ package_version_major=$(echo "${{ steps.get_package_json_node_engine_version.outputs.node_engine_major_version }}")
+ nvmrc_version_major=$(echo "${{ steps.get_nvmrc_node_version.outputs.nvmrc_node_version }}")
+ if [ "$package_version_major" != "$nvmrc_version_major" ]; then
+ echo "Node.js engine version has changed"
+ exit 1
+ else
+ echo "Node.js engine version has not changed"
+ fi
diff --git a/.github/workflows/secret-scan.yaml b/.github/workflows/secret-scan.yaml
index 42e1a63912..8f98b52ebf 100644
--- a/.github/workflows/secret-scan.yaml
+++ b/.github/workflows/secret-scan.yaml
@@ -8,7 +8,7 @@ jobs:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- - uses: gitleaks/gitleaks-action@v2
+ - uses: gitleaks/gitleaks-action@v2.3.4
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
diff --git a/.nvmrc b/.nvmrc
index 1a2f5bd204..209e3ef4b6 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-lts/*
\ No newline at end of file
+20
diff --git a/package.json b/package.json
index 2f07528573..f06f21ca57 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"eslint": "^8.40.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.0.0",
+ "eslint-plugin-react-refresh": "latest",
"http-server": "^14.1.1",
"husky": "^8.0.3",
"lint-staged": "^13.2.0",
@@ -43,6 +44,7 @@
"scripts": {
"build": "NODE_OPTIONS=--max-old-space-size=14366 wsrun -y 4 -p @imtbl/sdk -p @imtbl/checkout-widgets -e -r --serial build && yarn syncpack:format && yarn wsrun -p @imtbl/sdk -a -m copyBrowserBundles",
"build:onlysdk": "NODE_OPTIONS=--max-old-space-size=14366 wsrun -y 4 -p @imtbl/sdk --stages build && yarn syncpack:format",
+ "dev": "wsrun --parallel --exclude-missing dev",
"docs:build": "typedoc",
"docs:serve": "http-server ./docs --cors -p 8080 -c-1",
"lint": "wsrun --exclude-missing -e lint --no-error-on-unmatched-pattern",
diff --git a/packages/blockchain-data/sdk/package.json b/packages/blockchain-data/sdk/package.json
index 06422d104d..73b1fc1cbd 100644
--- a/packages/blockchain-data/sdk/package.json
+++ b/packages/blockchain-data/sdk/package.json
@@ -34,6 +34,7 @@
"repository": "immutable/ts-immutable-sdk.git",
"scripts": {
"build": "NODE_ENV=production rollup --config rollup.config.js",
+ "dev": "rollup --config rollup.config.js -w",
"generate-types": "typechain --target=ethers-v5 --out-dir=src/typechain/types 'abi/*.json'",
"lint": "eslint ./src --ext .ts,.jsx,.tsx --max-warnings=0",
"test": "jest",
diff --git a/packages/checkout/sdk-sample-app/src/components/PassportLoginCallback.tsx b/packages/checkout/sdk-sample-app/src/components/PassportLoginCallback.tsx
new file mode 100644
index 0000000000..3a9723caee
--- /dev/null
+++ b/packages/checkout/sdk-sample-app/src/components/PassportLoginCallback.tsx
@@ -0,0 +1,9 @@
+import { useEffect } from "react";
+import { passport } from "../passport";
+
+export function PassportLoginCallback() {
+ useEffect(() => {
+ passport?.loginCallback();
+ }, [passport])
+ return(<>>);
+}
\ No newline at end of file
diff --git a/packages/checkout/sdk-sample-app/src/index.tsx b/packages/checkout/sdk-sample-app/src/index.tsx
index 43c2aee591..630deb2350 100644
--- a/packages/checkout/sdk-sample-app/src/index.tsx
+++ b/packages/checkout/sdk-sample-app/src/index.tsx
@@ -8,6 +8,9 @@ import ConnectWidget from './pages/ConnectWidget';
import { onLightBase } from '@biom3/design-tokens';
import { BiomeCombinedProviders, Box } from '@biom3/react';
import SmartCheckout from './pages/SmartCheckout';
+import {
+ PassportLoginCallback
+} from "./components/PassportLoginCallback";
const router = createBrowserRouter([
{
@@ -22,6 +25,10 @@ const router = createBrowserRouter([
path: '/smart-checkout',
element: ,
},
+ {
+ path: '/login/callback',
+ element:
+ },
]);
const root = ReactDOM.createRoot(
diff --git a/packages/checkout/sdk-sample-app/src/pages/PassportLoginCallback.tsx b/packages/checkout/sdk-sample-app/src/pages/PassportLoginCallback.tsx
new file mode 100644
index 0000000000..2ce42133af
--- /dev/null
+++ b/packages/checkout/sdk-sample-app/src/pages/PassportLoginCallback.tsx
@@ -0,0 +1,9 @@
+import { useEffect } from 'react';
+import { passport } from '../passport';
+
+export function PassportLoginCallback() {
+ useEffect(() => {
+ passport?.loginCallback();
+ }, [passport])
+ return(<>>);
+}
diff --git a/packages/checkout/sdk/package.json b/packages/checkout/sdk/package.json
index 57250a728b..e01b2147d9 100644
--- a/packages/checkout/sdk/package.json
+++ b/packages/checkout/sdk/package.json
@@ -53,6 +53,7 @@
"scripts": {
"build": "rollup --config rollup.config.js",
"build:dev": "CHECKOUT_DEV_MODE=true yarn build",
+ "dev": "CHECKOUT_DEV_MODE=true rollup --config rollup.config.js -w",
"docs": "typedoc --plugin typedoc-plugin-markdown --skipErrorChecking --disableSources --out docs src/index.ts",
"lint": "eslint ./src --ext .ts,.jsx,.tsx --max-warnings=0",
"lint:fix": "eslint ./src --ext .ts,.jsx,.tsx --max-warnings=0 --fix",
diff --git a/packages/checkout/sdk/src/index.ts b/packages/checkout/sdk/src/index.ts
index cf4b6c593f..f37506efe3 100644
--- a/packages/checkout/sdk/src/index.ts
+++ b/packages/checkout/sdk/src/index.ts
@@ -15,6 +15,7 @@ export { IMMUTABLE_API_BASE_URL } from './env';
export {
getPassportProviderDetail,
getMetaMaskProviderDetail,
+ validateProvider,
} from './provider';
export {
diff --git a/packages/checkout/sdk/src/provider/getUnderlyingProvider.test.ts b/packages/checkout/sdk/src/provider/getUnderlyingProvider.test.ts
index 1c01dcbdfc..0878c65bb8 100644
--- a/packages/checkout/sdk/src/provider/getUnderlyingProvider.test.ts
+++ b/packages/checkout/sdk/src/provider/getUnderlyingProvider.test.ts
@@ -5,10 +5,23 @@ import { WalletAction } from '../types/wallet';
import { CheckoutErrorType } from '../errors';
describe('getUnderlyingChainId', () => {
- it('should return the underlying chain id', async () => {
+ it('should return underlying chain id from property', async () => {
const provider = {
provider: {
- request: jest.fn().mockResolvedValue('0xAA36A7'),
+ chainId: ChainId.SEPOLIA,
+ request: jest.fn(),
+ },
+ } as unknown as Web3Provider;
+
+ const chainId = await getUnderlyingChainId(provider);
+ expect(chainId).toEqual(ChainId.SEPOLIA);
+ expect(provider.provider.request).not.toBeCalled();
+ });
+
+ it('should return the underlying chain id from rpc call', async () => {
+ const provider = {
+ provider: {
+ request: jest.fn().mockResolvedValue('0xaa36a7'),
},
} as unknown as Web3Provider;
@@ -20,11 +33,35 @@ describe('getUnderlyingChainId', () => {
});
});
+ it('should properly parse chain id', async () => {
+ const intChainId = 13473;
+ const strChainId = intChainId.toString();
+ const hexChainId = `0x${intChainId.toString(16)}`;
+ const getMockProvider = (chainId: unknown) => ({ provider: { chainId } } as unknown as Web3Provider);
+
+ // Number
+ expect(await getUnderlyingChainId(getMockProvider(intChainId))).toEqual(
+ intChainId,
+ );
+
+ // String to Number
+ expect(await getUnderlyingChainId(getMockProvider(strChainId))).toEqual(
+ intChainId,
+ );
+
+ // Hex to Number
+ expect(await getUnderlyingChainId(getMockProvider(hexChainId))).toEqual(
+ intChainId,
+ );
+ });
+
it('should throw an error if provider missing from web3provider', async () => {
try {
await getUnderlyingChainId({} as Web3Provider);
} catch (err: any) {
- expect(err.message).toEqual('Parsed provider is not a valid Web3Provider');
+ expect(err.message).toEqual(
+ 'Parsed provider is not a valid Web3Provider',
+ );
expect(err.type).toEqual(CheckoutErrorType.WEB3_PROVIDER_ERROR);
}
});
@@ -33,8 +70,33 @@ describe('getUnderlyingChainId', () => {
try {
await getUnderlyingChainId({ provider: {} } as Web3Provider);
} catch (err: any) {
- expect(err.message).toEqual('Parsed provider is not a valid Web3Provider');
+ expect(err.message).toEqual(
+ 'Parsed provider is not a valid Web3Provider',
+ );
expect(err.type).toEqual(CheckoutErrorType.WEB3_PROVIDER_ERROR);
}
});
+
+ it('should throw an error if invalid chain id value from property', async () => {
+ const provider = {
+ provider: {
+ chainId: 'invalid',
+ request: jest.fn(),
+ },
+ } as unknown as Web3Provider;
+
+ expect(provider.provider.request).not.toHaveBeenCalled();
+ expect(getUnderlyingChainId(provider)).rejects.toThrow('Invalid chainId');
+ });
+
+ it('should throw an error if invalid chain id value returned from rpc call ', async () => {
+ const provider = {
+ provider: {
+ request: jest.fn().mockResolvedValue('invalid'),
+ },
+ } as unknown as Web3Provider;
+
+ expect(getUnderlyingChainId(provider)).rejects.toThrow('Invalid chainId');
+ expect(provider.provider.request).toHaveBeenCalled();
+ });
});
diff --git a/packages/checkout/sdk/src/provider/getUnderlyingProvider.ts b/packages/checkout/sdk/src/provider/getUnderlyingProvider.ts
index a987c41866..bd2c5edfd9 100644
--- a/packages/checkout/sdk/src/provider/getUnderlyingProvider.ts
+++ b/packages/checkout/sdk/src/provider/getUnderlyingProvider.ts
@@ -4,8 +4,27 @@ import { Web3Provider } from '@ethersproject/providers';
import { CheckoutError, CheckoutErrorType } from '../errors';
import { WalletAction } from '../types';
-// this gives us access to the properties of the underlying provider object
-export async function getUnderlyingChainId(web3Provider: Web3Provider) {
+const parseChainId = (chainId: unknown): number => {
+ if (typeof chainId === 'number') {
+ return chainId;
+ }
+
+ if (typeof chainId === 'string' && !Number.isNaN(Number(chainId))) {
+ return chainId.startsWith('0x') ? parseInt(chainId, 16) : Number(chainId);
+ }
+
+ throw new CheckoutError(
+ 'Invalid chainId',
+ CheckoutErrorType.WEB3_PROVIDER_ERROR,
+ );
+};
+
+/**
+ * Get chain id from RPC method
+ * @param web3Provider
+ * @returns chainId number
+ */
+async function requestChainId(web3Provider: Web3Provider): Promise {
if (!web3Provider.provider?.request) {
throw new CheckoutError(
'Parsed provider is not a valid Web3Provider',
@@ -13,9 +32,25 @@ export async function getUnderlyingChainId(web3Provider: Web3Provider) {
);
}
- const chainId = await web3Provider.provider.request({
+ const chainId: string = await web3Provider.provider.request({
method: WalletAction.GET_CHAINID,
params: [],
});
- return parseInt(chainId, 16);
+
+ return parseChainId(chainId);
+}
+
+/**
+ * Get the underlying chain id from the provider
+ * @param web3Provider
+ * @returns chainId number
+ */
+export async function getUnderlyingChainId(web3Provider: Web3Provider): Promise {
+ const chainId = (web3Provider.provider as any)?.chainId;
+
+ if (chainId) {
+ return parseChainId(chainId);
+ }
+
+ return requestChainId(web3Provider);
}
diff --git a/packages/checkout/sdk/src/smartCheckout/buy/buy.ts b/packages/checkout/sdk/src/smartCheckout/buy/buy.ts
index e94c74da76..c4d3fd7900 100644
--- a/packages/checkout/sdk/src/smartCheckout/buy/buy.ts
+++ b/packages/checkout/sdk/src/smartCheckout/buy/buy.ts
@@ -13,6 +13,7 @@ import {
OrderStatusName,
} from '@imtbl/orderbook';
import { GetTokenResult } from '@imtbl/generated-clients/dist/multi-rollup';
+import { track } from '@imtbl/metrics';
import * as instance from '../../instance';
import { CheckoutConfiguration, getL1ChainId, getL2ChainId } from '../../config';
import { CheckoutError, CheckoutErrorType } from '../../errors';
@@ -92,6 +93,8 @@ export const buy = async (
waitFulfillmentSettlements: true,
},
): Promise => {
+ track('checkout_sdk', 'buy_initiated');
+
if (orders.length === 0) {
throw new CheckoutError(
'No orders were provided to the orders array. Please provide at least one order.',
@@ -166,7 +169,9 @@ export const buy = async (
let fees: FeeValue[] = [];
if (takerFees && takerFees.length > 0) {
- fees = calculateFees(takerFees, buyToken.amount, decimals);
+ // eslint-disable-next-line max-len
+ const tokenQuantity = order.result.sell[0].type === ItemType.ERC721 ? BigNumber.from(1) : BigNumber.from(order.result.sell[0].amount);
+ fees = calculateFees(takerFees, buyToken.amount, decimals, tokenQuantity);
}
let unsignedApprovalTransactions: TransactionRequest[] = [];
diff --git a/packages/checkout/sdk/src/smartCheckout/fees/fees.test.ts b/packages/checkout/sdk/src/smartCheckout/fees/fees.test.ts
index 58755a24f2..4af4ba9b19 100644
--- a/packages/checkout/sdk/src/smartCheckout/fees/fees.test.ts
+++ b/packages/checkout/sdk/src/smartCheckout/fees/fees.test.ts
@@ -1,4 +1,4 @@
-import { utils } from 'ethers';
+import { BigNumber, utils } from 'ethers';
import { calculateFees } from './fees';
import { BuyToken, ItemType } from '../../types';
import { CheckoutErrorType } from '../../errors';
@@ -94,6 +94,22 @@ describe('orderbook fees', () => {
}]);
});
+ it('TD-1453 sell token quantity > 1 and non divisible fee amount', async () => {
+ const decimals = 18;
+ const amount = utils.parseUnits('40.32258064516129033', 18).toString();
+ const makerFees = [{
+ amount: { percentageDecimal: 0.01 },
+ recipient: '0x222',
+ }];
+
+ const result = calculateFees(makerFees, amount, decimals, BigNumber.from(10));
+
+ expect(result).toEqual([{
+ amount: '403225806451612900',
+ recipientAddress: '0x222',
+ }]);
+ });
+
it('should calculate the fees with multiple percentageDecimals', async () => {
const decimals = 18;
const amount = utils.parseUnits('10', 18).toString();
diff --git a/packages/checkout/sdk/src/smartCheckout/fees/fees.ts b/packages/checkout/sdk/src/smartCheckout/fees/fees.ts
index 78c6b76eb6..e2c221d9a3 100644
--- a/packages/checkout/sdk/src/smartCheckout/fees/fees.ts
+++ b/packages/checkout/sdk/src/smartCheckout/fees/fees.ts
@@ -9,6 +9,7 @@ export const MAX_FEE_DECIMAL_PLACES = 6; // will allow 0.000001 (0.0001%) as the
const calculateFeesPercent = (
orderFee:OrderFee,
amountBn: BigNumber,
+ tokenQuantity: BigNumber = BigNumber.from(1),
): BigNumber => {
const feePercentage = orderFee.amount as FeePercentage;
@@ -19,7 +20,8 @@ const calculateFeesPercent = (
.mul(BigNumber.from(feePercentageMultiplier))
.div(10 ** MAX_FEE_DECIMAL_PLACES);
- return bnFeeAmount;
+ // always round down to have a fee amount divisible by the token quantity
+ return bnFeeAmount.sub(bnFeeAmount.mod(tokenQuantity));
};
const calculateFeesToken = (
@@ -35,6 +37,7 @@ export const calculateFees = (
orderFees: Array,
weiAmount: string,
decimals: number = 18,
+ tokenQuantity: BigNumber = BigNumber.from(1),
):Array => {
let totalTokenFees: BigNumber = BigNumber.from(0);
@@ -45,12 +48,13 @@ export const calculateFees = (
.mul(MAX_FEE_PERCENTAGE_DECIMAL * (10 ** MAX_FEE_DECIMAL_PLACES))
.div(10 ** MAX_FEE_DECIMAL_PLACES);
- const calculateFeesResult:Array = [];
+ const calculateFeesResult: Array = [];
for (const orderFee of orderFees) {
let currentFeeBn = BigNumber.from(0);
if (Object.hasOwn(orderFee.amount, 'percentageDecimal')) {
- currentFeeBn = calculateFeesPercent(orderFee, amountBn);
+ currentFeeBn = calculateFeesPercent(orderFee, amountBn, tokenQuantity);
+
totalTokenFees = totalTokenFees.add(currentFeeBn);
} else if (Object.hasOwn(orderFee.amount, 'token')) {
currentFeeBn = calculateFeesToken(orderFee, decimals);
diff --git a/packages/checkout/sdk/src/smartCheckout/sell/sell.ts b/packages/checkout/sdk/src/smartCheckout/sell/sell.ts
index 9bef66803e..c9a8cc4285 100644
--- a/packages/checkout/sdk/src/smartCheckout/sell/sell.ts
+++ b/packages/checkout/sdk/src/smartCheckout/sell/sell.ts
@@ -10,6 +10,7 @@ import {
ERC1155Item as OrderbookERC1155Item,
} from '@imtbl/orderbook';
import { BigNumber, Contract, utils } from 'ethers';
+import { track } from '@imtbl/metrics';
import {
ERC721Item,
ERC1155Item,
@@ -91,6 +92,8 @@ export const sell = async (
let listing: PrepareListingResponse;
let spenderAddress = '';
+ track('checkout_sdk', 'sell_initiated');
+
if (orders.length === 0) {
throw new CheckoutError(
'No orders were provided to the orders array. Please provide at least one order.',
@@ -250,7 +253,12 @@ export const sell = async (
};
if (makerFees !== undefined) {
- const orderBookFees = calculateFees(makerFees, buyTokenOrNative.amount, decimals);
+ let tokenQuantity = BigNumber.from(1);
+
+ // if type exists in sellToken then it is valid ERC721 or ERC1155 and not deprecated type
+ if (sellTokenHasType && sellToken.type === ItemType.ERC1155) tokenQuantity = BigNumber.from(sellToken.amount);
+
+ const orderBookFees = calculateFees(makerFees, buyTokenOrNative.amount, decimals, tokenQuantity);
if (orderBookFees.length !== makerFees.length) {
throw new CheckoutError(
'One of the fees is too small, must be greater than 0.000001',
diff --git a/packages/checkout/sdk/src/widgets/definitions/configurations/sale.ts b/packages/checkout/sdk/src/widgets/definitions/configurations/sale.ts
index 08e3f0063a..2b98d37823 100644
--- a/packages/checkout/sdk/src/widgets/definitions/configurations/sale.ts
+++ b/packages/checkout/sdk/src/widgets/definitions/configurations/sale.ts
@@ -5,4 +5,5 @@ import { WidgetConfiguration } from './widget';
*/
export type SaleWidgetConfiguration = {
waitFulfillmentSettlements?: boolean;
+ hideExcludedPaymentTypes?: boolean;
} & WidgetConfiguration;
diff --git a/packages/checkout/sdk/src/widgets/definitions/parameters/sale.ts b/packages/checkout/sdk/src/widgets/definitions/parameters/sale.ts
index 9140bcaf89..d90e504cdb 100644
--- a/packages/checkout/sdk/src/widgets/definitions/parameters/sale.ts
+++ b/packages/checkout/sdk/src/widgets/definitions/parameters/sale.ts
@@ -24,6 +24,8 @@ export type SaleWidgetParams = {
language?: WidgetLanguage;
/** The disabled payment types */
excludePaymentTypes?: SalePaymentTypes[];
+ /** Preferred currency, replacing the backend's base currency */
+ preferredCurrency?: string;
};
/**
diff --git a/packages/checkout/widgets-lib/package.json b/packages/checkout/widgets-lib/package.json
index 783e6cbeb6..47d8330f81 100644
--- a/packages/checkout/widgets-lib/package.json
+++ b/packages/checkout/widgets-lib/package.json
@@ -83,6 +83,7 @@
"build:analyse": "yarn build --plugin visualizer",
"build:local": "yarn clean && yarn build && mkdir -p ../widgets-sample-app/public/lib/js && cp dist/*.js ../widgets-sample-app/public/lib/js/",
"clean": "rm -rf ./dist",
+ "dev": "NODE_ENV=development rollup --config rollup.config.js",
"lint": "eslint ./src --ext .ts,.jsx,.tsx --max-warnings=0",
"lint:fix": "eslint ./src --ext .ts,.jsx,.tsx --max-warnings=0 --fix",
"start": "yarn clean && NODE_ENV=development rollup --config rollup.config.js --watch",
diff --git a/packages/checkout/widgets-lib/src/components/CoinSelector/CoinSelectorOption.tsx b/packages/checkout/widgets-lib/src/components/CoinSelector/CoinSelectorOption.tsx
index dfb0e17fc1..7a31310445 100644
--- a/packages/checkout/widgets-lib/src/components/CoinSelector/CoinSelectorOption.tsx
+++ b/packages/checkout/widgets-lib/src/components/CoinSelector/CoinSelectorOption.tsx
@@ -1,6 +1,6 @@
import { MenuItem, AllIconKeys } from '@biom3/react';
import { useTranslation } from 'react-i18next';
-import { useMemo, useState } from 'react';
+import { TokenImage } from 'components/TokenImage/TokenImage';
export interface CoinSelectorOptionProps {
testId?: string;
@@ -19,23 +19,14 @@ export interface CoinSelectorOptionProps {
export function CoinSelectorOption({
onClick, icon, name, symbol, balance, defaultTokenImage, testId, id,
}: CoinSelectorOptionProps) {
- const [iconError, setIconError] = useState(false);
const { t } = useTranslation();
- const tokenUrl = useMemo(
- () => ((!icon || iconError) ? defaultTokenImage : icon),
- [icon, iconError, defaultTokenImage],
- );
return (