Commit 344fbaef authored by tom goriunov's avatar tom goriunov Committed by GitHub

GasHawk integration (#2232)

* GasHawk integration

Fixes #2205

* change link url
parent bbc37ef1
...@@ -25,6 +25,7 @@ export { default as publicTagsSubmission } from './publicTagsSubmission'; ...@@ -25,6 +25,7 @@ export { default as publicTagsSubmission } from './publicTagsSubmission';
export { default as restApiDocs } from './restApiDocs'; export { default as restApiDocs } from './restApiDocs';
export { default as rollup } from './rollup'; export { default as rollup } from './rollup';
export { default as safe } from './safe'; export { default as safe } from './safe';
export { default as saveOnGas } from './saveOnGas';
export { default as sentry } from './sentry'; export { default as sentry } from './sentry';
export { default as sol2uml } from './sol2uml'; export { default as sol2uml } from './sol2uml';
export { default as stats } from './stats'; export { default as stats } from './stats';
......
import type { Feature } from './types';
import { getEnvValue } from '../utils';
import marketplace from './marketplace';
const title = 'Save on gas with GasHawk';
const config: Feature<{
apiUrlTemplate: string;
}> = (() => {
if (getEnvValue('NEXT_PUBLIC_SAVE_ON_GAS_ENABLED') === 'true' && marketplace.isEnabled) {
return Object.freeze({
title,
isEnabled: true,
apiUrlTemplate: 'https://core.gashawk.io/apiv2/stats/address/<address>/savingsPotential/0x1',
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
...@@ -62,3 +62,4 @@ NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s-prod-1.blockscout.com ...@@ -62,3 +62,4 @@ NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s-prod-1.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_SAVE_ON_GAS_ENABLED=true
...@@ -801,6 +801,7 @@ const schema = yup ...@@ -801,6 +801,7 @@ const schema = yup
value => value === undefined, value => value === undefined,
), ),
}), }),
NEXT_PUBLIC_SAVE_ON_GAS_ENABLED: yup.boolean(),
// 6. External services envs // 6. External services envs
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(), NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(),
......
...@@ -84,3 +84,4 @@ NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE=stability ...@@ -84,3 +84,4 @@ 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_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_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': 'Need 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'} NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need 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'}
NEXT_PUBLIC_SAVE_ON_GAS_ENABLED=true
...@@ -59,9 +59,10 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will ...@@ -59,9 +59,10 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [Validators list](ENVS.md#validators-list) - [Validators list](ENVS.md#validators-list)
- [Sentry error monitoring](ENVS.md#sentry-error-monitoring) - [Sentry error monitoring](ENVS.md#sentry-error-monitoring)
- [OpenTelemetry](ENVS.md#opentelemetry) - [OpenTelemetry](ENVS.md#opentelemetry)
- [Swap button](ENVS.md#defi-dropdown) - [DeFi dropdown](ENVS.md#defi-dropdown)
- [Multichain balance button](ENVS.md#multichain-balance-button) - [Multichain balance button](ENVS.md#multichain-balance-button)
- [Get gas button](ENVS.md#get-gas-button) - [Get gas button](ENVS.md#get-gas-button)
- [Save on gas with GasHawk](ENVS.md#save-on-gas-with-gashawk)
- [3rd party services configuration](ENVS.md#external-services-configuration) - [3rd party services configuration](ENVS.md#external-services-configuration)
&nbsp; &nbsp;
...@@ -755,6 +756,16 @@ If the feature is enabled, a Get gas button will be displayed in the top bar, wh ...@@ -755,6 +756,16 @@ If the feature is enabled, a Get gas button will be displayed in the top bar, wh
&nbsp; &nbsp;
### Save on gas with GasHawk
The feature enables a "Save with GasHawk" button next to the "Gas used" value on the address page.
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_SAVE_ON_GAS_ENABLED | `boolean` | Set to "true" to enable the feature | - | - | `true` | v1.35.0+ |
&nbsp;
## External services configuration ## External services configuration
### Google ReCaptcha ### Google ReCaptcha
......
...@@ -6,6 +6,7 @@ function generateCspPolicy() { ...@@ -6,6 +6,7 @@ function generateCspPolicy() {
descriptors.app(), descriptors.app(),
descriptors.ad(), descriptors.ad(),
descriptors.cloudFlare(), descriptors.cloudFlare(),
descriptors.gasHawk(),
descriptors.googleAnalytics(), descriptors.googleAnalytics(),
descriptors.googleFonts(), descriptors.googleFonts(),
descriptors.googleReCaptcha(), descriptors.googleReCaptcha(),
......
import type CspDev from 'csp-dev';
import config from 'configs/app';
const feature = config.features.saveOnGas;
export function gasHawk(): CspDev.DirectiveDescriptor {
if (!feature.isEnabled) {
return {};
}
const apiOrigin = (() => {
try {
const url = new URL(feature.apiUrlTemplate);
return url.origin;
} catch (error) {
return '';
}
})();
if (!apiOrigin) {
return {};
}
return {
'connect-src': [
apiOrigin,
],
};
}
export { ad } from './ad'; export { ad } from './ad';
export { app } from './app'; export { app } from './app';
export { cloudFlare } from './cloudFlare'; export { cloudFlare } from './cloudFlare';
export { gasHawk } from './gasHawk';
export { googleAnalytics } from './googleAnalytics'; export { googleAnalytics } from './googleAnalytics';
export { googleFonts } from './googleFonts'; export { googleFonts } from './googleFonts';
export { googleReCaptcha } from './googleReCaptcha'; export { googleReCaptcha } from './googleReCaptcha';
......
<svg viewBox="0 0 15 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.4 12.07c-.164-.152-.302-.312-.456-.454-.703-.668-.168-1.364-.168-1.364s-.844.132-2.113-.128a4.273 4.273 0 0 0-4.44 1.175 2.618 2.618 0 0 0-2.084 2.334 4.722 4.722 0 0 0-.002.5 4.64 4.64 0 0 0 .024.209s.023.132.038.196l.025.095v.003l.028.094.014.04a2.6 2.6 0 0 1 .286-.97.38.38 0 0 1 .346-.196 7 7 0 0 1 1.352.218 4.28 4.28 0 0 1 2.343 2.9h.001a4.683 4.683 0 0 1-.152 2.937A7.22 7.22 0 0 0 14.4 12.07Zm-4.82-.62a.644.644 0 0 1-1.074-.138c.118-.184.239-.278.36-.328.473-.212.807-.22.807-.22.1.221.073.489-.094.686Z" fill="url(#a)"/>
<path d="M9.594 16.721a4.743 4.743 0 0 1 .107.5l1.551 1.474.088.021a7.247 7.247 0 0 0 2.415-2.86c-3.78-3.424-7.12-2.33-7.856-2.252.464.029.916.103 1.352.218a4.28 4.28 0 0 1 2.343 2.9Z" fill="url(#b)"/>
<path d="M7.605.209A.46.46 0 0 0 7.22 0a.459.459 0 0 0-.387.209L1.381 8.544A7.185 7.185 0 0 0 0 12.782c-.004 3.86 3.064 7.044 6.889 7.218.562-.986.571-2.159.565-2.426a4.866 4.866 0 0 1-.236.006h-.033a4.757 4.757 0 0 1-3.361-1.422 4.755 4.755 0 0 1-1.396-3.374 4.751 4.751 0 0 1 .915-2.81l3.875-5.92L9.53 7.69a6.433 6.433 0 0 1 3.095.254L7.605.209Z" fill="url(#c)"/>
<path d="M12.256 17.96c-.686-1.602-1.57-2.637-2.252-3.249-1.07-.958-1.997-1.25-2.875-1.24-.877.01-1.361.15-1.361.15a.408.408 0 0 1 .13-.017c.465.029.917.102 1.352.217a4.28 4.28 0 0 1 2.344 2.9 4.683 4.683 0 0 1-.152 2.937 7.22 7.22 0 0 0 2.814-1.7v.001Z" fill="url(#d)"/>
<defs>
<linearGradient id="a" x1="7.369" y1="12.845" x2="12.113" y2="14.707" gradientUnits="userSpaceOnUse">
<stop stop-color="#FDF0C2"/>
<stop offset="1" stop-color="#F6C789"/>
</linearGradient>
<linearGradient id="b" x1="9.827" y1="13.377" x2="9.837" y2="18.83" gradientUnits="userSpaceOnUse">
<stop stop-color="#FDF1C3"/>
<stop offset=".625" stop-color="#F5C57D"/>
</linearGradient>
<linearGradient id="c" x1="2.388" y1="15.63" x2="11.492" y2="4.616" gradientUnits="userSpaceOnUse">
<stop stop-color="#F5C060"/>
<stop offset="1" stop-color="#F0994D"/>
</linearGradient>
<linearGradient id="d" x1="9.012" y1="13.47" x2="9.009" y2="15.153" gradientUnits="userSpaceOnUse">
<stop stop-color="#F5C780"/>
<stop offset="1" stop-color="#FADFA1"/>
</linearGradient>
</defs>
</svg>
...@@ -21,6 +21,7 @@ import AddressBalance from './details/AddressBalance'; ...@@ -21,6 +21,7 @@ import AddressBalance from './details/AddressBalance';
import AddressImplementations from './details/AddressImplementations'; import AddressImplementations from './details/AddressImplementations';
import AddressNameInfo from './details/AddressNameInfo'; import AddressNameInfo from './details/AddressNameInfo';
import AddressNetWorth from './details/AddressNetWorth'; import AddressNetWorth from './details/AddressNetWorth';
import AddressSaveOnGas from './details/AddressSaveOnGas';
import TokenSelect from './tokenSelect/TokenSelect'; import TokenSelect from './tokenSelect/TokenSelect';
import useAddressCountersQuery from './utils/useAddressCountersQuery'; import useAddressCountersQuery from './utils/useAddressCountersQuery';
import type { AddressQuery } from './utils/useAddressQuery'; import type { AddressQuery } from './utils/useAddressQuery';
...@@ -211,6 +212,12 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -211,6 +212,12 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
/> />
) : ) :
0 } 0 }
{ !countersQuery.isPlaceholderData && countersQuery.data?.gas_usage_count && (
<AddressSaveOnGas
gasUsed={ countersQuery.data.gas_usage_count }
address={ data.hash }
/>
) }
</DetailsInfoItem.Value> </DetailsInfoItem.Value>
</> </>
) } ) }
......
import { Image, Skeleton } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import * as v from 'valibot';
import config from 'configs/app';
import LinkExternal from 'ui/shared/links/LinkExternal';
import TextSeparator from 'ui/shared/TextSeparator';
const feature = config.features.saveOnGas;
const responseSchema = v.object({
percent: v.number(),
});
const ERROR_NAME = 'Invalid response schema';
interface Props {
gasUsed: string;
address: string;
}
const AddressSaveOnGas = ({ gasUsed, address }: Props) => {
const gasUsedNumber = Number(gasUsed);
const query = useQuery({
queryKey: [ 'gas_hawk_saving_potential', { address } ],
queryFn: async() => {
if (!feature.isEnabled) {
return;
}
const response = await fetch(feature.apiUrlTemplate.replace('<address>', address));
const data = await response.json();
return data;
},
select: (response) => {
const parsedResponse = v.safeParse(responseSchema, response);
if (!parsedResponse.success) {
throw Error('Invalid response schema');
}
return parsedResponse.output;
},
placeholderData: { percent: 42 },
enabled: feature.isEnabled && gasUsedNumber > 0,
});
const errorMessage = query.error && 'message' in query.error ? query.error.message : undefined;
React.useEffect(() => {
if (errorMessage === ERROR_NAME) {
fetch('/node-api/monitoring/invalid-api-schema', {
method: 'POST',
body: JSON.stringify({
resource: 'gas_hawk_saving_potential',
url: feature.isEnabled ? feature.apiUrlTemplate.replace('<address>', address) : undefined,
}),
});
}
}, [ address, errorMessage ]);
if (gasUsedNumber <= 0 || !feature.isEnabled || query.isError || !query.data?.percent) {
return null;
}
const percent = Math.round(query.data.percent);
if (percent < 1) {
return null;
}
return (
<>
<TextSeparator color="divider"/>
<Skeleton isLoaded={ !query.isPlaceholderData } display="flex" alignItems="center" columnGap={ 2 }>
<Image src="/static/gas_hawk_logo.svg" w="15px" h="20px" alt="GasHawk logo"/>
<LinkExternal href="https://www.gashawk.io" fontSize="sm">
Save { percent.toLocaleString(undefined, { maximumFractionDigits: 0 }) }% with GasHawk
</LinkExternal>
</Skeleton>
</>
);
};
export default React.memo(AddressSaveOnGas);
...@@ -24,7 +24,7 @@ interface Params { ...@@ -24,7 +24,7 @@ interface Params {
addressQuery: AddressQuery; addressQuery: AddressQuery;
} }
export default function useAddressQuery({ hash, addressQuery }: Params): AddressCountersQuery { export default function useAddressCountersQuery({ hash, addressQuery }: Params): AddressCountersQuery {
const enabled = Boolean(hash) && !addressQuery.isPlaceholderData; const enabled = Boolean(hash) && !addressQuery.isPlaceholderData;
const apiQuery = useApiQuery<'address_counters', { status: number }>('address_counters', { const apiQuery = useApiQuery<'address_counters', { status: number }>('address_counters', {
......
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