-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(sdk-router): Gas.zip module [SYN-37] (#3528)
* feat: scaffold GasZip module * refactor: isolate logging utils * refactor: isolate API utils * feat: bridge, status first impl * feat: supported chain ID check * feat: `isSameAddress` * feat: gas.zip quotes * feat: no-op slippage * feat: add gas.zip module * fix: sanitize expected amount from gas.zip * fix: bridge module name * feat: prioritize gas.zip quotes for testing [REVERT LATER] * feat: regenerate bridge map with gas.zip [SYN-38] * feat: don't show slippage for gas.zip for now [SYN-39] * feat: add BNB, BERA and other native tokens * feat: add HyperEVM to the list of chains * feat: add HYPE to bridge map * chore: add hyperEVM to spellcheck * chore: clean up TODOs * fix: remove trailing slash * fix: remove unsupported gas.zip assets [SYN-53] * Revert "feat: prioritize gas.zip quotes for testing [REVERT LATER]" This reverts commit 3072d75. * feat: track gas.zip refund status * fix: gas.zip refund tracking * refactor: logs cleanup, better typing
- Loading branch information
1 parent
b3c1e93
commit 34db356
Showing
26 changed files
with
956 additions
and
69 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,6 +44,7 @@ | |
"gitbook", | ||
"gorm", | ||
"headlessui", | ||
"hyperevm", | ||
"hyperliquid", | ||
"incentivized", | ||
"interchain", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
import { BigNumber } from 'ethers' | ||
import { Zero } from '@ethersproject/constants' | ||
|
||
import { BigintIsh } from '../constants' | ||
import { getWithTimeout } from '../utils/api' | ||
|
||
const GAS_ZIP_API_URL = 'https://backend.gas.zip/v2' | ||
const GAS_ZIP_API_TIMEOUT = 2000 | ||
|
||
interface Transaction { | ||
status: string | ||
} | ||
|
||
interface TransactionStatusData { | ||
txs: Transaction[] | ||
} | ||
|
||
interface Chains { | ||
chains: [ | ||
{ | ||
name: string | ||
chain: number // native chain id | ||
short: number // unique Gas.zip id | ||
gas: string // gas usage of a simple transfer | ||
gwei: string // current gas price | ||
bal: string // balance of the Gas.zip reloader | ||
rpcs: string[] | ||
symbol: string | ||
price: number | ||
} | ||
] | ||
} | ||
|
||
interface CalldataQuoteResponse { | ||
calldata: string | ||
quotes: { | ||
chain: number | ||
expected: string | ||
gas: string | ||
speed: number | ||
usd: number | ||
}[] | ||
} | ||
|
||
export type GasZipQuote = { | ||
amountOut: BigNumber | ||
calldata: string | ||
} | ||
|
||
const EMPTY_GAS_ZIP_QUOTE: GasZipQuote = { | ||
amountOut: Zero, | ||
calldata: '0x', | ||
} | ||
|
||
export const getGasZipTxStatus = async (txHash: string): Promise<boolean> => { | ||
const response = await getWithTimeout( | ||
'Gas.Zip API', | ||
`${GAS_ZIP_API_URL}/search/${txHash}`, | ||
GAS_ZIP_API_TIMEOUT | ||
) | ||
if (!response) { | ||
return false | ||
} | ||
const data: TransactionStatusData = await response.json() | ||
return data.txs.length > 0 && data.txs[0].status === 'CONFIRMED' | ||
} | ||
|
||
export const getChainIds = async (): Promise<number[]> => { | ||
const response = await getWithTimeout( | ||
'Gas.Zip API', | ||
`${GAS_ZIP_API_URL}/chains`, | ||
GAS_ZIP_API_TIMEOUT | ||
) | ||
if (!response) { | ||
return [] | ||
} | ||
const data: Chains = await response.json() | ||
return data.chains.map((chain) => chain.chain) | ||
} | ||
|
||
export const getGasZipQuote = async ( | ||
originChainId: number, | ||
destChainId: number, | ||
amount: BigintIsh, | ||
to: string, | ||
from?: string | ||
): Promise<GasZipQuote> => { | ||
const response = await getWithTimeout( | ||
'Gas.Zip API', | ||
`${GAS_ZIP_API_URL}/quotes/${originChainId}/${amount}/${destChainId}`, | ||
GAS_ZIP_API_TIMEOUT, | ||
{ | ||
from, | ||
to, | ||
} | ||
) | ||
if (!response) { | ||
return EMPTY_GAS_ZIP_QUOTE | ||
} | ||
const data: CalldataQuoteResponse = await response.json() | ||
if (data.quotes.length === 0 || !data.quotes[0].expected) { | ||
return EMPTY_GAS_ZIP_QUOTE | ||
} | ||
return { | ||
amountOut: BigNumber.from(data.quotes[0].expected.toString()), | ||
calldata: data.calldata, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { Provider } from '@ethersproject/abstract-provider' | ||
import { BigNumber, PopulatedTransaction } from 'ethers' | ||
import invariant from 'tiny-invariant' | ||
|
||
import { Query, SynapseModule } from '../module' | ||
import { BigintIsh } from '../constants' | ||
import { isNativeToken } from '../utils/handleNativeToken' | ||
import { getGasZipQuote, getGasZipTxStatus } from './api' | ||
import { isSameAddress } from '../utils/addressUtils' | ||
|
||
export class GasZipModule implements SynapseModule { | ||
readonly address = '0x391E7C679d29bD940d63be94AD22A25d25b5A604' | ||
|
||
public readonly chainId: number | ||
public readonly provider: Provider | ||
|
||
constructor(chainId: number, provider: Provider) { | ||
invariant(chainId, 'CHAIN_ID_UNDEFINED') | ||
invariant(provider, 'PROVIDER_UNDEFINED') | ||
this.chainId = chainId | ||
this.provider = provider | ||
} | ||
|
||
/** | ||
* @inheritdoc SynapseModule.bridge | ||
*/ | ||
public async bridge( | ||
to: string, | ||
destChainId: number, | ||
token: string, | ||
amount: BigintIsh, | ||
originQuery: Query, | ||
destQuery: Query | ||
): Promise<PopulatedTransaction> { | ||
if (!isNativeToken(token)) { | ||
throw new Error('Non-native token not supported by gas.zip') | ||
} | ||
if (isSameAddress(to, destQuery.rawParams)) { | ||
return { | ||
to: this.address, | ||
value: BigNumber.from(amount), | ||
data: originQuery.rawParams, | ||
} | ||
} | ||
const quote = await getGasZipQuote(this.chainId, destChainId, amount, to) | ||
if (quote.amountOut.lt(destQuery.minAmountOut)) { | ||
throw new Error('Insufficient amount out') | ||
} | ||
return { | ||
to: this.address, | ||
value: BigNumber.from(amount), | ||
data: quote.calldata, | ||
} | ||
} | ||
|
||
/** | ||
* @inheritdoc SynapseModule.getSynapseTxId | ||
*/ | ||
public async getSynapseTxId(txHash: string): Promise<string> { | ||
return txHash | ||
} | ||
|
||
/** | ||
* @inheritdoc SynapseModule.getBridgeTxStatus | ||
*/ | ||
public async getBridgeTxStatus(synapseTxId: string): Promise<boolean> { | ||
return getGasZipTxStatus(synapseTxId) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
import { BigNumber } from 'ethers' | ||
import { Provider } from '@ethersproject/abstract-provider' | ||
import { AddressZero, Zero } from '@ethersproject/constants' | ||
|
||
import { | ||
BridgeRoute, | ||
createNoSwapQuery, | ||
FeeConfig, | ||
Query, | ||
SynapseModule, | ||
SynapseModuleSet, | ||
} from '../module' | ||
import { ChainProvider } from '../router' | ||
import { getChainIds, getGasZipQuote } from './api' | ||
import { GasZipModule } from './gasZipModule' | ||
import { isNativeToken } from '../utils/handleNativeToken' | ||
import { BigintIsh } from '../constants' | ||
|
||
const MEDIAN_TIME_GAS_ZIP = 30 | ||
|
||
export class GasZipModuleSet extends SynapseModuleSet { | ||
public readonly bridgeModuleName = 'Gas.zip' | ||
public readonly allEvents = [] | ||
|
||
public modules: { | ||
[chainId: number]: GasZipModule | ||
} | ||
public providers: { | ||
[chainId: number]: Provider | ||
} | ||
|
||
private cachedChainIds: number[] | ||
|
||
constructor(chains: ChainProvider[]) { | ||
super() | ||
this.modules = {} | ||
this.providers = {} | ||
this.cachedChainIds = [] | ||
chains.forEach(({ chainId, provider }) => { | ||
this.modules[chainId] = new GasZipModule(chainId, provider) | ||
this.providers[chainId] = provider | ||
}) | ||
} | ||
|
||
/** | ||
* @inheritdoc SynapseModuleSet.getModule | ||
*/ | ||
public getModule(chainId: number): SynapseModule | undefined { | ||
return this.modules[chainId] | ||
} | ||
|
||
/** | ||
* @inheritdoc SynapseModuleSet.getEstimatedTime | ||
*/ | ||
public getEstimatedTime(): number { | ||
return MEDIAN_TIME_GAS_ZIP | ||
} | ||
|
||
/** | ||
* @inheritdoc SynapseModuleSet.getGasDropAmount | ||
*/ | ||
public async getGasDropAmount(): Promise<BigNumber> { | ||
return Zero | ||
} | ||
|
||
/** | ||
* @inheritdoc SynapseModuleSet.getBridgeRoutes | ||
*/ | ||
public async getBridgeRoutes( | ||
originChainId: number, | ||
destChainId: number, | ||
tokenIn: string, | ||
tokenOut: string, | ||
amountIn: BigintIsh, | ||
originUserAddress?: string | ||
): Promise<BridgeRoute[]> { | ||
// Check that both chains are supported by gas.zip | ||
const supportedChainIds = await this.getChainIds() | ||
if ( | ||
!supportedChainIds.includes(originChainId) || | ||
!supportedChainIds.includes(destChainId) | ||
) { | ||
return [] | ||
} | ||
// Check that both tokens are native assets | ||
if (!isNativeToken(tokenIn) || !isNativeToken(tokenOut)) { | ||
return [] | ||
} | ||
const user = originUserAddress ?? AddressZero | ||
const quote = await getGasZipQuote( | ||
originChainId, | ||
destChainId, | ||
amountIn, | ||
user, | ||
user | ||
) | ||
// Check that non-zero amount is returned | ||
if (quote.amountOut.eq(Zero)) { | ||
return [] | ||
} | ||
// Save user address in the origin query raw params | ||
const originQuery = createNoSwapQuery(tokenIn, BigNumber.from(amountIn)) | ||
originQuery.rawParams = quote.calldata | ||
const destQuery = createNoSwapQuery(tokenOut, quote.amountOut) | ||
destQuery.rawParams = user | ||
const route: BridgeRoute = { | ||
originChainId, | ||
destChainId, | ||
originQuery, | ||
destQuery, | ||
bridgeToken: { | ||
symbol: 'NATIVE', | ||
token: tokenIn, | ||
}, | ||
bridgeModuleName: this.bridgeModuleName, | ||
} | ||
return [route] | ||
} | ||
|
||
/** | ||
* @inheritdoc SynapseModuleSet.getFeeData | ||
*/ | ||
public async getFeeData(): Promise<{ | ||
feeAmount: BigNumber | ||
feeConfig: FeeConfig | ||
}> { | ||
// There's no good way to determine the fee for gas.zip | ||
return { | ||
feeAmount: Zero, | ||
feeConfig: { | ||
bridgeFee: 0, | ||
minFee: BigNumber.from(0), | ||
maxFee: BigNumber.from(0), | ||
}, | ||
} | ||
} | ||
|
||
/** | ||
* @inheritdoc SynapseModuleSet.getDefaultPeriods | ||
*/ | ||
public getDefaultPeriods(): { | ||
originPeriod: number | ||
destPeriod: number | ||
} { | ||
// Deadline settings are not supported by gas.zip | ||
return { | ||
originPeriod: 0, | ||
destPeriod: 0, | ||
} | ||
} | ||
|
||
/** | ||
* @inheritdoc SynapseModuleSet.applySlippage | ||
*/ | ||
public applySlippage( | ||
originQueryPrecise: Query, | ||
destQueryPrecise: Query | ||
): { originQuery: Query; destQuery: Query } { | ||
// Slippage settings are not supported by gas.zip | ||
return { | ||
originQuery: originQueryPrecise, | ||
destQuery: destQueryPrecise, | ||
} | ||
} | ||
|
||
private async getChainIds(): Promise<number[]> { | ||
if (this.cachedChainIds.length === 0) { | ||
this.cachedChainIds = await getChainIds() | ||
} | ||
return this.cachedChainIds | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from './api' | ||
export * from './gasZipModule' | ||
export * from './gasZipModuleSet' |
Oops, something went wrong.