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

fix: add solana tx simulation and expiration handling #201

Merged
merged 2 commits into from
Jul 19, 2024
Merged
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
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
Loading