Skip to content

Commit c41a85d

Browse files
authored
Merge pull request #69 from nulllpc/npc/add-protocol-hook
Add useProtocol
2 parents d2eca17 + 3a46579 commit c41a85d

3 files changed

Lines changed: 158 additions & 0 deletions

File tree

src/hooks/useProtocol.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2026 Tether Operations Limited
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import { useCallback, useMemo } from 'react'
16+
import { AccountService } from '../services/accountService'
17+
import { getWalletStore } from '../store/walletStore'
18+
import { useAddressLoader } from './useAddressLoader'
19+
import { requireInitialized } from '../utils/storeHelpers'
20+
21+
export type UseProtocolParams = {
22+
accountIndex: number
23+
network: string
24+
protocolType: 'bridge' | 'swap' | 'lending' | 'fiat'
25+
protocolName: string
26+
}
27+
28+
/**
29+
* Returns a typed proxied interface for calling protocol methods
30+
* (bridge, swap, lending, fiat) on a WDK account.
31+
*
32+
* @example
33+
* const usdt0Bridge = useProtocol<Usdt0ProtocolEvm>({
34+
* network: 'ethereum',
35+
* accountIndex: 0,
36+
* protocolType: 'bridge',
37+
* protocolName: 'USDT0_EVM',
38+
* })
39+
*
40+
* const quote = await usdt0Bridge.quoteBridge({ targetChain: 'arbitrum', ... })
41+
* const result = await usdt0Bridge.bridge({ targetChain: 'arbitrum', ... })
42+
*/
43+
export function useProtocol<T extends object>(params: UseProtocolParams): T {
44+
const { accountIndex, network, protocolType, protocolName } = params
45+
46+
const { address } = useAddressLoader({ accountIndex, network })
47+
const activeWalletId = getWalletStore()((state) => state.activeWalletId)
48+
49+
const account = useMemo(
50+
() =>
51+
activeWalletId && address
52+
? { accountIndex, network, walletId: activeWalletId }
53+
: null,
54+
[accountIndex, network, activeWalletId, address],
55+
)
56+
57+
const protocol = useCallback((): T => {
58+
return new Proxy({} as T, {
59+
get: (_target, prop) => {
60+
if (prop === 'then') {
61+
return undefined
62+
}
63+
64+
return async (...args: unknown[]) => {
65+
await requireInitialized()
66+
67+
if (!account) {
68+
console.error(
69+
'[useProtocol] Protocol call failed: Account is not available. Ensure a wallet is active.',
70+
)
71+
return undefined
72+
}
73+
74+
if (typeof prop === 'string') {
75+
return await AccountService.callProtocolMethod(
76+
account.network,
77+
account.accountIndex,
78+
prop,
79+
protocolType,
80+
protocolName,
81+
...args,
82+
)
83+
}
84+
}
85+
},
86+
})
87+
}, [account, protocolType, protocolName])
88+
89+
return useMemo(() => protocol(), [protocol])
90+
}

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ export type { UseAddressesReturn } from './hooks/useAddresses'
4141
export { useAccount } from './hooks/useAccount'
4242
export type { UseAccountParams, UseAccountReturn, UseAccountResponse, TransactionParams, TransactionResult } from './hooks/useAccount'
4343

44+
export { useProtocol } from './hooks/useProtocol'
45+
export type { UseProtocolParams } from './hooks/useProtocol'
46+
4447
export type { UseWdkAppResult } from './hooks/useWdkApp'
4548

4649
export { useWalletManager } from './hooks/useWalletManager'

src/services/accountService.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,69 @@ export class AccountService {
117117
)
118118
}
119119
}
120+
121+
static async callProtocolMethod(
122+
network: string,
123+
accountIndex: number,
124+
methodName: string,
125+
protocolType: string,
126+
protocolName: string,
127+
...args: unknown[]
128+
): Promise<unknown> {
129+
if (typeof methodName !== 'string' || methodName.trim().length === 0) {
130+
throw new Error('methodName must be a non-empty string')
131+
}
132+
133+
validateNetworkName(network)
134+
validateAccountIndex(accountIndex)
135+
136+
const hrpc = await requireInitialized()
137+
138+
let argsString: string | undefined = undefined
139+
if (args !== undefined && args !== null) {
140+
argsString = safeStringify(args)
141+
}
142+
143+
const optionsString = safeStringify({ protocolType, protocolName })
144+
145+
try {
146+
const response = await hrpc.callMethod({
147+
methodName: String(methodName),
148+
network,
149+
accountIndex,
150+
args: argsString,
151+
options: optionsString,
152+
})
153+
154+
const validatedResponse = workletResponseSchema.parse(response)
155+
156+
if (!validatedResponse.result) {
157+
throw new Error(`Protocol method ${methodName} returned no result`)
158+
}
159+
160+
let parsed
161+
try {
162+
parsed = JSON.parse(validatedResponse.result)
163+
if (parsed === null || parsed === undefined) {
164+
throw new Error('Parsed result is null or undefined')
165+
}
166+
} catch (error) {
167+
if (error instanceof Error && error.message.includes('Parsed result is null')) {
168+
throw error
169+
}
170+
throw new Error(
171+
`Failed to parse result from ${methodName}: ${error instanceof Error ? error.message : String(error)}`,
172+
)
173+
}
174+
175+
return convertBigIntToString(parsed)
176+
} catch (error) {
177+
handleServiceError(
178+
error,
179+
'AccountService',
180+
`callProtocolMethod:${String(methodName)}`,
181+
{ network, accountIndex, methodName },
182+
)
183+
}
184+
}
120185
}

0 commit comments

Comments
 (0)