Skip to content

Support geas smart contract language 🪿 #2848

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion configs/envs/.env.eth_sepolia
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,5 @@ NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
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
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
NEXT_PUBLIC_VIEWS_CONTRACT_LANGUAGE_FILTERS=['solidity','vyper','yul','geas']
2 changes: 1 addition & 1 deletion docs/ENVS.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ Settings for meta tags, OG tags and SEO
| NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS | `Array<AddressViewId>` | Address views that should not be displayed. See below the list of the possible id values. | - | - | `'["top_accounts"]'` | v1.15.0+ |
| NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED | `boolean` | Set to `true` if SolidityScan reports are supported | - | - | `true` | v1.19.0+ |
| NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS | `Array<'solidity-hardhat' \| 'solidity-foundry'>` | Pass an array of additional methods from which users can choose while verifying a smart contract. Both methods are available by default, pass `'none'` string to disable them all. | - | - | `['solidity-hardhat']` | v1.33.0+ |
| NEXT_PUBLIC_VIEWS_CONTRACT_LANGUAGE_FILTERS | `Array<'solidity' \| 'vyper' \| 'yul' \| 'scilla'>` | Pass an array of contract languages that will be displayed as options in the filter on the verified contract page. | - | `['solidity','vyper','yul']` | `['solidity','vyper','yul','scilla']` | v1.37.0+ |
| NEXT_PUBLIC_VIEWS_CONTRACT_LANGUAGE_FILTERS | `Array<'solidity' \| 'vyper' \| 'yul' \| 'scilla' \| 'geas'>` | Pass an array of contract languages that will be displayed as options in the filter on the verified contract page. | - | `['solidity','vyper','yul']` | `['solidity','vyper','yul','scilla']` | v1.37.0+ |

