diff --git a/src/hooks/useProtocol.ts b/src/hooks/useProtocol.ts new file mode 100644 index 0000000..844d727 --- /dev/null +++ b/src/hooks/useProtocol.ts @@ -0,0 +1,90 @@ +// Copyright 2026 Tether Operations Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { useCallback, useMemo } from 'react' +import { AccountService } from '../services/accountService' +import { getWalletStore } from '../store/walletStore' +import { useAddressLoader } from './useAddressLoader' +import { requireInitialized } from '../utils/storeHelpers' + +export type UseProtocolParams = { + accountIndex: number + network: string + protocolType: 'bridge' | 'swap' | 'lending' | 'fiat' + protocolName: string +} + +/** + * Returns a typed proxied interface for calling protocol methods + * (bridge, swap, lending, fiat) on a WDK account. + * + * @example + * const usdt0Bridge = useProtocol({ + * network: 'ethereum', + * accountIndex: 0, + * protocolType: 'bridge', + * protocolName: 'USDT0_EVM', + * }) + * + * const quote = await usdt0Bridge.quoteBridge({ targetChain: 'arbitrum', ... }) + * const result = await usdt0Bridge.bridge({ targetChain: 'arbitrum', ... }) + */ +export function useProtocol(params: UseProtocolParams): T { + const { accountIndex, network, protocolType, protocolName } = params + + const { address } = useAddressLoader({ accountIndex, network }) + const activeWalletId = getWalletStore()((state) => state.activeWalletId) + + const account = useMemo( + () => + activeWalletId && address + ? { accountIndex, network, walletId: activeWalletId } + : null, + [accountIndex, network, activeWalletId, address], + ) + + const protocol = useCallback((): T => { + return new Proxy({} as T, { + get: (_target, prop) => { + if (prop === 'then') { + return undefined + } + + return async (...args: unknown[]) => { + await requireInitialized() + + if (!account) { + console.error( + '[useProtocol] Protocol call failed: Account is not available. Ensure a wallet is active.', + ) + return undefined + } + + if (typeof prop === 'string') { + return await AccountService.callProtocolMethod( + account.network, + account.accountIndex, + prop, + protocolType, + protocolName, + ...args, + ) + } + } + }, + }) + }, [account, protocolType, protocolName]) + + return useMemo(() => protocol(), [protocol]) +} diff --git a/src/index.ts b/src/index.ts index 0e41dd5..9154fe2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -41,6 +41,9 @@ export type { UseAddressesReturn } from './hooks/useAddresses' export { useAccount } from './hooks/useAccount' export type { UseAccountParams, UseAccountReturn, UseAccountResponse, TransactionParams, TransactionResult } from './hooks/useAccount' +export { useProtocol } from './hooks/useProtocol' +export type { UseProtocolParams } from './hooks/useProtocol' + export type { UseWdkAppResult } from './hooks/useWdkApp' export { useWalletManager } from './hooks/useWalletManager' diff --git a/src/services/accountService.ts b/src/services/accountService.ts index 572430a..d35ab4e 100644 --- a/src/services/accountService.ts +++ b/src/services/accountService.ts @@ -117,4 +117,69 @@ export class AccountService { ) } } + + static async callProtocolMethod( + network: string, + accountIndex: number, + methodName: string, + protocolType: string, + protocolName: string, + ...args: unknown[] + ): Promise { + if (typeof methodName !== 'string' || methodName.trim().length === 0) { + throw new Error('methodName must be a non-empty string') + } + + validateNetworkName(network) + validateAccountIndex(accountIndex) + + const hrpc = await requireInitialized() + + let argsString: string | undefined = undefined + if (args !== undefined && args !== null) { + argsString = safeStringify(args) + } + + const optionsString = safeStringify({ protocolType, protocolName }) + + try { + const response = await hrpc.callMethod({ + methodName: String(methodName), + network, + accountIndex, + args: argsString, + options: optionsString, + }) + + const validatedResponse = workletResponseSchema.parse(response) + + if (!validatedResponse.result) { + throw new Error(`Protocol method ${methodName} returned no result`) + } + + let parsed + try { + parsed = JSON.parse(validatedResponse.result) + if (parsed === null || parsed === undefined) { + throw new Error('Parsed result is null or undefined') + } + } catch (error) { + if (error instanceof Error && error.message.includes('Parsed result is null')) { + throw error + } + throw new Error( + `Failed to parse result from ${methodName}: ${error instanceof Error ? error.message : String(error)}`, + ) + } + + return convertBigIntToString(parsed) + } catch (error) { + handleServiceError( + error, + 'AccountService', + `callProtocolMethod:${String(methodName)}`, + { network, accountIndex, methodName }, + ) + } + } }