Commit 5f3b88d5 authored by Max Alekseenko's avatar Max Alekseenko

add get gas button

parent 6000e9cd
import type { Feature } from './types';
import type { GasRefuelProviderConfig } from 'types/client/gasRefuelProviderConfig';
import { getEnvValue, parseEnvJson } from '../utils';
import marketplace from './marketplace';
const value = parseEnvJson<GasRefuelProviderConfig>(getEnvValue('NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG'));
const title = 'Get gas button';
const config: Feature<{
name: string;
logoUrl?: string;
urlTemplate: string;
dappId?: string;
usdThreshold: number;
}> = (() => {
if (value) {
return Object.freeze({
title,
isEnabled: true,
name: value.name,
logoUrl: value.logo,
urlTemplate: value.url_template,
dappId: marketplace.isEnabled ? value.dapp_id : undefined,
usdThreshold: value.usd_threshold || 1,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
......@@ -11,6 +11,7 @@ export { default as dataAvailability } from './dataAvailability';
export { default as deFiDropdown } from './deFiDropdown';
export { default as faultProofSystem } from './faultProofSystem';
export { default as gasTracker } from './gasTracker';
export { default as getGasButton } from './getGasButton';
export { default as googleAnalytics } from './googleAnalytics';
export { default as graphqlApiDocs } from './graphqlApiDocs';
export { default as growthBook } from './growthBook';
......
......@@ -39,6 +39,7 @@ NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKj
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_METASUITES_ENABLED=true
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}
NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Get gas', 'dapp_id': 'smol-refuel', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}&partner=blockscout&utm_source=blockscout&utm_medium=address&disableBridges=true', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png', 'usd_threshold': 10}
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
......
......@@ -13,6 +13,7 @@ import { SUPPORTED_AD_TEXT_PROVIDERS, SUPPORTED_AD_BANNER_PROVIDERS, SUPPORTED_A
import type { AdTextProviders, AdBannerProviders, AdBannerAdditionalProviders } from '../../../types/client/adProviders';
import type { ContractCodeIde } from '../../../types/client/contract';
import type { DeFiDropdownItem } from '../../../types/client/deFiDropdown';
import type { GasRefuelProviderConfig } from '../../../types/client/gasRefuelProviderConfig';
import { GAS_UNITS } from '../../../types/client/gasTracker';
import type { GasUnit } from '../../../types/client/gasTracker';
import type { MarketplaceAppOverview, MarketplaceAppSecurityReportRaw, MarketplaceAppSecurityReport } from '../../../types/client/marketplace';
......@@ -638,6 +639,20 @@ const schema = yup
dapp_id: yup.string(),
});
return isUndefined || valueSchema.isValidSync(data);
}),
NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG: yup
.mixed()
.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG, it should have name and url template', (data) => {
const isUndefined = data === undefined;
const valueSchema = yup.object<GasRefuelProviderConfig>().transform(replaceQuotes).json().shape({
name: yup.string().required(),
url_template: yup.string().required(),
logo: yup.string(),
dapp_id: yup.string(),
usd_threshold: yup.number(),
});
return isUndefined || valueSchema.isValidSync(data);
}),
NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE: yup.string<ValidatorsChainType>().oneOf(VALIDATORS_CHAIN_TYPE),
......
......@@ -78,3 +78,4 @@ NEXT_PUBLIC_WEB3_WALLETS=['coinbase','metamask','token_pocket']
NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE=stability
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'uniswap'},{'text':'Payment link','icon':'payment_link','url':'https://example.com'}]
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}
NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Get gas', 'dapp_id': 'smol-refuel', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}&partner=blockscout&utm_source=blockscout&utm_medium=address&disableBridges=true', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png', 'usd_threshold': 10}
......@@ -702,7 +702,29 @@ If the feature is enabled, a Multichain balance button will be displayed on the
| name | `string` | Multichain portfolio application name | Required | - | `zerion` |
| url_template | `string` | Url template to the portfolio. Should be a template with `{address}` variable | Required | - | `https://app.zerion.io/{address}/overview` |
| dapp_id | `string` | Set for open a Blockscout dapp page with the portfolio instead of opening external app page | - | - | `zerion` |
| logo | `string` | Multichain portfolio application logo (.svg) url | - | - | `https://example.com/icon.svg` |
| logo | `string` | Multichain portfolio application logo url | - | - | `https://example.com/icon.svg` |
&nbsp;
### Get gas button
If the feature is enabled, a Get gas button will be displayed on the address page, which will take you to the gas refuel application in the marketplace or to an external site.
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG | `{ name: string; url_template: string; dapp_id?: string; logo?: string, usd_threshold: number }` | Get gas button config. See [below](#get-gas-button-configuration-properties) | - | - | `{ name: 'Get gas', dapp_id: 'smol-refuel', url_template: 'https://smolrefuel.com/?outboundChain={chainId}&partner=blockscout&utm_source=blockscout&utm_medium=address&disableBridges=true', logo: 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png', usd_threshold: 10 }` | v1.33.0+ |
&nbsp;
#### Get gas button configuration properties
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| name | `string` | Gas refuel application name | Required | - | `zerion` |
| url_template | `string` | Url template to the portfolio. Should be a template with `{address}` variable | Required | - | `https://app.zerion.io/{address}/overview` |
| dapp_id | `string` | Set for open a Blockscout dapp page with the portfolio instead of opening external app page | - | - | `zerion` |
| logo | `string` | Gas refuel application logo (.svg) url | - | - | `https://example.com/icon.png` |
| usd_threshold | `number` | Value in USD, at balance less than which the button will be displayed | - | `1` | `10` |
&nbsp;
......
export type GasRefuelProviderConfig = {
name: string;
dapp_id?: string;
url_template: string;
logo?: string;
usd_threshold?: number;
};
import { Grid, Box } from '@chakra-ui/react';
import React from 'react';
import * as addressMock from 'mocks/address/address';
import { test, expect, devices } from 'playwright/lib';
import AddressBalance from './AddressBalance';
const ICON_URL = 'https://localhost:3000/my-icon.png';
const eoaWithSmallBalance = {
...addressMock.eoa,
coin_balance: '500000000000000000', // 0.5 * 10^18
exchange_rate: '1', // 1 USD
};
test('base view', async({ render }) => {
const component = await render(
<Grid columnGap={ 8 } templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden">
<AddressBalance data={ addressMock.eoa } isLoading={ false }/>
</Grid>,
);
await expect(component).toHaveScreenshot();
});
test('with get gas button internal +@dark-mode', async({ render, mockEnvs, mockAssetResponse }) => {
await mockEnvs([
[
'NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG',
`{"name": "Get gas", "dapp_id": "duck", "url_template": "https://duck.url/{chainId}", "logo": "${ ICON_URL }", "usd_threshold": 1}`,
],
]);
await mockAssetResponse(ICON_URL, './playwright/mocks/image_svg.svg');
const component = await render(
<Grid columnGap={ 8 } templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden">
<AddressBalance data={ eoaWithSmallBalance } isLoading={ false }/>
</Grid>,
);
await expect(component).toHaveScreenshot();
});
test('with get gas button external', async({ render, mockEnvs, mockAssetResponse }) => {
await mockEnvs([
[
'NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG',
`{"name": "Get gas", "url_template": "https://duck.url/{chainId}", "logo": "${ ICON_URL }", "usd_threshold": 1}`,
],
]);
await mockAssetResponse(ICON_URL, './playwright/mocks/image_svg.svg');
const component = await render(
<Grid columnGap={ 8 } templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden">
<AddressBalance data={ eoaWithSmallBalance } isLoading={ false }/>
</Grid>,
);
await expect(component).toHaveScreenshot();
});
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('base view', async({ render, mockEnvs, mockAssetResponse }) => {
await mockEnvs([
[
'NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG',
`{"name": "Get gas", "dapp_id": "duck", "url_template": "https://duck.url/{chainId}", "logo": "${ ICON_URL }", "usd_threshold": 1}`,
],
]);
await mockAssetResponse(ICON_URL, './playwright/mocks/image_svg.svg');
const component = await render(
<Box w="300px">
<Grid columnGap={ 8 } templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden">
<AddressBalance data={ eoaWithSmallBalance } isLoading={ false }/>
</Grid>
</Box>,
);
await expect(component).toHaveScreenshot();
});
});
import { Image } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Address } from 'types/api/address';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import { getResourceKey } from 'lib/api/useApiQuery';
import getCurrencyValue from 'lib/getCurrencyValue';
import * as mixpanel from 'lib/mixpanel/index';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { currencyUnits } from 'lib/units';
import CurrencyValue from 'ui/shared/CurrencyValue';
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import LinkExternal from 'ui/shared/links/LinkExternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import NativeTokenIcon from 'ui/shared/NativeTokenIcon';
import TextSeparator from 'ui/shared/TextSeparator';
const TEMPLATE_CHAIN_ID = '{chainId}';
const getGasFeature = config.features.getGasButton;
interface Props {
data: Pick<Address, 'block_number_balance_updated_at' | 'coin_balance' | 'hash' | 'exchange_rate'>;
data: Pick<Address, 'block_number_balance_updated_at' | 'coin_balance' | 'hash' | 'exchange_rate' | 'is_contract'>;
isLoading: boolean;
}
......@@ -65,6 +77,67 @@ const AddressBalance = ({ data, isLoading }: Props) => {
handler: handleNewCoinBalanceMessage,
});
const value = data.coin_balance || '0';
const exchangeRate = data.exchange_rate;
const decimals = String(config.chain.currency.decimals);
const accuracyUsd = 2;
const accuracy = 8;
const onGetGasClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.BUTTON_CLICK, { Content: 'Get gas', Source: 'address' });
}, []);
let getGasButton = null;
const { usd: usdResult } = getCurrencyValue({ value, accuracy, accuracyUsd, exchangeRate, decimals });
if (getGasFeature.isEnabled && !data?.is_contract && Number(usdResult) < getGasFeature.usdThreshold) {
const buttonContent = (
<>
{ getGasFeature.logoUrl && (
<Image src={ getGasFeature.logoUrl } alt={ getGasFeature.name } boxSize={ 5 } mr={ 2 } borderRadius="4px" overflow="hidden"/>
) }
{ getGasFeature.name }
</>
);
const linkProps = {
display: 'flex',
alignItems: 'center',
fontSize: 'sm',
lineHeight: 5,
onClick: onGetGasClick,
};
try {
const getGasUrlString = getGasFeature.urlTemplate.replace(TEMPLATE_CHAIN_ID, config.chain.id || '');
const getGasUrl = new URL(getGasUrlString);
getGasUrl.searchParams.append('utm_source', 'blockscout');
getGasUrl.searchParams.append('utm_medium', 'address');
const dappId = getGasFeature.dappId;
getGasButton = (
<>
<TextSeparator mx={ 2 } color="gray.500"/>
{ typeof dappId === 'string' ? (
<LinkInternal
href={ route({ pathname: '/apps/[id]', query: { id: dappId, url: getGasUrl.toString() } }) }
{ ...linkProps }
>
{ buttonContent }
</LinkInternal>
) : (
<LinkExternal
href={ getGasUrl.toString() }
{ ...linkProps }
>
{ buttonContent }
</LinkExternal>
) }
</>
);
} catch (error) {}
}
return (
<>
<DetailsInfoItem.Label
......@@ -76,15 +149,16 @@ const AddressBalance = ({ data, isLoading }: Props) => {
<DetailsInfoItem.Value alignSelf="center" flexWrap="nowrap">
<NativeTokenIcon boxSize={ 6 } mr={ 2 } isLoading={ isLoading }/>
<CurrencyValue
value={ data.coin_balance || '0' }
exchangeRate={ data.exchange_rate }
decimals={ String(config.chain.currency.decimals) }
value={ value }
exchangeRate={ exchangeRate }
decimals={ decimals }
currency={ currencyUnits.ether }
accuracyUsd={ 2 }
accuracy={ 8 }
accuracyUsd={ accuracyUsd }
accuracy={ accuracy }
flexWrap="wrap"
isLoading={ isLoading }
/>
{ !isLoading && getGasButton }
</DetailsInfoItem.Value>
</>
);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment