Add support for Diamond proxy standard (#1963)
* add support for array of implementations * refactor ContainerWithScrollY component * contract source code: allow to choose between all implementations * fix layout of AddressNetWorth component * add scroll container to implementation address on write/read proxy contract tab * update screenshots
@@ -56,6 +56,7 @@ NEXT_PUBLIC_MARKETPLACE_FEATURED_APP=gearbox-protocol
|
||||
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
|
||||
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'uniswap'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'},{'text':'Get gas','icon':'gas','dappId':'smol-refuel'}]
|
||||
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_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'}]
|
||||
|
||||
#meta
|
||||
NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth.jpg?raw=true
|
||||
|
||||
@@ -8,7 +8,7 @@ export const hash = '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859';
|
||||
|
||||
export const withName: AddressParam = {
|
||||
hash: hash,
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: null,
|
||||
name: 'ArianeeStore',
|
||||
@@ -20,7 +20,7 @@ export const withName: AddressParam = {
|
||||
|
||||
export const withEns: AddressParam = {
|
||||
hash: hash,
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: null,
|
||||
name: 'ArianeeStore',
|
||||
@@ -32,7 +32,7 @@ export const withEns: AddressParam = {
|
||||
|
||||
export const withNameTag: AddressParam = {
|
||||
hash: hash,
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: null,
|
||||
name: 'ArianeeStore',
|
||||
@@ -50,7 +50,7 @@ export const withNameTag: AddressParam = {
|
||||
|
||||
export const withoutName: AddressParam = {
|
||||
hash: hash,
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: null,
|
||||
name: null,
|
||||
@@ -62,7 +62,7 @@ export const withoutName: AddressParam = {
|
||||
|
||||
export const token: Address = {
|
||||
hash: hash,
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: true,
|
||||
is_verified: false,
|
||||
name: null,
|
||||
@@ -76,7 +76,6 @@ export const token: Address = {
|
||||
creation_tx_hash: '0xc38cf7377bf72d6436f63c37b01b24d032101f20ec1849286dc703c712f10c98',
|
||||
creator_address_hash: '0x34A9c688512ebdB575e82C50c9803F6ba2916E72',
|
||||
exchange_rate: '0.04311',
|
||||
implementation_address: null,
|
||||
has_decompiled_code: false,
|
||||
has_logs: false,
|
||||
has_token_transfers: true,
|
||||
@@ -97,8 +96,9 @@ export const contract: Address = {
|
||||
has_tokens: false,
|
||||
has_validated_blocks: false,
|
||||
hash: hash,
|
||||
implementation_address: '0x2F4F4A52295940C576417d29F22EEb92B440eC89',
|
||||
implementation_name: 'HomeBridge',
|
||||
implementations: [
|
||||
{ address: '0x2F4F4A52295940C576417d29F22EEb92B440eC89', name: 'HomeBridge' },
|
||||
],
|
||||
is_contract: true,
|
||||
is_verified: true,
|
||||
name: 'EternalStorageProxy',
|
||||
@@ -122,8 +122,7 @@ export const validator: Address = {
|
||||
has_tokens: false,
|
||||
has_validated_blocks: true,
|
||||
hash: hash,
|
||||
implementation_address: null,
|
||||
implementation_name: null,
|
||||
implementations: [],
|
||||
is_contract: false,
|
||||
is_verified: false,
|
||||
name: 'Kiryl Ihnatsyeu',
|
||||
|
||||
@@ -15,7 +15,7 @@ export const base: Block = {
|
||||
height: 30146364,
|
||||
miner: {
|
||||
hash: '0xdAd49e6CbDE849353ab27DeC6319E687BFc91A41',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: null,
|
||||
name: 'Alex Emelyanov',
|
||||
@@ -65,7 +65,7 @@ export const genesis: Block = {
|
||||
height: 0,
|
||||
miner: {
|
||||
hash: '0x0000000000000000000000000000000000000000',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: null,
|
||||
name: null,
|
||||
@@ -94,7 +94,7 @@ export const base2: Block = {
|
||||
size: 592,
|
||||
miner: {
|
||||
hash: '0xDfE10D55d9248B2ED66f1647df0b0A46dEb25165',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: null,
|
||||
name: 'Kiryl Ihnatsyeu',
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { VerifiedContract, VerifiedContractsResponse } from 'types/api/cont
|
||||
export const contract1: VerifiedContract = {
|
||||
address: {
|
||||
hash: '0xef490030ac0d53B70E304b6Bc5bF657dc6780bEB',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: true,
|
||||
is_verified: null,
|
||||
name: 'MockERC20',
|
||||
@@ -26,7 +26,7 @@ export const contract1: VerifiedContract = {
|
||||
export const contract2: VerifiedContract = {
|
||||
address: {
|
||||
hash: '0xB2218bdEbe8e90f80D04286772B0968ead666942',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: true,
|
||||
is_verified: null,
|
||||
name: 'EternalStorageProxyWithSomeExternalLibrariesAndEvenMore',
|
||||
|
||||
@@ -6,7 +6,7 @@ export const data: OptimisticL2WithdrawalsResponse = {
|
||||
challenge_period_end: null,
|
||||
from: {
|
||||
hash: '0x67aab90c548b284be30b05c376001b4db90b87ba',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: false,
|
||||
name: null,
|
||||
|
||||
@@ -8,7 +8,7 @@ export const data: ShibariumDepositsResponse = {
|
||||
l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e',
|
||||
user: {
|
||||
hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: false,
|
||||
name: null,
|
||||
@@ -25,7 +25,7 @@ export const data: ShibariumDepositsResponse = {
|
||||
l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e',
|
||||
user: {
|
||||
hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: false,
|
||||
name: null,
|
||||
@@ -42,7 +42,7 @@ export const data: ShibariumDepositsResponse = {
|
||||
l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e',
|
||||
user: {
|
||||
hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: false,
|
||||
name: null,
|
||||
|
||||
@@ -8,7 +8,7 @@ export const data: ShibariumWithdrawalsResponse = {
|
||||
l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e',
|
||||
user: {
|
||||
hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: false,
|
||||
name: null,
|
||||
@@ -25,7 +25,7 @@ export const data: ShibariumWithdrawalsResponse = {
|
||||
l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e',
|
||||
user: {
|
||||
hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: false,
|
||||
name: null,
|
||||
@@ -42,7 +42,7 @@ export const data: ShibariumWithdrawalsResponse = {
|
||||
l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e',
|
||||
user: {
|
||||
hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: false,
|
||||
name: null,
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TokenTransfer, TokenTransferResponse } from 'types/api/tokenTransf
|
||||
export const erc20: TokenTransfer = {
|
||||
from: {
|
||||
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: true,
|
||||
is_verified: true,
|
||||
name: 'ArianeeStore',
|
||||
@@ -14,7 +14,7 @@ export const erc20: TokenTransfer = {
|
||||
},
|
||||
to: {
|
||||
hash: '0x7d20a8D54F955b4483A66aB335635ab66e151c51',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: true,
|
||||
is_verified: false,
|
||||
name: null,
|
||||
@@ -50,7 +50,7 @@ export const erc20: TokenTransfer = {
|
||||
export const erc721: TokenTransfer = {
|
||||
from: {
|
||||
hash: '0x621C2a125ec4A6D8A7C7A655A18a2868d35eb43C',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: false,
|
||||
name: null,
|
||||
@@ -61,7 +61,7 @@ export const erc721: TokenTransfer = {
|
||||
},
|
||||
to: {
|
||||
hash: '0x47eE48AEBc4ab9Ed908b805b8c8dAAa71B31Db1A',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: false,
|
||||
name: null,
|
||||
@@ -96,7 +96,7 @@ export const erc721: TokenTransfer = {
|
||||
export const erc1155A: TokenTransfer = {
|
||||
from: {
|
||||
hash: '0x0000000000000000000000000000000000000000',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: false,
|
||||
name: null,
|
||||
@@ -107,7 +107,7 @@ export const erc1155A: TokenTransfer = {
|
||||
},
|
||||
to: {
|
||||
hash: '0xBb36c792B9B45Aaf8b848A1392B0d6559202729E',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: false,
|
||||
name: null,
|
||||
@@ -173,7 +173,7 @@ export const erc1155D: TokenTransfer = {
|
||||
export const erc404A: TokenTransfer = {
|
||||
from: {
|
||||
hash: '0x0000000000000000000000000000000000000000',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: false,
|
||||
name: null,
|
||||
@@ -184,7 +184,7 @@ export const erc404A: TokenTransfer = {
|
||||
},
|
||||
to: {
|
||||
hash: '0xBb36c792B9B45Aaf8b848A1392B0d6559202729E',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: false,
|
||||
name: null,
|
||||
|
||||
@@ -6,7 +6,7 @@ export const base: InternalTransaction = {
|
||||
error: null,
|
||||
from: {
|
||||
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: true,
|
||||
is_verified: true,
|
||||
name: 'ArianeeStore',
|
||||
@@ -21,7 +21,7 @@ export const base: InternalTransaction = {
|
||||
timestamp: '2022-10-10T14:43:05.000000Z',
|
||||
to: {
|
||||
hash: '0x502a9C8af2441a1E276909405119FaE21F3dC421',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: true,
|
||||
is_verified: true,
|
||||
name: 'ArianeeCreditHistory',
|
||||
@@ -56,7 +56,7 @@ export const withContractCreated: InternalTransaction = {
|
||||
},
|
||||
created_contract: {
|
||||
hash: '0xdda21946FF3FAa027104b15BE6970CA756439F5a',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: true,
|
||||
is_verified: null,
|
||||
name: 'Shavuha token',
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TxStateChange, TxStateChanges } from 'types/api/txStateChanges';
|
||||
export const mintToken: TxStateChange = {
|
||||
address: {
|
||||
hash: '0x0000000000000000000000000000000000000000',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: false,
|
||||
name: null,
|
||||
@@ -41,7 +41,7 @@ export const mintToken: TxStateChange = {
|
||||
export const receiveMintedToken: TxStateChange = {
|
||||
address: {
|
||||
hash: '0xC8F71D0ae51AfBdB009E2eC1Ea8CC9Ee204A42B5',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: false,
|
||||
name: null,
|
||||
@@ -79,7 +79,7 @@ export const receiveMintedToken: TxStateChange = {
|
||||
export const transfer1155Token: TxStateChange = {
|
||||
address: {
|
||||
hash: '0x51243E83Db20F8FC2761D894067A2A9eb7B158DE',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: false,
|
||||
name: null,
|
||||
@@ -111,7 +111,7 @@ export const transfer1155Token: TxStateChange = {
|
||||
export const receiveCoin: TxStateChange = {
|
||||
address: {
|
||||
hash: '0x8dC847Af872947Ac18d5d63fA646EB65d4D99560',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: null,
|
||||
name: null,
|
||||
@@ -131,7 +131,7 @@ export const receiveCoin: TxStateChange = {
|
||||
export const sendCoin: TxStateChange = {
|
||||
address: {
|
||||
hash: '0xC8F71D0ae51AfBdB009E2eC1Ea8CC9Ee204A42B5',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: null,
|
||||
name: null,
|
||||
@@ -148,11 +148,11 @@ export const sendCoin: TxStateChange = {
|
||||
type: 'coin' as const,
|
||||
};
|
||||
|
||||
export const sendERC20Token = {
|
||||
export const sendERC20Token: TxStateChange = {
|
||||
address: {
|
||||
hash: '0x7f6479df95Aa3036a3BE02DB6300ea201ABd9981',
|
||||
ens_domain_name: null,
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: false,
|
||||
name: null,
|
||||
@@ -175,7 +175,6 @@ export const sendERC20Token = {
|
||||
symbol: 'USDT',
|
||||
total_supply: '39030615894320966',
|
||||
type: 'ERC-20' as const,
|
||||
token_id: null,
|
||||
},
|
||||
type: 'token' as const,
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ export const base: Transaction = {
|
||||
},
|
||||
from: {
|
||||
hash: '0x047A81aFB05D9B1f8844bf60fcA05DCCFbC584B9',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
name: null,
|
||||
is_verified: null,
|
||||
@@ -48,7 +48,7 @@ export const base: Transaction = {
|
||||
timestamp: '2022-10-10T14:34:30.000000Z',
|
||||
to: {
|
||||
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: true,
|
||||
name: null,
|
||||
@@ -92,7 +92,7 @@ export const withContractCreation: Transaction = {
|
||||
to: null,
|
||||
created_contract: {
|
||||
hash: '0xdda21946FF3FAa027104b15BE6970CA756439F5a',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: true,
|
||||
is_verified: null,
|
||||
name: 'Shavuha token',
|
||||
@@ -111,7 +111,7 @@ export const withTokenTransfer: Transaction = {
|
||||
hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3196',
|
||||
to: {
|
||||
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: true,
|
||||
is_verified: true,
|
||||
name: 'ArianeeStore',
|
||||
@@ -167,7 +167,7 @@ export const withRawRevertReason: Transaction = {
|
||||
},
|
||||
to: {
|
||||
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_verified: true,
|
||||
is_contract: true,
|
||||
name: 'Bad guy',
|
||||
@@ -283,7 +283,7 @@ export const stabilityTx: Transaction = {
|
||||
stability_fee: {
|
||||
dapp_address: {
|
||||
hash: '0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: null,
|
||||
name: null,
|
||||
@@ -308,7 +308,7 @@ export const stabilityTx: Transaction = {
|
||||
total_fee: '68762500000000',
|
||||
validator_address: {
|
||||
hash: '0x1432997a4058acbBe562F3c1E79738c142039044',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: null,
|
||||
name: null,
|
||||
|
||||
@@ -26,7 +26,7 @@ export const txInterpretation: TxInterpretationResponse = {
|
||||
type: 'address',
|
||||
value: {
|
||||
hash: '0x48c04ed5691981C42154C6167398f95e8f38a7fF',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: false,
|
||||
name: null,
|
||||
|
||||
@@ -25,7 +25,7 @@ export const userOpData: UserOp = {
|
||||
sender: {
|
||||
ens_domain_name: null,
|
||||
hash: '0xF0C14FF4404b188fAA39a3507B388998c10FE284',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: true,
|
||||
is_verified: null,
|
||||
name: null,
|
||||
@@ -34,7 +34,7 @@ export const userOpData: UserOp = {
|
||||
entry_point: {
|
||||
ens_domain_name: null,
|
||||
hash: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: true,
|
||||
is_verified: null,
|
||||
name: null,
|
||||
@@ -59,7 +59,7 @@ export const userOpData: UserOp = {
|
||||
bundler: {
|
||||
ens_domain_name: null,
|
||||
hash: '0xd53Eb5203e367BbDD4f72338938224881Fc501Ab',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: null,
|
||||
name: null,
|
||||
@@ -95,7 +95,7 @@ export const userOpData: UserOp = {
|
||||
paymaster: {
|
||||
ens_domain_name: null,
|
||||
hash: '0x7ceA357B5AC0639F89F9e378a1f03Aa5005C0a25',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: true,
|
||||
is_verified: null,
|
||||
name: null,
|
||||
|
||||
@@ -6,7 +6,7 @@ export const userOpsData: UserOpsResponse = {
|
||||
address: {
|
||||
ens_domain_name: null,
|
||||
hash: '0xF0C14FF4404b188fAA39a3507B388998c10FE284',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: true,
|
||||
is_verified: null,
|
||||
name: null,
|
||||
@@ -22,7 +22,7 @@ export const userOpsData: UserOpsResponse = {
|
||||
address:
|
||||
{ ens_domain_name: null,
|
||||
hash: '0x2c298CcaFFD1549e1C21F46966A6c236fCC66dB2',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: true,
|
||||
is_verified: null,
|
||||
name: null,
|
||||
@@ -38,7 +38,7 @@ export const userOpsData: UserOpsResponse = {
|
||||
address: {
|
||||
ens_domain_name: null,
|
||||
hash: '0x2c298CcaFFD1549e1C21F46966A6c236fCC66dB2',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: true,
|
||||
is_verified: null,
|
||||
name: null,
|
||||
|
||||
@@ -9,7 +9,7 @@ export const data: WithdrawalsResponse = {
|
||||
index: 11688,
|
||||
receiver: {
|
||||
hash: '0xf97e180c050e5Ab072211Ad2C213Eb5AEE4DF134',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: null,
|
||||
name: null,
|
||||
@@ -23,7 +23,7 @@ export const data: WithdrawalsResponse = {
|
||||
index: 11687,
|
||||
receiver: {
|
||||
hash: '0xf97e987c050e5Ab072211Ad2C213Eb5AEE4DF134',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: null,
|
||||
name: null,
|
||||
@@ -37,7 +37,7 @@ export const data: WithdrawalsResponse = {
|
||||
index: 11686,
|
||||
receiver: {
|
||||
hash: '0xf97e123c050e5Ab072211Ad2C213Eb5AEE4DF134',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: null,
|
||||
name: null,
|
||||
|
||||
@@ -25,8 +25,7 @@ export const ADDRESS_INFO: Address = {
|
||||
has_tokens: false,
|
||||
has_validated_blocks: false,
|
||||
hash: ADDRESS_HASH,
|
||||
implementation_address: null,
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: true,
|
||||
is_verified: true,
|
||||
name: 'ChainLink Token (goerli)',
|
||||
@@ -59,7 +58,7 @@ export const TOP_ADDRESS: AddressesItem = {
|
||||
coin_balance: '11886682377162664596540805',
|
||||
tx_count: '1835',
|
||||
hash: '0x4f7A67464B5976d7547c860109e4432d50AfB38e',
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: null,
|
||||
name: null,
|
||||
|
||||
@@ -4,7 +4,7 @@ export const ADDRESS_HASH = '0x2B51Ae4412F79c3c1cB12AA40Ea4ECEb4e80511a';
|
||||
|
||||
export const ADDRESS_PARAMS: AddressParam = {
|
||||
hash: ADDRESS_HASH,
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: null,
|
||||
name: null,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Transaction } from 'types/api/transaction';
|
||||
|
||||
import type { UserTags } from './addressParams';
|
||||
import type { UserTags, AddressImplementation } from './addressParams';
|
||||
import type { Block } from './block';
|
||||
import type { InternalTransaction } from './internalTransaction';
|
||||
import type { NFTTokenType, TokenInfo, TokenInstance, TokenType } from './token';
|
||||
@@ -21,8 +21,7 @@ export interface Address extends UserTags {
|
||||
has_tokens: boolean;
|
||||
has_validated_blocks: boolean;
|
||||
hash: string;
|
||||
implementation_address: string | null;
|
||||
implementation_name: string | null;
|
||||
implementations: Array<AddressImplementation> | null;
|
||||
is_contract: boolean;
|
||||
is_verified: boolean;
|
||||
name: string | null;
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import type { AddressMetadataTagApi } from './addressMetadata';
|
||||
|
||||
export interface AddressImplementation {
|
||||
address: string;
|
||||
name: string | null;
|
||||
}
|
||||
|
||||
export interface AddressTag {
|
||||
label: string;
|
||||
display_name: string;
|
||||
@@ -19,7 +24,9 @@ export interface UserTags {
|
||||
|
||||
export type AddressParamBasic = {
|
||||
hash: string;
|
||||
implementation_name: string | null;
|
||||
// API doesn't return hash in this model yet
|
||||
// will be fixed in the future releases
|
||||
implementations: Array<Omit<AddressImplementation, 'address'>> | null;
|
||||
name: string | null;
|
||||
is_contract: boolean;
|
||||
is_verified: boolean | null;
|
||||
|
||||
@@ -17,6 +17,7 @@ import BlockEntity from 'ui/shared/entities/block/BlockEntity';
|
||||
import TxEntity from 'ui/shared/entities/tx/TxEntity';
|
||||
|
||||
import AddressBalance from './details/AddressBalance';
|
||||
import AddressImplementations from './details/AddressImplementations';
|
||||
import AddressNameInfo from './details/AddressNameInfo';
|
||||
import AddressNetWorth from './details/AddressNetWorth';
|
||||
import TokenSelect from './tokenSelect/TokenSelect';
|
||||
@@ -48,8 +49,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
|
||||
const error404Data = React.useMemo(() => ({
|
||||
hash: addressHash || '',
|
||||
is_contract: false,
|
||||
implementation_name: null,
|
||||
implementation_address: null,
|
||||
implementations: null,
|
||||
token: null,
|
||||
watchlist_address_id: null,
|
||||
watchlist_names: null,
|
||||
@@ -112,22 +112,11 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
|
||||
</DetailsInfoItem.Value>
|
||||
</>
|
||||
) }
|
||||
|
||||
{ data.is_contract && data.implementation_address && (
|
||||
<>
|
||||
<DetailsInfoItem.Label
|
||||
hint="Implementation address of the proxy contract"
|
||||
>
|
||||
Implementation
|
||||
</DetailsInfoItem.Label>
|
||||
<DetailsInfoItem.Value>
|
||||
<AddressEntity
|
||||
address={{ hash: data.implementation_address, name: data.implementation_name, is_contract: true }}
|
||||
isLoading={ addressQuery.isPlaceholderData }
|
||||
noIcon
|
||||
/>
|
||||
</DetailsInfoItem.Value>
|
||||
</>
|
||||
{ data.is_contract && data.implementations && data.implementations?.length > 0 && (
|
||||
<AddressImplementations
|
||||
data={ data.implementations }
|
||||
isLoading={ addressQuery.isPlaceholderData }
|
||||
/>
|
||||
) }
|
||||
|
||||
<AddressBalance data={ data } isLoading={ addressQuery.isPlaceholderData }/>
|
||||
@@ -152,7 +141,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
|
||||
>
|
||||
Net worth
|
||||
</DetailsInfoItem.Label>
|
||||
<DetailsInfoItem.Value alignSelf="center">
|
||||
<DetailsInfoItem.Value alignSelf="center" py={ 0 }>
|
||||
<AddressNetWorth addressData={ addressQuery.data } addressHash={ addressHash } isLoading={ addressQuery.isPlaceholderData }/>
|
||||
</DetailsInfoItem.Value>
|
||||
</>
|
||||
|
||||
@@ -27,7 +27,7 @@ test.beforeEach(async({ mockApiResponse }) => {
|
||||
|
||||
test('full view +@mobile +@dark-mode', async({ render, mockApiResponse, createSocket }) => {
|
||||
await mockApiResponse('contract', contractMock.withChangedByteCode, { pathParams: { hash: addressMock.contract.hash } });
|
||||
await mockApiResponse('contract', contractMock.withChangedByteCode, { pathParams: { hash: addressMock.contract.implementation_address as string } });
|
||||
await mockApiResponse('contract', contractMock.withChangedByteCode, { pathParams: { hash: addressMock.contract.implementations?.[0].address as string } });
|
||||
|
||||
const component = await render(<ContractCode/>, { hooksConfig }, { withSocket: true });
|
||||
await createSocket();
|
||||
|
||||
@@ -224,7 +224,7 @@ const ContractCode = ({ addressHash, contractQuery, channel }: Props) => {
|
||||
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap">
|
||||
<span>Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB </span>
|
||||
<AddressEntity
|
||||
address={{ hash: data.verified_twin_address_hash, is_contract: true, implementation_name: null }}
|
||||
address={{ hash: data.verified_twin_address_hash, is_contract: true }}
|
||||
truncation="constant"
|
||||
fontSize="sm"
|
||||
fontWeight="500"
|
||||
@@ -240,7 +240,7 @@ const ContractCode = ({ addressHash, contractQuery, channel }: Props) => {
|
||||
<Alert status="warning" flexWrap="wrap" whiteSpace="pre-wrap">
|
||||
<span>Minimal Proxy Contract for </span>
|
||||
<AddressEntity
|
||||
address={{ hash: data.minimal_proxy_address_hash, is_contract: true, implementation_name: null }}
|
||||
address={{ hash: data.minimal_proxy_address_hash, is_contract: true }}
|
||||
truncation="constant"
|
||||
fontSize="sm"
|
||||
fontWeight="500"
|
||||
@@ -291,10 +291,10 @@ const ContractCode = ({ addressHash, contractQuery, channel }: Props) => {
|
||||
isLoading={ isPlaceholderData }
|
||||
/>
|
||||
) }
|
||||
{ data?.source_code && (
|
||||
{ data?.source_code && addressHash && (
|
||||
<ContractSourceCode
|
||||
address={ addressHash }
|
||||
implementationAddress={ addressInfo?.implementation_address ?? undefined }
|
||||
implementations={ addressInfo?.implementations || undefined }
|
||||
/>
|
||||
) }
|
||||
{ data?.compiler_settings ? (
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
import { Flex, Button, chakra, Popover, PopoverTrigger, PopoverBody, PopoverContent, useDisclosure, Image, useColorModeValue } from '@chakra-ui/react';
|
||||
import {
|
||||
Flex,
|
||||
Button,
|
||||
chakra,
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverBody,
|
||||
PopoverContent,
|
||||
Image,
|
||||
Skeleton,
|
||||
useDisclosure,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
@@ -8,9 +20,10 @@ import LinkExternal from 'ui/shared/links/LinkExternal';
|
||||
interface Props {
|
||||
className?: string;
|
||||
hash: string;
|
||||
isLoading?: string;
|
||||
}
|
||||
|
||||
const ContractCodeIde = ({ className, hash }: Props) => {
|
||||
const ContractCodeIde = ({ className, hash, isLoading }: Props) => {
|
||||
const { isOpen, onToggle, onClose } = useDisclosure();
|
||||
const defaultIconColor = useColorModeValue('gray.600', 'gray.500');
|
||||
|
||||
@@ -31,6 +44,10 @@ const ContractCodeIde = ({ className, hash }: Props) => {
|
||||
});
|
||||
}, [ defaultIconColor, hash ]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Skeleton h={ 8 } w="92px" borderRadius="base"/>;
|
||||
}
|
||||
|
||||
if (ideLinks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
PopoverBody,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Skeleton,
|
||||
StackDivider,
|
||||
useDisclosure,
|
||||
VStack,
|
||||
@@ -27,6 +28,7 @@ import IconSvg from 'ui/shared/IconSvg';
|
||||
interface Props {
|
||||
className?: string;
|
||||
data: Array<SmartContractExternalLibrary>;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const Item = (data: SmartContractExternalLibrary) => {
|
||||
@@ -34,7 +36,7 @@ const Item = (data: SmartContractExternalLibrary) => {
|
||||
<Flex flexDir="column" py={ 2 } w="100%" rowGap={ 1 }>
|
||||
<Box>{ data.name }</Box>
|
||||
<AddressEntity
|
||||
address={{ hash: data.address_hash, is_contract: true, implementation_name: null }}
|
||||
address={{ hash: data.address_hash, is_contract: true }}
|
||||
query={{ tab: 'contract' }}
|
||||
fontSize="sm"
|
||||
fontWeight="500"
|
||||
@@ -44,10 +46,14 @@ const Item = (data: SmartContractExternalLibrary) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ContractExternalLibraries = ({ className, data }: Props) => {
|
||||
const ContractExternalLibraries = ({ className, data, isLoading }: Props) => {
|
||||
const { isOpen, onToggle, onClose } = useDisclosure();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
if (isLoading) {
|
||||
return <Skeleton h={ 8 } w="150px" borderRadius="base"/>;
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -80,6 +86,8 @@ const ContractExternalLibraries = ({ className, data }: Props) => {
|
||||
divider={ <StackDivider borderColor="divider"/> }
|
||||
spacing={ 2 }
|
||||
mt={ 4 }
|
||||
maxH={{ lg: '50vh' }}
|
||||
overflowY="scroll"
|
||||
>
|
||||
{ data.map((item) => <Item key={ item.address_hash } { ...item }/>) }
|
||||
</VStack>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
|
||||
import type { Address as TAddress } from 'types/api/address';
|
||||
|
||||
import { getResourceKey } from 'lib/api/useApiQuery';
|
||||
import ContainerWithScrollY from 'ui/shared/ContainerWithScrollY';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
|
||||
interface Props {
|
||||
@@ -17,14 +18,31 @@ const ContractImplementationAddress = ({ hash }: Props) => {
|
||||
pathParams: { hash },
|
||||
}));
|
||||
|
||||
if (!data?.implementation_address) {
|
||||
if (!data?.implementations || data.implementations.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = data.implementations.length > 1 ? 'Implementation addresses:' : 'Implementation address:';
|
||||
|
||||
return (
|
||||
<Flex mb={ 6 } flexWrap="wrap" columnGap={ 2 }>
|
||||
<span>Implementation address:</span>
|
||||
<AddressEntity address={{ hash: data.implementation_address, is_contract: true }} noIcon noCopy/>
|
||||
<Flex mb={ 6 } flexWrap="wrap" columnGap={ 2 } rowGap={ 2 }>
|
||||
<span>{ label }</span>
|
||||
<Box position="relative" maxW="100%">
|
||||
<ContainerWithScrollY
|
||||
gradientHeight={ 24 }
|
||||
rowGap={ 2 }
|
||||
maxH="150px"
|
||||
>
|
||||
{ data.implementations.map((item) => (
|
||||
<AddressEntity
|
||||
key={ item.address }
|
||||
address={{ hash: item.address, is_contract: true }}
|
||||
noIcon
|
||||
noCopy
|
||||
/>
|
||||
)) }
|
||||
</ContainerWithScrollY>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,8 +11,6 @@ import LinkExternal from 'ui/shared/links/LinkExternal';
|
||||
|
||||
import ContractSubmitAuditForm from './contractSubmitAuditForm/ContractSubmitAuditForm';
|
||||
|
||||
const SCROLL_GRADIENT_HEIGHT = 24;
|
||||
|
||||
type Props = {
|
||||
addressHash?: string;
|
||||
}
|
||||
@@ -27,17 +25,6 @@ const ContractSecurityAudits = ({ addressHash }: Props) => {
|
||||
},
|
||||
});
|
||||
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const [ hasScroll, setHasScroll ] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setHasScroll(containerRef.current.scrollHeight >= containerRef.current.clientHeight + SCROLL_GRADIENT_HEIGHT / 2);
|
||||
}, []);
|
||||
|
||||
const formTitle = 'Submit audit';
|
||||
|
||||
const modalProps = useDisclosure();
|
||||
@@ -52,12 +39,10 @@ const ContractSecurityAudits = ({ addressHash }: Props) => {
|
||||
{ data?.items && data.items.length > 0 && (
|
||||
<Box position="relative">
|
||||
<ContainerWithScrollY
|
||||
gradientHeight={ SCROLL_GRADIENT_HEIGHT }
|
||||
hasScroll={ hasScroll }
|
||||
gradientHeight={ 24 }
|
||||
rowGap={ 1 }
|
||||
w="100%"
|
||||
maxH="80px"
|
||||
ref={ containerRef }
|
||||
mt={ 2 }
|
||||
>
|
||||
{ data.items.map(item => (
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Box, Flex, Select, Skeleton, Text, Tooltip } from '@chakra-ui/react';
|
||||
import { Flex, Select, Skeleton, Text, Tooltip } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { AddressImplementation } from 'types/api/addressParams';
|
||||
import type { SmartContract } from 'types/api/contract';
|
||||
import type { ArrayElement } from 'types/utils';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
@@ -16,12 +16,6 @@ import formatFilePath from 'ui/shared/monaco/utils/formatFilePath';
|
||||
import ContractCodeIdes from './ContractCodeIdes';
|
||||
import ContractExternalLibraries from './ContractExternalLibraries';
|
||||
|
||||
const SOURCE_CODE_OPTIONS = [
|
||||
{ id: 'primary', label: 'Proxy' } as const,
|
||||
{ id: 'secondary', label: 'Implementation' } as const,
|
||||
];
|
||||
type SourceCodeType = ArrayElement<typeof SOURCE_CODE_OPTIONS>['id'];
|
||||
|
||||
function getEditorData(contractInfo: SmartContract | undefined) {
|
||||
if (!contractInfo || !contractInfo.source_code) {
|
||||
return undefined;
|
||||
@@ -44,152 +38,130 @@ function getEditorData(contractInfo: SmartContract | undefined) {
|
||||
];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
address?: string;
|
||||
implementationAddress?: string;
|
||||
interface SourceContractOption {
|
||||
address: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const ContractSourceCode = ({ address, implementationAddress }: Props) => {
|
||||
const [ sourceType, setSourceType ] = React.useState<SourceCodeType>('primary');
|
||||
interface Props {
|
||||
address: string;
|
||||
implementations?: Array<AddressImplementation>;
|
||||
}
|
||||
|
||||
const primaryContractQuery = useApiQuery('contract', {
|
||||
pathParams: { hash: address },
|
||||
export const ContractSourceCode = ({ address, implementations }: Props) => {
|
||||
|
||||
const options: Array<SourceContractOption> = React.useMemo(() => {
|
||||
return [
|
||||
{ label: 'Proxy', address },
|
||||
...(implementations || [])
|
||||
.filter((item) => item.name && item.address !== address)
|
||||
.map(({ name, address }, item, array) => ({ address, label: array.length === 1 ? 'Implementation' : `Impl: ${ name }` })),
|
||||
];
|
||||
}, [ address, implementations ]);
|
||||
|
||||
const [ sourceContract, setSourceContract ] = React.useState<SourceContractOption>(options[0]);
|
||||
|
||||
const contractQuery = useApiQuery('contract', {
|
||||
pathParams: { hash: sourceContract.address },
|
||||
queryOptions: {
|
||||
enabled: Boolean(address),
|
||||
refetchOnMount: false,
|
||||
placeholderData: stubs.CONTRACT_CODE_VERIFIED,
|
||||
},
|
||||
});
|
||||
|
||||
const secondaryContractQuery = useApiQuery('contract', {
|
||||
pathParams: { hash: implementationAddress },
|
||||
queryOptions: {
|
||||
enabled: Boolean(implementationAddress),
|
||||
refetchOnMount: false,
|
||||
placeholderData: stubs.CONTRACT_CODE_VERIFIED,
|
||||
},
|
||||
});
|
||||
const editorData = React.useMemo(() => {
|
||||
return getEditorData(contractQuery.data);
|
||||
}, [ contractQuery.data ]);
|
||||
|
||||
const isLoading = implementationAddress ?
|
||||
primaryContractQuery.isPlaceholderData || secondaryContractQuery.isPlaceholderData :
|
||||
primaryContractQuery.isPlaceholderData;
|
||||
const isLoading = contractQuery.isPlaceholderData;
|
||||
|
||||
const primaryEditorData = React.useMemo(() => {
|
||||
return getEditorData(primaryContractQuery.data);
|
||||
}, [ primaryContractQuery.data ]);
|
||||
|
||||
const secondaryEditorData = React.useMemo(() => {
|
||||
return secondaryContractQuery.isPlaceholderData ? undefined : getEditorData(secondaryContractQuery.data);
|
||||
}, [ secondaryContractQuery.data, secondaryContractQuery.isPlaceholderData ]);
|
||||
|
||||
const activeContract = sourceType === 'secondary' ? secondaryContractQuery.data : primaryContractQuery.data;
|
||||
const activeContractData = sourceType === 'secondary' ? secondaryEditorData : primaryEditorData;
|
||||
const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const nextOption = options.find(({ address }) => address === event.target.value);
|
||||
if (nextOption) {
|
||||
setSourceContract(nextOption);
|
||||
}
|
||||
}, [ options ]);
|
||||
|
||||
const heading = (
|
||||
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>
|
||||
<span>Contract source code</span>
|
||||
{ activeContract?.language &&
|
||||
<Text whiteSpace="pre" as="span" variant="secondary" textTransform="capitalize"> ({ activeContract.language })</Text> }
|
||||
{ contractQuery.data?.language &&
|
||||
<Text whiteSpace="pre" as="span" variant="secondary" textTransform="capitalize"> ({ contractQuery.data.language })</Text> }
|
||||
</Skeleton>
|
||||
);
|
||||
|
||||
const diagramLinkAddress = (() => {
|
||||
if (!activeContract?.can_be_visualized_via_sol2uml) {
|
||||
return;
|
||||
}
|
||||
return sourceType === 'secondary' ? implementationAddress : address;
|
||||
})();
|
||||
const select = options.length > 1 ? (
|
||||
<Select
|
||||
size="xs"
|
||||
value={ sourceContract.address }
|
||||
onChange={ handleSelectChange }
|
||||
w="auto"
|
||||
maxW={{ lg: '200px', xl: '400px' }}
|
||||
whiteSpace="nowrap"
|
||||
textOverflow="ellipsis"
|
||||
fontWeight={ 600 }
|
||||
borderRadius="base"
|
||||
>
|
||||
{ options.map((option) => <option key={ option.address } value={ option.address }>{ option.label }</option>) }
|
||||
</Select>
|
||||
) : null;
|
||||
|
||||
const diagramLink = diagramLinkAddress ? (
|
||||
const externalLibraries = contractQuery.data?.external_libraries ?
|
||||
<ContractExternalLibraries data={ contractQuery.data.external_libraries } isLoading={ isLoading }/> :
|
||||
null;
|
||||
|
||||
const diagramLink = contractQuery?.data?.can_be_visualized_via_sol2uml ? (
|
||||
<Tooltip label="Visualize contract code using Sol2Uml JS library">
|
||||
<LinkInternal
|
||||
href={ route({ pathname: '/visualize/sol2uml', query: { address: diagramLinkAddress } }) }
|
||||
href={ route({ pathname: '/visualize/sol2uml', query: { address: sourceContract.address } }) }
|
||||
ml={{ base: '0', lg: 'auto' }}
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
<Skeleton isLoaded={ !isLoading }>
|
||||
View UML diagram
|
||||
View UML diagram
|
||||
</Skeleton>
|
||||
</LinkInternal>
|
||||
</Tooltip>
|
||||
) : null;
|
||||
|
||||
const copyToClipboard = activeContractData?.length === 1 ?
|
||||
<CopyToClipboard text={ activeContractData[0].source_code } isLoading={ isLoading } ml={{ base: 'auto', lg: diagramLink ? '0' : 'auto' }}/> :
|
||||
const ides = <ContractCodeIdes hash={ sourceContract.address } isLoading={ isLoading }/>;
|
||||
|
||||
const copyToClipboard = contractQuery.data && editorData?.length === 1 ? (
|
||||
<CopyToClipboard
|
||||
text={ contractQuery.data.source_code }
|
||||
isLoading={ isLoading }
|
||||
ml={{ base: 'auto', lg: diagramLink ? '0' : 'auto' }}
|
||||
/>
|
||||
) :
|
||||
null;
|
||||
|
||||
const ides = sourceType === 'secondary' ? <ContractCodeIdes hash={ implementationAddress }/> : <ContractCodeIdes hash={ address }/>;
|
||||
|
||||
const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setSourceType(event.target.value as SourceCodeType);
|
||||
}, []);
|
||||
|
||||
const editorSourceTypeSelector = !secondaryContractQuery.isPlaceholderData && secondaryContractQuery.data?.source_code ? (
|
||||
<Select
|
||||
size="xs"
|
||||
value={ sourceType }
|
||||
onChange={ handleSelectChange }
|
||||
w="auto"
|
||||
fontWeight={ 600 }
|
||||
borderRadius="base"
|
||||
>
|
||||
{ SOURCE_CODE_OPTIONS.map((option) => <option key={ option.id } value={ option.id }>{ option.label }</option>) }
|
||||
</Select>
|
||||
) : null;
|
||||
|
||||
const externalLibraries = (() => {
|
||||
if (sourceType === 'secondary') {
|
||||
return secondaryContractQuery.data?.external_libraries && <ContractExternalLibraries data={ secondaryContractQuery.data.external_libraries }/>;
|
||||
}
|
||||
|
||||
return primaryContractQuery.data?.external_libraries && <ContractExternalLibraries data={ primaryContractQuery.data.external_libraries }/>;
|
||||
})();
|
||||
|
||||
const content = (() => {
|
||||
if (isLoading) {
|
||||
return <Skeleton h="557px" w="100%"/>;
|
||||
}
|
||||
|
||||
if (!primaryEditorData) {
|
||||
if (!editorData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box display={ sourceType === 'primary' ? 'block' : 'none' }>
|
||||
<CodeEditor
|
||||
data={ primaryEditorData }
|
||||
remappings={ primaryContractQuery.data?.compiler_settings?.remappings }
|
||||
libraries={ primaryContractQuery.data?.external_libraries ?? undefined }
|
||||
language={ primaryContractQuery.data?.language ?? undefined }
|
||||
mainFile={ primaryEditorData[0]?.file_path }
|
||||
contractName={ primaryContractQuery.data?.name || undefined }
|
||||
/>
|
||||
</Box>
|
||||
{ secondaryEditorData && (
|
||||
<Box display={ sourceType === 'secondary' ? 'block' : 'none' }>
|
||||
<CodeEditor
|
||||
data={ secondaryEditorData }
|
||||
remappings={ secondaryContractQuery.data?.compiler_settings?.remappings }
|
||||
libraries={ secondaryContractQuery.data?.external_libraries ?? undefined }
|
||||
language={ secondaryContractQuery.data?.language ?? undefined }
|
||||
mainFile={ secondaryEditorData?.[0]?.file_path }
|
||||
contractName={ secondaryContractQuery.data?.name || undefined }
|
||||
/>
|
||||
</Box>
|
||||
) }
|
||||
</>
|
||||
<CodeEditor
|
||||
key={ sourceContract.address }
|
||||
data={ editorData }
|
||||
remappings={ contractQuery.data?.compiler_settings?.remappings }
|
||||
libraries={ contractQuery.data?.external_libraries ?? undefined }
|
||||
language={ contractQuery.data?.language ?? undefined }
|
||||
mainFile={ editorData[0]?.file_path }
|
||||
contractName={ contractQuery.data?.name ?? undefined }
|
||||
/>
|
||||
);
|
||||
})();
|
||||
|
||||
if (!primaryEditorData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Flex alignItems="center" mb={ 3 } columnGap={ 3 } rowGap={ 2 } flexWrap="wrap">
|
||||
{ heading }
|
||||
{ editorSourceTypeSelector }
|
||||
{ select }
|
||||
{ externalLibraries }
|
||||
{ diagramLink }
|
||||
{ ides }
|
||||
|
||||
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { AddressImplementation } from 'types/api/addressParams';
|
||||
|
||||
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
|
||||
interface Props {
|
||||
data: Array<AddressImplementation>;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const AddressImplementations = ({ data, isLoading }: Props) => {
|
||||
const hasManyItems = data.length > 1;
|
||||
const [ hasScroll, setHasScroll ] = React.useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DetailsInfoItem.Label
|
||||
hint={ `Implementation${ hasManyItems ? 's' : '' } address${ hasManyItems ? 'es' : '' } of the proxy contract` }
|
||||
isLoading={ isLoading }
|
||||
hasScroll={ hasScroll }
|
||||
>
|
||||
{ `Implementation${ hasManyItems ? 's' : '' }` }
|
||||
</DetailsInfoItem.Label>
|
||||
<DetailsInfoItem.ValueWithScroll
|
||||
gradientHeight={ 48 }
|
||||
onScrollVisibilityChange={ setHasScroll }
|
||||
rowGap={ 2 }
|
||||
maxH="200px"
|
||||
>
|
||||
{ data.map((item) => (
|
||||
<AddressEntity
|
||||
key={ item.address }
|
||||
address={{ hash: item.address, name: item.name, is_contract: true }}
|
||||
isLoading={ isLoading }
|
||||
noIcon
|
||||
/>
|
||||
)) }
|
||||
</DetailsInfoItem.ValueWithScroll>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(AddressImplementations);
|
||||
@@ -62,6 +62,7 @@ const AddressNetWorth = ({ addressData, isLoading, addressHash }: Props) => {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontSize: 'sm',
|
||||
lineHeight: 5,
|
||||
fontWeight: 500,
|
||||
onClick: onMultichainClick,
|
||||
};
|
||||
|
||||
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
@@ -79,8 +79,7 @@ export default function useAddressQuery({ hash }: Params): AddressQuery {
|
||||
has_token_transfers: false,
|
||||
has_tokens: false,
|
||||
has_validated_blocks: false,
|
||||
implementation_address: null,
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
is_contract: false,
|
||||
is_verified: false,
|
||||
name: null,
|
||||
|
||||
@@ -67,7 +67,7 @@ const OptimisticDepositsListItem = ({ item, isLoading }: Props) => {
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn origin</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<AddressEntityL1
|
||||
address={{ hash: item.l1_tx_origin, name: '', is_contract: false, is_verified: false, implementation_name: '', ens_domain_name: null }}
|
||||
address={{ hash: item.l1_tx_origin, name: '', is_contract: false, is_verified: false, ens_domain_name: null }}
|
||||
isLoading={ isLoading }
|
||||
noCopy
|
||||
truncation="constant"
|
||||
|
||||
@@ -59,7 +59,7 @@ const OptimisticDepositsTableItem = ({ item, isLoading }: Props) => {
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
<AddressEntityL1
|
||||
address={{ hash: item.l1_tx_origin, name: '', is_contract: false, is_verified: false, implementation_name: '', ens_domain_name: null }}
|
||||
address={{ hash: item.l1_tx_origin, name: '', is_contract: false, is_verified: false, ens_domain_name: null }}
|
||||
isLoading={ isLoading }
|
||||
truncation="constant"
|
||||
noCopy
|
||||
|
||||
@@ -198,7 +198,7 @@ const AddressPageContent = () => {
|
||||
config.features.validators.isEnabled && addressQuery.data?.has_validated_blocks ?
|
||||
{ slug: 'validator', name: 'Validator', tagType: 'custom' as const, ordinal: 10 } :
|
||||
undefined,
|
||||
addressQuery.data?.implementation_address ? { slug: 'proxy', name: 'Proxy', tagType: 'custom' as const, ordinal: -1 } : undefined,
|
||||
addressQuery.data?.implementations?.length ? { slug: 'proxy', name: 'Proxy', tagType: 'custom' as const, ordinal: -1 } : undefined,
|
||||
addressQuery.data?.token ? { slug: 'token', name: 'Token', tagType: 'custom' as const, ordinal: -1 } : undefined,
|
||||
isSafeAddress ? { slug: 'safe', name: 'Multisig: Safe', tagType: 'custom' as const, ordinal: -10 } : undefined,
|
||||
config.features.userOps.isEnabled && userOpsAccountQuery.data ?
|
||||
|
||||
@@ -86,7 +86,7 @@ const ContractVerificationForAddress = () => {
|
||||
backLink={ backLink }
|
||||
/>
|
||||
<AddressEntity
|
||||
address={{ hash, is_contract: true, implementation_name: null }}
|
||||
address={{ hash, is_contract: true }}
|
||||
noLink
|
||||
fontFamily="heading"
|
||||
fontSize="lg"
|
||||
|
||||
@@ -35,7 +35,7 @@ const Sol2Uml = () => {
|
||||
<Flex mb={ 10 } flexWrap="wrap" columnGap={ 3 }>
|
||||
<span>For contract</span>
|
||||
<AddressEntity
|
||||
address={{ hash: addressHash, is_contract: true, implementation_name: null }}
|
||||
address={{ hash: addressHash, is_contract: true }}
|
||||
/>
|
||||
</Flex>
|
||||
<Sol2UmlDiagram addressHash={ addressHash }/>
|
||||
|
||||
@@ -136,7 +136,7 @@ const TokenInstanceContent = () => {
|
||||
const address = {
|
||||
hash: hash || '',
|
||||
is_contract: true,
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
watchlist_names: [],
|
||||
watchlist_address_id: null,
|
||||
};
|
||||
|
||||
@@ -83,7 +83,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
|
||||
is_contract: data.type === 'contract',
|
||||
is_verified: data.is_smart_contract_verified,
|
||||
name: null,
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
ens_domain_name: null,
|
||||
};
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
|
||||
is_contract: data.type === 'contract',
|
||||
is_verified: data.is_smart_contract_verified,
|
||||
name: null,
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
ens_domain_name: null,
|
||||
};
|
||||
const expiresText = data.ens_info?.expiry_date ? ` (expires ${ dayjs(data.ens_info.expiry_date).fromNow() })` : '';
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
import { Flex, useColorModeValue, chakra } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
export type Props = {
|
||||
children: React.ReactNode;
|
||||
containerId?: string;
|
||||
gradientHeight: number;
|
||||
className?: string;
|
||||
hasScroll: boolean;
|
||||
onScrollVisibilityChange?: (isVisible: boolean) => void;
|
||||
}
|
||||
|
||||
const ContainerWithScrollY = ({ className, hasScroll, containerId, gradientHeight, children }: Props, ref: React.ForwardedRef<HTMLDivElement>) => {
|
||||
const ContainerWithScrollY = ({ className, gradientHeight, children, onScrollVisibilityChange }: Props) => {
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const [ hasScroll, setHasScroll ] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasScroll = ref.current.scrollHeight >= ref.current.clientHeight + gradientHeight / 2;
|
||||
setHasScroll(hasScroll);
|
||||
onScrollVisibilityChange?.(hasScroll);
|
||||
}, [ gradientHeight, onScrollVisibilityChange ]);
|
||||
|
||||
const gradientStartColor = useColorModeValue('whiteAlpha.600', 'blackAlpha.600');
|
||||
const gradientEndColor = useColorModeValue('whiteAlpha.900', 'blackAlpha.900');
|
||||
|
||||
return (
|
||||
<Flex
|
||||
id={ containerId }
|
||||
flexDirection="column"
|
||||
className={ className }
|
||||
overflowY={ hasScroll ? 'scroll' : 'auto' }
|
||||
@@ -37,4 +48,4 @@ const ContainerWithScrollY = ({ className, hasScroll, containerId, gradientHeigh
|
||||
);
|
||||
};
|
||||
|
||||
export default chakra(React.forwardRef(ContainerWithScrollY));
|
||||
export default chakra(ContainerWithScrollY);
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { Text } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import ContainerWithScrollY from 'ui/shared/ContainerWithScrollY';
|
||||
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
|
||||
|
||||
export const TX_ACTIONS_BLOCK_ID = 'tx-actions';
|
||||
const SCROLL_GRADIENT_HEIGHT = 48;
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
@@ -14,42 +11,28 @@ type Props = {
|
||||
}
|
||||
|
||||
const DetailsActionsWrapper = ({ children, isLoading, type }: Props) => {
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const [ hasScroll, setHasScroll ] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setHasScroll(containerRef.current.scrollHeight >= containerRef.current.clientHeight + SCROLL_GRADIENT_HEIGHT / 2);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DetailsInfoItem.Label
|
||||
id={ TX_ACTIONS_BLOCK_ID }
|
||||
hint={ `Highlighted events of the ${ type === 'tx' ? 'transaction' : 'user operation' }` }
|
||||
isLoading={ isLoading }
|
||||
hasScroll={ hasScroll }
|
||||
>
|
||||
<span>{ `${ type === 'tx' ? 'Transaction' : 'User operation' } action` }</span>
|
||||
{ hasScroll && <Text fontWeight={ 500 } variant="secondary" fontSize="xs" className="note" align="right">Scroll to see more</Text> }
|
||||
</DetailsInfoItem.Label>
|
||||
<DetailsInfoItem.Value
|
||||
position="relative"
|
||||
<DetailsInfoItem.ValueWithScroll
|
||||
gradientHeight={ 48 }
|
||||
onScrollVisibilityChange={ setHasScroll }
|
||||
alignItems="stretch"
|
||||
rowGap={ 5 }
|
||||
w="100%"
|
||||
maxH="200px"
|
||||
>
|
||||
<ContainerWithScrollY
|
||||
containerId={ TX_ACTIONS_BLOCK_ID }
|
||||
gradientHeight={ SCROLL_GRADIENT_HEIGHT }
|
||||
hasScroll={ hasScroll }
|
||||
alignItems="stretch"
|
||||
rowGap={ 5 }
|
||||
w="100%"
|
||||
maxH="200px"
|
||||
ref={ containerRef }
|
||||
>
|
||||
{ children }
|
||||
</ContainerWithScrollY>
|
||||
</DetailsInfoItem.Value>
|
||||
{ children }
|
||||
</DetailsInfoItem.ValueWithScroll>
|
||||
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import { chakra, GridItem, Flex, Skeleton } from '@chakra-ui/react';
|
||||
import { chakra, GridItem, Flex, Text, Skeleton } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import * as ContainerWithScrollY from 'ui/shared/ContainerWithScrollY';
|
||||
import Hint from 'ui/shared/Hint';
|
||||
|
||||
const LabelScrollText = () => (
|
||||
<Text fontWeight={ 500 } variant="secondary" fontSize="xs" className="note" align="right">
|
||||
Scroll to see more
|
||||
</Text>
|
||||
);
|
||||
|
||||
interface LabelProps {
|
||||
hint?: string;
|
||||
children: React.ReactNode;
|
||||
isLoading?: boolean;
|
||||
className?: string;
|
||||
id?: string;
|
||||
hasScroll?: boolean;
|
||||
}
|
||||
|
||||
const Label = chakra(({ hint, children, isLoading, id, className }: LabelProps) => {
|
||||
const Label = chakra(({ hint, children, isLoading, id, className, hasScroll }: LabelProps) => {
|
||||
return (
|
||||
<GridItem
|
||||
id={ id }
|
||||
@@ -24,6 +32,7 @@ const Label = chakra(({ hint, children, isLoading, id, className }: LabelProps)
|
||||
{ hint && <Hint label={ hint } isLoading={ isLoading } my={{ lg: '2px' }}/> }
|
||||
<Skeleton isLoaded={ !isLoading } fontWeight={{ base: 700, lg: 500 }}>
|
||||
{ children }
|
||||
{ hasScroll && <LabelScrollText/> }
|
||||
</Skeleton>
|
||||
</Flex>
|
||||
</GridItem>
|
||||
@@ -53,7 +62,22 @@ const Value = chakra(({ children, className }: ValueProps) => {
|
||||
);
|
||||
});
|
||||
|
||||
const ValueWithScroll = chakra(({ children, gradientHeight, onScrollVisibilityChange, className }: ContainerWithScrollY.Props) => {
|
||||
return (
|
||||
<Value position="relative">
|
||||
<ContainerWithScrollY.default
|
||||
className={ className }
|
||||
gradientHeight={ gradientHeight }
|
||||
onScrollVisibilityChange={ onScrollVisibilityChange }
|
||||
>
|
||||
{ children }
|
||||
</ContainerWithScrollY.default>
|
||||
</Value>
|
||||
);
|
||||
});
|
||||
|
||||
export {
|
||||
Label,
|
||||
Value,
|
||||
ValueWithScroll,
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ export function getTxCourseType(from: string, to: string | undefined, current?:
|
||||
export const unknownAddress: Omit<AddressParam, 'hash'> = {
|
||||
is_contract: false,
|
||||
is_verified: false,
|
||||
implementation_name: '',
|
||||
implementations: null,
|
||||
name: '',
|
||||
private_tags: [],
|
||||
public_tags: [],
|
||||
|
||||
@@ -86,14 +86,12 @@ const Icon = (props: IconProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip label={ props.address.implementation_name }>
|
||||
<Flex marginRight={ styles.marginRight }>
|
||||
<AddressIdenticon
|
||||
size={ props.iconSize === 'lg' ? 30 : 20 }
|
||||
hash={ props.address.hash }
|
||||
/>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
<Flex marginRight={ styles.marginRight }>
|
||||
<AddressIdenticon
|
||||
size={ props.iconSize === 'lg' ? 30 : 20 }
|
||||
hash={ props.address.hash }
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -142,7 +140,7 @@ const Copy = (props: CopyProps) => {
|
||||
const Container = EntityBase.Container;
|
||||
|
||||
export interface EntityProps extends EntityBase.EntityBaseProps {
|
||||
address: Pick<AddressParam, 'hash' | 'name' | 'is_contract' | 'is_verified' | 'implementation_name' | 'ens_domain_name' | 'metadata'>;
|
||||
address: Pick<AddressParam, 'hash' | 'name' | 'is_contract' | 'is_verified' | 'ens_domain_name' | 'metadata'>;
|
||||
isSafeAddress?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ const SearchBarSuggestAddress = ({ data, isMobile, searchTerm }: Props) => {
|
||||
is_contract: data.type === 'contract',
|
||||
name: '',
|
||||
is_verified: data.is_smart_contract_verified,
|
||||
implementation_name: null,
|
||||
ens_domain_name: null,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -29,7 +29,7 @@ const TokenInstanceCreatorAddress = ({ hash }: Props) => {
|
||||
const creatorAddress = {
|
||||
hash: addressQuery.data.creator_address_hash,
|
||||
is_contract: false,
|
||||
implementation_name: null,
|
||||
implementations: null,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -46,7 +46,6 @@ const TokensTableItem = ({
|
||||
const tokenAddress: AddressEntityProps['address'] = {
|
||||
hash: address,
|
||||
name: '',
|
||||
implementation_name: null,
|
||||
is_contract: true,
|
||||
is_verified: false,
|
||||
ens_domain_name: null,
|
||||
|
||||
@@ -82,7 +82,7 @@ const VerifiedAddressesListItem = ({ item, application, onAdd, onEdit, isLoading
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Address</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<AddressEntity
|
||||
address={{ hash: item.contractAddress, is_contract: true, implementation_name: null }}
|
||||
address={{ hash: item.contractAddress, is_contract: true }}
|
||||
isLoading={ isLoading }
|
||||
w="100%"
|
||||
/>
|
||||
|
||||
@@ -68,7 +68,7 @@ const VerifiedAddressesTableItem = ({ item, application, onAdd, onEdit, isLoadin
|
||||
<Tr>
|
||||
<Td>
|
||||
<AddressEntity
|
||||
address={{ hash: item.contractAddress, is_contract: true, implementation_name: null }}
|
||||
address={{ hash: item.contractAddress, is_contract: true }}
|
||||
isLoading={ isLoading }
|
||||
fontWeight="600"
|
||||
/>
|
||||
|
||||