Skip to content

Instantly share code, notes, and snippets.

@drichar
Last active August 13, 2024 07:12
Show Gist options
  • Save drichar/57fa10b51d56c5af50ee296746d27117 to your computer and use it in GitHub Desktop.
Save drichar/57fa10b51d56c5af50ee296746d27117 to your computer and use it in GitHub Desktop.
Factory for creating custom useMutation hooks to sign transactions from the NFDomains API
import * as React from 'react'
import { NfdRecord } from '@/api/api-client'
import { useNfdCacheUpdate } from '@/api/hooks/useNfd'
import { usePostContractLock } from '@/api/hooks/usePostContractLock'
interface ManageContractProps {
nfd: NfdRecord
}
export function ManageContract({ nfd }: ManageContractProps) {
const [isLocked, setIsLocked] = React.useState(true)
const handleError = useErrorToast()
const optimisticUpdate = useNfdCacheUpdate()
const { mutateAsync: lockContract } = usePostContractLock({
toasts: {
success: `Contract successfully ${isLocked ? `locked` : `unlocked`}.`
},
onSuccess(data, params) {
if (!params) return
const newNfd: NfdRecord = {
...nfd,
properties: {
...nfd.properties,
internal: {
...nfd.properties?.internal,
contractLocked: params.body.lock ? '1' : '0'
}
}
}
// Updates the query cache w/o refetching
optimisticUpdate(newNfd)
}
})
const handleClickLockContract = async () => {
try {
if (!activeAddress) {
throw new Error('Wallet not connected!)
}
await lockContract({
name: nfd.name,
body: {
sender: activeAddress,
lock: isLocked
}
})
} catch (e) {
handleError(e)
}
}
// render component...
}
import { ContractLockRequestBody, nfdContractLock } from '@/api/api-client'
import { MutationOptions, usePostTransaction } from './usePostTransaction'
type ContractLockParams = {
name: string
body: ContractLockRequestBody
}
export function usePostContractLock(options: MutationOptions<ContractLockParams> = {}) {
return usePostTransaction<ContractLockParams>({
mutationFn: ({ name, body }) => nfdContractLock(name, body),
...options
})
}
import { useMutation } from '@tanstack/react-query'
import { useWallet } from '@txnlab/use-wallet-react'
import algosdk from 'algosdk'
import * as React from 'react'
import toast from 'react-hot-toast'
import { encodeNFDTransactionsArray, type NFDTransactionsArray } from '@/helpers/encoding'
import { useExplorerStore } from '@/store/index'
import type { AxiosResponse } from 'axios'
import type { PendingTransactionResponse } from 'types/algosdk'
export type SendTxnsResponse = PendingTransactionResponse & { txId: string }
export type ToastProps<TParams = unknown, TContext = unknown> = {
data?: SendTxnsResponse
params: TParams
context?: TContext | undefined
explorerLink?: string
}
type ToastComponent<TParams, TContext> = ({
data,
params,
context,
explorerLink
}: ToastProps<TParams, TContext>) => JSX.Element
type TxnToastContent<TParams, TContext> = string | ToastComponent<TParams, TContext>
export type TransactionToasts<TParams, TContext> = {
loading?: TxnToastContent<TParams, TContext>
success?: TxnToastContent<TParams, TContext>
}
export type PostTransactionOptions<TParams, TContext = unknown> = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mutationFn: (params: TParams) => Promise<AxiosResponse<string | void, any>>
onMutate?: (params: TParams) => Promise<TContext | undefined> | TContext | undefined
onSuccess?: (data: SendTxnsResponse, params: TParams, context: TContext | undefined) => unknown
onError?: (error: unknown, params: TParams, context: TContext | undefined) => unknown
onSettled?: (
data: SendTxnsResponse | undefined,
error: unknown,
params: TParams,
context: TContext | undefined
) => unknown
toasts?: TransactionToasts<TParams, TContext>
}
export type MutationOptions<TParams, TContext = unknown> = Partial<
Omit<PostTransactionOptions<TParams, TContext>, 'mutationFn'>
>
export function usePostTransaction<TParams, TContext = unknown>(
options: PostTransactionOptions<TParams, TContext>
) {
const { mutationFn, onMutate, onSuccess, onError, onSettled, toasts = {} } = options
const { loading = 'Waiting for user to sign transaction...', success = 'Success!' } = {
...toasts
}
const { algodClient, signTransactions } = useWallet()
const lookupByTxnId = useExplorerStore((state) => state.lookupByTxnId)
const toastIdRef = React.useRef(`toast-${Date.now()}-${Math.random()}`)
const TOAST_ID = toastIdRef.current
const signAndSendTransactions = async (params: TParams): Promise<SendTxnsResponse> => {
const { data } = await mutationFn(params)
if (typeof data !== 'string') {
throw new Error('Failed to fetch transactions')
}
const nfdTxnsArray = JSON.parse(data) as NFDTransactionsArray
const encodedTxns = encodeNFDTransactionsArray(nfdTxnsArray)
const signTxnsResult = await signTransactions(encodedTxns)
const signedTxns = nfdTxnsArray.map((nfdTxn, index) =>
nfdTxn[0] === 's' ? encodedTxns[index] : signTxnsResult[index]
) as Uint8Array[]
toast.loading('Sending transaction...', { id: TOAST_ID })
const { lastRound, firstRound } = algosdk.decodeSignedTransaction(signedTxns[0]).txn
const waitRounds = lastRound - firstRound
const { txId } = await algodClient.sendRawTransaction(signedTxns).do()
const confirmation = await algosdk.waitForConfirmation(algodClient, txId, waitRounds)
return {
...(confirmation as PendingTransactionResponse),
txId
}
}
return useMutation<SendTxnsResponse, unknown, TParams, TContext>(
(params: TParams) => {
return signAndSendTransactions(params)
},
{
onMutate: (params) => {
console.info('Sending transaction...')
const toastMsg = typeof loading === 'string' ? loading : loading({ params })
toast.loading(toastMsg, { id: TOAST_ID })
return onMutate?.(params)
},
onSuccess: (data, params, context) => {
console.info(`Transaction ${data.txId} confirmed in round ${data['confirmed-round']}`)
const toastMsg =
typeof success === 'string'
? success
: success({ data, params, context, explorerLink: lookupByTxnId(data.txId) })
toast.success(toastMsg, {
id: TOAST_ID,
duration: 5000 // 5 seconds
})
onSuccess?.(data, params, context)
},
onError: (error, params, context) => {
toast.dismiss(TOAST_ID)
onError?.(error, params, context)
},
onSettled: (data, error, params, context) => {
onSettled?.(data, error, params, context)
}
}
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment