mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
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:
@@ -31,6 +31,7 @@ on:
|
||||
- optimism_sepolia
|
||||
- polygon
|
||||
- rootstock
|
||||
- rootstock_testnet
|
||||
- scroll_sepolia
|
||||
- shibarium
|
||||
- stability
|
||||
|
||||
@@ -34,6 +34,7 @@ on:
|
||||
- polygon
|
||||
- rari_testnet
|
||||
- rootstock
|
||||
- rootstock_testnet
|
||||
- shibarium
|
||||
- scroll_sepolia
|
||||
- stability
|
||||
|
||||
Vendored
+1
@@ -362,6 +362,7 @@
|
||||
"optimism_sepolia",
|
||||
"polygon",
|
||||
"rari_testnet",
|
||||
"rootstock",
|
||||
"rootstock_testnet",
|
||||
"scroll_sepolia",
|
||||
"shibarium",
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -102,6 +102,7 @@
|
||||
"DRPC",
|
||||
"DTEND",
|
||||
"duckduckgo",
|
||||
"dynamicwaas",
|
||||
"eamodio",
|
||||
"Eigenda",
|
||||
"Emelyanov",
|
||||
@@ -173,6 +174,7 @@
|
||||
"noves",
|
||||
"numine",
|
||||
"nums",
|
||||
"octav",
|
||||
"okex",
|
||||
"okhttp",
|
||||
"opblock",
|
||||
|
||||
@@ -13,4 +13,5 @@ export interface Result {
|
||||
isReconnecting: boolean;
|
||||
address: string | undefined;
|
||||
openModal: () => void;
|
||||
type?: 'dynamicwaas';
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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 || '';
|
||||
}
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Generated
+564
-226
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user