Skip to content

Commit

Permalink
explorer: moveCall execution (MystenLabs#6383)
Browse files Browse the repository at this point in the history
* explorer: moveCall execution

* execute a moveCall transaction using wallet-adapter

* explorer: addresses pr comments

* Label ui component
* Input ui component
* fixes useZodForm
* removes useFieldArray
* use 4 spacing instead of 3.75
* add input shadow color to config
* use useMutation
* add burner wallet adapter in dev
  • Loading branch information
pchrysochoidis authored Nov 29, 2022
1 parent ba6bd63 commit 97e96ba
Show file tree
Hide file tree
Showing 15 changed files with 382 additions and 17 deletions.
5 changes: 3 additions & 2 deletions apps/core/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

const { fontFamily } = require("tailwindcss/defaultTheme");
const colors = require('tailwindcss/colors');
const colors = require("tailwindcss/colors");

/** @type {import('tailwindcss').Config} */
module.exports = {
Expand Down Expand Up @@ -34,7 +34,7 @@ module.exports = {

sui: {
DEFAULT: "#6fbcf0",
bright: '#2A38EB',
bright: "#2A38EB",
light: "#E1F3FF",
dark: "#1F6493",
},
Expand Down Expand Up @@ -70,6 +70,7 @@ module.exports = {
},
offwhite: "#fefefe",
offblack: "#111111",
ebony: "#101828",
},

extend: {
Expand Down
2 changes: 1 addition & 1 deletion apps/explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@
"@hookform/resolvers": "^2.9.10",
"@mysten/core": "workspace:*",
"@mysten/sui.js": "workspace:*",
"@mysten/wallet-adapter-all-wallets": "workspace:*",
"@mysten/wallet-adapter-react": "workspace:*",
"@mysten/wallet-adapter-react-ui": "workspace:*",
"@mysten/wallet-adapter-wallet-standard": "workspace:*",
"@sentry/react": "^7.6.0",
"@sentry/tracing": "^7.6.0",
"@tanstack/react-query": "^4.14.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import {
getExecutionStatusType,
getExecutionStatusError,
} from '@mysten/sui.js';
import { useWallet } from '@mysten/wallet-adapter-react';
import { WalletWrapper } from '@mysten/wallet-adapter-react-ui';
import { useMutation } from '@tanstack/react-query';
import clsx from 'clsx';
import toast from 'react-hot-toast';
import { z } from 'zod';

import { useFunctionParamsDetails } from './useFunctionParamsDetails';

import type { SuiMoveNormalizedFunction, ObjectId } from '@mysten/sui.js';

import { useZodForm } from '~/hooks/useZodForm';
import { Button } from '~/ui/Button';
import { DisclosureBox } from '~/ui/DisclosureBox';
import { Input } from '~/ui/Input';

const argsSchema = z.object({
params: z.array(z.object({ value: z.string().trim().min(1) })),
});

export type ModuleFunctionProps = {
packageId: ObjectId;
Expand All @@ -12,14 +32,88 @@ export type ModuleFunctionProps = {
functionDetails: SuiMoveNormalizedFunction;
defaultOpen?: boolean;
};

export function ModuleFunction({
defaultOpen,
packageId,
moduleName,
functionName,
functionDetails,
}: ModuleFunctionProps) {
const { connected, signAndExecuteTransaction } = useWallet();
const paramsDetails = useFunctionParamsDetails(functionDetails.parameters);
const { handleSubmit, formState, register } = useZodForm({
schema: argsSchema,
});
const execute = useMutation({
mutationFn: async (params: string[]) => {
const result = await signAndExecuteTransaction({
kind: 'moveCall',
data: {
packageObjectId: packageId,
module: moduleName,
function: functionName,
arguments: params,
typeArguments: [], // TODO: currently move calls that expect type argument will fail
gasBudget: 2000,
},
});
if (getExecutionStatusType(result) === 'failure') {
throw new Error(
getExecutionStatusError(result) || 'Transaction failed'
);
}
return result;
},
});
const isExecuteDisabled =
formState.isValidating ||
!formState.isValid ||
formState.isSubmitting ||
!connected;
return (
<DisclosureBox defaultOpen={defaultOpen} title={functionName}>
<pre>{JSON.stringify(functionDetails, null, 2)}</pre>
<form
onSubmit={handleSubmit(({ params }) =>
toast
.promise(
execute.mutateAsync(
params.map(({ value }) => value)
),
{
loading: 'Executing...',
error: (e) => 'Transaction failed',
success: 'Done',
}
)
.catch((e) => null)
)}
autoComplete="off"
className="flex flex-col flex-nowrap items-stretch gap-4"
>
{paramsDetails.map(({ paramTypeText }, index) => {
return (
<Input
key={index}
label={`Arg${index}`}
{...register(`params.${index}.value` as const)}
placeholder={paramTypeText}
/>
);
})}
<div className="flex items-center justify-end gap-1.5">
<Button
variant="primary"
type="submit"
disabled={isExecuteDisabled}
>
Execute
</Button>
<div className={clsx('temp-ui-override', { connected })}>
<WalletWrapper />
</div>
</div>
</form>
</DisclosureBox>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,18 @@ export function ModuleFunctionsInteraction({

if (error) {
return (
<Banner variant="error" fullWidth>
<Banner variant="error">
Error loading module <strong>{moduleName}</strong> details.
</Banner>
);
}

if (!isLoading && !executableFunctions.length) {
return (
<Banner variant="message">No public entry functions found.</Banner>
);
}

return !isLoading && executableFunctions.length ? (
<div className="flex flex-col gap-3">
{executableFunctions.map(({ name, details }) => (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { useMemo } from 'react';

import { getNormalizedFunctionParameterTypeDetails } from '../utils';

import type { SuiMoveNormalizedType } from '@mysten/sui.js';

export function useFunctionParamsDetails(
params: SuiMoveNormalizedType[],
functionTypeArgNames?: string[]
) {
return useMemo(
() =>
params
.map((aParam) =>
getNormalizedFunctionParameterTypeDetails(
aParam,
functionTypeArgNames
)
)
.filter(({ isTxContext }) => !isTxContext),
[params, functionTypeArgNames]
);
}
79 changes: 79 additions & 0 deletions apps/explorer/src/components/module/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import type { SuiMoveNormalizedType } from '@mysten/sui.js';

/**
* Converts a SuiMoveNormalizedType to string
* @param param A parameter's normalized type of a function
* @param functionTypeArgNames Parameters can be generic like 0x2::coin::Coin<T>.
* T is provided on function level with the type_parameters field of SuiMoveNormalizedFunction that defines the abilities.
* This parameter can be an array of strings that define the actual type or names like T1 that can be used to make the type of the parameter more specific. If
* functionTypeArgNames or the index that the parameter expects are not defines then a default value T{index} is used.
* @param str This function is recursive and this field is used to pass the already resolved type
* @returns
*/
export function normalizedFunctionParameterTypeToString(
param: SuiMoveNormalizedType,
functionTypeArgNames?: string[],
str = ''
): string {
if (typeof param === 'string') {
return str + param;
}
if ('TypeParameter' in param) {
return (
str +
(functionTypeArgNames?.[param.TypeParameter] ??
`T${param.TypeParameter}`)
);
}
if ('Reference' in param || 'MutableReference' in param) {
const p =
'Reference' in param ? param.Reference : param.MutableReference;
return normalizedFunctionParameterTypeToString(
p,
functionTypeArgNames,
str
);
}
if ('Vector' in param) {
return (
normalizedFunctionParameterTypeToString(
param.Vector,
functionTypeArgNames,
`${str}Vector<`
) + '>'
);
}
if ('Struct' in param) {
const theType = param.Struct;
const theTypeArgs = theType.type_arguments;
const theTypeArgsStr = theTypeArgs
.map((aTypeArg) =>
normalizedFunctionParameterTypeToString(
aTypeArg,
functionTypeArgNames
)
)
.join(', ');
return `${[theType.address, theType.module, theType.name].join('::')}${
theTypeArgsStr ? `<${theTypeArgsStr}>` : ''
}`;
}
return str;
}

export function getNormalizedFunctionParameterTypeDetails(
param: SuiMoveNormalizedType,
functionTypeArgNames?: string[]
) {
const paramTypeText = normalizedFunctionParameterTypeToString(
param,
functionTypeArgNames
);
return {
isTxContext: paramTypeText === '0x2::tx_context::TxContext',
paramTypeText,
};
}
23 changes: 23 additions & 0 deletions apps/explorer/src/hooks/useZodForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';

import type { UseFormProps } from 'react-hook-form';
import type { ZodSchema, TypeOf } from 'zod';

interface UseZodFormProps<T extends ZodSchema<any>>
extends UseFormProps<TypeOf<T>> {
schema: T;
}

export const useZodForm = <T extends ZodSchema<any>>({
schema,
...formConfig
}: UseZodFormProps<T>) => {
return useForm({
...formConfig,
resolver: zodResolver(schema),
});
};
21 changes: 21 additions & 0 deletions apps/explorer/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,24 @@ select:focus {
text-size-adjust: none;
}
}

/* TODO: remove once we start using Wallet Kit */

/* ==================> */

/* This rule overrides styling of the wallet-adaptor UI button */
/* stylelint-disable selector-class-pattern */
.temp-ui-override > button.MuiButtonBase-root {
@apply bg-sui-dark py-2 px-3 rounded-md shadow-none text-bodySmall font-semibold font-sans capitalize !important;
}

.temp-ui-override.connected > button.MuiButtonBase-root {
@apply text-steel-dark bg-white border-steel border border-solid !important;
}

.temp-ui-override > button > .MuiSvgIcon-root {
@apply text-bodySmall !important;
}
/* stylelint-enable selector-class-pattern */

/* <================== */
23 changes: 20 additions & 3 deletions apps/explorer/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import {
UnsafeBurnerWalletAdapter,
WalletStandardAdapterProvider,
} from '@mysten/wallet-adapter-all-wallets';
import {
WalletProvider,
type WalletProviderProps,
} from '@mysten/wallet-adapter-react';
import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';
import React from 'react';
Expand All @@ -25,11 +33,20 @@ if (import.meta.env.PROD) {
});
}

const adapters: WalletProviderProps['adapters'] = [
new WalletStandardAdapterProvider(),
];
if (import.meta.env.DEV) {
adapters.push(new UnsafeBurnerWalletAdapter());
}

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Router>
<App />
</Router>
<WalletProvider adapters={adapters} autoConnect={false}>
<Router>
<App />
</Router>
</WalletProvider>
</React.StrictMode>
);

Expand Down
Loading

0 comments on commit 97e96ba

Please sign in to comment.