Skip to content

Commit

Permalink
Add unit tests for simulate transaction pipeline (#117)
Browse files Browse the repository at this point in the history
* refactor: eslint adjustments

* test: initial simulate transaction unit tests

* feat: add assemble transaction sp error

* test: complete coverage for simulate transaction pipeline

* refactor: clean up comments and adjust imports
  • Loading branch information
fazzatti authored Apr 2, 2024
1 parent 88a8b0a commit 619b65f
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { Account, Asset, Claimant, Operation, TransactionBuilder, xdr } from '@stellar/stellar-sdk'
import { ClassicSignRequirementsPipeline } from './index'
import { SignatureRequirement, SignatureThreshold } from 'stellar-plus/core/types'

import { Constants } from 'stellar-plus'
import { CSRError } from './errors'
import { SignatureRequirement, SignatureThreshold } from 'stellar-plus/core/types'
import { ConveyorBeltErrorMeta } from 'stellar-plus/error/helpers/conveyor-belt'
import { ClassicSignRequirementsPipelineInput, ClassicSignRequirementsPipelineType } from './types'
import { BeltMetadata } from 'stellar-plus/utils/pipeline/conveyor-belts/types'

import { CSRError } from './errors'
import { ClassicSignRequirementsPipelineInput, ClassicSignRequirementsPipelineType } from './types'

import { ClassicSignRequirementsPipeline } from './index'

const MOCKED_PK_A = 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI'
const MOCKED_PK_B = 'GB3MXH633VRECLZRUAR3QCLQJDMXNYNHKZCO6FJEWXVWSUEIS7NU376P'
const MOCKED_PK_C = 'GCPXAF4S5MBXA3DRNBA7XYP55S6F3UN2ZJRAS72BXEJMD7JVMGIGCKNA'
Expand Down Expand Up @@ -36,6 +39,7 @@ describe('ClassicSignRequirementsPipeline', () => {
describe('errors', () => {
it('should throw error if internal process fails', async () => {
const pipeline = new ClassicSignRequirementsPipeline()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
jest.spyOn(pipeline as any, 'bundleSignatureRequirements').mockImplementation(() => {
throw new Error('mocked error')
})
Expand Down Expand Up @@ -140,7 +144,7 @@ describe('ClassicSignRequirementsPipeline', () => {

describe('operations threshold calculation', () => {
let transactionBuilder: TransactionBuilder
let testOperationRequirement: Function
let testOperationRequirement: (operations: xdr.Operation[], expected: SignatureRequirement[]) => Promise<void>

const pipeline = new ClassicSignRequirementsPipeline()
const expectedLow = { publicKey: MOCKED_PK_A, thresholdLevel: SignatureThreshold.low }
Expand All @@ -149,7 +153,7 @@ describe('ClassicSignRequirementsPipeline', () => {

beforeEach(() => {
transactionBuilder = new TransactionBuilder(MOCKED_ACCOUNT_A, MOCKED_TX_OPTIONS)
testOperationRequirement = (operations: xdr.Operation[], expected: SignatureRequirement[]) => {
testOperationRequirement = (operations: xdr.Operation[], expected: SignatureRequirement[]): Promise<void> => {
operations.forEach((op) => {
transactionBuilder.addOperation(op)
})
Expand Down
29 changes: 27 additions & 2 deletions src/stellar-plus/core/pipelines/simulate-transaction/errors.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { SorobanRpc } from '@stellar/stellar-sdk'
import { SorobanRpc, Transaction } from '@stellar/stellar-sdk'

import { StellarPlusError } from 'stellar-plus/error'
import { ConveyorBeltErrorMeta } from 'stellar-plus/error/helpers/conveyor-belt'
import { extractTransactionData } from 'stellar-plus/error/helpers/transaction'
import { BeltMetadata } from 'stellar-plus/utils/pipeline/conveyor-belts/types'

import { SimulateTransactionPipelineInput } from './types'
Expand All @@ -15,6 +16,9 @@ export enum ErrorCodesPipelineSimulateTransaction {

//PSI1 Restore
PSI100 = 'PSI100',

//PSI2 Transaction
PSI201 = 'PSI201',
}

const failedToSimulateTransaction = (
Expand Down Expand Up @@ -67,7 +71,7 @@ const simulationMissingResult = (
}

const simulationResultCouldNotBeVerified = (
simulationResponse: SorobanRpc.Api.SimulateTransactionSuccessResponse,
simulationResponse: SorobanRpc.Api.SimulateTransactionResponse,
conveyorBeltErrorMeta: ConveyorBeltErrorMeta<SimulateTransactionPipelineInput, BeltMetadata>
): StellarPlusError => {
return new StellarPlusError({
Expand Down Expand Up @@ -99,10 +103,31 @@ const transactionNeedsRestore = (
})
}

const failedToAssembleTransaction = (
error: Error,
simulationResponse: SorobanRpc.Api.SimulateTransactionSuccessResponse,
transaction: Transaction,
conveyorBeltErrorMeta: ConveyorBeltErrorMeta<SimulateTransactionPipelineInput, BeltMetadata>
): StellarPlusError => {
return new StellarPlusError({
code: ErrorCodesPipelineSimulateTransaction.PSI201,
message: 'Failed to assemble transaction!',
source: 'PipelineSimulateTransaction',
details: `An issue occurred while assembling the transaction. Refer to the meta section for more details.`,
meta: {
error,
conveyorBeltErrorMeta,
sorobanSimulationData: simulationResponse,
transactionData: extractTransactionData(transaction),
},
})
}

export const PSIError = {
failedToSimulateTransaction,
simulationFailed,
transactionNeedsRestore,
simulationMissingResult,
simulationResultCouldNotBeVerified,
failedToAssembleTransaction,
}
20 changes: 13 additions & 7 deletions src/stellar-plus/core/pipelines/simulate-transaction/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SorobanRpc, Transaction, xdr } from '@stellar/stellar-sdk'
import { SorobanRpc, Transaction } from '@stellar/stellar-sdk'

import {
SimulateTransactionPipelineInput,
Expand Down Expand Up @@ -28,7 +28,6 @@ export class SimulateTransactionPipeline extends ConveyorBelt<
itemId: string
): Promise<SimulateTransactionPipelineOutput> {
const { transaction, rpcHandler }: SimulateTransactionPipelineInput = item as SimulateTransactionPipelineInput

let simulationResponse: SorobanRpc.Api.SimulateTransactionResponse

try {
Expand All @@ -44,14 +43,14 @@ export class SimulateTransactionPipeline extends ConveyorBelt<
if (SorobanRpc.Api.isSimulationRestore(simulationResponse) && simulationResponse.result) {
return {
response: simulationResponse as SorobanRpc.Api.SimulateTransactionRestoreResponse,
assembledTransaction: this.assembleTransaction(transaction, simulationResponse),
assembledTransaction: this.assembleTransaction(transaction, simulationResponse, item, itemId),
} as SimulateTransactionPipelineOutput
}

if (SorobanRpc.Api.isSimulationSuccess(simulationResponse)) {
return {
response: simulationResponse as SorobanRpc.Api.SimulateTransactionSuccessResponse,
assembledTransaction: this.assembleTransaction(transaction, simulationResponse),
assembledTransaction: this.assembleTransaction(transaction, simulationResponse, item, itemId),
} as SimulateTransactionPipelineOutput
}

Expand All @@ -63,12 +62,19 @@ export class SimulateTransactionPipeline extends ConveyorBelt<

private assembleTransaction(
transaction: Transaction,
simulationResponse: SorobanRpc.Api.SimulateTransactionSuccessResponse
simulationResponse: SorobanRpc.Api.SimulateTransactionSuccessResponse,
item: SimulateTransactionPipelineInput,
itemId: string
): Transaction {
try {
return SorobanRpc.assembleTransaction(transaction, simulationResponse).build()
} catch (e) {
throw new Error('assembleTransaction failed')
} catch (error) {
throw PSIError.failedToAssembleTransaction(
error as Error,
simulationResponse,
transaction,
extractConveyorBeltErrorMeta(item, this.getMeta(itemId))
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { Account, SorobanDataBuilder, SorobanRpc, TransactionBuilder, xdr } from '@stellar/stellar-sdk'

import { Constants } from 'stellar-plus'
import { SimulateTransactionPipeline } from 'stellar-plus/core/pipelines/simulate-transaction'
import { PSIError } from 'stellar-plus/core/pipelines/simulate-transaction/errors'
import {
SimulateTransactionPipelineInput,
SimulateTransactionPipelineType,
} from 'stellar-plus/core/pipelines/simulate-transaction/types'
import { ConveyorBeltErrorMeta } from 'stellar-plus/error/helpers/conveyor-belt'
import { RpcHandler } from 'stellar-plus/rpc/types'
import { BeltMetadata } from 'stellar-plus/utils/pipeline/conveyor-belts/types'

const MOCKED_SIMULATION_RESPONSE_BASE = {
events: [],
id: 'mocked-id',
latestLedger: 0,
_parsed: true,
}
const MOCKED_SIMULATION_RESPONSE_ERROR = {
...MOCKED_SIMULATION_RESPONSE_BASE,
error: 'mocked error',
} as SorobanRpc.Api.SimulateTransactionErrorResponse

const MOCKED_SIMULATION_RESPONSE_SUCCESS = {
...MOCKED_SIMULATION_RESPONSE_BASE,
transactionData: new SorobanDataBuilder(),
minResourceFee: '0',
cost: {
cpuInsns: '0',
memBytes: '0',
},
} as SorobanRpc.Api.SimulateTransactionSuccessResponse

const MOCKED_SIMULATION_RESPONSE_RESTORE = {
...MOCKED_SIMULATION_RESPONSE_SUCCESS,
result: {
auth: [],
xdr: 'mocked-xdr',
retval: xdr.ScVal.scvVoid(),
},
restorePreamble: {
minResourceFee: '0',
transactionData: new SorobanDataBuilder(),
},
} as SorobanRpc.Api.SimulateTransactionRestoreResponse

const MOCKED_INVALID_SIMULATION_RESPONSE =
MOCKED_SIMULATION_RESPONSE_BASE as unknown as SorobanRpc.Api.SimulateTransactionResponse

const MOCKED_PK_A = 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI'
const MOCKED_ACCOUNT_A = new Account(MOCKED_PK_A, '100')
const TESTNET_PASSPHRASE = Constants.testnet.networkPassphrase
const MOCKED_FEE = '100'
const MOCKED_TX_OPTIONS: TransactionBuilder.TransactionBuilderOptions = {
fee: MOCKED_FEE,
networkPassphrase: TESTNET_PASSPHRASE,
timebounds: {
minTime: 0,
maxTime: 0,
},
}
const MOCKED_TRANSACTION = new TransactionBuilder(MOCKED_ACCOUNT_A, MOCKED_TX_OPTIONS).build()
const MOCKED_EXCEPTION = new Error('simulateTransaction failed')

const mockConveyorBeltErrorMeta = (
item: SimulateTransactionPipelineInput
): ConveyorBeltErrorMeta<SimulateTransactionPipelineInput, BeltMetadata> => {
return {
item,
meta: {
itemId: 'mocked-id',
beltId: 'mocked-belt-id',
beltType: SimulateTransactionPipelineType.id,
},
} as ConveyorBeltErrorMeta<SimulateTransactionPipelineInput, BeltMetadata>
}

describe('SimulateTransactionPipeline', () => {
describe('Initialization', () => {
it('should initialize the pipeline successfully', async () => {
const pipeline = new SimulateTransactionPipeline()
expect(pipeline).toBeInstanceOf(SimulateTransactionPipeline)
})
})

describe('errors', () => {
it('should throw failedToSimulateTransaction error when simulateTransaction fails to perform the simulation', async () => {
const MOCKED_RPC = {
simulateTransaction: jest.fn().mockImplementation(() => {
throw MOCKED_EXCEPTION
}),
} as unknown as RpcHandler
const pipeline = new SimulateTransactionPipeline()
const item = {
transaction: MOCKED_TRANSACTION,
rpcHandler: MOCKED_RPC,
}

await expect(pipeline.execute(item)).rejects.toThrow(
PSIError.failedToSimulateTransaction(MOCKED_EXCEPTION, mockConveyorBeltErrorMeta(item))
)
})

it('should throw simulationFailed error when simulateTransaction provides a simulation that will fail to be executed', async () => {
const MOCKED_RPC = {
simulateTransaction: jest.fn().mockImplementation(() => {
return MOCKED_SIMULATION_RESPONSE_ERROR
}),
} as unknown as RpcHandler

const pipeline = new SimulateTransactionPipeline()
const item = {
transaction: MOCKED_TRANSACTION,
rpcHandler: MOCKED_RPC,
}

await expect(pipeline.execute(item)).rejects.toThrow(
PSIError.simulationFailed(MOCKED_SIMULATION_RESPONSE_ERROR, mockConveyorBeltErrorMeta(item))
)
})

it('should throw simulationResultCouldNotBeVerified error when simulateTransaction provides a simulation that cannot be verified', async () => {
const MOCKED_RPC = {
simulateTransaction: jest.fn().mockImplementation(() => {
return MOCKED_INVALID_SIMULATION_RESPONSE
}),
} as unknown as RpcHandler
const pipeline = new SimulateTransactionPipeline()
const item = {
transaction: MOCKED_TRANSACTION,
rpcHandler: MOCKED_RPC,
}

await expect(pipeline.execute(item)).rejects.toThrow(
PSIError.simulationResultCouldNotBeVerified(MOCKED_INVALID_SIMULATION_RESPONSE, mockConveyorBeltErrorMeta(item))
)
})

it('should throw failedToAssembleTransaction error when assembleTransaction fails to assemble the transaction', async () => {
const MOCKED_RPC = {
simulateTransaction: jest.fn().mockImplementation(() => {
return MOCKED_SIMULATION_RESPONSE_SUCCESS
}),
} as unknown as RpcHandler
const pipeline = new SimulateTransactionPipeline()
const item = {
transaction: MOCKED_TRANSACTION,
rpcHandler: MOCKED_RPC,
}

await expect(pipeline.execute(item)).rejects.toThrow('Failed to assemble transaction!')
})
})

describe('success', () => {
it('should return a successful response when simulateTransaction provides a simulation that can be verified as successfull', async () => {
const MOCKED_RPC = {
simulateTransaction: jest.fn().mockImplementation(() => {
return MOCKED_SIMULATION_RESPONSE_SUCCESS
}),
} as unknown as RpcHandler
const pipeline = new SimulateTransactionPipeline()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
jest.spyOn(pipeline as any, 'assembleTransaction').mockImplementation(() => {
return MOCKED_TRANSACTION
})
const item = {
transaction: MOCKED_TRANSACTION,
rpcHandler: MOCKED_RPC,
}

expect(await pipeline.execute(item)).toEqual({
response: MOCKED_SIMULATION_RESPONSE_SUCCESS,
assembledTransaction: MOCKED_TRANSACTION,
})
})

it('should return a successful response when simulateTransaction provides a simulation that can be verified as successfull but needs to be restored', async () => {
const MOCKED_RPC = {
simulateTransaction: jest.fn().mockImplementation(() => {
return MOCKED_SIMULATION_RESPONSE_RESTORE
}),
} as unknown as RpcHandler
const pipeline = new SimulateTransactionPipeline()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
jest.spyOn(pipeline as any, 'assembleTransaction').mockImplementation(() => {
return MOCKED_TRANSACTION
})
const item = {
transaction: MOCKED_TRANSACTION,
rpcHandler: MOCKED_RPC,
}

expect(await pipeline.execute(item)).toEqual({
response: MOCKED_SIMULATION_RESPONSE_RESTORE,
assembledTransaction: MOCKED_TRANSACTION,
})
})
})
})

0 comments on commit 619b65f

Please sign in to comment.