Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/sdk-router/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,3 +317,14 @@ transfer on HyperEVM. `sdk.synModuleSet.getBridgeDeliveryChainId(destChainId, tx
returns the verified destination (1337 for Core submission, 999 for fallback), or
`undefined` while pending. The existing boolean status reports completion of either
outcome. Core submission does not independently verify the subsequent balance update.

### HyperCore USDC deposits

`bridgeV2` requests Relay deposits to HyperCore Perps from Relay-supported EVM
origins, including HyperEVM. Pass `HYPERCORE_CHAIN_ID` (1337) and
`HYPERCORE_USDC_ADDRESS`
(`0x00000000000000000000000000000000`) as the destination; no HyperCore RPC
provider is needed. Output amounts use 8 decimals. Relay receives the selected
source token as the origin currency and quotes any conversion needed. A route is
available when Relay returns a usable quote. `toRecipient` sets the credited
HyperCore address. Track completion through the `Relay` module.
3 changes: 3 additions & 0 deletions packages/sdk-router/src/constants/addresses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ export const RELAY_ADDRESS_MAP: AddressMap = generateAddressMap(
RELAY_EXCEPTION_MAP
)

// Relay's native USDC currency identifier on HyperCore (Perps).
export const HYPERCORE_USDC_ADDRESS = '0x00000000000000000000000000000000'

/**
* USDT0 contract address for supported chains.
* Note: for Ethereum this is actually native USDT.
Expand Down
181 changes: 181 additions & 0 deletions packages/sdk-router/src/relay/relayModuleSet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { BigNumber, providers } from 'ethers'

import {
HYPERCORE_CHAIN_ID,
HYPERCORE_USDC_ADDRESS,
SupportedChainId,
TOKEN_ZAP_V1_ADDRESS_MAP,
} from '../constants'
import { EngineID, decodeZapData, getEmptyRoute } from '../swap'
import { TokenMetadataFetcher } from '../utils'
import * as relayApi from './api'
import { RelayModuleSet } from './relayModuleSet'

const base = 8453
const sourceToken = '0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb'
const sender = '0x1111111111111111111111111111111111111111'
const recipient = '0x2222222222222222222222222222222222222222'
const relayDeposit = '0x4cd00e387622c35bddb9b4c962c136462338bc31'
const relayCalldata = '0xe8017952'

const setup = () =>
new RelayModuleSet(
[base, 1, SupportedChainId.HYPEREVM].map((chainId) => ({
chainId,
provider: new providers.StaticJsonRpcProvider(
'http://localhost:1',
chainId
),
}))
)

afterEach(() => jest.restoreAllMocks())

it('passes the selected source token to Relay for HyperCore and other destinations', async () => {
const relay = setup()
const coreCandidates = await relay.getBridgeTokenCandidates({
fromChainId: base,
toChainId: HYPERCORE_CHAIN_ID,
fromToken: sourceToken,
toToken: HYPERCORE_USDC_ADDRESS,
})
expect(coreCandidates).toEqual([
{
originChainId: base,
destChainId: HYPERCORE_CHAIN_ID,
originToken: sourceToken,
destToken: HYPERCORE_USDC_ADDRESS,
},
])
expect(
await relay.getBridgeTokenCandidates({
fromChainId: base,
toChainId: HYPERCORE_CHAIN_ID,
fromToken: sourceToken,
toToken: sourceToken,
})
).toEqual([])
expect(
await relay.getBridgeTokenCandidates({
fromChainId: base,
toChainId: 1,
fromToken: sourceToken,
})
).toEqual([expect.objectContaining({ originToken: sourceToken })])
expect(
await relay.getBridgeTokenCandidates({
fromChainId: SupportedChainId.HYPEREVM,
toChainId: HYPERCORE_CHAIN_ID,
fromToken: '0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb',
toToken: HYPERCORE_USDC_ADDRESS,
})
).toEqual([
expect.objectContaining({
originChainId: SupportedChainId.HYPEREVM,
destChainId: HYPERCORE_CHAIN_ID,
originToken: '0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb',
}),
])
})

it('quotes one HyperCore deposit step for the requested recipient', async () => {
const relay = setup()
const [bridgeToken] = await relay.getBridgeTokenCandidates({
fromChainId: base,
toChainId: HYPERCORE_CHAIN_ID,
fromToken: sourceToken,
toToken: HYPERCORE_USDC_ADDRESS,
})
const quoteSpy = jest.spyOn(relayApi, 'getQuote').mockResolvedValue({
steps: [
{
id: 'deposit',
kind: 'transaction',
items: [
{
data: {
from: TOKEN_ZAP_V1_ADDRESS_MAP[base],
to: relayDeposit,
data: relayCalldata,
value: '0',
chainId: base,
},
},
],
},
],
requestId: 'test',
details: {
currencyIn: {} as relayApi.QuoteResponse['details']['currencyIn'],
currencyOut: {
currency: {
chainId: HYPERCORE_CHAIN_ID,
address: HYPERCORE_USDC_ADDRESS,
symbol: 'USDC',
name: 'USDC (Perps)',
decimals: 8,
},
amount: '9996999900',
amountFormatted: '99.969999',
amountUsd: '99.969999',
minimumAmount: '9987002900',
},
timeEstimate: 60,
},
} as relayApi.QuoteResponse)
const originSwapRoute = {
...getEmptyRoute(EngineID.NoOp),
chainId: base,
fromToken: sourceToken,
toToken: bridgeToken.originToken,
fromAmount: BigNumber.from('100000000000000000000'),
expectedToAmount: BigNumber.from('100000000000000000000'),
}
const route = await relay.getBridgeRouteV2({
bridgeToken,
originSwapRoute,
toToken: HYPERCORE_USDC_ADDRESS,
fromSender: sender,
toRecipient: recipient,
})
expect(quoteSpy).toHaveBeenCalledWith(
expect.objectContaining({
user: TOKEN_ZAP_V1_ADDRESS_MAP[base],
originCurrency: bridgeToken.originToken,
destinationChainId: HYPERCORE_CHAIN_ID,
destinationCurrency: HYPERCORE_USDC_ADDRESS,
recipient,
refundTo: sender,
useReceiver: true,
explicitDeposit: true,
usePermit: false,
})
)
expect(route?.expectedToAmount.toString()).toBe('9996999900')
expect(route?.minToAmount.toString()).toBe('9987002900')
expect(decodeZapData(route?.zapData ?? '0x')).toEqual(
expect.objectContaining({
target: relayDeposit,
payload: relayCalldata,
})
)
})

it('uses Relay status and 8-decimal native USDC without a HyperCore provider', async () => {
const relay = setup()
expect(relay.getModule(HYPERCORE_CHAIN_ID)).toBeDefined()
const fetcher = new TokenMetadataFetcher({})
expect(
await fetcher.getTokenDecimals(HYPERCORE_CHAIN_ID, HYPERCORE_USDC_ADDRESS)
).toBe(8)
jest.spyOn(relayApi, 'getRequests').mockResolvedValue({
requests: [
{
id: 'test',
status: relayApi.Status.Success,
data: { inTxs: [{ hash: '0x1234' }] },
},
],
})
expect(await relay.getBridgeTxStatus(HYPERCORE_CHAIN_ID, '0x1234')).toBe(true)
})
45 changes: 38 additions & 7 deletions packages/sdk-router/src/relay/relayModuleSet.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { Zero } from '@ethersproject/constants'
import { AddressZero, Zero } from '@ethersproject/constants'
import { BigNumber } from 'ethers'

import { RELAY_ADDRESS_MAP, TOKEN_ZAP_V1_ADDRESS_MAP } from '../constants'
import {
HYPERCORE_CHAIN_ID,
HYPERCORE_USDC_ADDRESS,
RELAY_ADDRESS_MAP,
TOKEN_ZAP_V1_ADDRESS_MAP,
} from '../constants'
import {
BridgeRoute,
BridgeRouteV2,
Expand All @@ -18,6 +23,7 @@ import { ChainProvider } from '../router'
import {
ETH_NATIVE_TOKEN_ADDRESS,
isNativeToken,
isSameAddress,
logExecutionTime,
} from '../utils'
import {
Expand Down Expand Up @@ -50,6 +56,11 @@ export class RelayModuleSet extends SynapseModuleSet {
}
this.modules[chainId] = new RelayModule(chainId, address)
})
// HyperCore is a Relay destination, but has no EVM provider or contract.
this.modules[HYPERCORE_CHAIN_ID] = new RelayModule(
HYPERCORE_CHAIN_ID,
AddressZero
)
}

public getModule(chainId: number): SynapseModule | undefined {
Expand All @@ -71,17 +82,25 @@ export class RelayModuleSet extends SynapseModuleSet {
toToken,
fromToken,
}: GetBridgeTokenCandidatesParameters): Promise<BridgeTokenCandidate[]> {
const hyperCore = toChainId === HYPERCORE_CHAIN_ID
// Check that both chains are supported
if (!this.getModule(fromChainId) || !this.getModule(toChainId)) {
if (
!this.getModule(fromChainId) ||
!this.getModule(toChainId) ||
(hyperCore && toToken !== undefined && !this.isHyperCoreUsdc(toToken))
) {
return []
}
// Relay supports origin swaps natively
return [
{
originChainId: fromChainId,
destChainId: toChainId,
originToken: fromToken,
destToken: toToken ?? ETH_NATIVE_TOKEN_ADDRESS,
destToken:
toToken ??
(toChainId === HYPERCORE_CHAIN_ID
? HYPERCORE_USDC_ADDRESS
: ETH_NATIVE_TOKEN_ADDRESS),
},
]
}
Expand All @@ -91,15 +110,23 @@ export class RelayModuleSet extends SynapseModuleSet {
params: GetBridgeRouteV2Parameters
): Promise<BridgeRouteV2 | undefined> {
const tokenZap = TOKEN_ZAP_V1_ADDRESS_MAP[params.bridgeToken.originChainId]
if (!this.validateBridgeRouteV2Params(params) || !tokenZap) {
if (
!this.validateBridgeRouteV2Params(params) ||
!tokenZap ||
(params.bridgeToken.destChainId === HYPERCORE_CHAIN_ID &&
!this.isHyperCoreUsdc(params.toToken))
) {
return undefined
}
const quoteRequest: QuoteRequest = {
user: tokenZap,
originChainId: params.bridgeToken.originChainId,
destinationChainId: params.bridgeToken.destChainId,
originCurrency: addressToCurrency(params.originSwapRoute.toToken),
destinationCurrency: addressToCurrency(params.toToken),
destinationCurrency:
params.bridgeToken.destChainId === HYPERCORE_CHAIN_ID
? HYPERCORE_USDC_ADDRESS
: addressToCurrency(params.toToken),
amount: params.originSwapRoute.expectedToAmount.toString(),
tradeType: TradeType.ExactInput,
recipient: params.toRecipient ?? USER_SIMULATED_ADDRESS,
Expand Down Expand Up @@ -201,4 +228,8 @@ export class RelayModuleSet extends SynapseModuleSet {
payload: relayData.data,
})
}

private isHyperCoreUsdc(token: string): boolean {
return isSameAddress(token, HYPERCORE_USDC_ADDRESS)
}
}
13 changes: 12 additions & 1 deletion packages/sdk-router/src/utils/TokenMetadataFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { Provider } from '@ethersproject/abstract-provider'
import { Contract } from '@ethersproject/contracts'

import { isNativeToken } from './addressUtils'
import { HYPERCORE_CHAIN_ID, SYN_ADDRESS_MAP } from '../constants'
import {
HYPERCORE_CHAIN_ID,
HYPERCORE_USDC_ADDRESS,
SYN_ADDRESS_MAP,
} from '../constants'
import { logger } from './logger'
import erc20ABI from '../abi/IERC20Metadata.json'
import { marshallChainToken } from '../rfq/ticker'
Expand Down Expand Up @@ -34,6 +38,13 @@ export class TokenMetadataFetcher {
chainId: number,
token: string
): Promise<number> {
// Relay represents HyperCore's native USDC balance in 8-decimal units.
if (
chainId === HYPERCORE_CHAIN_ID &&
token.toLowerCase() === HYPERCORE_USDC_ADDRESS
) {
return 8
}
// HyperCore SYN uses its native token identifier and 8-decimal balance units.
if (
chainId === HYPERCORE_CHAIN_ID &&
Expand Down
2 changes: 1 addition & 1 deletion packages/synapse-interface/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,4 @@ the Goldsky secret; without its QuickNode binding, the proxy returns HTTP 500.

### Routes

SYN uses the existing bridge UI for Ethereum ↔ HyperEVM and Ethereum → HyperCore. Select **Hyperliquid** for HyperCore (non-EVM routing identifier `1337`); HyperEVM is `999`. HyperCore SYN uses its native 16-byte token ID and 8-decimal amounts. HyperCore recipients must already be activated. SYN uses the composer in one source transaction, while USDC retains its Arbitrum deposit flow. Module pauses can target `SYN`, including destination `1337`. See the [SDK integration notes](../sdk-router/README.md#syn-bridging) for quote units, fees, and tracking.
SYN uses the existing bridge UI for Ethereum ↔ HyperEVM and Ethereum → HyperCore. Select **Hyperliquid** for HyperCore (non-EVM routing identifier `1337`); HyperEVM is `999`. HyperCore SYN uses its native 16-byte token ID and 8-decimal amounts. HyperCore recipients must already be activated. SYN uses the composer in one source transaction, while USDC uses Relay directly to Hyperliquid Perps. Module pauses can target `SYN`, including destination `1337`. See the [SDK integration notes](../sdk-router/README.md#syn-bridging) for quote units, fees, and tracking.
Original file line number Diff line number Diff line change
Expand Up @@ -91,28 +91,20 @@ describe('getPendingBridgeTransactionTrackingData', () => {
})
})

it('keeps saved Hyperliquid deposits working without an explicit tracked timestamp', () => {
const trackedTransaction = getPendingBridgeTransactionTrackingData(
{
id: 333,
originChain: mockChain,
originToken: mockToken,
originValue: '5',
destinationChain: { ...HYPERLIQUID, id: 998 },
transactionHash: '0xhyper',
isSubmitted: true,
},
zeroAddress
)

expect(trackedTransaction).toMatchObject({
originTxHash: '0xhyper',
destinationChain: HYPERLIQUID,
estimatedTime: 0,
timestamp: 333,
bridgeModuleName: '',
routerAddress: zeroAddress,
destinationToken: mockToken,
})
it('does not promote legacy Hyperliquid deposits without a tracked timestamp', () => {
expect(
getPendingBridgeTransactionTrackingData(
{
id: 333,
originChain: mockChain,
originToken: mockToken,
originValue: '5',
destinationChain: { ...HYPERLIQUID, id: 998 },
transactionHash: '0xhyper',
isSubmitted: true,
},
zeroAddress
)
).toBeNull()
})
})
Loading
Loading