Release v2.7.2 (#3326)

* debug wallet client error

* Support raw text/html for NFT animation on the token instance page

Resolves #2609

* fix writing to a contract with dynamic provider

* change default interval for hot contracts

* wait for load event of NFT embed frame

* update screenshots

* add network validation mode to dynamic provider

* add rootstock mainnet preset

* update preset for rootsrock mainnet

* [skip ci] disable essential dapps for rootstock mainnet

* pass required flag to FormFieldSelect controller

* bump dynamic version

* fix rabby wallet tx write
This commit is contained in:
tom goriunov
2026-04-08 11:10:23 +02:00
committed by tom
parent 68f952b5a9
commit 0ae5d6c8e5
23 changed files with 900 additions and 281 deletions
+1
View File
@@ -31,6 +31,7 @@ on:
- optimism_sepolia
- polygon
- rootstock
- rootstock_testnet
- scroll_sepolia
- shibarium
- stability
+1
View File
@@ -34,6 +34,7 @@ on:
- polygon
- rari_testnet
- rootstock
- rootstock_testnet
- shibarium
- scroll_sepolia
- stability
+1
View File
@@ -362,6 +362,7 @@
"optimism_sepolia",
"polygon",
"rari_testnet",
"rootstock",
"rootstock_testnet",
"scroll_sepolia",
"shibarium",
+71
View File
@@ -0,0 +1,71 @@
# Set of ENVs for Rootstock network explorer
# https://rootstock.blockscout.com
# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=rootstock"
# Local ENVs
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
NEXT_PUBLIC_ACCOUNT_DYNAMIC_ENVIRONMENT_ID=50fd93d4-3109-4914-af56-9e7bcb16c469
NEXT_PUBLIC_IS_TESTNET=false
# Instance ENVs
NEXT_PUBLIC_ACCOUNT_API_KEYS_BUTTON=false
NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER=dynamic
NEXT_PUBLIC_AD_BANNER_ENABLE_SPECIFY=true
NEXT_PUBLIC_ADDRESS_3RD_PARTY_WIDGETS=['talentprotocol','drops','blockscoutbadges','gitpoap','efp','etherscore','webacy','humanpassport','trustblock','smartmuv','humanode']
NEXT_PUBLIC_ADDRESS_3RD_PARTY_WIDGETS_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/widgets/config.json
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_DOCS_ALERT_MESSAGE=<b>Deprecation Notice:</b> Chain-specific API keys are being deprecated.<br>Please migrate to the <a href="https://dev.blockscout.com/?utm_source=blockscout_account" target="_blank">Blockscout PRO API</a> for multichain access. <br>Existing API keys will remain valid until July 1, 2026
NEXT_PUBLIC_API_HOST=rootstock.blockscout.com
NEXT_PUBLIC_API_KEYS_ALERT_MESSAGE=<b>Deprecation Notice:</b> Chain-specific API keys are being deprecated.<br>Please migrate to the <a href="https://dev.blockscout.com/?utm_source=blockscout_account" target="_blank">Blockscout PRO API</a> for multichain access. <br>Existing API keys will remain valid until July 1, 2026
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'swap','isEssentialDapp':true},{'text':'Revoke','icon':'integration/partial','dappId':'revoke','isEssentialDapp':true}, {'text':'Multisend','icon':'navigation/tokens','dappId':'multisend','isEssentialDapp':true}]
NEXT_PUBLIC_DEX_POOLS_ENABLED=false
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/rsk-mainnet.json
NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/rootstock.json
NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge
NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'url_template': 'https://www.gas.zip?utm_source=blockscout', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'}
NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS=true
NEXT_PUBLIC_HAS_USER_OPS=true
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs', 'coin_price']
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgb(255, 145, 0)'],'text_color':['rgb(255, 255, 255)']}
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://blockscout-marketing-banners.s3.us-east-1.amazonaws.com/%5Bmarketplace-banner%5D__badges-promo__banner.html
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://badges.blockscout.com/badges-list?utm_source=blockscout-explorer&utm_medium=marketplace-banner
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_CONFIG={'swap': {'url': 'https://swapscout.vercel.app', 'chains': ['1', '10', '30', '100', '122', '130', '137', '324', '480', '1135', '1514', '1868', '1923', '8453', '13371', '42161', '42220', '42793', '57073', '59144', '534352', '11155111', '1313161554'], 'fee': '0.004', 'integrator': 'blockscout'}, 'multisend': {'chains': ['1', '10', '30', '100', '122', '130', '137', '324', '480', '1135', '1514', '1868', '7000', '8453', '13371', '42161', '42220', '57073', '59144', '534352', '11155111', '1313161554'], 'posthogKey': 'phc_7O4WGsecqqDO1PeaKayHAxUWN1PjheOmQCiDxEMcmkx', 'posthogHost': 'https://us.i.posthog.com'}, 'revoke': {'chains': ['1', '10', '30', '61', '100', '122', '130', '137', '148', '314', '324', '360', '480', '1514', '1868', '4326', '7000', '7887', '8453', '13371', '42161', '42220', '42793', '57073', '534352', '245022934']}}
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/paggtimMHyz0AsOCC/form
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_MIXPANEL_CONFIG_OVERRIDES={"record_sessions_percent": 0.5,"record_heatmap_data": true}
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG=[{'name': 'octav', 'url_template': 'https://pro.octav.fi/?addresses={address}', 'logo': 'https://blockscout-icons.s3.us-east-1.amazonaws.com/Octav.png'}]
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
NEXT_PUBLIC_NAME_SERVICE_PROTOCOLS=["rns"]
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/account/api-key']
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=RBTC
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=RBTC
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Tenderly','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/rsk'}},{'title':'3xpl','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/3xpl.png','baseUrl':'https://3xpl.com/','paths':{'tx':'/rootstock/transaction','address':'/rootstock/address'}}]
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/rootstock-short.svg
NEXT_PUBLIC_NETWORK_ID=30
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/rootstock.svg
NEXT_PUBLIC_NETWORK_NAME=Rootstock
NEXT_PUBLIC_NETWORK_RPC_URL=https://public-node.rsk.co
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/rootstock-mainnet.png
NEXT_PUBLIC_PUZZLE_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/capyPuzzleBadge
NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-service
NEXT_PUBLIC_STATS_API_HOST=https://rootstock.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_USER_OPS_INDEXER_API_HOST=https://user-ops-indexer-rsk.k8s-prod-2.blockscout.com
NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['burnt_fees','total_reward','nonce']
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
+5 -1
View File
@@ -9,6 +9,11 @@ NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
NEXT_PUBLIC_ACCOUNT_DYNAMIC_ENVIRONMENT_ID=50fd93d4-3109-4914-af56-9e7bcb16c469
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_CONFIG={'swap': {'url': 'https://swapscout.vercel.app', 'chains': ['31', '11155111'], 'fee': '0.004', 'integrator': 'blockscout'}, 'revoke': {'chains': ['31', '11155111']}, 'multisend': {'chains': ['31', '11155111']}}
# Instance ENVs
NEXT_PUBLIC_ACCOUNT_API_KEYS_BUTTON=false
NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER=dynamic
@@ -30,7 +35,6 @@ NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgb(255, 145, 0)'],'text_color':['rgb(255, 255, 255)']}
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_MARKETPLACE_ENABLED=false
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_MIXPANEL_CONFIG_OVERRIDES={"record_sessions_percent": 0.5,"record_heatmap_data": true}
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
+2
View File
@@ -102,6 +102,7 @@
"DRPC",
"DTEND",
"duckduckgo",
"dynamicwaas",
"eamodio",
"Eigenda",
"Emelyanov",
@@ -173,6 +174,7 @@
"noves",
"numine",
"nums",
"octav",
"okex",
"okhttp",
"opblock",
+1
View File
@@ -13,4 +13,5 @@ export interface Result {
isReconnecting: boolean;
address: string | undefined;
openModal: () => void;
type?: 'dynamicwaas';
}
+4 -2
View File
@@ -13,7 +13,7 @@ export default function useWalletDynamic({ source, onConnect }: Params): Result
const [ isOpen ] = React.useState(false);
const [ isClientLoaded, setIsClientLoaded ] = React.useState(false);
const { setShowDynamicUserProfile, setShowAuthFlow, setAuthMode, removeWallet } = useDynamicContext();
const { setShowDynamicUserProfile, setShowAuthFlow, setAuthMode, removeWallet, primaryWallet } = useDynamicContext();
const openModal = React.useCallback(() => {
setShowDynamicUserProfile(true);
@@ -59,6 +59,7 @@ export default function useWalletDynamic({ source, onConnect }: Params): Result
const account = useAccountDynamic();
const address = account.address;
const isConnected = isClientLoaded && !account.isDisconnected && account.address !== undefined;
const isDynamicWaaS = primaryWallet?.key === 'dynamicwaas';
return React.useMemo(() => ({
connect: handleConnect,
@@ -68,5 +69,6 @@ export default function useWalletDynamic({ source, onConnect }: Params): Result
isReconnecting: false,
address,
openModal,
}), [ handleConnect, handleDisconnect, isOpen, isConnected, address, openModal ]);
type: isDynamicWaaS ? 'dynamicwaas' : undefined,
}), [ handleConnect, handleDisconnect, isOpen, isConnected, address, openModal, isDynamicWaaS ]);
}
@@ -0,0 +1,6 @@
import { nftHtmlEmbed } from './policies/nftHtmlEmbed';
import { makePolicyString } from './utils';
export default function generateNftHtmlEmbedCspPolicy(): string {
return makePolicyString(nftHtmlEmbed());
}
+12
View File
@@ -1,8 +1,12 @@
import type { NextRequest } from 'next/server';
import generateCspPolicy from './generateCspPolicy';
import generateNftHtmlEmbedCspPolicy from './generateNftHtmlEmbedCspPolicy';
const NFT_HTML_EMBED_PATH = '/nft-html-embed.html';
let cspPolicies: { 'private': string; 'default': string } | undefined = undefined;
let nftHtmlEmbedCsp: string | undefined = undefined;
async function initializeCspPolicies() {
if (!cspPolicies) {
@@ -26,5 +30,13 @@ export async function get(req?: NextRequest): Promise<string> {
const isPrivateMode = appProfile === 'private';
if (req?.nextUrl.pathname === NFT_HTML_EMBED_PATH && !isPrivateMode) {
if (!nftHtmlEmbedCsp) {
nftHtmlEmbedCsp = generateNftHtmlEmbedCspPolicy();
}
return nftHtmlEmbedCsp;
}
return isPrivateMode ? cspPolicies?.private || '' : cspPolicies?.default || '';
}
+65
View File
@@ -0,0 +1,65 @@
import type CspDev from 'csp-dev';
import { KEY_WORDS } from '../utils';
/**
* Content-Security-Policy used **only** for responses to `/nft-html-embed.html` (see `nextjs/csp/index.ts`).
*
* **What it is for**
* Interactive NFTs often ship as `data:text/html` or remote HTML with arbitrary inline and third-party
* scripts. The main explorer uses a strict `script-src` (hashed / `'self'` only). Any preview that
* inherits that policy—e.g. `srcdoc`, same-origin `blob:`, or framing `data:` under the main document—
* will block those scripts. We avoid widening CSP on the whole app by serving a tiny **first-party shell**
* on this URL. The shell listens for `postMessage` from the explorer and sets a **nested** iframe’s
* `src` to the NFT URL (commonly `data:text/html;base64,...`).
*
* **Why this policy is broad**
* The **untrusted** markup runs inside the **inner** iframe (see `public/nft-html-embed.html`), not in
* the shell’s inline script. The inner document still needs a permissive policy to run typical NFT
* animations (inline scripts, eval in some bundles, arbitrary `connect-src`, images, fonts, nested
* frames). This file’s CSP applies to the **shell document** and to what that document is allowed to
* embed; we keep `object-src` and `base-uri` tight where we can.
*
* **`frame-src` includes `data:`**
* In CSP, `frame-src *` does not cover `data:` URLs; those must be listed explicitly or framing fails.
*
* **`frame-ancestors 'self'`**
* Only our origin may frame this page, reducing drive-by embedding and tightening who can target the
* shell with `postMessage` (the shell also checks `event.origin` in script).
*/
export function nftHtmlEmbed(): CspDev.DirectiveDescriptor {
return {
'default-src': [ KEY_WORDS.NONE ],
'script-src': [
KEY_WORDS.UNSAFE_INLINE,
KEY_WORDS.UNSAFE_EVAL,
KEY_WORDS.SELF,
KEY_WORDS.DATA,
KEY_WORDS.BLOB,
'*',
],
'style-src': [
KEY_WORDS.UNSAFE_INLINE,
KEY_WORDS.SELF,
'*',
],
'img-src': [ KEY_WORDS.DATA, KEY_WORDS.BLOB, '*' ],
'font-src': [ KEY_WORDS.DATA, KEY_WORDS.SELF, '*' ],
'connect-src': [ '*' ],
'media-src': [ KEY_WORDS.BLOB, '*' ],
'object-src': [ KEY_WORDS.NONE ],
'base-uri': [ KEY_WORDS.NONE ],
'frame-ancestors': [ KEY_WORDS.SELF ],
'frame-src': [ '*', KEY_WORDS.DATA ],
};
}
+3 -3
View File
@@ -54,9 +54,9 @@
"@blockscout/visualizer-types": "0.2.0",
"@blockscout/zetachain-cctx-types": "^1.0.0-rc.6",
"@chakra-ui/react": "3.33.0",
"@dynamic-labs/ethereum": "4.67.2",
"@dynamic-labs/sdk-react-core": "4.67.2",
"@dynamic-labs/wagmi-connector": "4.67.2",
"@dynamic-labs/ethereum": "4.74.1",
"@dynamic-labs/sdk-react-core": "4.74.1",
"@dynamic-labs/wagmi-connector": "4.74.1",
"@emotion/react": "11.14.0",
"@graphiql/toolkit": "0.11.3",
"@growthbook/growthbook-react": "0.21.0",
+564 -226
View File
File diff suppressed because it is too large Load Diff
+51
View File
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="robots" content="noindex,nofollow">
<title></title>
<style>
html, body { margin: 0; height: 100%; overflow: hidden; }
</style>
</head>
<body>
<!--
Inner iframe: hosts API-provided NFT HTML (often data:text/html;base64,...).
sandbox="allow-scripts" only (no allow-same-origin):
- Untrusted NFT code runs here. Opaque origin prevents reading the explorer’s cookies,
localStorage, or authenticated same-origin fetches as the user’s session.
- We avoid pairing allow-scripts + allow-same-origin on this document: that combination would
let a same-origin framed page script remove or weaken its own sandbox via frameElement.
The outer iframe (NftHtml) uses allow-scripts allow-same-origin so this first-party shell can
participate in normal same-origin postMessage with the app; isolation for untrusted content is
enforced at this nested layer instead.
-->
<iframe width="100%" height="100%" frameborder="0" sandbox="allow-scripts"></iframe>
<script>
(function () {
'use strict';
window.addEventListener('message', function (event) {
if (event.origin !== window.location.origin) {
return;
}
var data = event.data;
if (!data || data.type !== 'nft-html-embed' || typeof data.src !== 'string') {
return;
}
const iframe = window.document.querySelector('iframe');
if (!iframe) {
return;
}
iframe.src = data.src;
iframe.onload = () => {
window.parent.postMessage({ type: 'nft-html-embed-loaded', src: data.src }, window.location.origin);
};
});
})();
</script>
</body>
</html>
@@ -23,7 +23,7 @@ const FormFieldSelectContent = <
const { field, fieldState, formState } = useController<FormFields, typeof name>({
control,
name,
rules,
rules: { ...rules, required: rest.required },
...controllerProps,
});
@@ -23,7 +23,7 @@ const FormFieldSelectAsyncContent = <
const { field, fieldState, formState } = useController<FormFields, typeof name>({
control,
name,
rules,
rules: { ...rules, required: rest.required },
...controllerProps,
});
+1
View File
@@ -25,6 +25,7 @@ const PRESETS = {
optimism_sepolia: 'https://optimism-sepolia.blockscout.com',
polygon: 'https://polygon.blockscout.com',
rari_testnet: 'https://rari-testnet.cloud.blockscout.com',
rootstock: 'https://rootstock.blockscout.com',
rootstock_testnet: 'https://rootstock-testnet.blockscout.com',
scroll_sepolia: 'https://scroll-sepolia.blockscout.com',
shibarium: 'https://www.shibariumscan.io',
@@ -1,12 +1,13 @@
import React from 'react';
import { encodeFunctionData, getAddress, type Abi } from 'viem';
import { useAccount, useWalletClient, useSwitchChain, usePublicClient } from 'wagmi';
import { getAddress, type Abi } from 'viem';
import { useAccount, useSwitchChain, useWriteContract, useSendTransaction } from 'wagmi';
import type { FormSubmitResult, SmartContractMethod } from './types';
import config from 'configs/app';
import { useMultichainContext } from 'lib/contexts/multichain';
import useRewardsActivity from 'lib/hooks/useRewardsActivity';
import useWallet from 'lib/web3/useWallet';
import { getNativeCoinValue } from './utils';
@@ -23,21 +24,19 @@ export default function useCallMethodWalletClient(): (params: Params) => Promise
const chainConfig = (multichainContext?.chain.app_config ?? config).chain;
const targetChainId = chainConfig?.id ? Number(chainConfig.id) : undefined;
const { data: walletClient } = useWalletClient({ chainId: targetChainId });
const publicClient = usePublicClient({ chainId: targetChainId });
const { isConnected, chainId, address: account } = useAccount();
const { switchChainAsync } = useSwitchChain();
const { trackTransaction, trackTransactionConfirm } = useRewardsActivity();
const { writeContractAsync } = useWriteContract();
const { sendTransactionAsync } = useSendTransaction();
const { type: walletType } = useWallet({ source: 'Smart contracts' });
return React.useCallback(async({ args, item, addressHash }) => {
if (!isConnected) {
throw new Error('Wallet is not connected');
}
if (!walletClient) {
throw new Error('Wallet Client is not defined');
}
if (chainId && targetChainId && chainId !== targetChainId) {
await switchChainAsync({ chainId: targetChainId });
}
@@ -56,18 +55,15 @@ export default function useCallMethodWalletClient(): (params: Params) => Promise
const data = typeof _args[0] === 'string' && _args[0].startsWith('0x') ? _args[0] as `0x${ string }` : undefined;
// seems like Dynamic WaaS (assigned when user signed up with email) does not estimate gas for transactions
// so we have to do it manually
const estimatedGas = feature.isEnabled && feature.connectorType === 'dynamic' ? await publicClient?.estimateGas({
account,
to: address,
value,
data: item.type === 'fallback' ? data : undefined,
}) : undefined;
// so we pass 0 as gas here
const estimatedGas = feature.isEnabled && feature.connectorType === 'dynamic' && walletType === 'dynamicwaas' ?
BigInt(0) : undefined;
const hash = await walletClient.sendTransaction({
const hash = await sendTransactionAsync({
to: address,
value,
gas: estimatedGas,
chainId: targetChainId,
...(data ? { data } : {}),
});
@@ -85,19 +81,11 @@ export default function useCallMethodWalletClient(): (params: Params) => Promise
}
// seems like Dynamic WaaS (assigned when user signed up with email) does not estimate gas for transactions
// so we have to do it manually
const estimatedGas = feature.isEnabled && feature.connectorType === 'dynamic' ? await publicClient?.estimateGas({
account,
to: address,
value,
data: encodeFunctionData({
abi: [ item ],
functionName: methodName,
args: _args,
}),
}) : undefined;
// so we pass 0 as gas here
const estimatedGas = feature.isEnabled && feature.connectorType === 'dynamic' && walletType === 'dynamicwaas' ?
BigInt(0) : undefined;
const hash = await walletClient.writeContract({
const hash = await writeContractAsync({
args: _args,
// Here we provide the ABI as an array containing only one item from the submitted form.
// This is a workaround for the issue with the "viem" library.
@@ -112,6 +100,7 @@ export default function useCallMethodWalletClient(): (params: Params) => Promise
value,
account,
gas: estimatedGas,
chainId: targetChainId,
});
if (activityResponse?.token) {
@@ -119,5 +108,16 @@ export default function useCallMethodWalletClient(): (params: Params) => Promise
}
return { source: 'wallet_client', data: { hash } };
}, [ isConnected, walletClient, chainId, targetChainId, trackTransaction, account, publicClient, switchChainAsync, trackTransactionConfirm ]);
}, [
isConnected,
chainId,
targetChainId,
trackTransaction,
account,
walletType,
writeContractAsync,
switchChainAsync,
sendTransactionAsync,
trackTransactionConfirm,
]);
}
+1 -1
View File
@@ -34,5 +34,5 @@ export const getIntervalValueFromQuery = (query: string | Array<string> | undefi
}
}
return INTERVAL_ITEMS[0].id;
return '1d';
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

+77 -16
View File
@@ -1,32 +1,52 @@
import { chakra } from '@chakra-ui/react';
import { Box, chakra } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import { LinkOverlay } from 'toolkit/chakra/link';
import type { MediaElementProps } from './utils';
interface Props extends MediaElementProps<'a'> {}
interface Props extends MediaElementProps<'div'> {
fullscreen?: boolean;
}
const NftHtml = ({ src, transport, onLoad, onError, onClick, ...rest }: Props) => {
const NFT_HTML_EMBED_PATH = '/nft-html-embed.html';
const NftHtml = ({ src, transport, onLoad, onError, onClick, fullscreen, ...rest }: Props) => {
const ref = React.useRef<HTMLIFrameElement>(null);
const [ isLoaded, setIsLoaded ] = React.useState(false);
const handleLoad = React.useCallback(() => {
const handleMesageFromFrame = React.useCallback((event: MessageEvent) => {
if (event.source !== ref.current?.contentWindow) {
return;
}
if (event.origin !== window.location.origin) {
return;
}
if (event.data?.type !== 'nft-html-embed-loaded' || event.data?.src !== src) {
return;
}
setIsLoaded(true);
onLoad?.();
}, [ onLoad ]);
}, [ onLoad, src ]);
const loadViaHttp = React.useCallback(async() => {
const handleFrameLoad = React.useCallback(() => {
ref.current?.contentWindow?.postMessage(
{ type: 'nft-html-embed', src }, window.location.origin,
);
}, [ src ]);
const loadViaHttp = React.useCallback(() => {
if (!ref.current) {
return;
}
ref.current.src = src;
ref.current.onload = handleLoad;
window.addEventListener('message', handleMesageFromFrame);
ref.current.onload = handleFrameLoad;
ref.current.src = `${ window.location.origin }${ NFT_HTML_EMBED_PATH }`;
onError && (ref.current.onerror = onError);
}, [ src, handleLoad, onError ]);
}, [ handleFrameLoad, handleMesageFromFrame, onError ]);
React.useEffect(() => {
// Disable iframe in private mode to prevent tracking
@@ -41,11 +61,24 @@ const NftHtml = ({ src, transport, onLoad, onError, onClick, ...rest }: Props) =
onError?.();
break;
}
case 'http':
case 'http': {
loadViaHttp();
break;
}
}
}, [ loadViaHttp, onError, transport ]);
const iframe = ref.current;
return () => {
if (transport === 'http') {
window.removeEventListener('message', handleMesageFromFrame);
if (iframe) {
iframe.onload = null;
iframe.onerror = null;
}
}
};
}, [ handleMesageFromFrame, loadViaHttp, onError, transport ]);
// Disable iframe in private mode to prevent tracking
if (config.app.isPrivateMode) {
@@ -53,19 +86,47 @@ const NftHtml = ({ src, transport, onLoad, onError, onClick, ...rest }: Props) =
}
return (
<LinkOverlay
onClick={ onClick }
<Box
h="100%"
w="100%"
position="relative"
onClick={ onClick }
_before={ !fullscreen ? {
content: '""',
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
zIndex: 1,
} : undefined }
{ ...rest }
>
{ /*
Sandbox split (see also `public/nft-html-embed.html`):
- `allow-scripts` — interactive NFT previews require JavaScript.
- `allow-same-origin` — the **direct** document is our trusted `/nft-html-embed.html` shell
(first-party). Same origin makes `postMessage` report a real tuple `origin` to the parent
(opaque origins serialize as the string `"null"`), and matches how we lock the shell to
`event.origin === window.location.origin`. MDN warns against `allow-scripts` +
`allow-same-origin` when the framed document is **untrusted**, because script could reach
`frameElement` and strip `sandbox`. We only use both here because the framed **top** document
is that small shell; API-supplied HTML loads in a **nested** iframe with `allow-scripts` only
(no `allow-same-origin`), so the NFT stays on an opaque origin and cannot read site cookies.
We omit e.g. `allow-top-navigation`, `allow-popups`, and `allow-forms` by default to limit what
even the shell could do if it were ever compromised.
*/ }
<chakra.iframe
ref={ ref }
h="100%"
w="100%"
sandbox="allow-scripts"
sandbox="allow-scripts allow-same-origin"
opacity={ isLoaded ? 1 : 0 }
/>
</LinkOverlay>
</Box>
);
};
+3 -1
View File
@@ -60,6 +60,7 @@ const NftMediaFullscreenModal = ({ open, onOpenChange, data, allowedTypes, field
onError={ handleMediaLoadError }
w="90vw"
h="90vh"
fullscreen
/>
);
case 'image':
@@ -71,7 +72,8 @@ const NftMediaFullscreenModal = ({ open, onOpenChange, data, allowedTypes, field
onError={ handleMediaLoadError }
maxW="90vw"
maxH="90vh"
objectFit="contain"/>
objectFit="contain"
/>
);
default:
return null;