Skip to content

Commit

Permalink
feat(sdk): validation errors and narrow types
Browse files Browse the repository at this point in the history
  • Loading branch information
ga-reth committed Feb 25, 2025
1 parent 8875622 commit 1098ef8
Show file tree
Hide file tree
Showing 2 changed files with 203 additions and 138 deletions.
147 changes: 9 additions & 138 deletions sdk/src/hooks/useOrder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { useQuery } from '@tanstack/react-query'
import { useCallback, useMemo } from 'react'
import { encodeFunctionData } from 'viem'
import { useCallback } from 'react'
import type { Hex, WriteContractErrorType } from 'viem'
import {
type Config,
Expand All @@ -19,7 +17,10 @@ import { encodeOrder } from '../utils/encodeOrder.js'
import { useDidFill } from './useDidFill.js'
import { useGetOpenOrder } from './useGetOpenOrder.js'
import { type InboxStatus, useInboxStatus } from './useInboxStatus.js'
import { toJSON } from './util.js'
import {
type UseValidateOrderResult,
useValidateOrder,
} from './useValidateOrder.js'

type UseOrderParams = {
order: Order
Expand All @@ -29,17 +30,14 @@ type UseOrderParams = {
type UseOrderReturnType = {
open: () => Promise<Hex>
orderId?: Hex
validation?: Validation
validation?: UseValidateOrderResult
txHash?: Hex
error?: WriteContractErrorType
validationError?: ValidationError
status: OrderStatus
canOpen: boolean
isTxPending: boolean
isTxSubmitted: boolean
isValidated: boolean
isOpen: boolean
isRejected: boolean
isError: boolean
txMutation: UseWriteContractReturnType<Config, unknown>
waitForTx: UseWaitForTransactionReceiptReturnType<Config, number>
Expand Down Expand Up @@ -68,25 +66,7 @@ export function useOrder(params: UseOrderParams): UseOrderReturnType {
wait.fetchStatus,
)

const validate = useValidateOrder(params)

const validation = useMemo(() => {
if (validate?.data?.error)
return {
error: {
code: validate.data.error.code,
message: validate.data.error.message,
},
}
if (validate?.data?.rejected)
return {
rejected: true,
rejectReason: validate.data.rejectReason,
rejectDescription: validate.data.rejectDescription,
} as const
if (validate?.data?.accepted) return { accepted: true } as const
return
}, [validate?.data])
const validation = useValidateOrder(params)

const { inbox } = useOmniContext()

Expand Down Expand Up @@ -130,125 +110,16 @@ export function useOrder(params: UseOrderParams): UseOrderReturnType {
txHash: txMutation.data,
status,
error: txMutation.error as WriteContractErrorType | undefined,
canOpen: validation?.accepted ?? false,
isValidated: validation?.status === 'accepted',
isTxPending: txMutation.isPending,
isTxSubmitted: txMutation.isSuccess,
isValidated: validation?.accepted ?? false,
isRejected: validation?.rejected ?? false,
isError: !!(validation?.error || txMutation.error),
isError: !!txMutation.error,
isOpen: !!wait.data,
txMutation,
waitForTx: wait,
}
}

////////////////////////
//// order validation
////////////////////////
type ValidationResponse = {
accepted?: boolean
rejected?: boolean
error?: {
code: number
message: string
}
rejectReason?: string
rejectDescription?: string
}

type ValidationRejected = {
rejected: true
rejectReason?: string
rejectDescription?: string
}

type ValidationAccepted = {
accepted: true
}

type ValidationError = {
error: {
code: number
message: string
}
}

type Validation = ValidationRejected | ValidationAccepted | ValidationError

// TODO: runtime assertions
function useValidateOrder({ order, validateEnabled = true }: UseOrderParams) {
// biome-ignore lint/correctness/useExhaustiveDependencies: deep compare on obj properties
const request = useMemo(() => {
if (!order.owner) return ''

function _encodeCalls() {
return order.calls.map((call) => {
const callData = encodeFunctionData({
abi: call.abi,
functionName: call.functionName,
args: call.args,
})
return {
target: call.target,
value: call.value,
data: callData,
}
})
}

return toJSON({
sourceChainId: order.srcChainId,
destChainId: order.destChainId,
fillDeadline: order.fillDeadline ?? Math.floor(Date.now() / 1000 + 86400),
calls: _encodeCalls(),
deposit: {
amount: order.deposit.amount,
token: order.deposit.token,
},
expenses: [
{
amount: order.expense.amount,
token: order.expense.token,
spender: order.expense.spender,
},
],
})
}, [
order.srcChainId,
order.destChainId,
order.deposit.amount?.toString(),
order.deposit.token,
order.expense.amount?.toString(),
order.expense.token,
order.expense.spender,
order.owner,
order.fillDeadline,
])

return useQuery<ValidationResponse>({
queryKey: ['check', request],
queryFn: async () => {
// TODO remove hardcoded api url
const response = await fetch(
'https://solver.staging.omni.network/api/v1/check',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: request,
},
)
return await response.json()
},
enabled:
!!order.owner &&
!!order.srcChainId &&
validateEnabled !== false &&
!(order.deposit.amount === 0n && order.expense.amount === 0n),
})
}

// deriveStatus returns a status derived from open tx, inbox and outbox statuses
function deriveStatus(
inboxStatus: InboxStatus,
Expand Down
194 changes: 194 additions & 0 deletions sdk/src/hooks/useValidateOrder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { type UseQueryResult, useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import { encodeFunctionData } from 'viem'
import { type FetchJSONError, fetchJSON } from '../internal/api.js'
import type { Order } from '../types/order.js'
import { toJSON } from './util.js'

type UseValidateOrderParams = {
order: Order
enabled?: boolean
}

type ValidationResponse = {
accepted?: boolean
rejected?: boolean
error?: {
code: number
message: string
}
rejectReason?: string
rejectDescription?: string
}

type Validation = {
status: 'pending' | 'rejected' | 'accepted' | 'error'
}

type ValidationPending = Validation & {
status: 'pending'
}

type ValidationRejected = Validation & {
status: 'rejected'
rejectReason: string
rejectDescription: string
}

type ValidationAccepted = Validation & {
status: 'accepted'
}

type ValidationError = Validation & {
status: 'error'
error:
| {
code: number
message: string
}
| FetchJSONError
}

export type UseValidateOrderResult =
| ValidationPending
| ValidationRejected
| ValidationAccepted
| ValidationError

export function useValidateOrder({
order,
enabled = true,
}: UseValidateOrderParams): UseValidateOrderResult {
const apiBaseUrl = 'https://solver.staging.omni.network/api/v1'

// biome-ignore lint/correctness/useExhaustiveDependencies: deep compare on obj properties
const request = useMemo(() => {
if (!order.owner) return ''

function _encodeCalls() {
return order.calls.map((call) => {
const callData = encodeFunctionData({
abi: call.abi,
functionName: call.functionName,
args: call.args,
})
return {
target: call.target,
value: call.value,
data: callData,
}
})
}

return toJSON({
sourceChainId: order.srcChainId,
destChainId: order.destChainId,
fillDeadline: order.fillDeadline ?? Math.floor(Date.now() / 1000 + 86400),
calls: _encodeCalls(),
deposit: {
amount: order.deposit.amount,
token: order.deposit.token,
},
expenses: [
{
amount: order.expense.amount,
token: order.expense.token,
spender: order.expense.spender,
},
],
})
}, [
order.srcChainId,
order.destChainId,
order.deposit.amount?.toString(),
order.deposit.token,
order.expense.amount?.toString(),
order.expense.token,
order.expense.spender,
order.owner,
order.fillDeadline,
])

const query = useQuery<ValidationResponse, FetchJSONError>({
queryKey: ['check', request],
queryFn: async () => doValidate(apiBaseUrl, request),
enabled:
!!order.owner &&
!!order.srcChainId &&
enabled !== false &&
!(order.deposit.amount === 0n && order.expense.amount === 0n),
})

return useResult(query)
}

async function doValidate(apiBaseUrl: string, request: string) {
const json = await fetchJSON(`${apiBaseUrl}/check`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: request,
})

if (!isValidateRes(json)) {
throw new Error(`Unexpected validation response: ${JSON.stringify(json)}`)
}

const validation = json

return validation as ValidationResponse
}

// TODO schema validation
const isValidateRes = (json: unknown): json is ValidationResponse => {
const res = json as ValidationResponse
return (
json != null &&
(res.accepted != null ||
(res.rejected != null &&
res.rejectReason != null &&
res.rejectDescription != null) ||
res.error != null)
)
}

const useResult = (
q: UseQueryResult<ValidationResponse, FetchJSONError>,
): UseValidateOrderResult =>
useMemo(() => {
if (q.isError) {
return {
status: 'error',
error: q.error,
}
}

if (q.isPending) {
return {
status: 'pending',
}
}

if (q.data.rejected) {
return {
status: 'rejected',
// TODO validation on rejections
rejectReason: q.data.rejectReason ?? 'Unknown reason',
rejectDescription:
q.data.rejectDescription ?? 'No description provided',
}
}

if (q.data.accepted) {
return {
status: 'accepted',
}
}

return {
status: 'error',
error: q.data?.error ?? {
code: 0,
message: 'Unknown validation error',
},
}
}, [q])

0 comments on commit 1098ef8

Please sign in to comment.