Skip to content

Commit

Permalink
fear: writeContract
Browse files Browse the repository at this point in the history
  • Loading branch information
TomatoDroid committed Oct 18, 2024
1 parent 941b990 commit 392f348
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 7 deletions.
1 change: 1 addition & 0 deletions nuxt/components/scaffold-eth/Address.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const displayAddress = computed(() => {
if (fetchedEns.value) {
return fetchedEns.value
}
// biome-ignore lint/style/noUselessElse: <explanation>
else if (props.format === 'long') {
return checkSumAddress
}
Expand Down
3 changes: 1 addition & 2 deletions nuxt/components/scaffold-eth/Tuple.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@
import ContractInput from './debug/ContractInput.vue'
import { replacer } from '~/utils/scaffold-eth/common'
import type { AbiParameterTuple } from '~/utils/scaffold-eth/contract'
import { getFunctionInputKey, getInitialFormState, getInitialTupleFormState } from '~/utils/scaffold-eth/utilsContract'
import { getFunctionInputKey, getInitialTupleFormState } from '~/utils/scaffold-eth/utilsContract'
interface TupleProps {
abiTupleParameter: AbiParameterTuple
parentStateObjectKey: string
// parentForm?: Record<string, any>
}
const { abiTupleParameter, parentStateObjectKey } = defineProps<TupleProps>()
Expand Down
8 changes: 6 additions & 2 deletions nuxt/components/scaffold-eth/debug/ContractUI.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const props = defineProps<{
contractName: ContractName
}>()
const refreshDisplayVariables = ref(false)
const { targetNetwork } = useTargetNetwork()
const networkColor = useNetworkColor()
const { data: deployedContractData, isLoading: deployedContractDataLoading } = useDeployedContractInfo(props.contractName)
Expand Down Expand Up @@ -41,7 +42,7 @@ const { data: deployedContractData, isLoading: deployedContractDataLoading } = u
</div>
<div class="bg-base-300 rounded-3xl px-6 lg:px-8 py-4 shadow-lg shadow-base-300">
<ContractVariables
:refresh-display-variables="true"
:refresh-display-variables="refreshDisplayVariables"
:deployed-contract-data="deployedContractData"
/>
</div>
Expand Down Expand Up @@ -71,7 +72,10 @@ const { data: deployedContractData, isLoading: deployedContractDataLoading } = u
</div>
</div>
<div class="p-5 divide-y divide-base-300">
ContractWriteMethods
<ContractWriteMethods
:deployed-contract-data
@change="refreshDisplayVariables = !refreshDisplayVariables"
/>
</div>
</div>
</div>
Expand Down
36 changes: 36 additions & 0 deletions nuxt/components/scaffold-eth/debug/ContractWriteMethods.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script setup lang="ts">
import type { AbiFunction } from 'viem'
import type { Contract, ContractName, InheritedFunctions } from '~/utils/scaffold-eth/contract'
const { deployedContractData } = defineProps<{
deployedContractData: Contract<ContractName>
}>()
defineEmits(['change'])
const functionsToDisplay = computed(() => {
return ((deployedContractData.abi.filter(part => part.type === 'function')) as AbiFunction[])
.filter(fn => fn.stateMutability !== 'view' && fn.stateMutability !== 'pure')
.map(fn => ({
fn,
inheritedFrom: (deployedContractData.inheritedFunctions as InheritedFunctions)?.[fn.name],
}))
.sort((a, b) => b.inheritedFrom ? b.inheritedFrom.localeCompare(a.inheritedFrom) : 1)
})
</script>

<template>
<div v-if="!functionsToDisplay.length">
No write methods
</div>
<WriteOnlyFunctionForm
v-for="({ fn, inheritedFrom }, index) in functionsToDisplay"
v-else
:key="`${fn.name}-${index}`"
:abi="deployedContractData.abi"
:abi-function="fn"
:contract-address="deployedContractData.address"
:inherited-from="inheritedFrom"
@change="$emit('change')"
/>
</template>
60 changes: 60 additions & 0 deletions nuxt/components/scaffold-eth/debug/TxReceipt.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<script setup lang="ts">
import type { TransactionReceipt } from 'viem'
import ObjectFieldDisplay from './ObjectFieldDisplay.vue'
import { replacer } from '~/utils/scaffold-eth/common'
const { txResult } = defineProps<{
txResult: TransactionReceipt
}>()
const txResultCopied = ref(false)
const { copy } = useClipboard()
function handleClick() {
txResultCopied.value = true
const text = JSON.stringify(txResult, replacer, 2)
copy(text)
setTimeout(() => {
txResultCopied.value = false
}, 800)
}
</script>

<template>
<div class="flex text-sm rounded-3xl peer-checked:rounded-b-none min-h-0 bg-secondary py-0">
<div class="mt-1 pl-2">
<button @click="handleClick">
<Icon
v-if="txResultCopied"
name="i-uil-check-circle"
class="ml-1.5 text-xl font-normal text-sky-600 h-5 w-5 cursor-pointer"
aria-hidden="true"
/>
<Icon
v-else
name="i-uil-copy"
class="ml-1.5 text-xl font-normal text-sky-600 h-5 w-5 cursor-pointer"
aria-hidden="true"
/>
</button>
</div>
<div class="flex-wrap collapse collapse-arrow">
<input type="checkbox" class="min-h-0 peer">
<div class="collapse-title text-sm min-h-0 py-1.5 pl-1">
<strong>Transaction Receipt</strong>
</div>
<div class="collapse-content overflow-auto bg-secondary rounded-t-none rounded-3xl !pl-0">
<pre class="text-xs">
<ObjectFieldDisplay
v-for="[k, v] in Object.entries(txResult)"
:key="k"
:name="k"
:value="v"
size="xs"
:left-pad="false"
/>
</pre>
</div>
</div>
</div>
</template>
119 changes: 119 additions & 0 deletions nuxt/components/scaffold-eth/debug/WriteOnlyFunctionForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<script setup lang="tsx">
import type { Abi, AbiFunction, Address } from 'viem'
import { useAccount, useWaitForTransactionReceipt, useWriteContract } from '@wagmi/vue'
import IntegerInput from '../input/IntegerInput.vue'
import InheritanceTooltip from './InheritanceTooltip.vue'
import ContractInput from './ContractInput.vue'
import { getFunctionInputKey, getInitialFormState, getParsedContractFunctionArgs, transformAbiFunction } from '~/utils/scaffold-eth/utilsContract'
interface WriteOnlyFunctionFormProps {
abi: Abi
abiFunction: AbiFunction
contractAddress: Address
inheritedFrom?: string
}
const { abiFunction, contractAddress, abi } = defineProps<WriteOnlyFunctionFormProps>()
const emit = defineEmits(['change'])
const form = ref(getInitialFormState(abiFunction))
const txValue = ref<string | bigint>('')
const writeTxn = useTransactor()
const { chain } = useAccount()
const { targetNetwork } = useTargetNetwork()
const writeDisabled = computed(() => {
return !chain.value || chain.value?.id !== targetNetwork.value.id
})
const { data: result, isPending, writeContractAsync } = useWriteContract()
const transformedFunction = transformAbiFunction(abiFunction)
function RenderInputs() {
return transformedFunction.inputs.map((input, index) => {
const key = getFunctionInputKey(abiFunction.name, input, index)
return (
<ContractInput
key={key}
stateObjectKey={key}
paramType={input}
v-model={form.value}
/>
)
})
}
const zeroInputs = computed(() => {
return transformedFunction.inputs.length === 0 && abiFunction.stateMutability !== 'payable'
})
const { data: txResult } = useWaitForTransactionReceipt({
hash: result,
})
async function handleClick() {
if (writeContractAsync) {
try {
const makeWriteWithParams = () => writeContractAsync({
address: contractAddress,
functionName: abiFunction.name,
abi,
args: getParsedContractFunctionArgs(form.value),
value: BigInt(txValue.value),
})
await writeTxn(makeWriteWithParams)
emit('change')
}
catch (e) {
console.error('⚡️ ~ file: WriteOnlyFunctionForm.tsx:handleWrite ~ error', e)
}
}
}
</script>

<template>
<div class="py-5 space-y-3 first:pt-0 last:pb-1">
<div
class="flex gap-3"
:class="[zeroInputs ? 'flex-grow justify-between items-center' : 'flex-col']"
>
<p class="font-medium my-0 break-words">
{{ abiFunction.name }}
<InheritanceTooltip :inherited-from />
</p>
<RenderInputs />
<div v-if="abiFunction.stateMutability === 'payable'" class="flex flex-col gap-1.5 w-full">
<div class="flex items-center ml-2">
<span class="text-xs font-medium mr-2 leading-none">
payable value
</span>
<span class="block text-xs font-extralight leading-none">wei</span>
</div>
<IntegerInput v-model="txValue" placeholder="vlaue (wei)" />
</div>
<div class="flex justify-between gap-2">
<div v-if="!zeroInputs" class="flex-grow basis-0">
<TxReceipt v-if="txResult" :tx-result />
</div>
<div
class="flex"
:class="{ 'tooltip before:content-[attr(data-tip)] before:right-[-10px] before:left-auto before:transform-none': writeDisabled }"
:data-tip="`${writeDisabled && 'Wallet not connected or in the wrong network'}`"
>
<button
class="btn btn-secondary btn-sm"
:disabled="writeDisabled || isPending"
@click="handleClick"
>
<span v-if="isPending" class="loading loading-spinner loading-xs" />
Send 💸
</button>
</div>
</div>
</div>
<div v-if="zeroInputs && txResult" class="flex-grow basis-0">
<TxReceipt :tx-result />
</div>
</div>
</template>
17 changes: 14 additions & 3 deletions nuxt/utils/scaffold-eth/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ type AddExternalFlag<T> = {
};
}

// biome-ignore lint/suspicious/noExplicitAny: <explanation>
function deepMergeContracts<L extends Record<PropertyKey, any>, E extends Record<PropertyKey, any>>(local: L, external: E) {
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
const result: Record<PropertyKey, any> = {}
const allKeys = Array.from(new Set([...Object.keys(external), ...Object.keys(local)]))
for (const key of allKeys) {
Expand Down Expand Up @@ -74,6 +76,7 @@ export const contracts = contractsData as GenericContractsDeclaration | null

type ConfiguredChainId = (typeof scaffoldConfig)['targetNetworks'][0]['id']

// biome-ignore lint/suspicious/noExplicitAny: <explanation>
type IsContractDeclarationMissing<TYes, TNo> = typeof contractsData extends { [key in ConfiguredChainId]: any }
? TNo
: TYes
Expand Down Expand Up @@ -105,7 +108,9 @@ export type AbiFunctionOutputs<TAbi extends Abi, TFunctionName extends string> =
>['outputs']

export type AbiFunctionReturnType<TAbi extends Abi, TFunctionName extends string> = IsContractDeclarationMissing<
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
any,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
AbiParametersToPrimitiveTypes<AbiFunctionOutputs<TAbi, TFunctionName>> extends readonly [any]
? AbiParametersToPrimitiveTypes<AbiFunctionOutputs<TAbi, TFunctionName>>[0]
: AbiParametersToPrimitiveTypes<AbiFunctionOutputs<TAbi, TFunctionName>>
Expand All @@ -117,9 +122,9 @@ export type AbiEventInputs<TAbi extends Abi, TEventName extends ExtractAbiEventN
>['inputs']

export enum ContractCodeStatus {
LOADING,
DEPLOYED,
NOT_FOUND,
LOADING = 0,
DEPLOYED = 1,
NOT_FOUND = 2,
}

type AbiStateMutability = 'pure' | 'view' | 'nonpayable' | 'payable'
Expand All @@ -144,6 +149,7 @@ export type FunctionNamesWithInputs<

type Expand<T> = T extends object ? (T extends infer O ? { [K in keyof O]: O[K] } : never) : T

// biome-ignore lint/suspicious/noExplicitAny: <explanation>
type UnionToIntersection<U> = Expand<(U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never>

type OptionalTupple<T> = T extends readonly [infer H, ...infer R] ? readonly [H | undefined, ...OptionalTupple<R>] : T
Expand Down Expand Up @@ -188,6 +194,7 @@ export type ScaffoldWriteContractVariables<
Omit<WriteContractParameters, 'chainId' | 'abi' | 'address' | 'functionName' | 'args'>
>

// biome-ignore lint/suspicious/noExplicitAny: <explanation>
type WriteVariables = WriteContractVariables<Abi, string, any[], Config, number>

export interface TransactorFuncOptions {
Expand Down Expand Up @@ -217,6 +224,7 @@ export type UseScaffoldEventConfig<
Omit<UseWatchContractEventParameters, 'onLogs' | 'address' | 'abi' | 'eventName'> & {
onLogs: (
logs: Simplify<
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
Omit<Log<bigint, number, any>, 'args' | 'eventName'> & {
args: Record<string, unknown>
eventName: string
Expand Down Expand Up @@ -251,11 +259,13 @@ export type EventFilters<
TContractName extends ContractName,
TEventName extends ExtractAbiEventNames<ContractAbi<TContractName>>,
> = IsContractDeclarationMissing<
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
any,
IndexedEventInputs<TContractName, TEventName> extends never
? never
: {
[Key in IsContractDeclarationMissing<
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
any,
IndexedEventInputs<TContractName, TEventName>['name']
>]?: AbiParameterToPrimitiveType<Extract<IndexedEventInputs<TContractName, TEventName>, { name: Key }>>;
Expand Down Expand Up @@ -292,6 +302,7 @@ export type UseScaffoldEventHistoryData<
>,
> =
| IsContractDeclarationMissing<
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
any[],
{
log: Log<bigint, number, false, TEvent, false, [TEvent], TEventName>
Expand Down

0 comments on commit 392f348

Please sign in to comment.