-
Notifications
You must be signed in to change notification settings - Fork 177
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[skip ci] wip: chainflip swapper #8049
base: develop
Are you sure you want to change the base?
Changes from 83 commits
f9cf7a9
b0fef83
6f4b2cd
ae0d083
56a4003
53cfa9f
8515f51
c4131d5
159650a
cca5688
09b820d
9f60594
8bab08e
09b9b02
e6a43ac
9c2f447
20216d6
d5b9fee
2861ba3
57e08d1
2207d5b
56697f1
9507b73
668856f
1765b86
7b3cc26
a6d42f9
dfa6a3a
ef79b52
fea992c
ee0d7bd
572135d
089fff0
1d25ce9
a1661cd
72f1c7e
3fd7b62
519546b
799320f
708a826
8a17ba6
12ae967
c090d43
34ea201
b253f4b
630f8ef
e931c5b
8f4b47a
8662c0d
3517d98
ae28709
1251fa3
15a0499
c3c1745
1e57f3d
4a227c5
22372ed
0c309d4
3997245
2cce1db
5626b31
5b4a64d
f69ae34
a906812
4924c16
1fd442b
dccd703
206a337
7cfe7c2
c2def52
5ef0169
2e77080
c32b370
9a10db4
19f5fcd
3111366
820283b
8c25692
ddd1339
1c1b711
3172d80
388b4dc
267eb4f
ae0dd35
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import type { AssetId } from '@shapeshiftoss/caip' | ||
import type { BTCSignTx } from '@shapeshiftoss/hdwallet-core' | ||
import type { Asset } from '@shapeshiftoss/types' | ||
|
||
import type { BuyAssetBySellIdInput, Swapper, UtxoTransactionExecutionProps } from '../../types' | ||
import { executeEvmTransaction } from '../../utils' | ||
import { CHAINFLIP_SUPPORTED_CHAIN_IDS } from './constants' | ||
import { isSupportedAssetId } from './utils/helpers' | ||
|
||
export const chainflipSwapper: Swapper = { | ||
executeEvmTransaction, | ||
|
||
executeUtxoTransaction: async ( | ||
txToSign: BTCSignTx, | ||
{ signAndBroadcastTransaction }: UtxoTransactionExecutionProps, | ||
): Promise<string> => { | ||
return await signAndBroadcastTransaction(txToSign) | ||
}, | ||
|
||
filterAssetIdsBySellable: (assets: Asset[]): Promise<AssetId[]> => { | ||
return Promise.resolve( | ||
assets | ||
.filter(asset => CHAINFLIP_SUPPORTED_CHAIN_IDS.sell.includes(asset.chainId)) | ||
.filter(asset => isSupportedAssetId(asset.chainId, asset.assetId)) | ||
.map(asset => asset.assetId), | ||
) | ||
}, | ||
|
||
filterBuyAssetsBySellAssetId: (input: BuyAssetBySellIdInput): Promise<AssetId[]> => { | ||
return Promise.resolve( | ||
input.assets | ||
.filter(asset => CHAINFLIP_SUPPORTED_CHAIN_IDS.buy.includes(asset.chainId)) | ||
.filter(asset => isSupportedAssetId(asset.chainId, asset.assetId)) | ||
.map(asset => asset.assetId), | ||
) | ||
}, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { | ||
arbitrumAssetId, | ||
type AssetId, | ||
btcAssetId, | ||
type ChainId, | ||
ethAssetId, | ||
flipAssetId, | ||
solAssetId, | ||
usdcAssetId, | ||
usdcOnArbitrumOneAssetId, | ||
usdcOnSolanaAssetId, | ||
usdtAssetId, | ||
} from '@shapeshiftoss/caip' | ||
import type { Asset } from '@shapeshiftoss/types' | ||
import { KnownChainIds } from '@shapeshiftoss/types' | ||
|
||
import type { SupportedChainIds, SwapSource } from '../../types' | ||
import { SwapperName } from '../../types' | ||
|
||
export const CHAINFLIP_REGULAR_QUOTE = 'regular' | ||
export const CHAINFLIP_DCA_QUOTE = 'dca' | ||
export const CHAINFLIP_BAAS_COMMISSION = 5 | ||
|
||
export const ChainflipSupportedChainIds = [ | ||
KnownChainIds.EthereumMainnet, | ||
KnownChainIds.ArbitrumMainnet, | ||
KnownChainIds.BitcoinMainnet, | ||
KnownChainIds.SolanaMainnet, | ||
] as const | ||
|
||
export type ChainflipSupportedChainId = (typeof ChainflipSupportedChainIds)[number] | ||
|
||
export const ChainflipSupportedAssetIdsByChainId: Partial<Record<KnownChainIds, AssetId[]>> = { | ||
[KnownChainIds.EthereumMainnet]: [ethAssetId, flipAssetId, usdcAssetId, usdtAssetId], | ||
[KnownChainIds.ArbitrumMainnet]: [arbitrumAssetId, usdcOnArbitrumOneAssetId], | ||
[KnownChainIds.BitcoinMainnet]: [btcAssetId], | ||
[KnownChainIds.SolanaMainnet]: [solAssetId, usdcOnSolanaAssetId], | ||
} | ||
|
||
export const chainIdToChainflipNetwork: Partial<Record<ChainId, string>> = { | ||
[KnownChainIds.EthereumMainnet]: 'eth', | ||
[KnownChainIds.ArbitrumMainnet]: 'arb', | ||
[KnownChainIds.BitcoinMainnet]: 'btc', | ||
[KnownChainIds.SolanaMainnet]: 'sol', | ||
} | ||
|
||
export const CHAINFLIP_SUPPORTED_CHAIN_IDS: SupportedChainIds = { | ||
sell: ChainflipSupportedChainIds as unknown as ChainId[], | ||
buy: ChainflipSupportedChainIds as unknown as ChainId[], | ||
} | ||
|
||
export const CHAINFLIP_SWAP_SOURCE: SwapSource = SwapperName.Chainflip | ||
export const CHAINFLIP_BOOST_SWAP_SOURCE: SwapSource = `${SwapperName.Chainflip} • Boost` | ||
export const CHAINFLIP_DCA_SWAP_SOURCE: SwapSource = `${SwapperName.Chainflip} • DCA` | ||
export const CHAINFLIP_DCA_BOOST_SWAP_SOURCE: SwapSource = `${SwapperName.Chainflip} • DCA • Boost` | ||
|
||
export const usdcAsset: Asset = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A fyi, the reason I copied this here is because the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's absolutely fine @CumpsD! Asking because we may be able to simply use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Network fee is always USDC, it is the 0.10% buy & burn fee the chainflip network uses. Liquidity is the LP fee, which I would have to double check but I believe the second leg is always USDC. It's possible this Will double check! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, so the second leg seems to be always usdc.eth for the liquidity fee. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Happy to keep the second branch for the sake of paranoia. Tyvm for disambiguating this! Looks like indeed, we need usdc as a static asset then, which means keeping it here as a const is absolutely fine. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Pretty much the reason I added all branches too :D |
||
assetId: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', | ||
chainId: KnownChainIds.EthereumMainnet, | ||
color: '#2373CB', | ||
explorer: 'https://etherscan.io', | ||
explorerAddressLink: 'https://etherscan.io/address/', | ||
explorerTxLink: 'https://etherscan.io/tx/', | ||
icon: 'https://rawcdn.githack.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', | ||
name: 'USDC on Ethereum', | ||
precision: 6, | ||
relatedAssetKey: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', | ||
symbol: 'USDC', | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
import { fromAssetId, fromChainId } from '@shapeshiftoss/caip' | ||
import type { GetFeeDataInput } from '@shapeshiftoss/chain-adapters' | ||
import { FeeDataKey } from '@shapeshiftoss/chain-adapters' | ||
import type { EvmChainId } from '@shapeshiftoss/types' | ||
import { TxStatus } from '@shapeshiftoss/unchained-client' | ||
import type { AxiosError } from 'axios' | ||
import type { InterpolationOptions } from 'node-polyglot' | ||
|
||
import type { EvmTransactionRequest, GetUnsignedEvmTransactionArgs, SwapperApi } from '../../types' | ||
import { isExecutableTradeQuote, isExecutableTradeStep, isToken } from '../../utils' | ||
import { CHAINFLIP_BAAS_COMMISSION, chainIdToChainflipNetwork } from './constants' | ||
import type { ChainflipBaasSwapDepositAddress } from './models/ChainflipBaasSwapDepositAddress' | ||
import { getTradeQuote, getTradeRate } from './swapperApi/getTradeQuote' | ||
import { getUnsignedUtxoTransaction } from './swapperApi/getUnsignedUtxoTransaction' | ||
import type { ChainFlipStatus } from './types' | ||
import { chainflipService } from './utils/chainflipService' | ||
import { getLatestChainflipStatusMessage } from './utils/getLatestChainflipStatusMessage' | ||
|
||
// Persists the ID so we can look it up later when checking the status | ||
const tradeQuoteMetadata: Map<string, ChainflipBaasSwapDepositAddress> = new Map() | ||
|
||
export const chainflipApi: SwapperApi = { | ||
getTradeQuote, | ||
getTradeRate, | ||
getUnsignedEvmTransaction: async ({ | ||
chainId, | ||
from, | ||
tradeQuote, | ||
assertGetEvmChainAdapter, | ||
config, | ||
supportsEIP1559, | ||
}: GetUnsignedEvmTransactionArgs): Promise<EvmTransactionRequest> => { | ||
if (!isExecutableTradeQuote(tradeQuote)) throw Error('Unable to execute trade') | ||
|
||
const brokerUrl = config.REACT_APP_CHAINFLIP_API_URL | ||
const apiKey = config.REACT_APP_CHAINFLIP_API_KEY | ||
|
||
const step = tradeQuote.steps[0] | ||
const sellChainflipChainKey = `${step.sellAsset.symbol.toLowerCase()}.${ | ||
chainIdToChainflipNetwork[step.sellAsset.chainId] | ||
}` | ||
const buyChainflipChainKey = `${step.buyAsset.symbol.toLowerCase()}.${ | ||
chainIdToChainflipNetwork[step.buyAsset.chainId] | ||
}` | ||
|
||
// Subtract the BaaS fee to end up at the final displayed commissionBps | ||
let serviceCommission = parseInt(tradeQuote.affiliateBps) - CHAINFLIP_BAAS_COMMISSION | ||
if (serviceCommission < 0) serviceCommission = 0 | ||
|
||
const maybeSwapResponse = await chainflipService.get<ChainflipBaasSwapDepositAddress>( | ||
`${brokerUrl}/swap` + | ||
`?apiKey=${apiKey}` + | ||
`&sourceAsset=${sellChainflipChainKey}` + | ||
`&destinationAsset=${buyChainflipChainKey}` + | ||
`&destinationAddress=${tradeQuote.receiveAddress}` + | ||
`&boostFee=10` + | ||
// TODO: Calculate minprice based on tradeQuote.slippageTolerancePercentageDecimal, step.sellAmountIncludingProtocolFeesCryptoBaseUnit, step.buyAmountAfterFeesCryptoBaseUnit | ||
// `&minimumPrice=` + | ||
// `&refundAddress=${from}` + | ||
// `&retryDurationInBlocks=10` + | ||
`&commissionBps=${serviceCommission}`, | ||
|
||
// TODO: Below is the reference code of Chainflip to calculate the minPrice parameter | ||
// const tolerance = new BigNumber(params.slippageTolerancePercent); | ||
// const estimatedPrice = new BigNumber(quote.estimatedPrice); | ||
// minPrice = estimatedPrice | ||
// .times(new BigNumber(100).minus(tolerance).dividedBy(100)) | ||
// .toFixed(assetConstants[destAsset].decimals); | ||
|
||
// TODO: For DCA swaps we need to add the numberOfChunks/chunkIntervalBlocks parameters | ||
) | ||
|
||
if (maybeSwapResponse.isErr()) { | ||
const error = maybeSwapResponse.unwrapErr() | ||
const cause = error.cause as AxiosError<any, any> | ||
throw Error(cause.response!.data.detail) | ||
} | ||
|
||
const { data: swapResponse } = maybeSwapResponse.unwrap() | ||
|
||
if (!swapResponse.id) throw Error('missing swap ID') | ||
|
||
tradeQuoteMetadata.set(tradeQuote.id, swapResponse) | ||
|
||
const depositAddress = swapResponse.address! | ||
const { assetReference } = fromAssetId(step.sellAsset.assetId) | ||
const isTokenSend = isToken(step.sellAsset.assetId) | ||
|
||
const adapter = assertGetEvmChainAdapter(step.sellAsset.chainId) | ||
|
||
const getFeeDataInput: GetFeeDataInput<EvmChainId> = { | ||
to: depositAddress, | ||
value: step.sellAmountIncludingProtocolFeesCryptoBaseUnit, | ||
chainSpecific: { | ||
from, | ||
contractAddress: isTokenSend ? assetReference : undefined, | ||
data: undefined, | ||
}, | ||
sendMax: false, | ||
} | ||
const feeData = await adapter.getFeeData(getFeeDataInput) | ||
const fees = feeData[FeeDataKey.Average] | ||
|
||
if (!isExecutableTradeStep(step)) throw Error('Unable to execute trade step') | ||
|
||
const unsignedTxInput = await adapter.buildSendApiTransaction({ | ||
to: depositAddress, | ||
from, | ||
value: step.sellAmountIncludingProtocolFeesCryptoBaseUnit, | ||
accountNumber: step.accountNumber, | ||
chainSpecific: { | ||
gasLimit: fees.chainSpecific.gasLimit, | ||
contractAddress: isTokenSend ? assetReference : undefined, | ||
...(supportsEIP1559 | ||
? { | ||
maxFeePerGas: fees.chainSpecific.maxFeePerGas!, | ||
maxPriorityFeePerGas: fees.chainSpecific.maxPriorityFeePerGas!, | ||
} | ||
: { | ||
gasPrice: fees.chainSpecific.gasPrice, | ||
}), | ||
}, | ||
}) | ||
|
||
return { | ||
chainId: Number(fromChainId(chainId).chainReference), | ||
data: unsignedTxInput.data, | ||
to: unsignedTxInput.to, | ||
from, | ||
value: unsignedTxInput.value, | ||
gasLimit: unsignedTxInput.gasLimit, | ||
maxFeePerGas: unsignedTxInput.maxFeePerGas, | ||
maxPriorityFeePerGas: unsignedTxInput.maxPriorityFeePerGas, | ||
gasPrice: unsignedTxInput.gasPrice, | ||
} | ||
}, | ||
getUnsignedUtxoTransaction, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should come back to this module as well since we'll need to add to trade quote meta for status polling |
||
checkTradeStatus: async ({ | ||
config, | ||
quoteId, | ||
}): Promise<{ | ||
status: TxStatus | ||
buyTxHash: string | undefined | ||
message: string | [string, InterpolationOptions] | undefined | ||
}> => { | ||
const swap = tradeQuoteMetadata.get(quoteId) | ||
if (!swap) throw Error(`missing trade quote metadata for quoteId ${quoteId}`) | ||
// Note, the swapId isn't the quoteId - we set the swapId at pre-execution time, when getting the receive addy and instantiating a flip swap | ||
const swapId = swap.id | ||
|
||
const brokerUrl = config.REACT_APP_CHAINFLIP_API_URL | ||
const apiKey = config.REACT_APP_CHAINFLIP_API_KEY | ||
|
||
const maybeStatusResponse = await chainflipService.get<ChainFlipStatus>( | ||
`${brokerUrl}/status-by-id?apiKey=${apiKey}&swapId=${swapId}`, | ||
) | ||
|
||
if (maybeStatusResponse.isErr()) { | ||
return { | ||
buyTxHash: undefined, | ||
status: TxStatus.Unknown, | ||
message: undefined, | ||
} | ||
} | ||
|
||
const { data: statusResponse } = maybeStatusResponse.unwrap() | ||
const { | ||
status: { swapEgress }, | ||
} = statusResponse | ||
|
||
// Assume no outbound Tx is a pending Tx | ||
if (!swapEgress?.transactionReference) { | ||
return { | ||
buyTxHash: undefined, | ||
status: TxStatus.Pending, | ||
message: getLatestChainflipStatusMessage(statusResponse), | ||
} | ||
} | ||
|
||
// Assume as soon as we have an outbound Tx, the swap is complete. | ||
// Chainflip waits for 3 confirmations to assume complete (vs. 1 for us), which is turbo long. | ||
return { | ||
buyTxHash: swapEgress.transactionReference, | ||
status: TxStatus.Confirmed, | ||
message: undefined, | ||
} | ||
}, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Give me a ping to set up Shapeshift's API key when going forward, this is currently mine for testing! :) The best person to reach out to me for this would be the one who will be responsible for redeeming affiliate rewards in the future. The one controlling the wallet to sign Chainflip transactions that is (redeeming is a chainflip tx)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good @CumpsD! Is there any reason we should wait for go-live to get an actual API key here, or can we already get one? Will get the convs going already
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The key in there right now is actually the one from my swapping Discord bot https://swappy.be/ :) It works, you can test with it, but it charges 0.15% fee (0.05 BaaS + 0.10 Swappy). I can make you a temp one that does only 0.05 (BaaS fee + 0% partner) if you want. You can go live with that as well and swap it out to the Shapeshift one afterwards. lmk
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@CumpsD just to be sure, we can parametrize fee bps right i.e this isn't tied to the API key used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Current one for dev is absolutely fine, so long as we go live on prod (and ideally on develop as a follow-up) with a proper API key 🙏🏽
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you can pass in
commissionBps
for both quote and opening a deposit channel (I think I added that in the PR already even! taking in affiliateBps from shapeshift)