Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GasHawk integration #2232

Merged
merged 2 commits into from
Sep 13, 2024
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
1 change: 1 addition & 0 deletions configs/app/features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export { default as publicTagsSubmission } from './publicTagsSubmission';
export { default as restApiDocs } from './restApiDocs';
export { default as rollup } from './rollup';
export { default as safe } from './safe';
export { default as saveOnGas } from './saveOnGas';
export { default as sentry } from './sentry';
export { default as sol2uml } from './sol2uml';
export { default as stats } from './stats';
Expand Down
25 changes: 25 additions & 0 deletions configs/app/features/saveOnGas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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;
1 change: 1 addition & 0 deletions configs/envs/.env.eth
Original file line number Diff line number Diff line change
Expand Up @@ -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_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_SAVE_ON_GAS_ENABLED=true
1 change: 1 addition & 0 deletions deploy/tools/envs-validator/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,7 @@ const schema = yup
value => value === undefined,
),
}),
NEXT_PUBLIC_SAVE_ON_GAS_ENABLED: yup.boolean(),

// 6. External services envs
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(),
Expand Down
1 change: 1 addition & 0 deletions deploy/tools/envs-validator/test/.env.base
Original file line number Diff line number Diff line change
Expand Up @@ -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_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_SAVE_ON_GAS_ENABLED=true
13 changes: 12 additions & 1 deletion docs/ENVS.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [Validators list](ENVS.md#validators-list)
- [Sentry error monitoring](ENVS.md#sentry-error-monitoring)
- [OpenTelemetry](ENVS.md#opentelemetry)
- [Swap button](ENVS.md#defi-dropdown)
- [DeFi dropdown](ENVS.md#defi-dropdown)
- [Multichain balance button](ENVS.md#multichain-balance-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)

&nbsp;
Expand Down Expand Up @@ -755,6 +756,16 @@ If the feature is enabled, a Get gas button will be displayed in the top bar, wh

&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

### Google ReCaptcha
Expand Down
1 change: 1 addition & 0 deletions nextjs/csp/generateCspPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ function generateCspPolicy() {
descriptors.app(),
descriptors.ad(),
descriptors.cloudFlare(),
descriptors.gasHawk(),
descriptors.googleAnalytics(),
descriptors.googleFonts(),
descriptors.googleReCaptcha(),
Expand Down
30 changes: 30 additions & 0 deletions nextjs/csp/policies/gasHawk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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,
],
};
}
1 change: 1 addition & 0 deletions nextjs/csp/policies/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { ad } from './ad';
export { app } from './app';
export { cloudFlare } from './cloudFlare';
export { gasHawk } from './gasHawk';
export { googleAnalytics } from './googleAnalytics';
export { googleFonts } from './googleFonts';
export { googleReCaptcha } from './googleReCaptcha';
Expand Down
24 changes: 24 additions & 0 deletions public/static/gas_hawk_logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions ui/address/AddressDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import AddressBalance from './details/AddressBalance';
import AddressImplementations from './details/AddressImplementations';
import AddressNameInfo from './details/AddressNameInfo';
import AddressNetWorth from './details/AddressNetWorth';
import AddressSaveOnGas from './details/AddressSaveOnGas';
import TokenSelect from './tokenSelect/TokenSelect';
import useAddressCountersQuery from './utils/useAddressCountersQuery';
import type { AddressQuery } from './utils/useAddressQuery';
Expand Down Expand Up @@ -211,6 +212,12 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
/>
) :
0 }
{ !countersQuery.isPlaceholderData && countersQuery.data?.gas_usage_count && (
<AddressSaveOnGas
gasUsed={ countersQuery.data.gas_usage_count }
address={ data.hash }
/>
) }
</DetailsInfoItem.Value>
</>
) }
Expand Down
88 changes: 88 additions & 0 deletions ui/address/details/AddressSaveOnGas.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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);
2 changes: 1 addition & 1 deletion ui/address/utils/useAddressCountersQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ interface Params {
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 apiQuery = useApiQuery<'address_counters', { status: number }>('address_counters', {
Expand Down
Loading