Skip to content

Commit

Permalink
fix: add solana tx simulation and expiration handling (#201)
Browse files Browse the repository at this point in the history
  • Loading branch information
chybisov authored Jul 19, 2024
1 parent 54b3b00 commit 0d52900
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 31 deletions.
72 changes: 50 additions & 22 deletions src/core/Solana/SolanaStepExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import {
type SendOptions,
type SignatureResult,
} from '@solana/web3.js'
import bs58 from 'bs58'
import { config } from '../../config.js'
import { LiFiErrorCode } from '../../errors/constants.js'
import { TransactionError } from '../../errors/errors.js'
import { getStepTransaction } from '../../services/api.js'
import { base64ToUint8Array } from '../../utils/base64ToUint8Array.js'
import { getTransactionFailedMessage } from '../../utils/index.js'
import { TransactionError } from '../../errors/errors.js'
import { LiFiErrorCode } from '../../errors/constants.js'
import { parseSolanaErrors } from './parseSolanaErrors.js'
import { BaseStepExecutor } from '../BaseStepExecutor.js'
import { checkBalance } from '../checkBalance.js'
import { getSubstatusMessage } from '../processMessages.js'
Expand All @@ -25,6 +25,7 @@ import type {
import { sleep } from '../utils.js'
import { waitForReceivingTransaction } from '../waitForReceivingTransaction.js'
import { getSolanaConnection } from './connection.js'
import { parseSolanaErrors } from './parseSolanaErrors.js'

export interface SolanaStepExecutorOptions extends StepExecutorOptions {
walletAdapter: SignerWalletAdapter
Expand Down Expand Up @@ -143,39 +144,54 @@ export class SolanaStepExecutor extends BaseStepExecutor {

this.checkWalletAdapter(step)

const signedTx =
await this.walletAdapter.signTransaction(versionedTransaction)
const signedTxPromise =
this.walletAdapter.signTransaction(versionedTransaction)

// We give users 2 minutes to sign the transaction or it should be considered expired
const signedTx = await Promise.race([
signedTxPromise,
// https://solana.com/docs/advanced/confirmation#transaction-expiration
// Use 2 minutes to account for fluctuations
sleep(120_000),
])

if (!signedTx) {
throw new TransactionError(
LiFiErrorCode.TransactionExpired,
'Transaction has expired: blockhash is no longer recent enough.'
)
}

process = this.statusManager.updateProcess(
step,
process.type,
'PENDING'
)

const rawTransactionOptions: SendOptions = {
// Setting max retries to 0 as we are handling retries manually
// Set this manually so that the default is skipped
maxRetries: 0,
// https://solana.com/docs/advanced/confirmation#use-an-appropriate-preflight-commitment-level
preflightCommitment: 'confirmed',
// minContextSlot: blockhashResult.context.slot,
}

const signedTxSerialized = signedTx.serialize()
const txSignature = await connection.sendRawTransaction(
signedTxSerialized,
rawTransactionOptions
const simulationResult = await connection.simulateTransaction(
signedTx,
{
commitment: 'processed',
replaceRecentBlockhash: true,
}
)

// We can skip preflight check after the first transaction has been sent
// https://solana.com/docs/advanced/retry#the-cost-of-skipping-preflight
rawTransactionOptions.skipPreflight = true
if (simulationResult.value.err) {
throw new TransactionError(
LiFiErrorCode.TransactionSimulationFailed,
'Transaction simulation failed'
)
}

// Create transaction hash (signature)
const txSignature = bs58.encode(signedTx.signatures[0])

// A known weirdness - MAX_RECENT_BLOCKHASHES is 300
// https://github.com/solana-labs/solana/blob/master/sdk/program/src/clock.rs#L123
// but MAX_PROCESSING_AGE is 150
// https://github.com/solana-labs/solana/blob/master/sdk/program/src/clock.rs#L129
// the blockhash queue in the bank tells you 300 + current slot, but it won't be accepted 150 blocks later.
// https://solana.com/docs/advanced/confirmation#transaction-expiration
const lastValidBlockHeight = blockhashResult.lastValidBlockHeight - 150

// In the following section, we wait and constantly check for the transaction to be confirmed
Expand All @@ -197,6 +213,18 @@ export class SolanaStepExecutor extends BaseStepExecutor {
let confirmedTx: SignatureResult | null = null
let blockHeight = await connection.getBlockHeight()

const rawTransactionOptions: SendOptions = {
// We can skip preflight check after the first transaction has been sent
// https://solana.com/docs/advanced/retry#the-cost-of-skipping-preflight
skipPreflight: true,
// Setting max retries to 0 as we are handling retries manually
maxRetries: 0,
// https://solana.com/docs/advanced/confirmation#use-an-appropriate-preflight-commitment-level
preflightCommitment: 'confirmed',
}

const signedTxSerialized = signedTx.serialize()

// https://solana.com/docs/advanced/retry#customizing-rebroadcast-logic
while (!confirmedTx && blockHeight < lastValidBlockHeight) {
await connection.sendRawTransaction(
Expand Down Expand Up @@ -239,7 +267,7 @@ export class SolanaStepExecutor extends BaseStepExecutor {
if (!confirmedTx) {
throw new TransactionError(
LiFiErrorCode.TransactionExpired,
'Failed to land the transaction'
'Transaction has expired: The block height has exceeded the maximum allowed limit.'
)
}

Expand Down
18 changes: 18 additions & 0 deletions src/core/Solana/parseSolanaErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,24 @@ const handleSpecificErrors = (e: any) => {
)
}

if (e.name === 'SendTransactionError') {
return new TransactionError(
LiFiErrorCode.TransactionFailed,
e.message,
undefined,
e
)
}

if (e.message?.includes('simulate')) {
return new TransactionError(
LiFiErrorCode.TransactionSimulationFailed,
e.message,
undefined,
e
)
}

if (e instanceof BaseError) {
return e
}
Expand Down
6 changes: 3 additions & 3 deletions src/errors/SDKError.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { BaseError } from './baseError.js'
import { type ErrorCode } from './constants.js'
import type { LiFiStep, Process } from '@lifi/types'
import { version } from '../version.js'
import type { BaseError } from './baseError.js'
import { type ErrorCode } from './constants.js'

// Note: SDKError is used to wrapper and present errors at the top level
// Where opportunity allows we also add the step and the process related to the error
Expand All @@ -13,7 +13,7 @@ export class SDKError extends Error {
override cause: BaseError

constructor(cause: BaseError, step?: LiFiStep, process?: Process) {
const errorMessage = `${cause.message ? `[${cause.name}] ${cause.message}` : 'Unknown error occurred'}\nLiFi SDK version: ${version}`
const errorMessage = `${cause.message ? `[${cause.name}] ${cause.message}` : 'Unknown error occurred'}\nLI.FI SDK version: ${version}`
super(errorMessage)
this.name = 'SDKError'
this.step = step
Expand Down
12 changes: 6 additions & 6 deletions src/errors/SDKError.unit.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { describe, expect, it } from 'vitest'
import { ErrorName, LiFiErrorCode } from './constants.js'
import { BaseError } from './baseError.js'
import { SDKError } from './SDKError.js'
import { version } from '../version.js'
import { BaseError } from './baseError.js'
import { ErrorName, LiFiErrorCode } from './constants.js'
import { HTTPError } from './httpError.js'
import { SDKError } from './SDKError.js'

const url = 'http://some.where'
const options = { method: 'POST' }
Expand Down Expand Up @@ -55,7 +55,7 @@ describe('SDKError', () => {
}

expect(() => testFunction()).toThrowError(
`[HTTPError] [ValidationError] Request failed with status code 400 Bad Request\n responseMessage: Oops\nLiFi SDK version: ${version}`
`[HTTPError] [ValidationError] Request failed with status code 400 Bad Request\n responseMessage: Oops\nLI.FI SDK version: ${version}`
)
})
})
Expand Down Expand Up @@ -131,7 +131,7 @@ describe('SDKError', () => {
}

expect(() => testFunction()).toThrowError(
`[UnknownError] There was an error\nLiFi SDK version: ${version}`
`[UnknownError] There was an error\nLI.FI SDK version: ${version}`
)
})

Expand All @@ -143,7 +143,7 @@ describe('SDKError', () => {
}

expect(() => testFunction()).toThrowError(
`Unknown error occurred\nLiFi SDK version: ${version}`
`Unknown error occurred\nLI.FI SDK version: ${version}`
)
})

Expand Down
1 change: 1 addition & 0 deletions src/errors/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export enum LiFiErrorCode {
ExchangeRateUpdateCanceled = 1016,
WalletChangedDuringExecution = 1017,
TransactionExpired = 1018,
TransactionSimulationFailed = 1019,
}

export enum ErrorMessage {
Expand Down

0 comments on commit 0d52900

Please sign in to comment.