Skip to content

Commit 08d320a

Browse files
authored
Merge pull request #732 from reservoirprotocol/pedro/fe-7784-investigate-hyperliquid-signing-on-app
Implement Hyperliquid withdrawals
2 parents b56c9bc + 0b227ca commit 08d320a

File tree

13 files changed

+528
-251
lines changed

13 files changed

+528
-251
lines changed

.changeset/strong-books-call.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@reservoir0x/relay-sdk': patch
3+
'@reservoir0x/relay-kit-ui': patch
4+
---
5+
6+
Add hyperliquid usd send functionality

demo/pages/_app.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const AppWrapper: FC<AppWrapperProps> = ({ children, dynamicChains }) => {
6161
Chain,
6262
...Chain[]
6363
]
64+
6465
const wagmiConfig = createConfig({
6566
chains: viemChains,
6667
multiInjectedProviderDiscovery: false,

packages/sdk/src/utils/chain.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const configureViemChain = (
2121
chain: RelayAPIChain
2222
): RelayChain & Required<Pick<RelayChain, 'viemChain'>> => {
2323
let viemChain: Chain
24-
const overriddenChains = [999]
24+
const overriddenChains = [999, 1337]
2525
const staticChain = overriddenChains.includes(chain.id)
2626
? undefined
2727
: viemChainMap[chain.id]

packages/sdk/src/utils/executeSteps.ts

Lines changed: 102 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type {
55
SignatureStepItem
66
} from '../types/index.js'
77
import { pollUntilHasData, pollUntilOk } from './pollApi.js'
8-
import { axios } from '../utils/index.js'
8+
import { axios, prepareHyperliquidSignatureStep } from '../utils/index.js'
99
import type { AxiosRequestConfig } from 'axios'
1010
import { getClient } from '../client.js'
1111
import { LogLevel } from '../utils/logger.js'
@@ -14,6 +14,7 @@ import {
1414
canBatchTransactions,
1515
prepareBatchTransaction
1616
} from './prepareBatchTransaction.js'
17+
import { sendUsd } from './hyperliquid.js'
1718

1819
export type SetStateData = Pick<
1920
Execute,
@@ -91,6 +92,20 @@ export async function executeSteps(
9192
}
9293
}
9394

95+
//Check if Hyperliquid and if so, rewrite steps to become a signature step
96+
if (
97+
chainId === 1337 &&
98+
json.steps[0] &&
99+
(json.steps[0].id as any) !== 'sign'
100+
) {
101+
const activeWalletChainId = await wallet?.getChainId()
102+
const signatureStep = prepareHyperliquidSignatureStep(
103+
json.steps,
104+
activeWalletChainId
105+
)
106+
json.steps = [signatureStep]
107+
}
108+
94109
// Update state on first call or recursion
95110
setState({
96111
steps: [...json?.steps],
@@ -345,6 +360,11 @@ export async function executeSteps(
345360
}
346361
}
347362

363+
//Special Logic for Hyperliquid to send signature
364+
if (chainId === 1337 && signature) {
365+
await sendUsd(client, signature, stepItem)
366+
}
367+
348368
if (postData) {
349369
client.log(['Execute Steps: Posting order'], LogLevel.Verbose)
350370
stepItem.progressState = 'posting'
@@ -405,86 +425,6 @@ export async function executeSteps(
405425
break
406426
}
407427

408-
// If check, poll check until validated
409-
if (stepItem?.check) {
410-
stepItem.progressState = 'validating'
411-
setState({
412-
steps: [...json.steps],
413-
fees: { ...json?.fees },
414-
breakdown: json?.breakdown,
415-
details: json?.details
416-
})
417-
stepItem.isValidatingSignature = true
418-
setState({
419-
steps: [...json?.steps],
420-
fees: { ...json?.fees },
421-
breakdown: json?.breakdown,
422-
details: json?.details
423-
})
424-
425-
await pollUntilOk(
426-
{
427-
url: `${request.baseURL}${stepItem?.check.endpoint}`,
428-
method: stepItem?.check.method,
429-
headers
430-
},
431-
(res) => {
432-
client.log(
433-
[
434-
`Execute Steps: Polling execute status to check if indexed`,
435-
res
436-
],
437-
LogLevel.Verbose
438-
)
439-
440-
//set status
441-
if (
442-
res?.data?.status === 'success' &&
443-
res?.data?.txHashes
444-
) {
445-
const chainTxHashes: NonNullable<
446-
Execute['steps'][0]['items']
447-
>[0]['txHashes'] = res.data?.txHashes?.map(
448-
(hash: string) => {
449-
return {
450-
txHash: hash,
451-
chainId:
452-
res.data.destinationChainId ?? chain?.id
453-
}
454-
}
455-
)
456-
457-
if (res?.data?.inTxHashes) {
458-
const chainInTxHashes: NonNullable<
459-
Execute['steps'][0]['items']
460-
>[0]['txHashes'] = res.data?.inTxHashes?.map(
461-
(hash: string) => {
462-
return {
463-
txHash: hash,
464-
chainId: chain?.id ?? res.data.originChainId
465-
}
466-
}
467-
)
468-
stepItem.internalTxHashes = chainInTxHashes
469-
}
470-
stepItem.txHashes = chainTxHashes
471-
472-
return true
473-
} else if (res?.data?.status === 'failure') {
474-
throw Error(
475-
res?.data?.details || 'Transaction failed'
476-
)
477-
} else if (res?.data?.status === 'delayed') {
478-
stepItem.progressState = 'validating_delayed'
479-
}
480-
return false
481-
},
482-
maximumAttempts,
483-
0,
484-
pollingInterval
485-
)
486-
}
487-
488428
if (res.status > 299 || res.status < 200) throw res.data
489429

490430
if (res.data.results) {
@@ -509,6 +449,87 @@ export async function executeSteps(
509449
}
510450
}
511451

452+
// If check, poll check until validated
453+
if (stepItem?.check) {
454+
stepItem.progressState = 'validating'
455+
setState({
456+
steps: [...json.steps],
457+
fees: { ...json?.fees },
458+
breakdown: json?.breakdown,
459+
details: json?.details
460+
})
461+
stepItem.isValidatingSignature = true
462+
setState({
463+
steps: [...json?.steps],
464+
fees: { ...json?.fees },
465+
breakdown: json?.breakdown,
466+
details: json?.details
467+
})
468+
469+
const headers = {
470+
'Content-Type': 'application/json'
471+
}
472+
473+
await pollUntilOk(
474+
{
475+
url: `${request.baseURL}${stepItem?.check.endpoint}`,
476+
method: stepItem?.check.method,
477+
headers
478+
},
479+
(res) => {
480+
client.log(
481+
[
482+
`Execute Steps: Polling execute status to check if indexed`,
483+
res
484+
],
485+
LogLevel.Verbose
486+
)
487+
488+
//set status
489+
if (
490+
res?.data?.status === 'success' &&
491+
res?.data?.txHashes
492+
) {
493+
const chainTxHashes: NonNullable<
494+
Execute['steps'][0]['items']
495+
>[0]['txHashes'] = res.data?.txHashes?.map(
496+
(hash: string) => {
497+
return {
498+
txHash: hash,
499+
chainId: res.data.destinationChainId ?? chain?.id
500+
}
501+
}
502+
)
503+
504+
if (res?.data?.inTxHashes) {
505+
const chainInTxHashes: NonNullable<
506+
Execute['steps'][0]['items']
507+
>[0]['txHashes'] = res.data?.inTxHashes?.map(
508+
(hash: string) => {
509+
return {
510+
txHash: hash,
511+
chainId: chain?.id ?? res.data.originChainId
512+
}
513+
}
514+
)
515+
stepItem.internalTxHashes = chainInTxHashes
516+
}
517+
stepItem.txHashes = chainTxHashes
518+
519+
return true
520+
} else if (res?.data?.status === 'failure') {
521+
throw Error(res?.data?.details || 'Transaction failed')
522+
} else if (res?.data?.status === 'delayed') {
523+
stepItem.progressState = 'validating_delayed'
524+
}
525+
return false
526+
},
527+
maximumAttempts,
528+
0,
529+
pollingInterval
530+
)
531+
}
532+
512533
break
513534
}
514535

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { parseSignature } from 'viem'
2+
import type { Execute } from '../types/Execute.js'
3+
import axios from 'axios'
4+
import type { RelayClient } from '../client.js'
5+
import { LogLevel } from './logger.js'
6+
7+
export function prepareHyperliquidSignatureStep(
8+
steps: Execute['steps'],
9+
chainId: number
10+
) {
11+
const items = steps[0]?.items
12+
const amount = items[0]?.data?.action?.parameters?.amount
13+
const destination = items[0]?.data?.action?.parameters?.destination
14+
const signatureStep = {
15+
id: 'sign' as any,
16+
action: 'Confirm transaction in your wallet',
17+
description: `Sign a message to confirm the transaction`,
18+
kind: 'signature' as const,
19+
items: [
20+
{
21+
status: 'incomplete' as 'incomplete' | 'complete',
22+
data: {
23+
sign: {
24+
signatureKind: 'eip712',
25+
domain: {
26+
name: 'HyperliquidSignTransaction',
27+
version: '1',
28+
chainId: chainId,
29+
verifyingContract: '0x0000000000000000000000000000000000000000'
30+
},
31+
types: {
32+
'HyperliquidTransaction:UsdSend': [
33+
{ name: 'hyperliquidChain', type: 'string' },
34+
{ name: 'destination', type: 'string' },
35+
{ name: 'amount', type: 'string' },
36+
{ name: 'time', type: 'uint64' }
37+
],
38+
EIP712Domain: [
39+
{ name: 'name', type: 'string' },
40+
{ name: 'version', type: 'string' },
41+
{ name: 'chainId', type: 'uint256' },
42+
{ name: 'verifyingContract', type: 'address' }
43+
]
44+
},
45+
primaryType: 'HyperliquidTransaction:UsdSend',
46+
value: {
47+
type: 'usdSend',
48+
signatureChainId: `0x${chainId.toString(16)}`,
49+
hyperliquidChain: 'Mainnet',
50+
destination: destination?.toLowerCase(),
51+
amount,
52+
time: new Date().getTime()
53+
}
54+
}
55+
},
56+
check: {
57+
endpoint: `/intents/status?requestId=${steps[0]?.requestId}`,
58+
method: 'GET'
59+
}
60+
}
61+
],
62+
requestId: steps[0]?.requestId,
63+
depositAddress: steps[0]?.depositAddress
64+
}
65+
66+
return signatureStep
67+
}
68+
69+
export async function sendUsd(
70+
client: RelayClient,
71+
signature: string,
72+
stepItem: Execute['steps'][0]['items'][0]
73+
) {
74+
client.log(
75+
['Execute Steps: Sending signature to Hyperliquid', signature],
76+
LogLevel.Verbose
77+
)
78+
const { r, s, v } = parseSignature(signature as `0x${string}`)
79+
const currentTime = stepItem?.data?.sign?.value?.time ?? new Date().getTime()
80+
const res = await axios.post('https://api.hyperliquid.xyz/exchange', {
81+
signature: {
82+
r,
83+
s,
84+
v: Number(v ?? 0n)
85+
},
86+
nonce: currentTime,
87+
action: {
88+
type: stepItem?.data?.sign?.value?.type,
89+
signatureChainId: `0x${stepItem?.data?.sign?.domain?.chainId?.toString(16)}`,
90+
hyperliquidChain: 'Mainnet',
91+
destination: stepItem?.data?.sign?.value?.destination?.toLowerCase(),
92+
amount: stepItem?.data?.sign?.value?.amount,
93+
time: currentTime
94+
}
95+
})
96+
if (
97+
!res ||
98+
!res.data ||
99+
(res && res.status !== 200) ||
100+
res.data.status != 'ok'
101+
) {
102+
throw 'Failed to send signature to HyperLiquid'
103+
}
104+
client.log(
105+
['Execute Steps: Signature sent to Hyperliquid', res.data],
106+
LogLevel.Verbose
107+
)
108+
return res.data
109+
}

packages/sdk/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ export {
1515
} from './simulateContract.js'
1616
export { safeStructuredClone } from './structuredClone.js'
1717
export { repeatUntilOk } from './repeatUntilOk.js'
18+
export { prepareHyperliquidSignatureStep } from './hyperliquid.js'

packages/sdk/src/utils/viemWallet.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,18 @@ export const adaptViemWallet = (wallet: WalletClient): AdaptedWallet => {
5656
})
5757
}
5858
} else if (signData.signatureKind === 'eip712') {
59-
client.log(['Execute Steps: Signing with eip712'], LogLevel.Verbose)
60-
signature = await wallet.signTypedData({
59+
const signatureData = {
6160
account: wallet.account as Account,
6261
domain: signData.domain as any,
6362
types: signData.types as any,
6463
primaryType: signData.primaryType,
6564
message: signData.value
66-
})
65+
}
66+
client.log(
67+
['Execute Steps: Signing with eip712', signatureData],
68+
LogLevel.Verbose
69+
)
70+
signature = await wallet.signTypedData(signatureData)
6771
}
6872
}
6973
return signature

packages/ui/src/components/common/TokenSelector/TokenSelector.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ const TokenSelector: FC<TokenSelectorProps> = ({
163163
(chain.vmType === 'evm' ||
164164
chain.vmType === 'suivm' ||
165165
chain.vmType === 'tvm' ||
166+
chain.vmType === 'hypevm' ||
166167
chain.id === solana.id ||
167168
chain.id === eclipse.id ||
168169
chain.id === bitcoin.id) &&

0 commit comments

Comments
 (0)