Skip to content
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

feat: consume unchained-client tx history in Osmosis swapper #5108

Closed
wants to merge 2 commits into from
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
selectTradeSlippagePercentageDecimal,
} from 'state/slices/tradeQuoteSlice/selectors'
import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice'
import { store, useAppDispatch, useAppSelector } from 'state/store'
import { useAppDispatch, useAppSelector } from 'state/store'

import { useAccountIds } from '../useAccountIds'

Expand Down Expand Up @@ -105,7 +105,6 @@ export const useTradeExecution = ({
wallet,
supportsEIP1559,
slippageTolerancePercentageDecimal,
getState: store.getState,
})
})
}, [
Expand Down
2 changes: 0 additions & 2 deletions src/lib/swapper/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type { TxStatus } from '@shapeshiftoss/unchained-client'
import type { Result } from '@sniptt/monads'
import type { Asset } from 'lib/asset-service'
import type { PartialRecord } from 'lib/utils'
import type { ReduxState } from 'state/reducer'
import type { AccountMetadata } from 'state/slices/portfolioSlice/portfolioSliceCommon'

import type { TradeQuoteDeps } from './types'
Expand Down Expand Up @@ -217,7 +216,6 @@ export type CheckTradeStatusInput = {
stepIndex: number
quoteSellAssetAccountId?: AccountId
quoteBuyAssetAccountId?: AccountId
getState: () => ReduxState
}

// a result containing all routes that were successfully generated, or an error in the case where
Expand Down
3 changes: 0 additions & 3 deletions src/lib/swapper/swappers/OsmosisSwapper/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,6 @@ export const osmosisApi: Swapper2Api = {
stepIndex,
quoteSellAssetAccountId,
quoteBuyAssetAccountId,
getState,
}): Promise<{ status: TxStatus; buyTxHash: string | undefined; message: string | undefined }> => {
try {
const quote = tradeQuoteMetadata.get(quoteId)
Expand All @@ -217,7 +216,6 @@ export const osmosisApi: Swapper2Api = {
const pollResult = await pollForCrossChainComplete({
initiatingChainTxid,
initiatingChainAccountId: stepSellAssetAccountId,
getState,
})
const status = pollResult === 'success' ? TxStatus.Confirmed : TxStatus.Failed

Expand All @@ -239,7 +237,6 @@ export const osmosisApi: Swapper2Api = {

const pollResult = await pollForComplete({
txid,
getState,
})

const status = pollResult === 'success' ? TxStatus.Confirmed : TxStatus.Failed
Expand Down
77 changes: 43 additions & 34 deletions src/lib/swapper/swappers/OsmosisSwapper/utils/poll.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,44 @@
import type { AccountId } from '@shapeshiftoss/caip'
import { cosmosChainId, fromAccountId, osmosisChainId, toAccountId } from '@shapeshiftoss/caip'
import { cosmosChainId, fromAccountId, osmosisChainId } from '@shapeshiftoss/caip'
import type { ChainAdapter } from '@shapeshiftoss/chain-adapters'
import { TxStatus } from '@shapeshiftoss/unchained-client'
import type { IbcMetadata } from '@shapeshiftoss/unchained-client/src/cosmossdk'
import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingleton'
import { SwapError, SwapErrorType } from 'lib/swapper/api'
import type { ReduxState } from 'state/reducer'
import { selectTxById, selectTxsByFilter } from 'state/slices/selectors'
import { deserializeTxIndex } from 'state/slices/txHistorySlice/utils'

// TODO: leverage chain-adapters websockets
export const pollForComplete = ({
txid,
getState,
}: {
txid: string
getState: () => ReduxState
}): Promise<string> => {
import type { OsmosisSupportedChainId } from './types'

export const pollForComplete = ({ txid }: { txid: string }): Promise<string> => {
return new Promise((resolve, reject) => {
const timeout = 300000 // 5 mins
const startTime = Date.now()
const interval = 5000 // 5 seconds

const poll = function () {
// TODO: this should just be a fetch to unchained
const tx = selectTxById(getState(), txid)
const poll = async function () {
const { accountId, txid: txHash } = deserializeTxIndex(txid)
const { account: pubkey } = fromAccountId(accountId)
const { chainId } = fromAccountId(accountId)
const chainAdapter = getChainAdapterManager().get(
chainId,
) as ChainAdapter<OsmosisSupportedChainId>
const txHistory = await chainAdapter.getTxHistory({ pubkey })

const tx = txHistory.transactions.find(tx => tx.txid === txHash)

if (tx?.status === TxStatus.Confirmed) {
resolve('success')
} else if (Date.now() - startTime > timeout) {
reject(
new SwapError(`Couldnt find tx ${txid}`, {
new SwapError(`Couldn't find tx ${txid}`, {
code: SwapErrorType.RESPONSE_ERROR,
}),
)
} else {
setTimeout(poll, interval)
}
}

poll()
})
}
Expand All @@ -43,50 +48,53 @@ export const pollForComplete = ({
// 2. MsgRecvPacket on the receiving chain i.e receive on destination chain from source chain
// While 1. can simply be polled for (which we do on the method above),
// the destination Tx needs to be picked by validators on the destination chain, and we don't know anything about said Tx in advance
// TODO(gomes): Now that we're relying on Txhistory Txs, we could make this a usePoll hook, reactive on the TxHistory slice?

export const pollForCrossChainComplete = ({
initiatingChainTxid,
initiatingChainAccountId,
getState,
}: {
initiatingChainAccountId: AccountId
initiatingChainTxid: string
// Injecting this since we can't avoid circular dependencies when calling this from OsmosisSwapper/endpoints
getState: () => ReduxState
}): Promise<string> => {
return new Promise((resolve, reject) => {
const timeout = 300000 // 5 mins
const startTime = Date.now()
const interval = 5000 // 5 seconds

const poll = function () {
// TODO: this should just be a fetch to unchained
const initiatingChainTx = selectTxById(getState(), initiatingChainTxid)
if (initiatingChainTx && initiatingChainTx.status === TxStatus.Confirmed) {
// Initiating Tx is successful, now we need to wait for the destination tx to be picked up by validators
const poll = async function () {
const { accountId: initiatingAccountId, txid: initiatingTxid } =
deserializeTxIndex(initiatingChainTxid)
const { account: pubkey } = fromAccountId(initiatingAccountId)
const { chainId: initiatingChainId } = fromAccountId(initiatingAccountId)
const initiatingChainAdapter = getChainAdapterManager().get(
initiatingChainId,
) as ChainAdapter<OsmosisSupportedChainId>
const initiatingChainTxHistory = await initiatingChainAdapter.getTxHistory({ pubkey })
const initiatingChainTx = initiatingChainTxHistory.transactions.find(
tx => tx.txid === initiatingTxid,
)

if (initiatingChainTx && initiatingChainTx.status === TxStatus.Confirmed) {
const initiatingChainId = fromAccountId(initiatingChainAccountId).chainId
const initiatingChainSequence = (initiatingChainTx.data as IbcMetadata | undefined)
?.sequence
const destinationChainAddress = (initiatingChainTx.data as IbcMetadata | undefined)
?.ibcDestination

// None of these two should ever happen but it may - a confirmed MsgTransfer Tx contains an initiating sequence and a destination address
// if we don't parse them, we have bigger problems at unchained-client level
if (!initiatingChainSequence) throw new Error('sequence not found in initiating Tx')
if (!destinationChainAddress) throw new Error('ibcDestination not found in initiating Tx')

const destinationChainId =
initiatingChainId === cosmosChainId ? osmosisChainId : cosmosChainId
const destinationChainAccountId = toAccountId({
chainId: destinationChainId,
account: destinationChainAddress,
})

const destinationAccountTxs = selectTxsByFilter(getState(), {
accountId: destinationChainAccountId,
const destinationChainAdapter = getChainAdapterManager().get(
destinationChainId,
) as ChainAdapter<OsmosisSupportedChainId>
const destinationChainTxHistory = await destinationChainAdapter.getTxHistory({
pubkey: destinationChainAddress,
})
const maybeFoundTx = destinationAccountTxs.some(

const maybeFoundTx = destinationChainTxHistory.transactions.some(
tx => (tx.data as IbcMetadata | undefined)?.sequence === initiatingChainSequence,
)

Expand All @@ -97,14 +105,15 @@ export const pollForCrossChainComplete = ({
}
} else if (Date.now() - startTime > timeout) {
reject(
new SwapError(`Couldnt find tx ${initiatingChainTxid}`, {
new SwapError(`Couldn't find tx ${initiatingChainTxid}`, {
code: SwapErrorType.RESPONSE_ERROR,
}),
)
} else {
setTimeout(poll, interval)
}
}

poll()
})
}
2 changes: 0 additions & 2 deletions src/lib/swapper/tradeExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export class TradeExecution {
wallet,
supportsEIP1559,
slippageTolerancePercentageDecimal,
getState,
}: TradeExecutionInput) {
try {
const maybeSwapper = swappers.find(swapper => swapper.swapperName === swapperName)
Expand Down Expand Up @@ -95,7 +94,6 @@ export class TradeExecution {
stepIndex,
quoteSellAssetAccountId,
quoteBuyAssetAccountId,
getState,
})

const payload: StatusArgs = { stepIndex, status, message, buyTxHash }
Expand Down
2 changes: 0 additions & 2 deletions src/lib/swapper/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { AccountId, AssetId } from '@shapeshiftoss/caip'
import type { HDWallet } from '@shapeshiftoss/hdwallet-core'
import type { Result } from '@sniptt/monads/build'
import type { Asset } from 'lib/asset-service'
import type { ReduxState } from 'state/reducer'
import type { AccountMetadata } from 'state/slices/portfolioSlice/portfolioSliceCommon'

import type { SwapErrorRight, SwapperName, TradeQuote2 } from './api'
Expand All @@ -29,5 +28,4 @@ export type TradeExecutionInput = {
wallet: HDWallet
supportsEIP1559: boolean
slippageTolerancePercentageDecimal: string
getState: () => ReduxState
}