##### Address views list
| Id | Description |
Expand Down
6 changes: 6 additions & 0 deletions mocks/contract/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ export const write: Array<SmartContractMethodWrite> = [
payable: true,
stateMutability: 'payable',
type: 'fallback',
inputs: [
{ internalType: 'bytes', name: 'input', type: 'bytes' },
],
outputs: [
{ internalType: 'bytes', name: 'output', type: 'bytes' },
],
},
{
constant: false,
Expand Down
6 changes: 4 additions & 2 deletions types/api/contracts.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { AddressParam } from './addressParams';
import type { SmartContractLicenseType } from './contract';

export type VerifiedContractsLanguage = 'solidity' | 'vyper' | 'yul' | 'scilla' | 'stylus_rust' | 'geas';

export interface VerifiedContract {
address: AddressParam;
certified?: boolean;
coin_balance: string;
compiler_version: string | null;
language: 'vyper' | 'yul' | 'solidity' | 'stylus_rust';
language: VerifiedContractsLanguage;
has_constructor_args: boolean;
optimization_enabled: boolean;
transactions_count: number | null;
Expand All @@ -24,7 +26,7 @@ export interface VerifiedContractsResponse {
} | null;
}

export type VerifiedContractsFilter = 'solidity' | 'vyper' | 'yul' | 'scilla';
export type VerifiedContractsFilter = Exclude<VerifiedContractsLanguage, 'stylus_rust'>;

export interface VerifiedContractsFilters {
q: string | undefined;
Expand Down
1 change: 1 addition & 0 deletions types/client/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ export const SMART_CONTRACT_LANGUAGE_FILTERS: Array<VerifiedContractsFilter> = [
'vyper',
'yul',
'scilla',
'geas',
];
3 changes: 2 additions & 1 deletion ui/address/AddressContract.pw.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import type { Abi } from 'viem';

import * as addressMock from 'mocks/address/address';
import * as contractInfoMock from 'mocks/contract/info';
Expand All @@ -15,7 +16,7 @@ test.beforeEach(async({ mockApiResponse }) => {
await mockApiResponse('general:address', addressMock.contract, { pathParams: { hash } });
await mockApiResponse(
'general:contract',
{ ...contractInfoMock.verified, abi: [ ...contractMethodsMock.read, ...contractMethodsMock.write ] },
{ ...contractInfoMock.verified, abi: [ ...contractMethodsMock.read, ...contractMethodsMock.write ] as Abi },
{ pathParams: { hash } },
);
});
Expand Down
2 changes: 2 additions & 0 deletions ui/address/contract/ContractSourceCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ function getEditorData(contractInfo: SmartContract | undefined) {
return 'scilla';
case 'stylus_rust':
return 'rs';
case 'geas':
return 'eas';
default:
return 'sol';
}
Expand Down
4 changes: 2 additions & 2 deletions ui/address/contract/methods/ContractMethodsCustom.pw.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { BrowserContext } from '@playwright/test';
import React from 'react';
import type { AbiItem } from 'viem';
import type { Abi } from 'viem';

import * as addressMock from 'mocks/address/address';
import * as methodsMock from 'mocks/contract/methods';
Expand All @@ -27,7 +27,7 @@ authTest('without data', async({ render }) => {
});

authTest('with data', async({ render, mockApiResponse }) => {
const abi: Array<AbiItem> = [ ...methodsMock.read, ...methodsMock.write ];
const abi: Abi = [ ...methodsMock.read, ...methodsMock.write ] as Abi;
await mockApiResponse('general:custom_abi', [ {
abi,
contract_address_hash: addressHash,
Expand Down
7 changes: 3 additions & 4 deletions ui/address/contract/methods/ContractMethodsRegular.pw.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';

import type { SmartContractMethod } from './types';
import type { Abi } from 'viem';

import * as addressMock from 'mocks/address/address';
import * as methodsMock from 'mocks/contract/methods';
Expand Down Expand Up @@ -41,7 +40,7 @@ test('all methods +@dark-mode', async({ render }) => {
},
};

const abi: Array<SmartContractMethod> = [ ...methodsMock.read, ...methodsMock.write ];
const abi: Abi = [ ...methodsMock.read, ...methodsMock.write ] as Abi;
const component = await render(<ContractMethodsRegular abi={ abi }/>, { hooksConfig });
await component.getByText(/expand all/i).click();
await expect(component.getByText('HTTP request failed')).toBeVisible();
Expand All @@ -61,7 +60,7 @@ test.describe('all methods', () => {
},
};

const abi: Array<SmartContractMethod> = [ ...methodsMock.read, ...methodsMock.write ];
const abi: Abi = [ ...methodsMock.read, ...methodsMock.write ] as Abi;
const component = await render(<ContractMethodsRegular abi={ abi }/>, { hooksConfig });
await component.getByText(/expand all/i).click();
// await expect(component.getByText('HTTP request failed')).toBeVisible();
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 15 additions & 5 deletions ui/address/contract/methods/form/ContractMethodForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import { SECOND } from 'toolkit/utils/consts';
import IconSvg from 'ui/shared/IconSvg';

import { isReadMethod } from '../utils';
import { isReadMethod, isWriteMethod } from '../utils';
import ContractMethodFieldAccordion from './ContractMethodFieldAccordion';
import ContractMethodFieldInput from './ContractMethodFieldInput';
import ContractMethodFieldInputArray from './ContractMethodFieldInputArray';
Expand Down Expand Up @@ -69,16 +69,25 @@ const ContractMethodForm = ({ data, attempt, onSubmit, onReset, isOpen }: Props)
const args = transformFormDataToMethodArgs(formData);

if (callStrategyRef.current === 'copy_calldata') {
if (!('name' in data) || !data.name) {
if (!('inputs' in data)) {
return;
}

// since we have added additional input for native coin value
// we need to slice it off
const argsToPass = args.slice(0, data.inputs.length);

if (!('name' in data)) {
// this condition means that the fallback method acts as a read method with inputs
const data = typeof argsToPass[0] === 'string' && argsToPass[0].startsWith('0x') ? argsToPass[0] as `0x${ string }` : '0x';
await navigator.clipboard.writeText(data);
return;
}

const callData = encodeFunctionData({
abi: [ data ],
functionName: data.name,
// since we have added additional input for native coin value
// we need to slice it off
args: args.slice(0, data.inputs.length),
args: argsToPass,
});
await navigator.clipboard.writeText(callData);
return;
Expand Down Expand Up @@ -254,6 +263,7 @@ const ContractMethodForm = ({ data, attempt, onSubmit, onReset, isOpen }: Props)
basePath: `${ index }`,
isDisabled: isLoading,
level: 0,
isOptional: data.type === 'fallback' && isWriteMethod(data),
};

if ('components' in input && input.components && input.type === 'tuple') {
Expand Down
4 changes: 2 additions & 2 deletions ui/address/contract/methods/form/ContractMethodOutput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { Flex } from '@chakra-ui/react';
import React from 'react';
import type { AbiFunction } from 'viem';

import type { ResultViewMode } from '../types';
import type { AbiFallback, ResultViewMode } from '../types';

import ResultItem from './resultPublicClient/Item';

export interface Props {
data: unknown;
abiItem: AbiFunction;
abiItem: AbiFunction | AbiFallback;
onSettle: () => void;
mode: ResultViewMode;
}
Expand Down
15 changes: 14 additions & 1 deletion ui/address/contract/methods/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import type { AbiFunction, AbiFallback, AbiReceive } from 'abitype';
import type { AbiFunction, AbiFallback as AbiFallbackViem, AbiReceive } from 'abitype';
import type { AbiParameter, AbiStateMutability } from 'viem';

export type ContractAbiItemInput = AbiFunction['inputs'][number] & { fieldType?: 'native_coin' };

export type MethodType = 'read' | 'write' | 'all';
export type MethodCallStrategy = 'read' | 'write' | 'simulate' | 'copy_calldata';
export type ResultViewMode = 'preview' | 'result';

// we manually add inputs and outputs to the fallback method because viem doesn't support it
// but as we discussed with @k1rill-fedoseev, it's a good idea to have them for fallback method of any contract
// also, according to @k1rill-fedoseev, fallback method can act as a read method when it has 'view' state mutability
// but viem doesn't aware of this and thinks that fallback method state mutability can only be 'payable' or 'nonpayable'
// so we have to redefine the stateMutability as well to include "view" option
// see "addInputsToFallback" and "isReadMethod" functions in utils.ts
export interface AbiFallback extends Pick<AbiFallbackViem, 'type' | 'payable'> {
inputs: Array<AbiParameter>;
outputs: Array<AbiParameter>;
stateMutability: Exclude<AbiStateMutability, 'pure'>;
}

export type SmartContractMethodCustomFields = { method_id: string } | { is_invalid: boolean };
export type SmartContractMethodRead = AbiFunction & SmartContractMethodCustomFields;
export type SmartContractMethodWrite = AbiFunction & SmartContractMethodCustomFields | AbiFallback | AbiReceive;
Expand Down
32 changes: 25 additions & 7 deletions ui/address/contract/methods/useCallMethodPublicClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface Params {
item: SmartContractMethod;
args: Array<unknown>;
addressHash: string;
strategy: Exclude<MethodCallStrategy, 'write'>;
strategy: Exclude<MethodCallStrategy, 'write' | 'copy_calldata'>;
}

export default function useCallMethodPublicClient(): (params: Params) => Promise<FormSubmitResult> {
Expand All @@ -24,19 +24,37 @@ export default function useCallMethodPublicClient(): (params: Params) => Promise
const { address: account } = useAccount();

return React.useCallback(async({ args, item, addressHash, strategy }) => {
if (!('name' in item)) {
throw new Error('Unknown contract method');
if (item.type === 'receive') {
throw new Error('Incorrect contract method');
}

if (!publicClient) {
throw new Error('Public Client is not defined');
}

const address = getAddress(addressHash);
// for write payable methods we add additional input for native coin value
// so in simulate mode we need to strip it off
const _args = args.slice(0, item.inputs.length);
const value = getNativeCoinValue(args[item.inputs.length]);

// for payable methods we add additional input for native coin value
const inputs = 'inputs' in item ? item.inputs : [];
const _args = args.slice(0, inputs.length);
const value = getNativeCoinValue(args[inputs.length]);

if (item.type === 'fallback') {
// if the fallback method acts as a read method, it can only have one input of type bytes
// so we pass the input value as data without encoding it
const data = typeof _args[0] === 'string' && _args[0].startsWith('0x') ? _args[0] as `0x${ string }` : undefined;
const result = await publicClient.call({
account,
to: address,
value,
...(data ? { data } : {}),
});

return {
source: 'public_client' as const,
data: result.data,
};
}

const params = {
abi: [ item ],
Expand Down
13 changes: 9 additions & 4 deletions ui/address/contract/methods/useCallMethodWalletClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,19 @@ export default function useCallMethodWalletClient(): (params: Params) => Promise
const address = getAddress(addressHash);
const activityResponse = await trackTransaction(account ?? '', address);

// for payable methods we add additional input for native coin value
const inputs = 'inputs' in item ? item.inputs : [];
const _args = args.slice(0, inputs.length);
const value = getNativeCoinValue(args[inputs.length]);

if (item.type === 'receive' || item.type === 'fallback') {
const value = getNativeCoinValue(args[0]);
// if the fallback method acts as a read method, it can only have one input of type bytes
// so we pass the input value as data without encoding it
const data = typeof _args[0] === 'string' && _args[0].startsWith('0x') ? _args[0] as `0x${ string }` : undefined;
const hash = await walletClient.sendTransaction({
to: address,
value,
...(data ? { data } : {}),
});

if (activityResponse?.token) {
Expand All @@ -61,9 +69,6 @@ export default function useCallMethodWalletClient(): (params: Params) => Promise
throw new Error('Method name is not defined');
}

const _args = args.slice(0, item.inputs.length);
const value = getNativeCoinValue(args[item.inputs.length]);

const hash = await walletClient.writeContract({
args: _args,
// Here we provide the ABI as an array containing only one item from the submitted form.
Expand Down
39 changes: 28 additions & 11 deletions ui/address/contract/methods/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { Abi, AbiFallback, AbiReceive } from 'abitype';
import type { AbiFunction } from 'viem';
import type { Abi } from 'abitype';
import { toFunctionSelector } from 'viem';

import type { MethodType, SmartContractMethod, SmartContractMethodRead, SmartContractMethodWrite } from './types';
Expand All @@ -12,19 +11,22 @@ export const getNativeCoinValue = (value: unknown) => {
return BigInt(value);
};

export const isMethod = (method: Abi[number]): method is SmartContractMethod =>
export const isMethod = (method: Abi[number]) =>
(method.type === 'function' || method.type === 'fallback' || method.type === 'receive');

export const isReadMethod = (method: Abi[number]): method is SmartContractMethodRead =>
method.type === 'function' && (
method.constant || method.stateMutability === 'view' || method.stateMutability === 'pure'
export const isReadMethod = (method: SmartContractMethod): method is SmartContractMethodRead =>
(
method.type === 'function' &&
(method.constant || method.stateMutability === 'view' || method.stateMutability === 'pure')
) || (
method.type === 'fallback' && method.stateMutability === 'view'
);

export const isWriteMethod = (method: Abi[number]): method is SmartContractMethodWrite =>
export const isWriteMethod = (method: SmartContractMethod): method is SmartContractMethodWrite =>
(method.type === 'function' || method.type === 'fallback' || method.type === 'receive') &&
!isReadMethod(method);

export const enrichWithMethodId = (method: AbiFunction | AbiFallback | AbiReceive): SmartContractMethod => {
export const enrichWithMethodId = (method: SmartContractMethod): SmartContractMethod => {
if (method.type !== 'function') {
return method;
}
Expand All @@ -42,7 +44,19 @@ export const enrichWithMethodId = (method: AbiFunction | AbiFallback | AbiReceiv
}
};

const getNameForSorting = (method: SmartContractMethod | AbiFallback | AbiReceive) => {
export const addInputsToFallback = (method: SmartContractMethod): SmartContractMethod => {
if (method.type === 'fallback') {
return {
...method,
inputs: [ { internalType: 'bytes', name: 'input', type: 'bytes' } ],
outputs: [ { internalType: 'bytes', name: 'output', type: 'bytes' } ],
};
}

return method;
};

const getNameForSorting = (method: SmartContractMethod) => {
if ('name' in method) {
return method.name;
}
Expand All @@ -51,9 +65,12 @@ const getNameForSorting = (method: SmartContractMethod | AbiFallback | AbiReceiv
};

export const formatAbi = (abi: Abi) => {
return abi
.filter(isMethod)

const methods = abi.filter(isMethod) as Array<SmartContractMethod>;

return methods
.map(enrichWithMethodId)
.map(addInputsToFallback)
.sort((a, b) => {
const aName = getNameForSorting(a);
const bName = getNameForSorting(b);
Expand Down
Loading
Loading