From b6ff8215449cf56a3f0e0528fca3178c8212309a Mon Sep 17 00:00:00 2001 From: Bruno Nascimento Date: Tue, 26 Mar 2024 13:29:02 -0300 Subject: [PATCH 01/30] chore: add jest setup to coverage all files (#101) --- package-lock.json | 32 ++++++++++++++++++++++++++++++++ package.json | 5 +++-- src/jest.config.js | 13 +++++++++++++ src/jest.config.ts | 11 ----------- src/setup-tests.ts | 6 ++++++ 5 files changed, 54 insertions(+), 13 deletions(-) create mode 100644 src/jest.config.js delete mode 100644 src/jest.config.ts create mode 100644 src/setup-tests.ts diff --git a/package-lock.json b/package-lock.json index 5400609..25c8849 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "eslint-plugin-unicorn": "^49.0.0", "husky": "^8.0.0", "jest": "^29.7.0", + "jest-extended": "^4.0.2", "lint-staged": "^15.2.2", "nodemon": "^3.0.1", "prettier": "^3.1.0", @@ -6041,6 +6042,27 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-extended": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-4.0.2.tgz", + "integrity": "sha512-FH7aaPgtGYHc9mRjriS0ZEHYM5/W69tLrFTIdzm+yJgeoCmmrSB/luSfMSqWP9O29QWHPEmJ4qmU6EwsZideog==", + "dev": true, + "dependencies": { + "jest-diff": "^29.0.0", + "jest-get-type": "^29.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "jest": ">=27.2.5" + }, + "peerDependenciesMeta": { + "jest": { + "optional": true + } + } + }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", @@ -13623,6 +13645,16 @@ "jest-util": "^29.7.0" } }, + "jest-extended": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-4.0.2.tgz", + "integrity": "sha512-FH7aaPgtGYHc9mRjriS0ZEHYM5/W69tLrFTIdzm+yJgeoCmmrSB/luSfMSqWP9O29QWHPEmJ4qmU6EwsZideog==", + "dev": true, + "requires": { + "jest-diff": "^29.0.0", + "jest-get-type": "^29.0.0" + } + }, "jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", diff --git a/package.json b/package.json index 63087f5..2463b1e 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "scripts": { "build": "tsc && tsc-alias -p tsconfig.json", "dev": "nodemon -e ts,js --exec ts-node -r tsconfig-paths/register ./src/index-test.ts", - "test": "npx jest --config src/jest.config.ts", - "coverage": "npx jest --config src/jest.config.ts --coverage", + "test": "npx jest --config src/jest.config.js", + "coverage": "npx jest --config src/jest.config.js --coverage", "lint": "eslint --ext .js,.ts .", "format": "prettier --ignore-path .gitignore --write \"**/*.+(js|ts|json)\"", "prepare": "husky install" @@ -49,6 +49,7 @@ "eslint-plugin-unicorn": "^49.0.0", "husky": "^8.0.0", "jest": "^29.7.0", + "jest-extended": "^4.0.2", "lint-staged": "^15.2.2", "nodemon": "^3.0.1", "prettier": "^3.1.0", diff --git a/src/jest.config.js b/src/jest.config.js new file mode 100644 index 0000000..9a4c8f8 --- /dev/null +++ b/src/jest.config.js @@ -0,0 +1,13 @@ +// eslint-disable-next-line no-undef +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + collectCoverage: true, + coverageReporters: ['cobertura', 'json', 'html', 'text'], + coveragePathIgnorePatterns: ['/node_modules/', '/*/.*\\.types.ts', '.*\\mocks.ts'], + collectCoverageFrom: ['./**/*.{ts,js}'], + setupFilesAfterEnv: ['./setup-tests.ts'], + modulePathIgnorePatterns: ['/dist/'], + moduleDirectories: ['node_modules', 'src'], + rootDir: './', +} diff --git a/src/jest.config.ts b/src/jest.config.ts deleted file mode 100644 index 21f59ca..0000000 --- a/src/jest.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Config } from '@jest/types' -import { pathsToModuleNameMapper } from 'ts-jest' - -const config: Config.InitialOptions = { - preset: 'ts-jest', - testEnvironment: 'node', - verbose: false, - automock: false, - moduleDirectories: ['node_modules', 'src'] -} -export default config diff --git a/src/setup-tests.ts b/src/setup-tests.ts new file mode 100644 index 0000000..5829630 --- /dev/null +++ b/src/setup-tests.ts @@ -0,0 +1,6 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +import * as matchers from 'jest-extended' +expect.extend(matchers) + +jest.setTimeout(10000) +process.env.DEBUG = 'false' From e78148deee23b598751c6109a252b138fd7b210e Mon Sep 17 00:00:00 2001 From: Fabricius Zatti <62725221+fazzatti@users.noreply.github.com> Date: Tue, 26 Mar 2024 14:16:12 -0300 Subject: [PATCH 02/30] refactor: fix circular import --- src/stellar-plus/core/pipelines/submit-transaction/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stellar-plus/core/pipelines/submit-transaction/index.ts b/src/stellar-plus/core/pipelines/submit-transaction/index.ts index c528b23..7f20f6d 100644 --- a/src/stellar-plus/core/pipelines/submit-transaction/index.ts +++ b/src/stellar-plus/core/pipelines/submit-transaction/index.ts @@ -1,7 +1,6 @@ import { FeeBumpTransaction, SorobanRpc, Transaction } from '@stellar/stellar-sdk' import { HorizonApi } from '@stellar/stellar-sdk/lib/horizon' -import { HorizonHandler } from 'stellar-plus' import { SubmitTransactionPipelineInput, SubmitTransactionPipelineOutput, @@ -13,6 +12,7 @@ import { RpcHandler } from 'stellar-plus/rpc/types' import { ConveyorBelt } from 'stellar-plus/utils/pipeline/conveyor-belts' import { PSUError } from './errors' +import { HorizonHandlerClient } from 'stellar-plus/horizon' export class SubmitTransactionPipeline extends ConveyorBelt< SubmitTransactionPipelineInput, @@ -37,7 +37,7 @@ export class SubmitTransactionPipeline extends ConveyorBelt< // Horizon Submission // =================== // - if (networkHandler instanceof HorizonHandler) { + if (networkHandler instanceof HorizonHandlerClient) { let response: HorizonApi.SubmitTransactionResponse try { response = await this.submitTransactionThroughHorizon(transaction, networkHandler) @@ -86,7 +86,7 @@ export class SubmitTransactionPipeline extends ConveyorBelt< private async submitTransactionThroughHorizon( transaction: Transaction | FeeBumpTransaction, - horizonHandler: HorizonHandler + horizonHandler: HorizonHandlerClient ): Promise { const response = (await horizonHandler.server.submitTransaction(transaction, { skipMemoRequiredCheck: true, // Not skipping memo required check causes an error when submitting fee bump transactions From e9497cb381f40b9a1b921a3c9d82ae6727d352c5 Mon Sep 17 00:00:00 2001 From: Fabricius Zatti <62725221+fazzatti@users.noreply.github.com> Date: Tue, 26 Mar 2024 14:16:58 -0300 Subject: [PATCH 03/30] test: contract engine initial structure --- src/jest.config.js | 4 +- .../core/contract-engine/index.unit.test.ts | 48 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 src/stellar-plus/core/contract-engine/index.unit.test.ts diff --git a/src/jest.config.js b/src/jest.config.js index 9a4c8f8..be21348 100644 --- a/src/jest.config.js +++ b/src/jest.config.js @@ -4,8 +4,8 @@ module.exports = { testEnvironment: 'node', collectCoverage: true, coverageReporters: ['cobertura', 'json', 'html', 'text'], - coveragePathIgnorePatterns: ['/node_modules/', '/*/.*\\.types.ts', '.*\\mocks.ts'], - collectCoverageFrom: ['./**/*.{ts,js}'], + coveragePathIgnorePatterns: ['/node_modules/', '/*/.*\\.types.ts', '.*\\mocks.ts', './coverage/'], + collectCoverageFrom: ['./**/*.ts'], setupFilesAfterEnv: ['./setup-tests.ts'], modulePathIgnorePatterns: ['/dist/'], moduleDirectories: ['node_modules', 'src'], diff --git a/src/stellar-plus/core/contract-engine/index.unit.test.ts b/src/stellar-plus/core/contract-engine/index.unit.test.ts new file mode 100644 index 0000000..e328019 --- /dev/null +++ b/src/stellar-plus/core/contract-engine/index.unit.test.ts @@ -0,0 +1,48 @@ +import { ContractSpec } from '@stellar/stellar-sdk' +import { ContractEngine } from '../contract-engine' +import { Constants } from 'stellar-plus' +import { spec as tokenSpec } from 'stellar-plus/asset/soroban-token/constants' + +const MOCKED_CONTRACT_ID = 'CBJT4BOMRHYKHZ6HF3QG4YR7Q63BE44G73M4MALDTQ3SQVUZDE7GN35I' +const MOCKED_WASM_HASH = 'eb94566536d7f56c353b4760f6e359eca3631b70d295820fb6de55a796e019ae' +const MOCKED_CONTRACT_SPEC = tokenSpec +const MOCKED_WASM_FILE = Buffer.from('mockWasm', 'utf-8') +const NETWORK_CONFIG = Constants.testnet + +describe('ContractEngine', () => { + let contractEngine: ContractEngine + + beforeEach(() => { + // contractEngine = new ContractEngine(); + }) + + describe('Intialization', () => { + it('should initialize with wasm file', () => { + contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + wasm: MOCKED_WASM_FILE, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + expect(contractEngine.getWasm()).toEqual(MOCKED_WASM_FILE) + }) + + it('should initialize with wasm hash', () => { + contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + wasmHash: MOCKED_WASM_HASH, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + expect(contractEngine.getWasmHash()).toEqual(MOCKED_WASM_HASH) + }) + }) + + // it('should initialize with default values', () => { + // expect(contractEngine.getContracts()).toEqual([]) + // }) +}) From a4a6bde6a2380aa9a00c1c6886eef7d09a97ffc1 Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Thu, 28 Mar 2024 15:39:24 -0300 Subject: [PATCH 04/30] Contract Engine unit tests (#103) * refactor: contract engine errors * feat: validate required initialization arguments * test: contract engine initialization workflow * test: complete contract engine main coverage * refactor: update deprecated methods --- .../core/contract-engine/errors.ts | 18 +- .../core/contract-engine/index.ts | 3 + .../core/contract-engine/index.unit.test.ts | 615 +++++++++++++++++- 3 files changed, 623 insertions(+), 13 deletions(-) diff --git a/src/stellar-plus/core/contract-engine/errors.ts b/src/stellar-plus/core/contract-engine/errors.ts index b5dd4f7..a27fdee 100644 --- a/src/stellar-plus/core/contract-engine/errors.ts +++ b/src/stellar-plus/core/contract-engine/errors.ts @@ -17,6 +17,7 @@ export enum ContractEngineErrorCodes { CE006 = 'CE006', CE007 = 'CE007', CE008 = 'CE008', + CE009 = 'CE009', // CE1 Simulation CE100 = 'CE100', @@ -119,6 +120,16 @@ const contractCodeMissingLiveUntilLedgerSeq = ( }) } +const contractEngineClassFailedToInitialize = () => { + return new StellarPlusError({ + code: ContractEngineErrorCodes.CE009, + message: 'Contract engine class failed to initialize!', + source: 'ContractEngine', + details: + 'Contract engine class failed to initialize because of missing parameters! The Contract Engine must be initialized with either the wasm file, the wasm hash, or the contract ID. Please review the initialization parameters and try again.', + }) +} + const simulationFailed = (simulation: SorobanRpc.Api.SimulateTransactionErrorResponse): StellarPlusError => { return new StellarPlusError({ code: ContractEngineErrorCodes.CE101, @@ -183,7 +194,7 @@ const failedToUploadWasm = (error: StellarPlusError): StellarPlusError => { return new StellarPlusError({ code: ContractEngineErrorCodes.CE201, message: 'Failed to upload wasm!', - source: 'SorobanTransactionProcessor', + source: 'ContractEngine', details: 'The wasm file could not be uploaded. Review the meta error to identify the underlying cause for this issue.', meta: { message: error.message, error: error }, @@ -194,7 +205,7 @@ const failedToDeployContract = (error: StellarPlusError): StellarPlusError => { return new StellarPlusError({ code: ContractEngineErrorCodes.CE202, message: 'Failed to deploy contract!', - source: 'SorobanTransactionProcessor', + source: 'ContractEngine', details: 'The contract could not be deployed. Review the meta error to identify the underlying cause for this issue.', meta: { message: error.message, ...error.meta }, @@ -205,7 +216,7 @@ const failedToWrapAsset = (error: StellarPlusError): StellarPlusError => { return new StellarPlusError({ code: ContractEngineErrorCodes.CE203, message: 'Failed to wrap asset!', - source: 'SorobanTransactionProcessor', + source: 'ContractEngine', details: 'The asset could not be wrapped. Review the meta error to identify the underlying cause for this issue.', meta: { message: error.message, ...error.meta }, }) @@ -222,6 +233,7 @@ export const CEError = { contractInstanceMissingLiveUntilLedgerSeq, contractCodeNotFound, contractCodeMissingLiveUntilLedgerSeq, + contractEngineClassFailedToInitialize, transactionNeedsRestore, simulationMissingResult, restoreOptionNotSet, diff --git a/src/stellar-plus/core/contract-engine/index.ts b/src/stellar-plus/core/contract-engine/index.ts index f1e1083..b0e094b 100644 --- a/src/stellar-plus/core/contract-engine/index.ts +++ b/src/stellar-plus/core/contract-engine/index.ts @@ -104,6 +104,9 @@ export class ContractEngine { this.contractId = contractParameters.contractId this.wasm = contractParameters.wasm this.wasmHash = contractParameters.wasmHash + + if (!this.contractId && !this.wasm && !this.wasmHash) throw CEError.contractEngineClassFailedToInitialize() + this.options = { ...options } this.sorobanTransactionPipeline = new SorobanTransactionPipeline(networkConfig, { diff --git a/src/stellar-plus/core/contract-engine/index.unit.test.ts b/src/stellar-plus/core/contract-engine/index.unit.test.ts index e328019..9c7e50c 100644 --- a/src/stellar-plus/core/contract-engine/index.unit.test.ts +++ b/src/stellar-plus/core/contract-engine/index.unit.test.ts @@ -1,24 +1,59 @@ -import { ContractSpec } from '@stellar/stellar-sdk' import { ContractEngine } from '../contract-engine' import { Constants } from 'stellar-plus' -import { spec as tokenSpec } from 'stellar-plus/asset/soroban-token/constants' +import { spec as tokenSpec, methods as tokenMethods } from 'stellar-plus/asset/soroban-token/constants' +import { CEError } from './errors' +import { TransactionInvocation } from 'stellar-plus/types' +import { SorobanInvokeArgs } from './types' +import { SorobanTransactionPipeline } from 'stellar-plus/core/pipelines/soroban-transaction' +import { ContractIdOutput, ContractWasmHashOutput } from '../pipelines/soroban-get-transaction/types' +import { Asset, Contract, SorobanRpc, xdr } from '@stellar/stellar-sdk' +import { DefaultRpcHandler } from 'stellar-plus/rpc' +import { StellarPlusError } from 'stellar-plus/error' + +jest.mock('stellar-plus/core/pipelines/soroban-transaction', () => ({ + SorobanTransactionPipeline: jest.fn(), +})) + +jest.mock('stellar-plus/rpc/default-handler', () => ({ + DefaultRpcHandler: jest.fn(), +})) + +const MOCKED_SOROBAN_TRANSACTION_PIPELINE = SorobanTransactionPipeline as jest.Mock +const MOCKED_DEFAULT_RPC_HANDLER = DefaultRpcHandler as jest.Mock const MOCKED_CONTRACT_ID = 'CBJT4BOMRHYKHZ6HF3QG4YR7Q63BE44G73M4MALDTQ3SQVUZDE7GN35I' const MOCKED_WASM_HASH = 'eb94566536d7f56c353b4760f6e359eca3631b70d295820fb6de55a796e019ae' const MOCKED_CONTRACT_SPEC = tokenSpec const MOCKED_WASM_FILE = Buffer.from('mockWasm', 'utf-8') +const MOCKED_STELLAR_ASSET = Asset.native() + +const MOCKED_CONTRACT_CODE_KEY = new xdr.LedgerKeyContractCode({ + hash: Buffer.from(MOCKED_WASM_HASH, 'hex'), +}) const NETWORK_CONFIG = Constants.testnet +const MOCKED_TX_INVOCATION: TransactionInvocation = { + header: { + source: 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI', + fee: '100', + timeout: 100, + }, + signers: [], +} -describe('ContractEngine', () => { - let contractEngine: ContractEngine +const MOCKED_SOROBAN_INVOKE_ARGS: SorobanInvokeArgs<{}> = { + method: tokenMethods.name, + methodArgs: {}, + ...MOCKED_TX_INVOCATION, +} +describe('ContractEngine', () => { beforeEach(() => { - // contractEngine = new ContractEngine(); + jest.clearAllMocks() }) describe('Intialization', () => { it('should initialize with wasm file', () => { - contractEngine = new ContractEngine({ + const contractEngine = new ContractEngine({ networkConfig: NETWORK_CONFIG, contractParameters: { wasm: MOCKED_WASM_FILE, @@ -30,7 +65,7 @@ describe('ContractEngine', () => { }) it('should initialize with wasm hash', () => { - contractEngine = new ContractEngine({ + const contractEngine = new ContractEngine({ networkConfig: NETWORK_CONFIG, contractParameters: { wasmHash: MOCKED_WASM_HASH, @@ -40,9 +75,569 @@ describe('ContractEngine', () => { expect(contractEngine.getWasmHash()).toEqual(MOCKED_WASM_HASH) }) + + it('should initialize with contract id', () => { + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + contractId: MOCKED_CONTRACT_ID, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + expect(contractEngine.getContractId()).toEqual(MOCKED_CONTRACT_ID) + }) + }) + + describe('Initialization Errors', () => { + it('should throw error if no wasm file, wasm hash or contract id is provided', () => { + expect(() => { + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + spec: MOCKED_CONTRACT_SPEC, + }, + }) + }).toThrow(CEError.contractEngineClassFailedToInitialize()) + }) + + it('should throw error if wasm file is required but is not present', async () => { + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + spec: MOCKED_CONTRACT_SPEC, + wasmHash: MOCKED_WASM_HASH, + }, + }) + + expect(() => contractEngine.getWasm()).toThrow(CEError.missingWasm()) + + await expect(contractEngine.uploadWasm(MOCKED_TX_INVOCATION)).rejects.toThrow(CEError.missingWasm()) + }) + + it('should throw error if wasm hash is required but is not present', async () => { + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + spec: MOCKED_CONTRACT_SPEC, + wasm: MOCKED_WASM_FILE, + }, + }) + + expect(() => contractEngine.getWasmHash()).toThrow(CEError.missingWasmHash()) + + await expect(contractEngine.deploy(MOCKED_TX_INVOCATION)).rejects.toThrow(CEError.missingWasmHash()) + await expect(contractEngine.getContractCodeLiveUntilLedgerSeq()).rejects.toThrow(CEError.missingWasmHash()) + await expect(contractEngine.getContractInstanceLiveUntilLedgerSeq()).rejects.toThrow(CEError.missingWasmHash()) + await expect(contractEngine.restoreContractCode(MOCKED_TX_INVOCATION)).rejects.toThrow(CEError.missingWasmHash()) + await expect(contractEngine.restoreContractInstance(MOCKED_TX_INVOCATION)).rejects.toThrow( + CEError.missingWasmHash() + ) + }) + + it('should throw error if contract id is required but is not present', async () => { + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + spec: MOCKED_CONTRACT_SPEC, + wasmHash: MOCKED_WASM_HASH, + }, + }) + + expect(() => contractEngine.getContractId()).toThrow(CEError.missingContractId()) + expect(() => contractEngine.getContractFootprint()).toThrow(CEError.missingContractId()) + + await expect(contractEngine.invokeContract(MOCKED_SOROBAN_INVOKE_ARGS)).rejects.toThrow( + CEError.missingContractId() + ) + await expect(contractEngine.readFromContract(MOCKED_SOROBAN_INVOKE_ARGS)).rejects.toThrow( + CEError.missingContractId() + ) + await expect(contractEngine.runTransactionPipeline(MOCKED_SOROBAN_INVOKE_ARGS)).rejects.toThrow( + CEError.missingContractId() + ) + }) + }) + + describe('Initialization workflow', () => { + it('should upload wasm', async () => { + MOCKED_SOROBAN_TRANSACTION_PIPELINE.mockImplementation(() => { + return { + execute: jest.fn().mockResolvedValue({ + output: { + wasmHash: MOCKED_WASM_HASH, + } as ContractWasmHashOutput, + }), + } + }) + + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + wasm: MOCKED_WASM_FILE, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + await expect(contractEngine.uploadWasm(MOCKED_TX_INVOCATION)).resolves.toBeUndefined() + expect(contractEngine.getWasm()).toEqual(MOCKED_WASM_FILE) + }) + + it('should deploy contract', async () => { + MOCKED_SOROBAN_TRANSACTION_PIPELINE.mockImplementation(() => { + return { + execute: jest.fn().mockResolvedValue({ + output: { + contractId: MOCKED_CONTRACT_ID, + } as ContractIdOutput, + }), + } + }) + + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + wasmHash: MOCKED_WASM_HASH, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + await expect(contractEngine.deploy(MOCKED_TX_INVOCATION)).resolves.toBeUndefined() + expect(contractEngine.getContractId()).toEqual(MOCKED_CONTRACT_ID) + }) }) - // it('should initialize with default values', () => { - // expect(contractEngine.getContracts()).toEqual([]) - // }) + describe('Additional getters', () => { + it('should return contract footprint', () => { + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + contractId: MOCKED_CONTRACT_ID, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + const footprint = new Contract(MOCKED_CONTRACT_ID).getFootprint() + expect(contractEngine.getContractFootprint()).toEqual(footprint) + }) + + it('should return the rpc handler', () => { + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + contractId: MOCKED_CONTRACT_ID, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + expect(contractEngine.getRpcHandler()).toBeDefined() + }) + + it('should throw if contract code is missing the live until ledger seq', async () => { + MOCKED_DEFAULT_RPC_HANDLER.mockImplementation(() => { + return { + getLedgerEntries: jest.fn().mockResolvedValue({ + entries: [ + { + key: xdr.LedgerKey.contractCode(MOCKED_CONTRACT_CODE_KEY), + xdr: 'xdr', + }, + ], + latestLedger: 1, + }), + } + }) + + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + wasmHash: MOCKED_WASM_HASH, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + await expect(contractEngine.getContractCodeLiveUntilLedgerSeq()).rejects.toThrow( + CEError.contractCodeMissingLiveUntilLedgerSeq() + ) + }) + + it('should return the live until ledger seq for contract code', async () => { + MOCKED_DEFAULT_RPC_HANDLER.mockImplementation(() => { + return { + getLedgerEntries: jest.fn().mockResolvedValue({ + entries: [ + { + key: Object.assign(xdr.LedgerKey.contractCode(MOCKED_CONTRACT_CODE_KEY)), + xdr: 'xdr', + liveUntilLedgerSeq: 1, + }, + ], + latestLedger: 1, + }), + } + }) + + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + wasmHash: MOCKED_WASM_HASH, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + await expect(contractEngine.getContractCodeLiveUntilLedgerSeq()).resolves.toEqual(1) + }) + + it('should throw if contract instance is missing the live until ledger seq', async () => { + const footprint = new Contract(MOCKED_CONTRACT_ID).getFootprint() + + MOCKED_DEFAULT_RPC_HANDLER.mockImplementation(() => { + return { + getLedgerEntries: jest.fn().mockResolvedValue({ + entries: [ + { + key: footprint, + xdr: 'xdr', + }, + ], + latestLedger: 1, + }), + } + }) + + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + contractId: MOCKED_CONTRACT_ID, + wasmHash: MOCKED_WASM_HASH, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + await expect(contractEngine.getContractInstanceLiveUntilLedgerSeq()).rejects.toThrow( + CEError.contractInstanceMissingLiveUntilLedgerSeq() + ) + }) + + it('should return the live until ledger seq for contract instance', async () => { + const footprint = new Contract(MOCKED_CONTRACT_ID).getFootprint() + + MOCKED_DEFAULT_RPC_HANDLER.mockImplementation(() => { + return { + getLedgerEntries: jest.fn().mockResolvedValue({ + entries: [ + { + key: footprint, + xdr: 'xdr', + liveUntilLedgerSeq: 1, + }, + ], + latestLedger: 1, + }), + } + }) + + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + contractId: MOCKED_CONTRACT_ID, + wasmHash: MOCKED_WASM_HASH, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + await expect(contractEngine.getContractInstanceLiveUntilLedgerSeq()).resolves.toEqual(1) + }) + }) + + describe('Contract restore workflows', () => { + it('should fail to restore a contract code when contract code is not found', async () => { + MOCKED_DEFAULT_RPC_HANDLER.mockImplementation(() => { + return { + getLedgerEntries: jest.fn().mockResolvedValue({ + entries: [], + latestLedger: 1, + }), + } + }) + + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + wasmHash: MOCKED_WASM_HASH, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + await expect(contractEngine.restoreContractCode(MOCKED_TX_INVOCATION)).rejects.toThrow( + CEError.contractCodeNotFound({ + entries: [], + latestLedger: 1, + } as SorobanRpc.Api.GetLedgerEntriesResponse) + ) + }) + + it('should restore a contract code', async () => { + MOCKED_DEFAULT_RPC_HANDLER.mockImplementation(() => { + return { + getLedgerEntries: jest.fn().mockResolvedValue({ + entries: [ + { + key: xdr.LedgerKey.contractCode(MOCKED_CONTRACT_CODE_KEY), + xdr: 'xdr', + }, + ], + latestLedger: 1, + }), + } + }) + + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + wasmHash: MOCKED_WASM_HASH, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + await expect(contractEngine.restoreContractCode(MOCKED_TX_INVOCATION)).resolves.toBeUndefined() + }) + + it('should fail to restore a contract instance when contract instance is not found', async () => { + MOCKED_DEFAULT_RPC_HANDLER.mockImplementation(() => { + return { + getLedgerEntries: jest.fn().mockResolvedValue({ + entries: [], + latestLedger: 1, + }), + } + }) + + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + contractId: MOCKED_CONTRACT_ID, + wasmHash: MOCKED_WASM_HASH, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + await expect(contractEngine.restoreContractInstance(MOCKED_TX_INVOCATION)).rejects.toThrow( + CEError.contractInstanceNotFound({ + entries: [], + latestLedger: 1, + } as SorobanRpc.Api.GetLedgerEntriesResponse) + ) + }) + + it('should restore a contract instance', async () => { + const footprint = new Contract(MOCKED_CONTRACT_ID).getFootprint() + + MOCKED_DEFAULT_RPC_HANDLER.mockImplementation(() => { + return { + getLedgerEntries: jest.fn().mockResolvedValue({ + entries: [ + { + key: footprint, + xdr: 'xdr', + }, + ], + latestLedger: 1, + }), + } + }) + + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + contractId: MOCKED_CONTRACT_ID, + wasmHash: MOCKED_WASM_HASH, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + await expect(contractEngine.restoreContractInstance(MOCKED_TX_INVOCATION)).resolves.toBeUndefined() + }) + }) + + describe('Contract invocation', () => { + it('should not wrap and deploy with a contract id', async () => { + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + contractId: MOCKED_CONTRACT_ID, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + await expect( + contractEngine.wrapAndDeployClassicAsset({ asset: MOCKED_STELLAR_ASSET, ...MOCKED_TX_INVOCATION }) + ).rejects.toThrow(CEError.contractIdAlreadySet()) + }) + + it('should wrap and deploy a classic asset', async () => { + MOCKED_SOROBAN_TRANSACTION_PIPELINE.mockImplementation(() => { + return { + execute: jest.fn().mockResolvedValue({ + output: { + contractId: MOCKED_CONTRACT_ID, + } as ContractIdOutput, + }), + } + }) + + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + wasmHash: MOCKED_WASM_HASH, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + await expect( + contractEngine.wrapAndDeployClassicAsset({ asset: MOCKED_STELLAR_ASSET, ...MOCKED_TX_INVOCATION }) + ).resolves.toBeUndefined() + + expect(contractEngine.getContractId()).toEqual(MOCKED_CONTRACT_ID) + }) + + it('should surface exceptions from the transaction pipeline when wrapping and deploying a classic asset', async () => { + MOCKED_SOROBAN_TRANSACTION_PIPELINE.mockImplementation(() => { + return { + execute: jest.fn().mockRejectedValue(StellarPlusError.unexpectedError()), + } + }) + + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + wasmHash: MOCKED_WASM_HASH, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + await expect( + contractEngine.wrapAndDeployClassicAsset({ asset: MOCKED_STELLAR_ASSET, ...MOCKED_TX_INVOCATION }) + ).rejects.toThrow(CEError.failedToWrapAsset(StellarPlusError.unexpectedError())) + }) + + it('should surface exceptions from the transaction pipeline when deploying a contract', async () => { + MOCKED_SOROBAN_TRANSACTION_PIPELINE.mockImplementation(() => { + return { + execute: jest.fn().mockRejectedValue(StellarPlusError.unexpectedError()), + } + }) + + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + wasm: MOCKED_WASM_FILE, + wasmHash: MOCKED_WASM_HASH, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + await expect(contractEngine.deploy(MOCKED_TX_INVOCATION)).rejects.toThrow( + CEError.failedToDeployContract(StellarPlusError.unexpectedError()) + ) + }) + + it('should surface exceptions from the transaction pipeline when uploading a wasm', async () => { + MOCKED_SOROBAN_TRANSACTION_PIPELINE.mockImplementation(() => { + return { + execute: jest.fn().mockRejectedValue(StellarPlusError.unexpectedError()), + } + }) + + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + wasm: MOCKED_WASM_FILE, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + await expect(contractEngine.uploadWasm(MOCKED_TX_INVOCATION)).rejects.toThrow( + CEError.failedToUploadWasm(StellarPlusError.unexpectedError()) + ) + }) + + it('should surface exceptions from the transaction pipeline when invoking a contract', async () => { + MOCKED_SOROBAN_TRANSACTION_PIPELINE.mockImplementation(() => { + return { + execute: jest.fn().mockRejectedValue(StellarPlusError.unexpectedError()), + } + }) + + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + contractId: MOCKED_CONTRACT_ID, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + await expect(contractEngine.invokeContract(MOCKED_SOROBAN_INVOKE_ARGS)).rejects.toThrow( + StellarPlusError.unexpectedError() + ) + }) + + it('should surface exceptions from the transaction pipeline when reading from a contract', async () => { + MOCKED_SOROBAN_TRANSACTION_PIPELINE.mockImplementation(() => { + return { + execute: jest.fn().mockRejectedValue(StellarPlusError.unexpectedError()), + } + }) + + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + contractId: MOCKED_CONTRACT_ID, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + await expect(contractEngine.readFromContract(MOCKED_SOROBAN_INVOKE_ARGS)).rejects.toThrow( + StellarPlusError.unexpectedError() + ) + }) + + it('should invoke a contract', async () => { + MOCKED_SOROBAN_TRANSACTION_PIPELINE.mockImplementation(() => { + return { + execute: jest.fn().mockResolvedValue({ + output: { value: true }, + }), + } + }) + + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + contractId: MOCKED_CONTRACT_ID, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + await expect(contractEngine.invokeContract(MOCKED_SOROBAN_INVOKE_ARGS)).resolves.toEqual(true) + }) + + it('should read from a contract', async () => { + MOCKED_SOROBAN_TRANSACTION_PIPELINE.mockImplementation(() => { + return { + execute: jest.fn().mockResolvedValue(true), + } + }) + + const contractEngine = new ContractEngine({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + contractId: MOCKED_CONTRACT_ID, + spec: MOCKED_CONTRACT_SPEC, + }, + }) + + await expect(contractEngine.readFromContract(MOCKED_SOROBAN_INVOKE_ARGS)).resolves.toEqual(true) + }) + }) }) From f66a40cba1e78e5bf19bd125d169d423733638e2 Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Thu, 28 Mar 2024 15:40:26 -0300 Subject: [PATCH 05/30] refactor: contract engine errors (#104) --- .../core/contract-engine/errors.ts | 90 +------------------ .../extract-auth-entries-output/index.ts | 2 + 2 files changed, 6 insertions(+), 86 deletions(-) diff --git a/src/stellar-plus/core/contract-engine/errors.ts b/src/stellar-plus/core/contract-engine/errors.ts index a27fdee..73fbb0a 100644 --- a/src/stellar-plus/core/contract-engine/errors.ts +++ b/src/stellar-plus/core/contract-engine/errors.ts @@ -19,17 +19,10 @@ export enum ContractEngineErrorCodes { CE008 = 'CE008', CE009 = 'CE009', - // CE1 Simulation - CE100 = 'CE100', + // CE1 Meta CE101 = 'CE101', CE102 = 'CE102', CE103 = 'CE103', - - // CE2 Meta - CE200 = 'CE200', - CE201 = 'CE201', - CE202 = 'CE202', - CE203 = 'CE203', } const missingContractId = (): StellarPlusError => { @@ -120,79 +113,10 @@ const contractCodeMissingLiveUntilLedgerSeq = ( }) } -const contractEngineClassFailedToInitialize = () => { - return new StellarPlusError({ - code: ContractEngineErrorCodes.CE009, - message: 'Contract engine class failed to initialize!', - source: 'ContractEngine', - details: - 'Contract engine class failed to initialize because of missing parameters! The Contract Engine must be initialized with either the wasm file, the wasm hash, or the contract ID. Please review the initialization parameters and try again.', - }) -} - -const simulationFailed = (simulation: SorobanRpc.Api.SimulateTransactionErrorResponse): StellarPlusError => { - return new StellarPlusError({ - code: ContractEngineErrorCodes.CE101, - message: 'Transaction simulation failed!', - source: 'ContractEngine', - details: - 'Transaction simulation failed! The transaction simulation returned a failure status. Review the meta data for further information about this error.', - meta: { - sorobanSimulationData: extractSimulationErrorData(simulation), - data: { simulation }, - }, - }) -} - -const simulationMissingResult = (simulation: SorobanRpc.Api.SimulateTransactionSuccessResponse): StellarPlusError => { - return new StellarPlusError({ - code: ContractEngineErrorCodes.CE103, - message: 'Transaction simulation is missing the result data!', - source: 'ContractEngine', - details: - 'Transaction simulation is missing the result data! The transaction simulation returned a success status, but the result data is missing. Review the simulated transaction parameters for further for troubleshooting.', - meta: { data: { simulation } }, - }) -} - -const transactionNeedsRestore = (simulation: SorobanRpc.Api.SimulateTransactionRestoreResponse): StellarPlusError => { - return new StellarPlusError({ - code: ContractEngineErrorCodes.CE102, - message: 'A footprint restore is required!', - source: 'ContractEngine', - details: - 'The transaction simulation returned a restore status. This usually indicates the contract instance or the storage data has reached its limit. Review the meta data for further information about this error. It might be possible to restore the contract state by extending the contract instance or the storage data TTL.', - meta: { sorobanSimulationData: extractSimulationRestoreData(simulation), data: { simulation } }, - }) -} - -const couldntVerifyTransactionSimulation = ( - simulation: SorobanRpc.Api.SimulateTransactionResponse -): StellarPlusError => { - return new StellarPlusError({ - code: ContractEngineErrorCodes.CE100, - message: 'Unexpected error in transaction simulation!', - source: 'ContractEngine', - details: - 'Unexpected error in transaction simulation! The transaction simulation returned an unexpected status. Review the meta data for further information about this error.', - meta: { sorobanSimulationData: extractSimulationBaseData(simulation), data: { simulation } }, - }) -} - -const restoreOptionNotSet = (simulation: SorobanRpc.Api.SimulateTransactionRestoreResponse): StellarPlusError => { - return new StellarPlusError({ - code: ContractEngineErrorCodes.CE200, - message: 'Restore option not set!', - source: 'ContractEngine', - details: - 'Restore option not set! This function requires a restore option to be defined in this instance. You can either initialize the contract engine with a restore option or use the "restore" function to restore the contract state from a previous transaction.', - meta: { sorobanSimulationData: extractSimulationRestoreData(simulation), data: { simulation } }, - }) -} const failedToUploadWasm = (error: StellarPlusError): StellarPlusError => { return new StellarPlusError({ - code: ContractEngineErrorCodes.CE201, + code: ContractEngineErrorCodes.CE101, message: 'Failed to upload wasm!', source: 'ContractEngine', details: @@ -203,7 +127,7 @@ const failedToUploadWasm = (error: StellarPlusError): StellarPlusError => { const failedToDeployContract = (error: StellarPlusError): StellarPlusError => { return new StellarPlusError({ - code: ContractEngineErrorCodes.CE202, + code: ContractEngineErrorCodes.CE102, message: 'Failed to deploy contract!', source: 'ContractEngine', details: @@ -214,7 +138,7 @@ const failedToDeployContract = (error: StellarPlusError): StellarPlusError => { const failedToWrapAsset = (error: StellarPlusError): StellarPlusError => { return new StellarPlusError({ - code: ContractEngineErrorCodes.CE203, + code: ContractEngineErrorCodes.CE103, message: 'Failed to wrap asset!', source: 'ContractEngine', details: 'The asset could not be wrapped. Review the meta error to identify the underlying cause for this issue.', @@ -226,17 +150,11 @@ export const CEError = { missingContractId, missingWasm, missingWasmHash, - couldntVerifyTransactionSimulation, - simulationFailed, contractIdAlreadySet, contractInstanceNotFound, contractInstanceMissingLiveUntilLedgerSeq, contractCodeNotFound, contractCodeMissingLiveUntilLedgerSeq, - contractEngineClassFailedToInitialize, - transactionNeedsRestore, - simulationMissingResult, - restoreOptionNotSet, failedToUploadWasm, failedToDeployContract, failedToWrapAsset, diff --git a/src/stellar-plus/utils/pipeline/plugins/simulate-transaction/extract-auth-entries-output/index.ts b/src/stellar-plus/utils/pipeline/plugins/simulate-transaction/extract-auth-entries-output/index.ts index a53fc47..9c89f88 100644 --- a/src/stellar-plus/utils/pipeline/plugins/simulate-transaction/extract-auth-entries-output/index.ts +++ b/src/stellar-plus/utils/pipeline/plugins/simulate-transaction/extract-auth-entries-output/index.ts @@ -24,6 +24,8 @@ export class ExtractAuthEntriesFromSimulationPlugin const { response, output } = item if (!response.result) { + // TODO: + // implement error handling here and migrate older CE Error // throw CEError.simulationMissingResult(simulated) throw new Error('simulationMissingResult') } From 47ccf165ab669955f0b9d168393fff8d6166c9a9 Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Thu, 28 Mar 2024 16:35:14 -0300 Subject: [PATCH 06/30] Update docs and minor adjustments to jsdocs (#105) * fix: missing error CE009 * docs: update jsdoc * docs: update readme for public release --- README.md | 34 +++++++++++++------ .../account-handler/freighter/index.ts | 2 +- src/stellar-plus/account/base/index.ts | 2 +- src/stellar-plus/asset/classic/index.ts | 22 ++++++------ src/stellar-plus/asset/soroban-token/index.ts | 11 ++++-- .../asset/stellar-asset-contract/index.ts | 19 ++++++----- .../core/contract-engine/errors.ts | 10 ++++++ .../core/contract-engine/index.ts | 14 ++++---- 8 files changed, 72 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 6be581f..a6e3a92 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,30 @@ - # Stellar-Plus +

npm version Weekly Downloads

-stellar-plus is an all-in-one Javascript library for building and interacting with the Stellar network. It bundles the main resources from the community into an easy-to-use set of tools and capabilities. -It provides: +
+ + + + +
+
+ +Stellar-plus is a robust JavaScript library built by [Cheesecake Labs](./) and designed to streamline the development of applications on the Stellar network. By integrating the Stellar community's primary resources, Stellar-plus offers developers an efficient, easy-to-use toolkit. This library simplifies the complexities of Stellar network interaction, making it accessible for both novice and experienced developers alike. -* **Account**: Handlers to create, load, and interact with stellar accounts, managing signatures and automatically integrating with Freighter Wallet for web applications. -* **Asset**: Classic token handlers follow the standard token interface for triggering different asset capabilities as well as a suite of additional features for asset management and usage. -* **Core**: Key engines for managing the different pipelines for building, submitting, and processing both Classic and Soroban transactions. These engines can be extended into your own tooling or used out-of-the-box with minimal configuration. -* **Contracts**: Default contract client implementations for selected dApp use cases. -* **RPC**: Handlers for connecting and using different RPC solutions, including a ready-to-use integration with Validation Cloud's RPC API. +## Features + +- **Account Handling**: Seamless management of signatures throughout the transaction lifecycle. +- **Asset Management**: Full suite of asset management capabilities, including standard and custom assets. +- **Core Engines**: Essential for building, submitting, signing, and processing transactions on the Stellar network. +- **Contract Development**: Simplifies the development of decentralized applications (dApps). +- **RPC Integration**: Connects to and leverages various RPC services for a broader range of applications. +- **Plugins and Extensions**: Supports plugins and tools to enhance functionality and tailor the library to specific needs. ## Quick start @@ -35,15 +45,19 @@ npm install --save stellar-plus require/import it in your JavaScript: ```js -var StellarPlus = require("stellar-plus"); +var StellarPlus = require('stellar-plus') ``` or ```js -import { StellarPlus } from "stellar-plus"; +import { StellarPlus } from 'stellar-plus' ``` ## Documentation For the full documentation, refer to our [Gitbook Documentation](https://cheesecake-labs.gitbook.io/stellar-plus/?utm_source=github&utm_medium=codigo-fonte). + +- [Code of Conduct](https://github.com/cheesecakelabs/stellar-plus/blob/main/CODE_OF_CONDUCT.md) +- [Contributing Guidelines](https://github.com/cheesecakelabs/stellar-plus/blob/main/CONTRIBUTING.md) +- [MIT License](https://github.com/cheesecakelabs/stellar-plus/blob/main/LICENSE) diff --git a/src/stellar-plus/account/account-handler/freighter/index.ts b/src/stellar-plus/account/account-handler/freighter/index.ts index eeafe1e..843c1ee 100644 --- a/src/stellar-plus/account/account-handler/freighter/index.ts +++ b/src/stellar-plus/account/account-handler/freighter/index.ts @@ -128,7 +128,7 @@ export class FreighterAccountHandlerClient extends AccountBaseClient implements * * @param {xdr.SorobanAuthorizationEntry} entry - The soroban authorization entry to sign. * @param {number} validUntilLedgerSeq - The ledger sequence number until which the entry signature is valid. - * + * @param {string} networkPassphrase - The network passphrase for the network to sign the entry for. * @description - Signs the given Soroban authorization entry with the account's secret key. * * @returns {xdr.SorobanAuthorizationEntry} The signed entry. diff --git a/src/stellar-plus/account/base/index.ts b/src/stellar-plus/account/base/index.ts index 8d01caf..56b4b8d 100644 --- a/src/stellar-plus/account/base/index.ts +++ b/src/stellar-plus/account/base/index.ts @@ -8,7 +8,7 @@ export class AccountBaseClient extends AccountHelpers implements AccountBase { * * @args {} payload - The payload for the account. Additional parameters may be provided to enable different helpers. * @param {string} payload.publicKey The public key of the account. - * @param {NetworkConfig=} payload.networkConfig The network to use. + * @param {NetworkConfig=} payload.networkConfig The network config for the target network. * * @description - The base account is used for handling accounts with no management actions. */ diff --git a/src/stellar-plus/asset/classic/index.ts b/src/stellar-plus/asset/classic/index.ts index fa29cfb..cad5560 100644 --- a/src/stellar-plus/asset/classic/index.ts +++ b/src/stellar-plus/asset/classic/index.ts @@ -26,10 +26,10 @@ export class ClassicAssetHandler implements IClassicAssetHandler { /** * * @param {string} code - The asset code. - * @param {string} issuerPublicKey - The public key of the asset issuer. - * @param {NetworkConfig} networkConfig - The network to use. - * @param {AccountHandler=} issuerAccount - The issuer account handler. When provided, it'll enable management functions and be used to sign transactions as the issuer. - * @param {TransactionSubmitter=} transactionSubmitter - The transaction submitter to use. + * @param {string | AccountHandler} issuerAccount - The issuer account. When an account handler is provided, it'll enable management functions and be used to sign transactions as the issuer. + * @param {NetworkConfig} networkConfig - The network configuration to use. + * @param {ClassicTransactionPipelineOptions} options - The options for the classic transaction pipeline. + @param {ClassicTransactionPipelineOptions} options.classicTransactionPipeline - The options for the classic transaction pipeline. These allow for custom configurations for how the transaction pipeline will operate for this asset. * * @description - The Classic asset handler is used for handling classic assets with user-based and management functionalities. * @@ -92,9 +92,9 @@ export class ClassicAssetHandler implements IClassicAssetHandler { return this.code } - // /** - // * @description - Not implemented in pure classic assets. Only available for Soroban assets. - // */ + /** + * @description - Not implemented in the current version for pure classic assets. Only available for Soroban assets. + */ public async approve(): Promise { throw new Error('Method not implemented.') @@ -186,10 +186,10 @@ export class ClassicAssetHandler implements IClassicAssetHandler { // /** - * + * @args * @param {string} to - The account id to mint the asset to. * @param {i128} amount - The amount of the asset to mint. - * @param {TransactionInvocation} txInvocation - The transaction invocation object. The Issuer account will be automatically added as a signer. + * @param {TransactionInvocation} txInvocation - The transaction invocation object spread. The Issuer account will be automatically added as a signer. * * @description - Mints the given amount of the asset to the 'to' account. * @requires - The issuer account to be set in the asset. @@ -241,7 +241,7 @@ export class ClassicAssetHandler implements IClassicAssetHandler { * * @param {string} to - The account id to mint the asset to. * @param {number} amount - The amount of the asset to mint. - * @param {TransactionInvocation} txInvocation - The transaction invocation object. The The Issuer account will be automatically added as a signer. + * @param {TransactionInvocation} txInvocation - The transaction invocation object spread. The Issuer account will be automatically added as a signer. * * @requires - The issuer account to be set in the asset. * @requires - The 'to' account to be set as a signer in the transaction invocation. @@ -289,7 +289,7 @@ export class ClassicAssetHandler implements IClassicAssetHandler { /** * * @param {string} to - The account id to add the trustline. - * @param {TransactionInvocation} txInvocation - The transaction invocation object. + * @param {TransactionInvocation} txInvocation - The transaction invocation object spread. * * @requires - The 'to' account to be set as a signer in the transaction invocation. * diff --git a/src/stellar-plus/asset/soroban-token/index.ts b/src/stellar-plus/asset/soroban-token/index.ts index fce35d2..292a838 100644 --- a/src/stellar-plus/asset/soroban-token/index.ts +++ b/src/stellar-plus/asset/soroban-token/index.ts @@ -14,14 +14,19 @@ export class SorobanTokenHandler extends ContractEngine implements SorobanTokenI * * @args args * @param {NetworkConfig} args.networkConfig - Network to connect to - * @param {ContractSpec=} args.spec - Contract specification object - * @param {string=} args.contractId - Contract ID - * @param {RpcHandler=} args.rpcHandler - RPC Handler + * @param args.contractParameters - Contract parameters + * @param {ContractSpec=} args.contractParameters.spec - Contract specification + * @param {string=} args.contractParameters.contractId - Contract ID * @param {Buffer=} args.wasm - Contract WASM file as Buffer * @param {string=} args.wasmHash - Contract WASM hash identifier + * @param {Options=} args.options - Contract options + * @param {SorobanTransactionPipelineOptions=} args.options.sorobanTransactionPipeline - Soroban transaction pipeline options. Allows for customizing how transaction pipeline will be executed for this contract. * * @description Create a new SorobanTokenHandler instance to interact with a Soroban Token contract. * This class is a subclass of ContractEngine and implements the Soroban token interface. + * The contract spec is set to the default Soroban Token spec. When initializing the contract, the spec can be overridden with a custom spec. + * The contract ID, WASM file, and WASM hash can be provided to initialize the contract with the given parameters. At least one of these parameters must be provided. + * */ constructor(args: SorobanTokenHandlerConstructorArgs) { super({ diff --git a/src/stellar-plus/asset/stellar-asset-contract/index.ts b/src/stellar-plus/asset/stellar-asset-contract/index.ts index 9fff1f8..238683a 100644 --- a/src/stellar-plus/asset/stellar-asset-contract/index.ts +++ b/src/stellar-plus/asset/stellar-asset-contract/index.ts @@ -6,6 +6,7 @@ import { SorobanTokenHandler } from 'stellar-plus/asset/soroban-token' import { SorobanTokenHandlerConstructorArgs } from 'stellar-plus/asset/soroban-token/types' import { SACConstructorArgs, SACHandler as SACHandlerType } from 'stellar-plus/asset/stellar-asset-contract/types' import { AssetTypes } from 'stellar-plus/asset/types' + import { TransactionInvocation } from 'stellar-plus/core/types' export class SACHandler implements SACHandlerType { @@ -20,15 +21,15 @@ export class SACHandler implements SACHandlerType { * @param {NetworkConfig} args.networkConfig - The network to connect to. * Parameters related to the classic asset. * @param {string} args.code - The asset code. - * @param {string} args.issuerPublicKey - The issuer public key. - * @param {AccountHandler=} args.issuerAccount - The issuer account. - * @param {TransactionSubmitter=} args.transactionSubmitter - The classic transaction submitter. - * Parameters related to the Soroban token. - * @param {ContractSpec=} args.spec - The contract specification object. - * @param {Buffer=} args.wasm - The contract wasm file as a buffer. - * @param {string=} args.wasmHash - The contract wasm hash id. - * @param {string=} args.contractId - The contract id. - * @param {RpcHandler=} args.rpcHandler - A custom Soroban RPC handler. + * @param {string | AccountHandler} args.issuerAccount - The issuer account. Can be a public key or an account handler. If it's an account handler, it will enable management functions. + * @param contractParameters - The contract parameters. + * @param {ContractSpec=} contractParameters.spec - The contract specification object. + * @param {Buffer=} contractParameters.wasm - The contract wasm file as a buffer. + * @param {string=} contractParameters.wasmHash - The contract wasm hash id. + * @param {string=} contractParameters.contractId - The contract id. + * @param options - The contract options. + * @param {SorobanTransactionPipelineOptions=} options.sorobanTransactionPipeline - The Soroban transaction pipeline. + * @param { ClassicTransactionPipelineOptions=} options.classicTransactionPipeline - The classic transaction pipeline. * * * @description - The Stellar Asset Contract handler. It combines the classic asset handler and the Soroban token handler. diff --git a/src/stellar-plus/core/contract-engine/errors.ts b/src/stellar-plus/core/contract-engine/errors.ts index 73fbb0a..d9f041d 100644 --- a/src/stellar-plus/core/contract-engine/errors.ts +++ b/src/stellar-plus/core/contract-engine/errors.ts @@ -113,6 +113,15 @@ const contractCodeMissingLiveUntilLedgerSeq = ( }) } +const contractEngineClassFailedToInitialize = () => { + return new StellarPlusError({ + code: ContractEngineErrorCodes.CE009, + message: 'Contract engine class failed to initialize!', + source: 'ContractEngine', + details: + 'Contract engine class failed to initialize because of missing parameters! The Contract Engine must be initialized with either the wasm file, the wasm hash, or the contract ID. Please review the initialization parameters and try again.', + }) +} const failedToUploadWasm = (error: StellarPlusError): StellarPlusError => { return new StellarPlusError({ @@ -155,6 +164,7 @@ export const CEError = { contractInstanceMissingLiveUntilLedgerSeq, contractCodeNotFound, contractCodeMissingLiveUntilLedgerSeq, + contractEngineClassFailedToInitialize, failedToUploadWasm, failedToDeployContract, failedToWrapAsset, diff --git a/src/stellar-plus/core/contract-engine/index.ts b/src/stellar-plus/core/contract-engine/index.ts index b0e094b..609e515 100644 --- a/src/stellar-plus/core/contract-engine/index.ts +++ b/src/stellar-plus/core/contract-engine/index.ts @@ -57,13 +57,13 @@ export class ContractEngine { /** * * @param {NetworkConfig} networkConfig - The network to use. - * @param {ContractSpec} spec - The contract specification. - * @param {string=} contractId - The contract id. - * @param {RpcHandler=} rpcHandler - A custom RPC handler to use when interacting with the network RPC server. - * @param {Options=} options - A set of custom options to modify the behavior of the contract engine. - * @param {boolean=} options.debug - A flag to enable debug mode. This will toggle the extraction of transaction resources consumed with each transaction/simiulation. - * @param {CostHandler=} options.costHandler - A custom function to handle the transaction resources consumed with each transaction/simulation. Whn not provided, the default cost handler will be used and the resources will be logged to the console. - * @param {TransactionInvocation=} options.restoreTxInvocation - The transaction invocation object to use when automatically restoring the contract footprint. When this parameter is provided, whenever a simulation indicates that the contract footprint needs to be restored, the contract engine will automatically restore the footprint using the provided transaction invocation object. + * @param contractParameters - The contract parameters. + * @param {ContractSpec} contractParameters.spec - The contract specification object. + * @param {string=} contractParameters.contractId - The contract id. + * @param {Buffer=} contractParameters.wasm - The contract wasm file as a buffer. + * @param {string=} contractParameters.wasmHash - The contract wasm hash id. + * @param {Options=} options - A set of custom options to modify the behavior of the contract engine. + * @param {SorobanTransactionPipelineOptions=} options.sorobanTransactionPipeline - The Soroban transaction pipeline. * @description - The contract engine is used for interacting with contracts on the network. This class can be extended to create a contract client, abstracting away the Soroban integration. * * @example - The following example shows how to invoke a contract method that alters the state of the contract. From c30ab86b95d826a1ee1aade78dde7b753225db9a Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Thu, 28 Mar 2024 17:03:51 -0300 Subject: [PATCH 07/30] packaging: bump version to 0.7.0 (#108) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2463b1e..3f3bcb7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stellar-plus", - "version": "0.6.2", + "version": "0.7.0", "description": "beta version of stellar-plus, an all-in-one sdk for the Stellar blockchain", "main": "./lib/index.js", "types": "./lib/index.d.ts", From 81b25eb6c5b9dd83cde2fc7c15e211e589402177 Mon Sep 17 00:00:00 2001 From: Bruno Nascimento Date: Mon, 1 Apr 2024 12:13:57 -0300 Subject: [PATCH 08/30] test: add build transaction unit tests --- .eslintrc.json | 4 +- .../build-transaction/index.unit.test.ts | 214 ++++++++++++++++++ .../core/pipelines/build-transaction/types.ts | 2 +- src/stellar-plus/horizon/index.ts | 2 +- 4 files changed, 218 insertions(+), 4 deletions(-) create mode 100644 src/stellar-plus/core/pipelines/build-transaction/index.unit.test.ts diff --git a/.eslintrc.json b/.eslintrc.json index ed995db..ce6fdfd 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -2,9 +2,9 @@ "root": true, "parser": "@typescript-eslint/parser", "parserOptions": { - "project": ["tsconfig.json"] + "project": ["tsconfig.json"], + "exclude": "commitlint.config.js" }, - "exclude": "commitlint.config.js", "plugins": ["@typescript-eslint", "prettier", "unicorn", "import"], "extends": [ "eslint:recommended", diff --git a/src/stellar-plus/core/pipelines/build-transaction/index.unit.test.ts b/src/stellar-plus/core/pipelines/build-transaction/index.unit.test.ts new file mode 100644 index 0000000..5f43d92 --- /dev/null +++ b/src/stellar-plus/core/pipelines/build-transaction/index.unit.test.ts @@ -0,0 +1,214 @@ +/* eslint-disable import/order */ +import { Horizon, TransactionBuilder } from '@stellar/stellar-sdk' +import { AccountResponse } from '@stellar/stellar-sdk/lib/horizon' +import { Asset, Operation } from '@stellar/stellar-base' +import { testnet } from 'stellar-plus/constants' +import { + BuildTransactionPipelineInput as BTInput, + BuildTransactionPipelineOutput as BTOutput, +} from 'stellar-plus/core/pipelines/build-transaction/types' +import { HorizonHandler } from 'stellar-plus/horizon/types' + +import { BuildTransactionPipeline } from '.' + +jest.mock('@stellar/stellar-sdk', () => ({ + Horizon: { + Server: jest.fn(), + }, + Account: jest.fn(), + TransactionBuilder: jest.fn(() => { + return { + addOperation: jest.fn(), + setTimeout: jest.fn(), + setSorobanData: jest.fn(() => { + return {} + }), + build: jest.fn(() => { + return {} + }), + } + }), +})) + +export function createMockedHorizonHandler(): jest.Mocked { + return { + loadAccount: jest.fn().mockResolvedValue({} as AccountResponse), + server: new Horizon.Server(testnet.horizonUrl), + } +} + +const mockedHorizonHandler = createMockedHorizonHandler() + +const MOCKED_BT_INPUT: BTInput = { + header: { + source: 'source', + fee: '100', + timeout: 2, + }, + horizonHandler: mockedHorizonHandler, + operations: [], + networkPassphrase: 'networkPassphrase', +} +const MOCKED_BT_OUTPUT: BTOutput = {} as BTOutput + +describe('BuildTransactionPipeline', () => { + let buildTransactionPipeline: BuildTransactionPipeline + + beforeEach(() => { + jest.clearAllMocks() + mockedHorizonHandler.loadAccount.mockClear() + }) + + describe('Load Account', () => { + beforeEach(() => { + buildTransactionPipeline = new BuildTransactionPipeline() + jest.clearAllMocks() + }) + + it('should load account successfully', async () => { + await buildTransactionPipeline.execute(MOCKED_BT_INPUT) + + expect(mockedHorizonHandler.loadAccount).toHaveBeenCalledWith('source') + expect(mockedHorizonHandler.loadAccount).toHaveBeenCalledTimes(1) + }) + + it('should throw error', async () => { + mockedHorizonHandler.loadAccount.mockRejectedValueOnce(new Error('error')) + + await expect(buildTransactionPipeline.execute(MOCKED_BT_INPUT)).rejects.toThrow('Could not load account!') + expect(mockedHorizonHandler.loadAccount).toHaveBeenCalledWith('source') + expect(mockedHorizonHandler.loadAccount).toHaveBeenCalledTimes(1) + }) + }) + + describe('Build Envelope', () => { + const transactionBuilderOptions = { + fee: MOCKED_BT_INPUT.header.fee, + networkPassphrase: MOCKED_BT_INPUT.networkPassphrase, + } + + beforeEach(() => { + buildTransactionPipeline = new BuildTransactionPipeline() + jest.clearAllMocks() + }) + + it('should create envelope successfully', async () => { + await buildTransactionPipeline.execute(MOCKED_BT_INPUT) + + expect(TransactionBuilder).toHaveBeenCalledWith({}, transactionBuilderOptions) + }) + + it('should add sorobanData successfully', async () => { + await buildTransactionPipeline.execute({ + ...MOCKED_BT_INPUT, + sorobanData: 'sorobanData', + }) + + expect(TransactionBuilder).toHaveBeenCalledWith({}, transactionBuilderOptions) + }) + + it('should add single operation successfully', async () => { + await buildTransactionPipeline.execute({ + ...MOCKED_BT_INPUT, + operations: [ + Operation.payment({ + destination: 'GB3MXH633VRECLZRUAR3QCLQJDMXNYNHKZCO6FJEWXVWSUEIS7NU376P', + asset: Asset.native(), + amount: '100', + }), + ], + }) + + expect(TransactionBuilder).toHaveBeenCalledWith({}, transactionBuilderOptions) + }) + + it('should add multiple operation successfully', async () => { + await buildTransactionPipeline.execute({ + ...MOCKED_BT_INPUT, + operations: [ + Operation.payment({ + destination: 'GB3MXH633VRECLZRUAR3QCLQJDMXNYNHKZCO6FJEWXVWSUEIS7NU376P', + asset: Asset.native(), + amount: '100', + }), + Operation.payment({ + destination: 'GB3MXH633VRECLZRUAR3QCLQJDMXNYNHKZCO6FJEWXVWSUEIS7NU376P', + asset: Asset.native(), + amount: '100', + }), + Operation.payment({ + destination: 'GB3MXH633VRECLZRUAR3QCLQJDMXNYNHKZCO6FJEWXVWSUEIS7NU376P', + asset: Asset.native(), + amount: '100', + }), + ], + }) + + expect(TransactionBuilder).toHaveBeenCalledWith({}, transactionBuilderOptions) + }) + + it('should throw error', async () => { + ;(TransactionBuilder as unknown as jest.Mock).mockImplementationOnce(() => { + throw new Error('error') + }) + + await expect(buildTransactionPipeline.execute(MOCKED_BT_INPUT)).rejects.toThrow( + 'Could not create transaction builder!' + ) + expect(TransactionBuilder).toHaveBeenCalledWith({}, transactionBuilderOptions) + }) + + it('should throw error adding sorobanData', async () => { + ;(TransactionBuilder as unknown as jest.Mock).mockImplementationOnce(() => { + return { + setSorobanData: jest.fn(() => { + throw new Error('error') + }), + } + }) + + await expect( + buildTransactionPipeline.execute({ + ...MOCKED_BT_INPUT, + sorobanData: 'sorobanData', + }) + ).rejects.toThrow('Could not set Soroban data!') + expect(TransactionBuilder).toHaveBeenCalledWith({}, transactionBuilderOptions) + }) + + it('should throw error adding operation', async () => { + ;(TransactionBuilder as unknown as jest.Mock).mockImplementationOnce(() => { + return { + addOperation: jest.fn(() => { + throw new Error('error') + }), + } + }) + + await expect( + buildTransactionPipeline.execute({ + ...MOCKED_BT_INPUT, + operations: [ + Operation.payment({ + destination: 'GB3MXH633VRECLZRUAR3QCLQJDMXNYNHKZCO6FJEWXVWSUEIS7NU376P', + asset: Asset.native(), + amount: '100', + }), + ], + }) + ).rejects.toThrow('Could not add operations!') + expect(TransactionBuilder).toHaveBeenCalledWith({}, transactionBuilderOptions) + }) + }) + + describe('Process', () => { + beforeEach(() => { + buildTransactionPipeline = new BuildTransactionPipeline() + jest.clearAllMocks() + }) + + it('should build transaction successfully', async () => { + await expect(buildTransactionPipeline.execute(MOCKED_BT_INPUT)).resolves.toEqual(MOCKED_BT_OUTPUT) + }) + }) +}) diff --git a/src/stellar-plus/core/pipelines/build-transaction/types.ts b/src/stellar-plus/core/pipelines/build-transaction/types.ts index 4b74bee..af1b3de 100644 --- a/src/stellar-plus/core/pipelines/build-transaction/types.ts +++ b/src/stellar-plus/core/pipelines/build-transaction/types.ts @@ -1,6 +1,6 @@ import { xdr } from '@stellar/stellar-sdk' -import { HorizonHandler } from 'stellar-plus' +import { HorizonHandler } from 'stellar-plus/horizon/types' import { EnvelopeHeader, Transaction } from 'stellar-plus/types' import { BeltPluginType, GenericPlugin } from 'stellar-plus/utils/pipeline/conveyor-belts/types' diff --git a/src/stellar-plus/horizon/index.ts b/src/stellar-plus/horizon/index.ts index 28c5047..6f76954 100644 --- a/src/stellar-plus/horizon/index.ts +++ b/src/stellar-plus/horizon/index.ts @@ -5,7 +5,7 @@ import { HorizonHandler } from 'stellar-plus/horizon/types' import { NetworkConfig } from 'stellar-plus/types' export class HorizonHandlerClient implements HorizonHandler { - private networkConfig: NetworkConfig + public networkConfig: NetworkConfig public server: Horizon.Server /** From 961ad28164848d3af4c39665d64288e2c8850518 Mon Sep 17 00:00:00 2001 From: Bruno Nascimento Date: Mon, 1 Apr 2024 12:15:15 -0300 Subject: [PATCH 09/30] chore: merge with develop --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 25c8849..5fa8a3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "stellar-plus", - "version": "0.6.2", + "version": "0.7.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "stellar-plus", - "version": "0.6.2", + "version": "0.7.0", "license": "ISC", "dependencies": { "@stellar/freighter-api": "^1.7.1", From 7249e02bbb82b7a3a2065f9f41b1af688ee982b3 Mon Sep 17 00:00:00 2001 From: Bruno Nascimento Date: Mon, 1 Apr 2024 13:45:08 -0300 Subject: [PATCH 10/30] test: add build transaction failure tests --- .../pipelines/build-transaction/index.unit.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/stellar-plus/core/pipelines/build-transaction/index.unit.test.ts b/src/stellar-plus/core/pipelines/build-transaction/index.unit.test.ts index 5f43d92..d00dae5 100644 --- a/src/stellar-plus/core/pipelines/build-transaction/index.unit.test.ts +++ b/src/stellar-plus/core/pipelines/build-transaction/index.unit.test.ts @@ -199,6 +199,20 @@ describe('BuildTransactionPipeline', () => { ).rejects.toThrow('Could not add operations!') expect(TransactionBuilder).toHaveBeenCalledWith({}, transactionBuilderOptions) }) + + it('should throw error building operation', async () => { + ;(TransactionBuilder as unknown as jest.Mock).mockImplementationOnce(() => { + return { + setTimeout: jest.fn(), + build: jest.fn(() => { + throw new Error('error') + }), + } + }) + + await expect(buildTransactionPipeline.execute(MOCKED_BT_INPUT)).rejects.toThrow('Could not build transaction!') + expect(TransactionBuilder).toHaveBeenCalledWith({}, transactionBuilderOptions) + }) }) describe('Process', () => { From 44561cbb145670b9aca6dd823b140c6bb8b6ba63 Mon Sep 17 00:00:00 2001 From: Bruno Nascimento Date: Mon, 1 Apr 2024 13:52:52 -0300 Subject: [PATCH 11/30] refactor: make networkConfig variable private --- src/stellar-plus/horizon/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stellar-plus/horizon/index.ts b/src/stellar-plus/horizon/index.ts index 6f76954..28c5047 100644 --- a/src/stellar-plus/horizon/index.ts +++ b/src/stellar-plus/horizon/index.ts @@ -5,7 +5,7 @@ import { HorizonHandler } from 'stellar-plus/horizon/types' import { NetworkConfig } from 'stellar-plus/types' export class HorizonHandlerClient implements HorizonHandler { - public networkConfig: NetworkConfig + private networkConfig: NetworkConfig public server: Horizon.Server /** From d8924db50a5b6b9a33ede0aeb5e05dc3938f6778 Mon Sep 17 00:00:00 2001 From: Bruno Nascimento Date: Mon, 1 Apr 2024 16:27:41 -0300 Subject: [PATCH 12/30] test: add unit test for fee bump pipeline (#113) --- src/jest.config.js | 10 +- .../core/contract-engine/index.unit.test.ts | 16 +-- .../build-transaction/index.unit.test.ts | 4 +- .../pipelines/fee-bump/index.unit.test.ts | 103 ++++++++++++++++++ 4 files changed, 122 insertions(+), 11 deletions(-) create mode 100644 src/stellar-plus/core/pipelines/fee-bump/index.unit.test.ts diff --git a/src/jest.config.js b/src/jest.config.js index be21348..4d255ff 100644 --- a/src/jest.config.js +++ b/src/jest.config.js @@ -4,8 +4,14 @@ module.exports = { testEnvironment: 'node', collectCoverage: true, coverageReporters: ['cobertura', 'json', 'html', 'text'], - coveragePathIgnorePatterns: ['/node_modules/', '/*/.*\\.types.ts', '.*\\mocks.ts', './coverage/'], - collectCoverageFrom: ['./**/*.ts'], + collectCoverageFrom: [ + './**/*.ts', + '!/*/.*\\.types.ts', + '!/*/.*\\.mocks.ts', + '!/dist/', + '!/node_modules/', + '!/coverage/', + ], setupFilesAfterEnv: ['./setup-tests.ts'], modulePathIgnorePatterns: ['/dist/'], moduleDirectories: ['node_modules', 'src'], diff --git a/src/stellar-plus/core/contract-engine/index.unit.test.ts b/src/stellar-plus/core/contract-engine/index.unit.test.ts index 9c7e50c..773fc04 100644 --- a/src/stellar-plus/core/contract-engine/index.unit.test.ts +++ b/src/stellar-plus/core/contract-engine/index.unit.test.ts @@ -1,14 +1,16 @@ -import { ContractEngine } from '../contract-engine' +import { Asset, Contract, SorobanRpc, xdr } from '@stellar/stellar-sdk' + import { Constants } from 'stellar-plus' -import { spec as tokenSpec, methods as tokenMethods } from 'stellar-plus/asset/soroban-token/constants' -import { CEError } from './errors' +import { methods as tokenMethods, spec as tokenSpec } from 'stellar-plus/asset/soroban-token/constants' +import { SorobanTransactionPipeline } from 'stellar-plus/core/pipelines/soroban-transaction' +import { StellarPlusError } from 'stellar-plus/error' +import { DefaultRpcHandler } from 'stellar-plus/rpc' import { TransactionInvocation } from 'stellar-plus/types' + +import { CEError } from './errors' import { SorobanInvokeArgs } from './types' -import { SorobanTransactionPipeline } from 'stellar-plus/core/pipelines/soroban-transaction' +import { ContractEngine } from '../contract-engine' import { ContractIdOutput, ContractWasmHashOutput } from '../pipelines/soroban-get-transaction/types' -import { Asset, Contract, SorobanRpc, xdr } from '@stellar/stellar-sdk' -import { DefaultRpcHandler } from 'stellar-plus/rpc' -import { StellarPlusError } from 'stellar-plus/error' jest.mock('stellar-plus/core/pipelines/soroban-transaction', () => ({ SorobanTransactionPipeline: jest.fn(), diff --git a/src/stellar-plus/core/pipelines/build-transaction/index.unit.test.ts b/src/stellar-plus/core/pipelines/build-transaction/index.unit.test.ts index d00dae5..aaa3400 100644 --- a/src/stellar-plus/core/pipelines/build-transaction/index.unit.test.ts +++ b/src/stellar-plus/core/pipelines/build-transaction/index.unit.test.ts @@ -1,7 +1,7 @@ -/* eslint-disable import/order */ +import { Asset, Operation } from '@stellar/stellar-base' import { Horizon, TransactionBuilder } from '@stellar/stellar-sdk' import { AccountResponse } from '@stellar/stellar-sdk/lib/horizon' -import { Asset, Operation } from '@stellar/stellar-base' + import { testnet } from 'stellar-plus/constants' import { BuildTransactionPipelineInput as BTInput, diff --git a/src/stellar-plus/core/pipelines/fee-bump/index.unit.test.ts b/src/stellar-plus/core/pipelines/fee-bump/index.unit.test.ts new file mode 100644 index 0000000..be3da20 --- /dev/null +++ b/src/stellar-plus/core/pipelines/fee-bump/index.unit.test.ts @@ -0,0 +1,103 @@ +import { FeeBumpTransaction, Transaction, TransactionBuilder } from '@stellar/stellar-sdk' + +import { + FeeBumpPipelineInput as FBInput, + FeeBumpPipelineOutput as FBOutput, +} from 'stellar-plus/core/pipelines/fee-bump/types' + +import { FeeBumpPipeline } from '.' + +jest.mock('@stellar/stellar-sdk', () => ({ + Horizon: { + Server: jest.fn(), + }, + Transaction: jest.fn(() => { + return { + networkPassphrase: 'networkPassphrase', + toXDR: jest.fn(() => { + return 'toXDR' + }), + } + }), + TransactionBuilder: { + fromXDR: jest.fn(() => { + return {} + }), + buildFeeBumpTransaction: jest.fn(() => { + return {} + }), + }, +})) + +const MOCKED_BUMP_FEE = '101' + +const MOCKED_TRANSACTION = { + networkPassphrase: 'networkPassphrase', + toXDR: jest.fn(() => 'toXDR'), +} as unknown as Transaction +const MOCKED_FEE_BUMPED_TRANSACTION: FeeBumpTransaction = {} as FeeBumpTransaction +const MOCKED_BT_INPUT: FBInput = { + innerTransaction: MOCKED_TRANSACTION, + feeBumpHeader: { + header: { + source: 'source', + fee: MOCKED_BUMP_FEE, + timeout: 2, + }, + signers: [], + }, +} +const MOCKED_BT_OUTPUT: FBOutput = MOCKED_FEE_BUMPED_TRANSACTION + +describe('FeeBumpPipeline', () => { + let feeBumpPipeline: FeeBumpPipeline + + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('Wrap Transaction', () => { + beforeEach(() => { + feeBumpPipeline = new FeeBumpPipeline() + jest.clearAllMocks() + }) + + it('should wrap transaction successfully', async () => { + await expect(feeBumpPipeline.execute(MOCKED_BT_INPUT)).resolves.toEqual(MOCKED_BT_OUTPUT) + expect(TransactionBuilder.fromXDR).toHaveBeenCalledTimes(1) + expect(TransactionBuilder.buildFeeBumpTransaction).toHaveBeenCalledTimes(1) + expect(TransactionBuilder.buildFeeBumpTransaction).toHaveBeenCalledWith( + MOCKED_BT_INPUT.feeBumpHeader.header.source, + MOCKED_BT_INPUT.feeBumpHeader.header.fee, + {}, + 'networkPassphrase' + ) + }) + + it('should throw error', async () => { + const mockedInput = { + ...MOCKED_BT_INPUT, + feeBumpHeader: { + ...MOCKED_BT_INPUT.feeBumpHeader, + header: { + ...MOCKED_BT_INPUT.feeBumpHeader.header, + fee: '0', + }, + }, + } + ;(TransactionBuilder.buildFeeBumpTransaction as unknown as jest.Mock).mockImplementationOnce(() => { + throw new Error('Error building fee bump transaction!') + }) + + await expect(feeBumpPipeline.execute(mockedInput)).rejects.toThrow('Unexpected error!') + expect(TransactionBuilder.fromXDR).toHaveBeenCalledTimes(1) + expect(TransactionBuilder.buildFeeBumpTransaction).toHaveBeenCalledTimes(1) + expect(TransactionBuilder.buildFeeBumpTransaction).toHaveBeenCalledWith( + MOCKED_BT_INPUT.feeBumpHeader.header.source, + '0', + {}, + 'networkPassphrase' + ) + }) + }) +}) From 88a8b0a7b15b2fd2434f52f66bd1d299aa432d74 Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Mon, 1 Apr 2024 16:30:16 -0300 Subject: [PATCH 13/30] test: add unit tests for classic signature requirements pipeline (#115) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adjustments to release library as open source 🚀 (#107) * chore: add jest setup to coverage all files (#101) * refactor: fix circular import * test: contract engine initial structure * Contract Engine unit tests (#103) * refactor: contract engine errors * feat: validate required initialization arguments * test: contract engine initialization workflow * test: complete contract engine main coverage * refactor: update deprecated methods * refactor: contract engine errors (#104) * Update docs and minor adjustments to jsdocs (#105) * fix: missing error CE009 * docs: update jsdoc * docs: update readme for public release * packaging: bump version to 0.7.0 (#108) --------- Co-authored-by: Bruno Nascimento * fix: setoptions op threshold requirement * fix: revoke operation variants threshold calculation * test: add unit tests to classig sign requirements pipeline * refactor: rearrange as per aaa pattern --------- Co-authored-by: Bruno Nascimento --- .../classic-sign-requirements/index.ts | 21 +- .../index.unit.test.ts | 456 ++++++++++++++++++ 2 files changed, 470 insertions(+), 7 deletions(-) create mode 100644 src/stellar-plus/core/pipelines/classic-sign-requirements/index.unit.test.ts diff --git a/src/stellar-plus/core/pipelines/classic-sign-requirements/index.ts b/src/stellar-plus/core/pipelines/classic-sign-requirements/index.ts index a207992..f5c919a 100644 --- a/src/stellar-plus/core/pipelines/classic-sign-requirements/index.ts +++ b/src/stellar-plus/core/pipelines/classic-sign-requirements/index.ts @@ -79,7 +79,7 @@ export class ClassicSignRequirementsPipeline extends ConveyorBelt< let thresholdLevel = SignatureThreshold.medium - switch (operation.type) { + switch (operation.type as string) { case xdr.OperationType.createAccount().name: return { publicKey: setSourceSigner(operation.source), @@ -121,11 +121,11 @@ export class ClassicSignRequirementsPipeline extends ConveyorBelt< case xdr.OperationType.setOptions().name: if ( - !(operation as Operation.SetOptions).masterWeight && - !(operation as Operation.SetOptions).signer && - !(operation as Operation.SetOptions).lowThreshold && - !(operation as Operation.SetOptions).medThreshold && - !(operation as Operation.SetOptions).highThreshold + (operation as Operation.SetOptions).masterWeight || + (operation as Operation.SetOptions).signer || + (operation as Operation.SetOptions).lowThreshold || + (operation as Operation.SetOptions).medThreshold || + (operation as Operation.SetOptions).highThreshold ) { thresholdLevel = SignatureThreshold.high } @@ -150,7 +150,7 @@ export class ClassicSignRequirementsPipeline extends ConveyorBelt< case xdr.OperationType.accountMerge().name: return { publicKey: setSourceSigner(operation.source), - thresholdLevel, + thresholdLevel: SignatureThreshold.high, } case xdr.OperationType.manageData().name: @@ -190,6 +190,13 @@ export class ClassicSignRequirementsPipeline extends ConveyorBelt< } case xdr.OperationType.revokeSponsorship().name: + case 'revokeAccountSponsorship': + case 'revokeTrustlineSponsorship': + case 'revokeOfferSponsorship': + case 'revokeDataSponsorship': + case 'revokeClaimableBalanceSponsorship': + case 'revokeLiquidityPoolSponsorship': + case 'revokeSignerSponsorship': return { publicKey: setSourceSigner(operation.source), thresholdLevel, diff --git a/src/stellar-plus/core/pipelines/classic-sign-requirements/index.unit.test.ts b/src/stellar-plus/core/pipelines/classic-sign-requirements/index.unit.test.ts new file mode 100644 index 0000000..8db9845 --- /dev/null +++ b/src/stellar-plus/core/pipelines/classic-sign-requirements/index.unit.test.ts @@ -0,0 +1,456 @@ +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 { ConveyorBeltErrorMeta } from 'stellar-plus/error/helpers/conveyor-belt' +import { ClassicSignRequirementsPipelineInput, ClassicSignRequirementsPipelineType } from './types' +import { BeltMetadata } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +const MOCKED_PK_A = 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI' +const MOCKED_PK_B = 'GB3MXH633VRECLZRUAR3QCLQJDMXNYNHKZCO6FJEWXVWSUEIS7NU376P' +const MOCKED_PK_C = 'GCPXAF4S5MBXA3DRNBA7XYP55S6F3UN2ZJRAS72BXEJMD7JVMGIGCKNA' + +const MOCKED_ACCOUNT_A = new Account(MOCKED_PK_A, '100') + +const TESTNET_PASSPHRASE = Constants.testnet.networkPassphrase +const MOCKED_FEE = '100' +const MOCKED_BUMP_FEE = '101' + +const MOCKED_TX_OPTIONS: TransactionBuilder.TransactionBuilderOptions = { + fee: MOCKED_FEE, + networkPassphrase: TESTNET_PASSPHRASE, + timebounds: { + minTime: 0, + maxTime: 0, + }, +} + +describe('ClassicSignRequirementsPipeline', () => { + describe('Initialization', () => { + it('should initialize pipeline', () => { + const pipeline = new ClassicSignRequirementsPipeline() + expect(pipeline).toBeDefined() + }) + }) + describe('errors', () => { + it('should throw error if internal process fails', async () => { + const pipeline = new ClassicSignRequirementsPipeline() + jest.spyOn(pipeline as any, 'bundleSignatureRequirements').mockImplementation(() => { + throw new Error('mocked error') + }) + const transaction = new TransactionBuilder(MOCKED_ACCOUNT_A, MOCKED_TX_OPTIONS).build() + const MOCKED_PROCESS_FAILED_ERROR = CSRError.processFailed( + new Error('mocked error'), + { + item: transaction, + meta: { + itemId: 'mocked-id', + beltId: 'mocked-belt-id', + beltType: ClassicSignRequirementsPipelineType.id, + }, + } as ConveyorBeltErrorMeta, + transaction + ) + + await expect(pipeline.execute(transaction)).rejects.toThrow(MOCKED_PROCESS_FAILED_ERROR.message) + await expect(pipeline.execute(transaction)).rejects.toHaveProperty('code', MOCKED_PROCESS_FAILED_ERROR.code) + }) + }) + + describe('core requirement calculation', () => { + it('should return signature requirements for an envelope source without operations', async () => { + const expectedResult = [{ publicKey: MOCKED_PK_A, thresholdLevel: SignatureThreshold.medium }] + const pipeline = new ClassicSignRequirementsPipeline() + const transaction = new TransactionBuilder(MOCKED_ACCOUNT_A, MOCKED_TX_OPTIONS).build() + + await expect(pipeline.execute(transaction)).resolves.toHaveLength(expectedResult.length) + await expect(pipeline.execute(transaction)).resolves.toEqual(expect.arrayContaining(expectedResult)) + }) + + it('should return signature requirements for a fee bump envelope', async () => { + const expectedResult = [{ publicKey: MOCKED_PK_B, thresholdLevel: SignatureThreshold.low }] + const pipeline = new ClassicSignRequirementsPipeline() + const transaction = new TransactionBuilder(MOCKED_ACCOUNT_A, MOCKED_TX_OPTIONS).build() + const feeBumpTransaction = TransactionBuilder.buildFeeBumpTransaction( + MOCKED_PK_B, + MOCKED_BUMP_FEE, + transaction, + TESTNET_PASSPHRASE + ) + + await expect(pipeline.execute(feeBumpTransaction)).resolves.toHaveLength(expectedResult.length) + await expect(pipeline.execute(feeBumpTransaction)).resolves.toEqual(expect.arrayContaining(expectedResult)) + }) + + it('should return signature requirements for an envelope source with one operation from same source', async () => { + const expectedResult = [{ publicKey: MOCKED_PK_A, thresholdLevel: SignatureThreshold.medium }] + const pipeline = new ClassicSignRequirementsPipeline() + const transaction = new TransactionBuilder(MOCKED_ACCOUNT_A, MOCKED_TX_OPTIONS) + .addOperation(Operation.payment({ destination: MOCKED_PK_B, asset: Asset.native(), amount: '10' })) + .build() + + await expect(pipeline.execute(transaction)).resolves.toHaveLength(expectedResult.length) + await expect(pipeline.execute(transaction)).resolves.toEqual(expect.arrayContaining(expectedResult)) + }) + + it('should return signature requirements for an envelope source with one operation from different source', async () => { + const expectedResult = [ + { publicKey: MOCKED_PK_A, thresholdLevel: SignatureThreshold.medium }, + { publicKey: MOCKED_PK_C, thresholdLevel: SignatureThreshold.medium }, + ] + const pipeline = new ClassicSignRequirementsPipeline() + const transaction = new TransactionBuilder(MOCKED_ACCOUNT_A, MOCKED_TX_OPTIONS) + .addOperation( + Operation.payment({ destination: MOCKED_PK_B, asset: Asset.native(), amount: '10', source: MOCKED_PK_C }) + ) + .build() + + await expect(pipeline.execute(transaction)).resolves.toHaveLength(expectedResult.length) + await expect(pipeline.execute(transaction)).resolves.toEqual(expect.arrayContaining(expectedResult)) + }) + + it('should return signature requirements for an envelope source with one operation of higher threshold for same source', async () => { + const expectedResult = [{ publicKey: MOCKED_PK_A, thresholdLevel: SignatureThreshold.high }] + const pipeline = new ClassicSignRequirementsPipeline() + const transaction = new TransactionBuilder(MOCKED_ACCOUNT_A, MOCKED_TX_OPTIONS) + .addOperation(Operation.setOptions({ signer: { ed25519PublicKey: MOCKED_PK_B, weight: 2 } })) + .build() + + await expect(pipeline.execute(transaction)).resolves.toHaveLength(expectedResult.length) + await expect(pipeline.execute(transaction)).resolves.toEqual(expect.arrayContaining(expectedResult)) + }) + + it('should return signature requirements for an envelope source with one operation of higher threshold for different source', async () => { + const expectedResult = [ + { publicKey: MOCKED_PK_A, thresholdLevel: SignatureThreshold.medium }, + { publicKey: MOCKED_PK_C, thresholdLevel: SignatureThreshold.high }, + ] + const pipeline = new ClassicSignRequirementsPipeline() + const transaction = new TransactionBuilder(MOCKED_ACCOUNT_A, MOCKED_TX_OPTIONS) + .addOperation( + Operation.setOptions({ signer: { ed25519PublicKey: MOCKED_PK_B, weight: 2 }, source: MOCKED_PK_C }) + ) + .build() + + await expect(pipeline.execute(transaction)).resolves.toHaveLength(expectedResult.length) + await expect(pipeline.execute(transaction)).resolves.toEqual(expect.arrayContaining(expectedResult)) + }) + }) + + describe('operations threshold calculation', () => { + let transactionBuilder: TransactionBuilder + let testOperationRequirement: Function + + const pipeline = new ClassicSignRequirementsPipeline() + const expectedLow = { publicKey: MOCKED_PK_A, thresholdLevel: SignatureThreshold.low } + const expectedMedium = { publicKey: MOCKED_PK_A, thresholdLevel: SignatureThreshold.medium } + const expectedHigh = { publicKey: MOCKED_PK_A, thresholdLevel: SignatureThreshold.high } + + beforeEach(() => { + transactionBuilder = new TransactionBuilder(MOCKED_ACCOUNT_A, MOCKED_TX_OPTIONS) + testOperationRequirement = (operations: xdr.Operation[], expected: SignatureRequirement[]) => { + operations.forEach((op) => { + transactionBuilder.addOperation(op) + }) + const transaction = transactionBuilder.build() + + return pipeline.execute(transaction).then((result) => expect(result).toEqual(expected)) + } + }) + + it('should return threshold medium for createAccount operation', async () => { + const operation = Operation.createAccount({ destination: MOCKED_PK_B, startingBalance: '10' }) + + await testOperationRequirement([operation], [expectedMedium]) + }) + + it('should return threshold medium for payment operation', async () => { + const operation = Operation.payment({ destination: MOCKED_PK_B, asset: Asset.native(), amount: '10' }) + + await testOperationRequirement([operation], [expectedMedium]) + }) + + it('should return threshold medium for pathPaymentStrictSend operation', async () => { + const operation = Operation.pathPaymentStrictSend({ + sendAsset: Asset.native(), + destMin: '1', + sendAmount: '10', + destination: MOCKED_PK_B, + destAsset: Asset.native(), + }) + + await testOperationRequirement([operation], [expectedMedium]) + }) + + it('should return threshold medium for pathPaymentStrictReceive operation', async () => { + const operation = Operation.pathPaymentStrictReceive({ + sendAsset: Asset.native(), + sendMax: '10', + destAmount: '1', + destination: MOCKED_PK_B, + destAsset: Asset.native(), + }) + + await testOperationRequirement([operation], [expectedMedium]) + }) + it('should return threshold medium for manageSellOffer operation', async () => { + const operation = Operation.manageSellOffer({ + selling: Asset.native(), + buying: new Asset('USD', MOCKED_PK_B), + amount: '10', + price: '1', + }) + + await testOperationRequirement([operation], [expectedMedium]) + }) + + it('should return threshold medium for manageBuyOffer operation', async () => { + const operation = Operation.manageBuyOffer({ + selling: Asset.native(), + buying: new Asset('USD', MOCKED_PK_B), + buyAmount: '10', + price: '1', + }) + + await testOperationRequirement([operation], [expectedMedium]) + }) + + it('should return threshold medium for createPassiveSellOffer operation', async () => { + const operation = Operation.createPassiveSellOffer({ + selling: Asset.native(), + buying: new Asset('USD', MOCKED_PK_B), + amount: '10', + price: '1', + }) + + await testOperationRequirement([operation], [expectedMedium]) + }) + it('should return threshold high for setOptions operation with signer', async () => { + const operation = Operation.setOptions({ signer: { ed25519PublicKey: MOCKED_PK_B, weight: 1 } }) + + await testOperationRequirement([operation], [expectedHigh]) + }) + it('should return threshold high for setOptions operation with masterWeight', async () => { + const operation = Operation.setOptions({ masterWeight: 1 }) + + await testOperationRequirement([operation], [expectedHigh]) + }) + it('should return threshold high for setOptions operation with lowThreshold', async () => { + const operation = Operation.setOptions({ lowThreshold: 1 }) + + await testOperationRequirement([operation], [expectedHigh]) + }) + it('should return threshold high for setOptions operation with medThreshold', async () => { + const operation = Operation.setOptions({ medThreshold: 1 }) + + await testOperationRequirement([operation], [expectedHigh]) + }) + it('should return threshold high for setOptions operation with highThreshold', async () => { + const operation = Operation.setOptions({ highThreshold: 1 }) + + await testOperationRequirement([operation], [expectedHigh]) + }) + + it('should return threshold medium for setOptions operation without any threshold', async () => { + const emptySetOptionsOperation = Operation.setOptions({}) + const homeDomainSetOptionsOperation = Operation.setOptions({ homeDomain: 'example.com' }) + const setFlagsSetOptionsOperation = Operation.setOptions({ setFlags: 1 }) + const clearFlagsSetOptionsOperation = Operation.setOptions({ clearFlags: 1 }) + const inflationDestinationSetOptionsOperation = Operation.setOptions({ inflationDest: MOCKED_PK_B }) + + await testOperationRequirement([emptySetOptionsOperation], [expectedMedium]) + await testOperationRequirement([homeDomainSetOptionsOperation], [expectedMedium]) + await testOperationRequirement([setFlagsSetOptionsOperation], [expectedMedium]) + await testOperationRequirement([clearFlagsSetOptionsOperation], [expectedMedium]) + await testOperationRequirement([inflationDestinationSetOptionsOperation], [expectedMedium]) + }) + + it('should return threshold medium changeTrust operation', async () => { + const operation = Operation.changeTrust({ asset: Asset.native() }) + + await testOperationRequirement([operation], [expectedMedium]) + }) + + it('should return threshold low allowTrust operation', async () => { + // If the source of the envelope was the same account as the one allowing the trust + // the threshold would be medium due to the envelope requirement. + // To properly test the low threshold requirement, we need to use a different account as the source. + const operation = Operation.allowTrust({ + source: MOCKED_PK_C, + trustor: MOCKED_PK_B, + assetCode: Asset.native().code, + authorize: true, + }) + + await testOperationRequirement([operation], [expectedMedium, { ...expectedLow, publicKey: MOCKED_PK_C }]) + }) + + it('should return threshold high for accountMerge operation', async () => { + const operation = Operation.accountMerge({ destination: MOCKED_PK_B }) + + await testOperationRequirement([operation], [expectedHigh]) + }) + + it('should return threshold medium for manageData operation', async () => { + const operation = Operation.manageData({ + name: 'key', + value: Buffer.from('value'), + }) + + await testOperationRequirement([operation], [expectedMedium]) + }) + + it('should return threshold low for bumpSequence operation', async () => { + // If the source of the envelope was the same account as the one allowing the trust + // the threshold would be medium due to the envelope requirement. + // To properly test the low threshold requirement, we need to use a different account as the source. + const operation = Operation.bumpSequence({ bumpTo: '1', source: MOCKED_PK_C }) + + await testOperationRequirement([operation], [expectedMedium, { ...expectedLow, publicKey: MOCKED_PK_C }]) + }) + + it('should return threshold medium for createClaimableBalance operation', async () => { + const operation = Operation.createClaimableBalance({ + asset: Asset.native(), + amount: '10', + claimants: [new Claimant(MOCKED_PK_B)], + }) + + await testOperationRequirement([operation], [expectedMedium]) + }) + + it('should return threshold medium for claimClaimableBalance operation', async () => { + const operation = Operation.claimClaimableBalance({ + balanceId: '000000007a10d2aa862a610c88bdb1aacf3abf54160b09bec9adc62cfe4e0431e6b8b4c3', + }) + + await testOperationRequirement([operation], [expectedMedium]) + }) + + it('should return threshold medium for beginSponsoringFutureReserves operation', async () => { + const operation = Operation.beginSponsoringFutureReserves({ sponsoredId: MOCKED_PK_B }) + + await testOperationRequirement([operation], [expectedMedium]) + }) + + it('should return threshold medium for endSponsoringFutureReserves operation', async () => { + const operation = Operation.endSponsoringFutureReserves({ source: MOCKED_PK_A }) + + await testOperationRequirement([operation], [expectedMedium]) + }) + + it('should return threshold medium for revokeSponsorship operation variations', async () => { + const revokeAccountOperation = Operation.revokeAccountSponsorship({ source: MOCKED_PK_C, account: MOCKED_PK_B }) + const revokeTrustlineOperation = Operation.revokeTrustlineSponsorship({ + source: MOCKED_PK_C, + account: MOCKED_PK_B, + asset: Asset.native(), + }) + const revokeOfferOperation = Operation.revokeOfferSponsorship({ + source: MOCKED_PK_C, + seller: MOCKED_PK_B, + offerId: '1', + }) + const revokeDataOperation = Operation.revokeDataSponsorship({ + source: MOCKED_PK_C, + account: MOCKED_PK_B, + name: 'key', + }) + const revokeClaimableBalanceOperation = Operation.revokeClaimableBalanceSponsorship({ + source: MOCKED_PK_C, + balanceId: '000000007a10d2aa862a610c88bdb1aacf3abf54160b09bec9adc62cfe4e0431e6b8b4c3', + }) + const revokeLiquidityPoolOperation = Operation.revokeLiquidityPoolSponsorship({ + source: MOCKED_PK_C, + liquidityPoolId: '076bbb9f17f8d47209515a345420d40ea1ebcd3cb6a370ea0d56fc12dbf084cb', + }) + const revokeSignerOperation = Operation.revokeSignerSponsorship({ + source: MOCKED_PK_C, + account: MOCKED_PK_B, + signer: { ed25519PublicKey: MOCKED_PK_B }, + }) + + await testOperationRequirement( + [revokeAccountOperation], + [expectedMedium, { ...expectedMedium, publicKey: MOCKED_PK_C }] + ) + await testOperationRequirement( + [revokeTrustlineOperation], + [expectedMedium, { ...expectedMedium, publicKey: MOCKED_PK_C }] + ) + await testOperationRequirement( + [revokeOfferOperation], + [expectedMedium, { ...expectedMedium, publicKey: MOCKED_PK_C }] + ) + await testOperationRequirement( + [revokeDataOperation], + [expectedMedium, { ...expectedMedium, publicKey: MOCKED_PK_C }] + ) + await testOperationRequirement( + [revokeClaimableBalanceOperation], + [expectedMedium, { ...expectedMedium, publicKey: MOCKED_PK_C }] + ) + await testOperationRequirement( + [revokeLiquidityPoolOperation], + [expectedMedium, { ...expectedMedium, publicKey: MOCKED_PK_C }] + ) + await testOperationRequirement( + [revokeSignerOperation], + [expectedMedium, { ...expectedMedium, publicKey: MOCKED_PK_C }] + ) + }) + + it('should return threshold medium for clawback operation', async () => { + const operation = Operation.clawback({ from: MOCKED_PK_B, amount: '10', asset: Asset.native() }) + + await testOperationRequirement([operation], [expectedMedium]) + }) + it('should return threshold medium for clawbackClaimableBalance operation', async () => { + const operation = Operation.clawbackClaimableBalance({ + balanceId: '000000007a10d2aa862a610c88bdb1aacf3abf54160b09bec9adc62cfe4e0431e6b8b4c3', + }) + + await testOperationRequirement([operation], [expectedMedium]) + }) + + it('should return threshold low for setTrustLineFlags operation', async () => { + // If the source of the envelope was the same account as the one allowing the trust + // the threshold would be medium due to the envelope requirement. + // To properly test the low threshold requirement, we need to use a different account as the source. + const operation = Operation.setTrustLineFlags({ + asset: Asset.native(), + source: MOCKED_PK_C, + trustor: MOCKED_PK_B, + flags: { + authorized: true, + }, + }) + + await testOperationRequirement([operation], [expectedMedium, { ...expectedLow, publicKey: MOCKED_PK_C }]) + }) + + it('should return threshold medium for liquidityPoolDeposit operation', async () => { + const operation = Operation.liquidityPoolDeposit({ + maxAmountA: '10', + maxAmountB: '10', + minPrice: 1, + maxPrice: 1, + liquidityPoolId: '076bbb9f17f8d47209515a345420d40ea1ebcd3cb6a370ea0d56fc12dbf084cb', + }) + + await testOperationRequirement([operation], [expectedMedium]) + }) + + it('should return threshold medium for liquidityPoolWithdraw operation', async () => { + const operation = Operation.liquidityPoolWithdraw({ + minAmountA: '10', + minAmountB: '10', + amount: '10', + liquidityPoolId: '076bbb9f17f8d47209515a345420d40ea1ebcd3cb6a370ea0d56fc12dbf084cb', + }) + + await testOperationRequirement([operation], [expectedMedium]) + }) + }) +}) From 619b65f3d3a9e57aec9629db468003396ab9fa9b Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:30:35 -0300 Subject: [PATCH 14/30] Add unit tests for simulate transaction pipeline (#117) * 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 --- .../index.unit.test.ts | 16 +- .../pipelines/simulate-transaction/errors.ts | 29 ++- .../pipelines/simulate-transaction/index.ts | 20 +- .../simulate-transaction/index.unit.test.ts | 201 ++++++++++++++++++ 4 files changed, 251 insertions(+), 15 deletions(-) create mode 100644 src/stellar-plus/core/pipelines/simulate-transaction/index.unit.test.ts diff --git a/src/stellar-plus/core/pipelines/classic-sign-requirements/index.unit.test.ts b/src/stellar-plus/core/pipelines/classic-sign-requirements/index.unit.test.ts index 8db9845..c19d877 100644 --- a/src/stellar-plus/core/pipelines/classic-sign-requirements/index.unit.test.ts +++ b/src/stellar-plus/core/pipelines/classic-sign-requirements/index.unit.test.ts @@ -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' @@ -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') }) @@ -140,7 +144,7 @@ describe('ClassicSignRequirementsPipeline', () => { describe('operations threshold calculation', () => { let transactionBuilder: TransactionBuilder - let testOperationRequirement: Function + let testOperationRequirement: (operations: xdr.Operation[], expected: SignatureRequirement[]) => Promise const pipeline = new ClassicSignRequirementsPipeline() const expectedLow = { publicKey: MOCKED_PK_A, thresholdLevel: SignatureThreshold.low } @@ -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 => { operations.forEach((op) => { transactionBuilder.addOperation(op) }) diff --git a/src/stellar-plus/core/pipelines/simulate-transaction/errors.ts b/src/stellar-plus/core/pipelines/simulate-transaction/errors.ts index 08c35ef..dfaa31c 100644 --- a/src/stellar-plus/core/pipelines/simulate-transaction/errors.ts +++ b/src/stellar-plus/core/pipelines/simulate-transaction/errors.ts @@ -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' @@ -15,6 +16,9 @@ export enum ErrorCodesPipelineSimulateTransaction { //PSI1 Restore PSI100 = 'PSI100', + + //PSI2 Transaction + PSI201 = 'PSI201', } const failedToSimulateTransaction = ( @@ -67,7 +71,7 @@ const simulationMissingResult = ( } const simulationResultCouldNotBeVerified = ( - simulationResponse: SorobanRpc.Api.SimulateTransactionSuccessResponse, + simulationResponse: SorobanRpc.Api.SimulateTransactionResponse, conveyorBeltErrorMeta: ConveyorBeltErrorMeta ): StellarPlusError => { return new StellarPlusError({ @@ -99,10 +103,31 @@ const transactionNeedsRestore = ( }) } +const failedToAssembleTransaction = ( + error: Error, + simulationResponse: SorobanRpc.Api.SimulateTransactionSuccessResponse, + transaction: Transaction, + conveyorBeltErrorMeta: ConveyorBeltErrorMeta +): 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, } diff --git a/src/stellar-plus/core/pipelines/simulate-transaction/index.ts b/src/stellar-plus/core/pipelines/simulate-transaction/index.ts index 2e66dbf..139be82 100644 --- a/src/stellar-plus/core/pipelines/simulate-transaction/index.ts +++ b/src/stellar-plus/core/pipelines/simulate-transaction/index.ts @@ -1,4 +1,4 @@ -import { SorobanRpc, Transaction, xdr } from '@stellar/stellar-sdk' +import { SorobanRpc, Transaction } from '@stellar/stellar-sdk' import { SimulateTransactionPipelineInput, @@ -28,7 +28,6 @@ export class SimulateTransactionPipeline extends ConveyorBelt< itemId: string ): Promise { const { transaction, rpcHandler }: SimulateTransactionPipelineInput = item as SimulateTransactionPipelineInput - let simulationResponse: SorobanRpc.Api.SimulateTransactionResponse try { @@ -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 } @@ -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)) + ) } } } diff --git a/src/stellar-plus/core/pipelines/simulate-transaction/index.unit.test.ts b/src/stellar-plus/core/pipelines/simulate-transaction/index.unit.test.ts new file mode 100644 index 0000000..4e9f7f2 --- /dev/null +++ b/src/stellar-plus/core/pipelines/simulate-transaction/index.unit.test.ts @@ -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 => { + return { + item, + meta: { + itemId: 'mocked-id', + beltId: 'mocked-belt-id', + beltType: SimulateTransactionPipelineType.id, + }, + } as ConveyorBeltErrorMeta +} + +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, + }) + }) + }) +}) From 971284791e9dba174ae60a166bdc88dcc7f26e7e Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:31:15 -0300 Subject: [PATCH 15/30] Add unit tests for classic transaction pipeline (#118) * test: add unit tests for classic transaction pipeline * refactor: adjust imports --- .../pipelines/classic-transaction/index.ts | 1 - .../classic-transaction/index.unit.test.ts | 286 ++++++++++++++++++ 2 files changed, 286 insertions(+), 1 deletion(-) create mode 100644 src/stellar-plus/core/pipelines/classic-transaction/index.unit.test.ts diff --git a/src/stellar-plus/core/pipelines/classic-transaction/index.ts b/src/stellar-plus/core/pipelines/classic-transaction/index.ts index f7a18a3..a2782bf 100644 --- a/src/stellar-plus/core/pipelines/classic-transaction/index.ts +++ b/src/stellar-plus/core/pipelines/classic-transaction/index.ts @@ -67,7 +67,6 @@ export class ClassicTransactionPipeline extends MultiBeltPipeline< if (options?.executionPlugins) executionPlugins.push(...options.executionPlugins) // ======================= Build Transaction ========================== - const buildTransactionPipelinePlugins = this.getInnerPluginsByType( executionPlugins, 'BuildTransactionPipeline' as BuildTransactionPipelineType diff --git a/src/stellar-plus/core/pipelines/classic-transaction/index.unit.test.ts b/src/stellar-plus/core/pipelines/classic-transaction/index.unit.test.ts new file mode 100644 index 0000000..dffd8b5 --- /dev/null +++ b/src/stellar-plus/core/pipelines/classic-transaction/index.unit.test.ts @@ -0,0 +1,286 @@ +import { Constants } from 'stellar-plus' +import { BuildTransactionPipeline } from 'stellar-plus/core/pipelines/build-transaction' +import { + BuildTransactionPipelinePlugin, + BuildTransactionPipelineType, +} from 'stellar-plus/core/pipelines/build-transaction/types' +import { ClassicSignRequirementsPipeline } from 'stellar-plus/core/pipelines/classic-sign-requirements' +import { + ClassicSignRequirementsPipelinePlugin, + ClassicSignRequirementsPipelineType, +} from 'stellar-plus/core/pipelines/classic-sign-requirements/types' +import { ClassicTransactionPipeline } from 'stellar-plus/core/pipelines/classic-transaction' +import { + ClassicTransactionPipelineInput, + ClassicTransactionPipelinePlugin, + ClassicTransactionPipelineType, +} from 'stellar-plus/core/pipelines/classic-transaction/types' +import { SignTransactionPipeline } from 'stellar-plus/core/pipelines/sign-transaction' +import { + SignTransactionPipelinePlugin, + SignTransactionPipelineType, +} from 'stellar-plus/core/pipelines/sign-transaction/types' +import { SubmitTransactionPipeline } from 'stellar-plus/core/pipelines/submit-transaction' +import { + SubmitTransactionPipelinePlugin, + SubmitTransactionPipelineType, +} from 'stellar-plus/core/pipelines/submit-transaction/types' +import { TransactionInvocation } from 'stellar-plus/types' +import { ClassicChannelAccountsPlugin } from 'stellar-plus/utils/pipeline/plugins/classic-transaction/channel-accounts' +import { DebugPlugin } from 'stellar-plus/utils/pipeline/plugins/generic/debug' +import { FeeBumpWrapperPlugin } from 'stellar-plus/utils/pipeline/plugins/submit-transaction/fee-bump' + +jest.mock('stellar-plus/core/pipelines/build-transaction', () => ({ + BuildTransactionPipeline: jest.fn().mockImplementation(() => ({ + execute: jest.fn(), + })), +})) + +jest.mock('stellar-plus/core/pipelines/classic-sign-requirements', () => ({ + ClassicSignRequirementsPipeline: jest.fn().mockImplementation(() => ({ + execute: jest.fn(), + })), +})) + +jest.mock('stellar-plus/core/pipelines/sign-transaction', () => ({ + SignTransactionPipeline: jest.fn().mockImplementation(() => ({ + execute: jest.fn(), + })), +})) + +jest.mock('stellar-plus/core/pipelines/submit-transaction', () => ({ + SubmitTransactionPipeline: jest.fn().mockImplementation(() => ({ + execute: jest.fn(), + })), +})) + +const MOCKED_BUILD_TRANSACTION_PIPELINE = BuildTransactionPipeline as jest.Mock +const MOCKED_CLASSIC_SIGN_REQUIREMENTS_PIPELINE = ClassicSignRequirementsPipeline as jest.Mock +const MOCKED_SIGN_TRANSACTION_PIPELINE = SignTransactionPipeline as jest.Mock +const MOCKED_SUBMIT_TRANSACTION_PIPELINE = SubmitTransactionPipeline as jest.Mock + +const MOCKED_PLUGIN_BASE = { + preProcess: jest.fn(), + postProcess: jest.fn(), +} + +const MOCKED_BUILD_TRANSACTION_PLUGIN = jest.mocked({ + ...MOCKED_PLUGIN_BASE, + type: 'BuildTransactionPipeline' as BuildTransactionPipelineType, +}) as unknown as BuildTransactionPipelinePlugin + +const MOCKED_CLASSIC_SIGN_REQUIREMENTS_PLUGIN = jest.mocked({ + ...MOCKED_PLUGIN_BASE, + type: 'ClassicSignRequirementsPipeline' as ClassicSignRequirementsPipelineType, +}) as unknown as ClassicSignRequirementsPipelinePlugin + +const MOCKED_SIGN_TRANSACTION_PLUGIN = jest.mocked({ + ...MOCKED_PLUGIN_BASE, + type: 'SignTransactionPipeline' as SignTransactionPipelineType, +}) as unknown as SignTransactionPipelinePlugin + +const MOCKED_SUBMIT_TRANSACTION_PLUGIN = jest.mocked({ + ...MOCKED_PLUGIN_BASE, + type: 'SubmitTransactionPipeline' as SubmitTransactionPipelineType, +}) as unknown as SubmitTransactionPipelinePlugin + +const TESTNET_NETWORK_CONFIG = Constants.testnet + +const MOCKED_TX_INVOCATION: TransactionInvocation = { + header: { + source: 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI', + fee: '100', + timeout: 100, + }, + signers: [], +} + +const MOCKED_PIPELINE_ITEM: ClassicTransactionPipelineInput = { + operations: [], + txInvocation: MOCKED_TX_INVOCATION, +} + +describe('Classic Transaction Pipeline', () => { + describe('Initialize', () => { + it('should initialize the Classic Transaction Pipeline', () => { + const pipeline = new ClassicTransactionPipeline(TESTNET_NETWORK_CONFIG) + + expect(pipeline).toBeInstanceOf(ClassicTransactionPipeline) + expect(pipeline.type).toEqual(ClassicTransactionPipelineType.id) + }) + + it('should initialize the Classic Transaction Pipeline with no plugins', () => { + const pipeline = new ClassicTransactionPipeline(TESTNET_NETWORK_CONFIG, { + plugins: [], + }) + + expect(pipeline).toBeInstanceOf(ClassicTransactionPipeline) + expect(pipeline.type).toEqual(ClassicTransactionPipelineType.id) + }) + + it('should initialize the Classic Transaction Pipeline with generic plugins', () => { + const debugPlugin = new DebugPlugin('error') + const pipeline = new ClassicTransactionPipeline(TESTNET_NETWORK_CONFIG, { + plugins: [debugPlugin as ClassicTransactionPipelinePlugin], + }) + + expect(pipeline).toBeInstanceOf(ClassicTransactionPipeline) + expect(pipeline.type).toEqual(ClassicTransactionPipelineType.id) + }) + + it('should initialize the Classic Transaction Pipeline with classic transaction pipeline plugins', () => { + const channelAccountsPlugin = new ClassicChannelAccountsPlugin({ channels: [] }) + const pipeline = new ClassicTransactionPipeline(TESTNET_NETWORK_CONFIG, { + plugins: [channelAccountsPlugin], + }) + + expect(pipeline).toBeInstanceOf(ClassicTransactionPipeline) + expect(pipeline.type).toEqual(ClassicTransactionPipelineType.id) + }) + + it('should initialize the Classic Transaction Pipeline with classic transaction internal pipelines plugins', () => { + const feeBumpWrapperPlugin = new FeeBumpWrapperPlugin(MOCKED_TX_INVOCATION) + const pipeline = new ClassicTransactionPipeline(TESTNET_NETWORK_CONFIG, { + plugins: [feeBumpWrapperPlugin], + }) + + expect(pipeline).toBeInstanceOf(ClassicTransactionPipeline) + expect(pipeline.type).toEqual(ClassicTransactionPipelineType.id) + }) + + it('should initialize the Classic Transaction Pipeline with multiple plugin types', () => { + const debugPlugin = new DebugPlugin('error') + const feeBumpWrapperPlugin = new FeeBumpWrapperPlugin(MOCKED_TX_INVOCATION) + const channelAccountsPlugin = new ClassicChannelAccountsPlugin({ channels: [] }) + const pipeline = new ClassicTransactionPipeline(TESTNET_NETWORK_CONFIG, { + plugins: [debugPlugin as ClassicTransactionPipelinePlugin, feeBumpWrapperPlugin, channelAccountsPlugin], + }) + + expect(pipeline).toBeInstanceOf(ClassicTransactionPipeline) + expect(pipeline.type).toEqual(ClassicTransactionPipelineType.id) + }) + + it('should initialize the Classic Transaction Pipeline with build transaction pipeline plugins', async () => { + const pipeline = new ClassicTransactionPipeline(TESTNET_NETWORK_CONFIG, { + plugins: [MOCKED_BUILD_TRANSACTION_PLUGIN], + }) + + await pipeline.execute(MOCKED_PIPELINE_ITEM) + + expect(MOCKED_BUILD_TRANSACTION_PIPELINE).toHaveBeenCalledWith([MOCKED_BUILD_TRANSACTION_PLUGIN]) + }) + + it('should initialize the Classic Transaction Pipeline with classic sign requirements pipeline plugins', async () => { + const pipeline = new ClassicTransactionPipeline(TESTNET_NETWORK_CONFIG, { + plugins: [MOCKED_CLASSIC_SIGN_REQUIREMENTS_PLUGIN], + }) + + await pipeline.execute(MOCKED_PIPELINE_ITEM) + + expect(MOCKED_CLASSIC_SIGN_REQUIREMENTS_PIPELINE).toHaveBeenCalledWith([MOCKED_CLASSIC_SIGN_REQUIREMENTS_PLUGIN]) + }) + + it('should initialize the Classic Transaction Pipeline with sign transaction pipeline plugins', async () => { + const pipeline = new ClassicTransactionPipeline(TESTNET_NETWORK_CONFIG, { + plugins: [MOCKED_SIGN_TRANSACTION_PLUGIN], + }) + + await pipeline.execute(MOCKED_PIPELINE_ITEM) + + expect(MOCKED_SIGN_TRANSACTION_PIPELINE).toHaveBeenCalledWith([MOCKED_SIGN_TRANSACTION_PLUGIN]) + }) + + it('should initialize the Classic Transaction Pipeline with submit transaction pipeline plugins', async () => { + const pipeline = new ClassicTransactionPipeline(TESTNET_NETWORK_CONFIG, { + plugins: [MOCKED_SUBMIT_TRANSACTION_PLUGIN], + }) + + await pipeline.execute(MOCKED_PIPELINE_ITEM) + + expect(MOCKED_SUBMIT_TRANSACTION_PIPELINE).toHaveBeenCalledWith([MOCKED_SUBMIT_TRANSACTION_PLUGIN]) + }) + }) + + describe('Core functionalities', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should execute each internal transaction pipeline once', async () => { + const pipeline = new ClassicTransactionPipeline(TESTNET_NETWORK_CONFIG) + + await pipeline.execute(MOCKED_PIPELINE_ITEM) + + expect(MOCKED_BUILD_TRANSACTION_PIPELINE).toHaveBeenCalledOnce() + expect(MOCKED_CLASSIC_SIGN_REQUIREMENTS_PIPELINE).toHaveBeenCalledOnce() + expect(MOCKED_SIGN_TRANSACTION_PIPELINE).toHaveBeenCalledOnce() + expect(MOCKED_SUBMIT_TRANSACTION_PIPELINE).toHaveBeenCalledOnce() + }) + + it('should execute each internal transaction pipeline in order', async () => { + const pipeline = new ClassicTransactionPipeline(TESTNET_NETWORK_CONFIG) + + await pipeline.execute(MOCKED_PIPELINE_ITEM) + + expect(MOCKED_CLASSIC_SIGN_REQUIREMENTS_PIPELINE).toHaveBeenCalledAfter(MOCKED_BUILD_TRANSACTION_PIPELINE) + expect(MOCKED_SIGN_TRANSACTION_PIPELINE).toHaveBeenCalledAfter(MOCKED_CLASSIC_SIGN_REQUIREMENTS_PIPELINE) + expect(MOCKED_SUBMIT_TRANSACTION_PIPELINE).toHaveBeenCalledAfter(MOCKED_SIGN_TRANSACTION_PIPELINE) + }) + }) + + it('should process the Input into an Output', async () => { + MOCKED_SUBMIT_TRANSACTION_PIPELINE.mockImplementationOnce(() => ({ + execute: jest.fn().mockResolvedValueOnce('output'), + })) + const pipeline = new ClassicTransactionPipeline(TESTNET_NETWORK_CONFIG) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const processSpy = jest.spyOn(pipeline as any, 'process') + + const result = await pipeline.execute(MOCKED_PIPELINE_ITEM, '0') + + expect(processSpy).toHaveBeenCalledOnce() + expect(processSpy).toHaveBeenCalledWith(MOCKED_PIPELINE_ITEM, '0') + expect(processSpy).toHaveBeenCalledExactlyOnceWith(MOCKED_PIPELINE_ITEM, '0') + expect(result).toEqual('output') + }) + + it('should accept build transaction pipeline execution plugins', async () => { + const pipeline = new ClassicTransactionPipeline(TESTNET_NETWORK_CONFIG) + const executionPlugins = [MOCKED_BUILD_TRANSACTION_PLUGIN] + const MOCKED_ITEM_WITH_PLUGINS = { ...MOCKED_PIPELINE_ITEM, options: { executionPlugins } } + + await pipeline.execute(MOCKED_ITEM_WITH_PLUGINS) + + expect(MOCKED_BUILD_TRANSACTION_PIPELINE).toHaveBeenCalledWith([MOCKED_BUILD_TRANSACTION_PLUGIN]) + }) + + it('should accept classic sign requirements pipeline execution plugins', async () => { + const pipeline = new ClassicTransactionPipeline(TESTNET_NETWORK_CONFIG) + const executionPlugins = [MOCKED_CLASSIC_SIGN_REQUIREMENTS_PLUGIN] + const MOCKED_ITEM_WITH_PLUGINS = { ...MOCKED_PIPELINE_ITEM, options: { executionPlugins } } + + await pipeline.execute(MOCKED_ITEM_WITH_PLUGINS) + + expect(MOCKED_CLASSIC_SIGN_REQUIREMENTS_PIPELINE).toHaveBeenCalledWith([MOCKED_CLASSIC_SIGN_REQUIREMENTS_PLUGIN]) + }) + + it('should accept sign transaction pipeline execution plugins', async () => { + const pipeline = new ClassicTransactionPipeline(TESTNET_NETWORK_CONFIG) + const executionPlugins = [MOCKED_SIGN_TRANSACTION_PLUGIN] + const MOCKED_ITEM_WITH_PLUGINS = { ...MOCKED_PIPELINE_ITEM, options: { executionPlugins } } + + await pipeline.execute(MOCKED_ITEM_WITH_PLUGINS) + + expect(MOCKED_SIGN_TRANSACTION_PIPELINE).toHaveBeenCalledWith([MOCKED_SIGN_TRANSACTION_PLUGIN]) + }) + + it('should accept submit transaction pipeline execution plugins', async () => { + const pipeline = new ClassicTransactionPipeline(TESTNET_NETWORK_CONFIG) + const executionPlugins = [MOCKED_SUBMIT_TRANSACTION_PLUGIN] + const MOCKED_ITEM_WITH_PLUGINS = { ...MOCKED_PIPELINE_ITEM, options: { executionPlugins } } + + await pipeline.execute(MOCKED_ITEM_WITH_PLUGINS) + + expect(MOCKED_SUBMIT_TRANSACTION_PIPELINE).toHaveBeenCalledWith([MOCKED_SUBMIT_TRANSACTION_PLUGIN]) + }) +}) From e2d382adff1884bc61a2ea1b902249dc9fe81655 Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:31:42 -0300 Subject: [PATCH 16/30] test: add unit tests for soroban transaction pipeline (#119) --- .../soroban-transaction/index.unit.test.ts | 419 ++++++++++++++++++ 1 file changed, 419 insertions(+) create mode 100644 src/stellar-plus/core/pipelines/soroban-transaction/index.unit.test.ts diff --git a/src/stellar-plus/core/pipelines/soroban-transaction/index.unit.test.ts b/src/stellar-plus/core/pipelines/soroban-transaction/index.unit.test.ts new file mode 100644 index 0000000..0efdb4b --- /dev/null +++ b/src/stellar-plus/core/pipelines/soroban-transaction/index.unit.test.ts @@ -0,0 +1,419 @@ +import { Constants } from 'stellar-plus' +import { BuildTransactionPipeline } from 'stellar-plus/core/pipelines/build-transaction' +import { + BuildTransactionPipelinePlugin, + BuildTransactionPipelineType, +} from 'stellar-plus/core/pipelines/build-transaction/types' +import { ClassicSignRequirementsPipeline } from 'stellar-plus/core/pipelines/classic-sign-requirements' +import { + ClassicSignRequirementsPipelinePlugin, + ClassicSignRequirementsPipelineType, +} from 'stellar-plus/core/pipelines/classic-sign-requirements/types' +import { SignTransactionPipeline } from 'stellar-plus/core/pipelines/sign-transaction' +import { + SignTransactionPipelinePlugin, + SignTransactionPipelineType, +} from 'stellar-plus/core/pipelines/sign-transaction/types' +import { SimulateTransactionPipeline } from 'stellar-plus/core/pipelines/simulate-transaction' +import { + SimulateTransactionPipelinePlugin, + SimulateTransactionPipelineType, +} from 'stellar-plus/core/pipelines/simulate-transaction/types' +import { SorobanAuthPipeline } from 'stellar-plus/core/pipelines/soroban-auth' +import { SorobanAuthPipelinePlugin, SorobanAuthPipelineType } from 'stellar-plus/core/pipelines/soroban-auth/types' +import { SorobanGetTransactionPipeline } from 'stellar-plus/core/pipelines/soroban-get-transaction' +import { + SorobanGetTransactionPipelinePlugin, + SorobanGetTransactionPipelineType, +} from 'stellar-plus/core/pipelines/soroban-get-transaction/types' +import { SorobanTransactionPipeline } from 'stellar-plus/core/pipelines/soroban-transaction' +import { + SorobanTransactionPipelineInput, + SorobanTransactionPipelinePlugin, + SorobanTransactionPipelineType, +} from 'stellar-plus/core/pipelines/soroban-transaction/types' +import { SubmitTransactionPipeline } from 'stellar-plus/core/pipelines/submit-transaction' +import { + SubmitTransactionPipelinePlugin, + SubmitTransactionPipelineType, +} from 'stellar-plus/core/pipelines/submit-transaction/types' +import { TransactionInvocation } from 'stellar-plus/types' +import { DebugPlugin } from 'stellar-plus/utils/pipeline/plugins/generic/debug' +import { SorobanChannelAccountsPlugin } from 'stellar-plus/utils/pipeline/plugins/soroban-transaction/channel-accounts' +import { FeeBumpWrapperPlugin } from 'stellar-plus/utils/pipeline/plugins/submit-transaction/fee-bump' + +jest.mock('stellar-plus/core/pipelines/build-transaction', () => ({ + BuildTransactionPipeline: jest.fn().mockImplementation(() => ({ + execute: jest.fn(), + })), +})) + +jest.mock('stellar-plus/core/pipelines/simulate-transaction', () => ({ + SimulateTransactionPipeline: jest.fn().mockImplementation(() => ({ + execute: jest.fn().mockResolvedValue({ + assembledTransaction: 'assembledTransaction', + }), + })), +})) + +jest.mock('stellar-plus/core/pipelines/soroban-auth', () => ({ + SorobanAuthPipeline: jest.fn().mockImplementation(() => ({ + execute: jest.fn(), + })), +})) + +jest.mock('stellar-plus/core/pipelines/classic-sign-requirements', () => ({ + ClassicSignRequirementsPipeline: jest.fn().mockImplementation(() => ({ + execute: jest.fn(), + })), +})) + +jest.mock('stellar-plus/core/pipelines/sign-transaction', () => ({ + SignTransactionPipeline: jest.fn().mockImplementation(() => ({ + execute: jest.fn(), + })), +})) + +jest.mock('stellar-plus/core/pipelines/submit-transaction', () => ({ + SubmitTransactionPipeline: jest.fn().mockImplementation(() => ({ + execute: jest.fn().mockResolvedValue({ + response: jest.fn(), + }), + })), +})) + +jest.mock('stellar-plus/core/pipelines/soroban-get-transaction', () => ({ + SorobanGetTransactionPipeline: jest.fn().mockImplementation(() => ({ + execute: jest.fn(), + })), +})) + +const MOCKED_BUILD_TRANSACTION_PIPELINE = BuildTransactionPipeline as jest.Mock +const MOCKED_SIMULATE_TRANSACTION_PIPELINE = SimulateTransactionPipeline as jest.Mock +const MOCKED_SOROBAN_AUTH_PIPELINE = SorobanAuthPipeline as jest.Mock +const MOCKED_CLASSIC_SIGN_REQUIREMENTS_PIPELINE = ClassicSignRequirementsPipeline as jest.Mock +const MOCKED_SIGN_TRANSACTION_PIPELINE = SignTransactionPipeline as jest.Mock +const MOCKED_SUBMIT_TRANSACTION_PIPELINE = SubmitTransactionPipeline as jest.Mock +const MOCKED_SOROBAN_GET_TRANSACTION_PIPELINE = SorobanGetTransactionPipeline as jest.Mock + +const MOCKED_PLUGIN_BASE = { + preProcess: jest.fn(), + postProcess: jest.fn(), +} + +const MOCKED_BUILD_TRANSACTION_PLUGIN = jest.mocked({ + ...MOCKED_PLUGIN_BASE, + type: 'BuildTransactionPipeline' as BuildTransactionPipelineType, +}) as unknown as BuildTransactionPipelinePlugin + +const MOCKED_SIMULATE_TRANSACTION_PLUGIN = jest.mocked({ + ...MOCKED_PLUGIN_BASE, + type: 'SimulateTransactionPipeline' as SimulateTransactionPipelineType, +}) as unknown as SimulateTransactionPipelinePlugin + +const MOCKED_SOROBAN_AUTH_PLUGIN = jest.mocked({ + ...MOCKED_PLUGIN_BASE, + type: 'SorobanAuthPipeline' as SorobanAuthPipelineType, +}) as unknown as SorobanAuthPipelinePlugin + +const MOCKED_CLASSIC_SIGN_REQUIREMENTS_PLUGIN = jest.mocked({ + ...MOCKED_PLUGIN_BASE, + type: 'ClassicSignRequirementsPipeline' as ClassicSignRequirementsPipelineType, +}) as unknown as ClassicSignRequirementsPipelinePlugin + +const MOCKED_SIGN_TRANSACTION_PLUGIN = jest.mocked({ + ...MOCKED_PLUGIN_BASE, + type: 'SignTransactionPipeline' as SignTransactionPipelineType, +}) as unknown as SignTransactionPipelinePlugin + +const MOCKED_SUBMIT_TRANSACTION_PLUGIN = jest.mocked({ + ...MOCKED_PLUGIN_BASE, + type: 'SubmitTransactionPipeline' as SubmitTransactionPipelineType, +}) as unknown as SubmitTransactionPipelinePlugin + +const MOCKED_SOROBAN_GET_TRANSACTION_PLUGIN = jest.mocked({ + ...MOCKED_PLUGIN_BASE, + type: 'SorobanGetTransactionPipeline' as SorobanGetTransactionPipelineType, +}) as unknown as SorobanGetTransactionPipelinePlugin + +const TESTNET_NETWORK_CONFIG = Constants.testnet + +const MOCKED_TX_INVOCATION: TransactionInvocation = { + header: { + source: 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI', + fee: '100', + timeout: 100, + }, + signers: [], +} + +const MOCKED_PIPELINE_ITEM: SorobanTransactionPipelineInput = { + operations: [], + txInvocation: MOCKED_TX_INVOCATION, +} + +describe('Soroban Transaction Pipeline', () => { + describe('Initialize', () => { + it('should initialize the Soroban Transaction Pipeline', () => { + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG) + + expect(pipeline).toBeInstanceOf(SorobanTransactionPipeline) + expect(pipeline.type).toEqual(SorobanTransactionPipelineType.id) + }) + + it('should initialize the Soroban Transaction Pipeline with no plugins', () => { + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG, { + plugins: [], + }) + + expect(pipeline).toBeInstanceOf(SorobanTransactionPipeline) + expect(pipeline.type).toEqual(SorobanTransactionPipelineType.id) + }) + + it('should initialize the Soroban Transaction Pipeline with generic plugins', () => { + const debugPlugin = new DebugPlugin('error') + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG, { + plugins: [debugPlugin as SorobanTransactionPipelinePlugin], + }) + + expect(pipeline).toBeInstanceOf(SorobanTransactionPipeline) + expect(pipeline.type).toEqual(SorobanTransactionPipelineType.id) + }) + + it('should initialize the Soroban Transaction Pipeline with Soroban transaction pipeline plugins', () => { + const channelAccountsPlugin = new SorobanChannelAccountsPlugin({ channels: [] }) + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG, { + plugins: [channelAccountsPlugin], + }) + + expect(pipeline).toBeInstanceOf(SorobanTransactionPipeline) + expect(pipeline.type).toEqual(SorobanTransactionPipelineType.id) + }) + + it('should initialize the Soroban Transaction Pipeline with Soroban transaction internal pipelines plugins', () => { + const feeBumpWrapperPlugin = new FeeBumpWrapperPlugin(MOCKED_TX_INVOCATION) + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG, { + plugins: [feeBumpWrapperPlugin], + }) + + expect(pipeline).toBeInstanceOf(SorobanTransactionPipeline) + expect(pipeline.type).toEqual(SorobanTransactionPipelineType.id) + }) + + it('should initialize the Soroban Transaction Pipeline with multiple plugin types', () => { + const debugPlugin = new DebugPlugin('error') + const feeBumpWrapperPlugin = new FeeBumpWrapperPlugin(MOCKED_TX_INVOCATION) + const channelAccountsPlugin = new SorobanChannelAccountsPlugin({ channels: [] }) + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG, { + plugins: [debugPlugin as SorobanTransactionPipelinePlugin, feeBumpWrapperPlugin, channelAccountsPlugin], + }) + + expect(pipeline).toBeInstanceOf(SorobanTransactionPipeline) + expect(pipeline.type).toEqual(SorobanTransactionPipelineType.id) + }) + + it('should initialize the Soroban Transaction Pipeline with build transaction pipeline plugins', async () => { + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG, { + plugins: [MOCKED_BUILD_TRANSACTION_PLUGIN], + }) + + await pipeline.execute(MOCKED_PIPELINE_ITEM) + + expect(MOCKED_BUILD_TRANSACTION_PIPELINE).toHaveBeenCalledWith([MOCKED_BUILD_TRANSACTION_PLUGIN]) + }) + + it('should initialize the Soroban Transaction Pipeline with simulate transaction pipeline plugins', async () => { + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG, { + plugins: [MOCKED_SIMULATE_TRANSACTION_PLUGIN], + }) + + await pipeline.execute(MOCKED_PIPELINE_ITEM) + + expect(MOCKED_SIMULATE_TRANSACTION_PIPELINE).toHaveBeenCalledWith([MOCKED_SIMULATE_TRANSACTION_PLUGIN]) + }) + + it('should initialize the Soroban Transaction Pipeline with Soroban auth pipeline plugins', async () => { + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG, { + plugins: [MOCKED_SOROBAN_AUTH_PLUGIN], + }) + + await pipeline.execute(MOCKED_PIPELINE_ITEM) + + expect(MOCKED_SOROBAN_AUTH_PIPELINE).toHaveBeenCalledWith([MOCKED_SOROBAN_AUTH_PLUGIN]) + }) + + it('should initialize the Soroban Transaction Pipeline with Soroban sign requirements pipeline plugins', async () => { + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG, { + plugins: [MOCKED_CLASSIC_SIGN_REQUIREMENTS_PLUGIN], + }) + + await pipeline.execute(MOCKED_PIPELINE_ITEM) + + expect(MOCKED_CLASSIC_SIGN_REQUIREMENTS_PIPELINE).toHaveBeenCalledWith([MOCKED_CLASSIC_SIGN_REQUIREMENTS_PLUGIN]) + }) + + it('should initialize the Soroban Transaction Pipeline with sign transaction pipeline plugins', async () => { + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG, { + plugins: [MOCKED_SIGN_TRANSACTION_PLUGIN], + }) + + await pipeline.execute(MOCKED_PIPELINE_ITEM) + + expect(MOCKED_SIGN_TRANSACTION_PIPELINE).toHaveBeenCalledWith([MOCKED_SIGN_TRANSACTION_PLUGIN]) + }) + + it('should initialize the Soroban Transaction Pipeline with submit transaction pipeline plugins', async () => { + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG, { + plugins: [MOCKED_SUBMIT_TRANSACTION_PLUGIN], + }) + + await pipeline.execute(MOCKED_PIPELINE_ITEM) + + expect(MOCKED_SUBMIT_TRANSACTION_PIPELINE).toHaveBeenCalledWith([MOCKED_SUBMIT_TRANSACTION_PLUGIN]) + }) + + it('should initialize the Soroban Transaction Pipeline with get transaction pipeline plugins', async () => { + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG, { + plugins: [MOCKED_SOROBAN_GET_TRANSACTION_PLUGIN], + }) + + await pipeline.execute(MOCKED_PIPELINE_ITEM) + + expect(MOCKED_SOROBAN_GET_TRANSACTION_PIPELINE).toHaveBeenCalledWith([MOCKED_SOROBAN_GET_TRANSACTION_PLUGIN]) + }) + }) + describe('Core functionalities', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + it('should execute each internal transaction pipeline once', async () => { + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG) + + await pipeline.execute(MOCKED_PIPELINE_ITEM) + + expect(MOCKED_BUILD_TRANSACTION_PIPELINE).toHaveBeenCalledOnce() + expect(MOCKED_SIMULATE_TRANSACTION_PIPELINE).toHaveBeenCalledOnce() + expect(MOCKED_SOROBAN_AUTH_PIPELINE).toHaveBeenCalledOnce() + expect(MOCKED_CLASSIC_SIGN_REQUIREMENTS_PIPELINE).toHaveBeenCalledOnce() + expect(MOCKED_SIGN_TRANSACTION_PIPELINE).toHaveBeenCalledOnce() + expect(MOCKED_SUBMIT_TRANSACTION_PIPELINE).toHaveBeenCalledOnce() + expect(MOCKED_SOROBAN_GET_TRANSACTION_PIPELINE).toHaveBeenCalledOnce() + }) + + it('should execute each internal transaction pipeline in order', async () => { + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG) + + await pipeline.execute(MOCKED_PIPELINE_ITEM) + + expect(MOCKED_SIMULATE_TRANSACTION_PIPELINE).toHaveBeenCalledAfter(MOCKED_BUILD_TRANSACTION_PIPELINE) + expect(MOCKED_SOROBAN_AUTH_PIPELINE).toHaveBeenCalledAfter(MOCKED_SIMULATE_TRANSACTION_PIPELINE) + expect(MOCKED_CLASSIC_SIGN_REQUIREMENTS_PIPELINE).toHaveBeenCalledAfter(MOCKED_SOROBAN_AUTH_PIPELINE) + expect(MOCKED_SIGN_TRANSACTION_PIPELINE).toHaveBeenCalledAfter(MOCKED_CLASSIC_SIGN_REQUIREMENTS_PIPELINE) + expect(MOCKED_SUBMIT_TRANSACTION_PIPELINE).toHaveBeenCalledAfter(MOCKED_SIGN_TRANSACTION_PIPELINE) + expect(MOCKED_SOROBAN_GET_TRANSACTION_PIPELINE).toHaveBeenCalledAfter(MOCKED_SUBMIT_TRANSACTION_PIPELINE) + }) + + it('should process the Input into an Output', async () => { + MOCKED_SOROBAN_GET_TRANSACTION_PIPELINE.mockImplementationOnce(() => ({ + execute: jest.fn().mockResolvedValueOnce('output'), + })) + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const processSpy = jest.spyOn(pipeline as any, 'process') + + const result = await pipeline.execute(MOCKED_PIPELINE_ITEM, '0') + + expect(processSpy).toHaveBeenCalledOnce() + expect(processSpy).toHaveBeenCalledWith(MOCKED_PIPELINE_ITEM, '0') + expect(processSpy).toHaveBeenCalledExactlyOnceWith(MOCKED_PIPELINE_ITEM, '0') + expect(result).toEqual('output') + }) + + it('should process the Input into a simulation Output when simulateOnly flag is set', async () => { + MOCKED_SIMULATE_TRANSACTION_PIPELINE.mockImplementationOnce(() => ({ + execute: jest.fn().mockResolvedValueOnce('output'), + })) + const MOCKED_SIMULATION_ITEM = { ...MOCKED_PIPELINE_ITEM, options: { simulateOnly: true } } + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const processSpy = jest.spyOn(pipeline as any, 'process') + + const result = await pipeline.execute(MOCKED_SIMULATION_ITEM, '0') + + expect(processSpy).toHaveBeenCalledOnce() + expect(processSpy).toHaveBeenCalledWith(MOCKED_SIMULATION_ITEM, '0') + expect(processSpy).toHaveBeenCalledExactlyOnceWith(MOCKED_SIMULATION_ITEM, '0') + expect(result).toEqual('output') + }) + + it('should accept build transaction pipeline execution plugins', async () => { + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG) + const executionPlugins = [MOCKED_BUILD_TRANSACTION_PLUGIN] + const MOCKED_ITEM_WITH_PLUGINS = { ...MOCKED_PIPELINE_ITEM, options: { executionPlugins } } + + await pipeline.execute(MOCKED_ITEM_WITH_PLUGINS) + + expect(MOCKED_BUILD_TRANSACTION_PIPELINE).toHaveBeenCalledWith([MOCKED_BUILD_TRANSACTION_PLUGIN]) + }) + + it('should accept simulate transaction pipeline execution plugins', async () => { + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG) + const executionPlugins = [MOCKED_SIMULATE_TRANSACTION_PLUGIN] + const MOCKED_ITEM_WITH_PLUGINS = { ...MOCKED_PIPELINE_ITEM, options: { executionPlugins } } + + await pipeline.execute(MOCKED_ITEM_WITH_PLUGINS) + + expect(MOCKED_SIMULATE_TRANSACTION_PIPELINE).toHaveBeenCalledWith([MOCKED_SIMULATE_TRANSACTION_PLUGIN]) + }) + + it('should accept Soroban auth pipeline execution plugins', async () => { + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG) + const executionPlugins = [MOCKED_SOROBAN_AUTH_PLUGIN] + const MOCKED_ITEM_WITH_PLUGINS = { ...MOCKED_PIPELINE_ITEM, options: { executionPlugins } } + + await pipeline.execute(MOCKED_ITEM_WITH_PLUGINS) + + expect(MOCKED_SOROBAN_AUTH_PIPELINE).toHaveBeenCalledWith([MOCKED_SOROBAN_AUTH_PLUGIN]) + }) + + it('should accept classic sign requirements pipeline execution plugins', async () => { + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG) + const executionPlugins = [MOCKED_CLASSIC_SIGN_REQUIREMENTS_PLUGIN] + const MOCKED_ITEM_WITH_PLUGINS = { ...MOCKED_PIPELINE_ITEM, options: { executionPlugins } } + + await pipeline.execute(MOCKED_ITEM_WITH_PLUGINS) + + expect(MOCKED_CLASSIC_SIGN_REQUIREMENTS_PIPELINE).toHaveBeenCalledWith([MOCKED_CLASSIC_SIGN_REQUIREMENTS_PLUGIN]) + }) + + it('should accept sign transaction pipeline execution plugins', async () => { + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG) + const executionPlugins = [MOCKED_SIGN_TRANSACTION_PLUGIN] + const MOCKED_ITEM_WITH_PLUGINS = { ...MOCKED_PIPELINE_ITEM, options: { executionPlugins } } + + await pipeline.execute(MOCKED_ITEM_WITH_PLUGINS) + + expect(MOCKED_SIGN_TRANSACTION_PIPELINE).toHaveBeenCalledWith([MOCKED_SIGN_TRANSACTION_PLUGIN]) + }) + + it('should accept submit transaction pipeline execution plugins', async () => { + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG) + const executionPlugins = [MOCKED_SUBMIT_TRANSACTION_PLUGIN] + const MOCKED_ITEM_WITH_PLUGINS = { ...MOCKED_PIPELINE_ITEM, options: { executionPlugins } } + + await pipeline.execute(MOCKED_ITEM_WITH_PLUGINS) + + expect(MOCKED_SUBMIT_TRANSACTION_PIPELINE).toHaveBeenCalledWith([MOCKED_SUBMIT_TRANSACTION_PLUGIN]) + }) + + it('should accept get transaction pipeline execution plugins', async () => { + const pipeline = new SorobanTransactionPipeline(TESTNET_NETWORK_CONFIG) + const executionPlugins = [MOCKED_SOROBAN_GET_TRANSACTION_PLUGIN] + const MOCKED_ITEM_WITH_PLUGINS = { ...MOCKED_PIPELINE_ITEM, options: { executionPlugins } } + + await pipeline.execute(MOCKED_ITEM_WITH_PLUGINS) + + expect(MOCKED_SOROBAN_GET_TRANSACTION_PIPELINE).toHaveBeenCalledWith([MOCKED_SOROBAN_GET_TRANSACTION_PLUGIN]) + }) + }) +}) From 0da0a454128baa2ab35b058e680fe033387b96f8 Mon Sep 17 00:00:00 2001 From: Bruno Nascimento Date: Tue, 2 Apr 2024 16:54:52 -0300 Subject: [PATCH 17/30] test: add unit test for sign transaction pipeline --- .../core/pipelines/sign-transaction/errors.ts | 8 +- .../core/pipelines/sign-transaction/index.ts | 7 +- .../sign-transaction/index.unit.test.ts | 218 ++++++++++++++++++ .../test/mocks/transaction-mock.ts | 47 ++-- 4 files changed, 256 insertions(+), 24 deletions(-) create mode 100644 src/stellar-plus/core/pipelines/sign-transaction/index.unit.test.ts diff --git a/src/stellar-plus/core/pipelines/sign-transaction/errors.ts b/src/stellar-plus/core/pipelines/sign-transaction/errors.ts index 0721a97..7c0ac07 100644 --- a/src/stellar-plus/core/pipelines/sign-transaction/errors.ts +++ b/src/stellar-plus/core/pipelines/sign-transaction/errors.ts @@ -21,9 +21,9 @@ const noRequirementsProvided = ( ): StellarPlusError => { return new StellarPlusError({ code: ErrorCodesPipelineSignTransaction.PSIG001, - message: 'No signers provided!', + message: 'No signature requirements provided!', source: 'PipelineSignTransaction', - details: `No signers provided. Review your transaction workflow to ensure the proper signers are being provided for the transaction.`, + details: `No signature requirements provided for the transaction. It is possible to use the classicSignRequirements pipeline to automatically identify Stellar Classic signature requirements for a given transaction.`, meta: { transactionData: extractTransactionData(transaction), conveyorBeltErrorMeta, @@ -36,9 +36,9 @@ const noSignersProvided = ( ): StellarPlusError => { return new StellarPlusError({ code: ErrorCodesPipelineSignTransaction.PSIG002, - message: 'No signature requirements provided!', + message: 'No signers provided!', source: 'PipelineSignTransaction', - details: `No signature requirements provided for the transaction. It is possible to use the classicSignRequirements pipeline to automatically identify Stellar Classic signature requirements for a given transaction.`, + details: `No signers provided. Review your transaction workflow to ensure the proper signers are being provided for the transaction.`, meta: { transactionData: extractTransactionData(transaction), conveyorBeltErrorMeta, diff --git a/src/stellar-plus/core/pipelines/sign-transaction/index.ts b/src/stellar-plus/core/pipelines/sign-transaction/index.ts index bd59c41..bba277d 100644 --- a/src/stellar-plus/core/pipelines/sign-transaction/index.ts +++ b/src/stellar-plus/core/pipelines/sign-transaction/index.ts @@ -7,6 +7,7 @@ import { SignTransactionPipelinePlugin, SignTransactionPipelineType, } from 'stellar-plus/core/pipelines/sign-transaction/types' +import { StellarPlusError } from 'stellar-plus/error' import { extractConveyorBeltErrorMeta } from 'stellar-plus/error/helpers/conveyor-belt' import { ConveyorBelt } from 'stellar-plus/utils/pipeline/conveyor-belts' @@ -64,7 +65,11 @@ export class SignTransactionPipeline extends ConveyorBelt< ) } } else { - throw new Error('Multisignature support not implemented yet') + throw StellarPlusError.unexpectedError({ + message: 'Multisignature support not implemented yet', + source: 'PipelineSignTransaction', + details: `The signer has a signature schema. The signature schema is: ${signer.signatureSchema}.`, + }) } } diff --git a/src/stellar-plus/core/pipelines/sign-transaction/index.unit.test.ts b/src/stellar-plus/core/pipelines/sign-transaction/index.unit.test.ts new file mode 100644 index 0000000..2adebec --- /dev/null +++ b/src/stellar-plus/core/pipelines/sign-transaction/index.unit.test.ts @@ -0,0 +1,218 @@ +import { Keypair, TransactionBuilder } from '@stellar/stellar-sdk' + +import { testnet } from 'stellar-plus/constants' +import { SignTransactionPipeline } from 'stellar-plus/core/pipelines/sign-transaction' +import { + SignTransactionPipelineInput as STInput, + SignTransactionPipelineOutput as STOutput, +} from 'stellar-plus/core/pipelines/sign-transaction/types' +import { mockAccountHandler, mockSignatureSchema } from 'stellar-plus/test/mocks/transaction-mock' + +const MOCKED_KEYPAIRS = [ + Keypair.fromSecret('SAO45YQLDI4LIEPP2HXYVX72XBKEN4OBWYKR3P6AOS7EMOLJCJX5IF5A'), + Keypair.fromSecret('SA2WW3DO6AVJQO5V4MU64DSDL34FRXVIQXIUMKS7JMAENCCI3ORMQVLA'), + Keypair.fromSecret('SCHH7OAC6MC4NF3TG2JML56WJT5U7ZE355USOKGXZCQ2FCJZEX62OEKR'), +] +const TESTNET_PASSPHRASE = testnet.networkPassphrase +const MOCKED_UNSGINED_TRANSACTION_XDR = + 'AAAAAgAAAAC8CrO4sEcs28O8U8KWvl4CpiGpCgRlbEwf2fp21SRe0gAAAGQADg/kAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +const MOCKED_SIGNED_TRANSACTION_XDR = + 'AAAAAgAAAAC8CrO4sEcs28O8U8KWvl4CpiGpCgRlbEwf2fp21SRe0gAAAGQADg/kAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB1SRe0gAAAEBH30tw2MS4pbpLZ8RbEBTLxF2xalFGJRLfDhgcbildNTgujl6hbNxmw2qltop/SZfe34R4q9v+KVhp5pQ4DmkL' +const MOCKED_UNSIGNED_TRANSACTION = TransactionBuilder.fromXDR(MOCKED_UNSGINED_TRANSACTION_XDR, TESTNET_PASSPHRASE) +const MOCKED_SIGNED_TRANSACTION = TransactionBuilder.fromXDR(MOCKED_SIGNED_TRANSACTION_XDR, TESTNET_PASSPHRASE) +const MOCKED_SIGNATURE_REQUIREMENTS = [ + { + publicKey: MOCKED_KEYPAIRS[0].publicKey(), + thresholdLevel: 1, + }, +] +const MOCKED_SIGNER = mockAccountHandler(MOCKED_KEYPAIRS[0].publicKey(), MOCKED_SIGNED_TRANSACTION_XDR) +const MOCKED_SIGNERS = [MOCKED_SIGNER] +const MOCKED_ST_INPUT: STInput = { + transaction: MOCKED_UNSIGNED_TRANSACTION, + signatureRequirements: MOCKED_SIGNATURE_REQUIREMENTS, + signers: MOCKED_SIGNERS, +} +const MOCKED_ST_OUTPUT: STOutput = MOCKED_SIGNED_TRANSACTION + +describe('SignTransactionPipeline', () => { + let signTransactionPipeline: SignTransactionPipeline + + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('Sign Transaction', () => { + beforeEach(() => { + signTransactionPipeline = new SignTransactionPipeline() + jest.clearAllMocks() + }) + + it('should sign transaction successfully', async () => { + jest.spyOn(MOCKED_SIGNER, 'sign').mockResolvedValueOnce(MOCKED_SIGNED_TRANSACTION_XDR) + + await expect(signTransactionPipeline.execute(MOCKED_ST_INPUT)).resolves.toEqual(MOCKED_ST_OUTPUT) + expect(MOCKED_SIGNER.sign).toHaveBeenCalledWith(MOCKED_UNSIGNED_TRANSACTION) + expect(MOCKED_SIGNER.getPublicKey).toHaveBeenCalledTimes(1) + }) + + it('should sign transaction successfully with multiple signers', async () => { + const mockedSigners = MOCKED_SIGNERS.concat( + mockAccountHandler(MOCKED_KEYPAIRS[1].publicKey(), MOCKED_SIGNED_TRANSACTION_XDR), + mockAccountHandler(MOCKED_KEYPAIRS[2].publicKey(), MOCKED_SIGNED_TRANSACTION_XDR) + ) + const mockedInput = { + ...MOCKED_ST_INPUT, + signers: mockedSigners, + } + jest.spyOn(MOCKED_SIGNER, 'sign').mockResolvedValueOnce(MOCKED_SIGNED_TRANSACTION_XDR) + + await expect(signTransactionPipeline.execute(mockedInput)).resolves.toEqual(MOCKED_ST_OUTPUT) + expect(MOCKED_SIGNER.sign).toHaveBeenCalledWith(MOCKED_UNSIGNED_TRANSACTION) + expect(MOCKED_SIGNER.getPublicKey).toHaveBeenCalledTimes(1) + expect(mockedSigners[1].sign).not.toHaveBeenCalledOnce() + expect(mockedSigners[2].sign).not.toHaveBeenCalledOnce() + }) + + it('should sign transaction successfully with multiple signers and multiple requirements', async () => { + const mockedSigners = MOCKED_SIGNERS.concat( + mockAccountHandler(MOCKED_KEYPAIRS[1].publicKey(), MOCKED_SIGNED_TRANSACTION_XDR), + mockAccountHandler(MOCKED_KEYPAIRS[2].publicKey(), MOCKED_SIGNED_TRANSACTION_XDR) + ) + const mockedSignatureRequirements = MOCKED_SIGNATURE_REQUIREMENTS.concat( + { + publicKey: mockedSigners[1].getPublicKey(), + thresholdLevel: 1, + }, + { + publicKey: mockedSigners[2].getPublicKey(), + thresholdLevel: 1, + } + ) + const mockedInput = { + ...MOCKED_ST_INPUT, + signatureRequirements: mockedSignatureRequirements, + signers: mockedSigners, + } + jest.spyOn(MOCKED_SIGNER, 'sign').mockResolvedValueOnce(MOCKED_SIGNED_TRANSACTION_XDR) + + await expect(signTransactionPipeline.execute(mockedInput)).resolves.toEqual(MOCKED_ST_OUTPUT) + expect(mockedSigners[0].sign).toHaveBeenCalledWith(MOCKED_UNSIGNED_TRANSACTION) + expect(mockedSigners[1].sign).toHaveBeenCalledWith(MOCKED_SIGNED_TRANSACTION) + expect(mockedSigners[2].sign).toHaveBeenCalledWith(MOCKED_SIGNED_TRANSACTION) + }) + + it('should sign transaction successfully with same signer multiple times', async () => { + const mockedSigners = MOCKED_SIGNERS.concat( + mockAccountHandler(MOCKED_KEYPAIRS[0].publicKey(), MOCKED_SIGNED_TRANSACTION_XDR), + mockAccountHandler(MOCKED_KEYPAIRS[0].publicKey(), MOCKED_SIGNED_TRANSACTION_XDR), + mockAccountHandler(MOCKED_KEYPAIRS[0].publicKey(), MOCKED_SIGNED_TRANSACTION_XDR) + ) + const mockedInput = { + ...MOCKED_ST_INPUT, + signers: mockedSigners, + } + jest.spyOn(MOCKED_SIGNER, 'sign').mockResolvedValueOnce(MOCKED_SIGNED_TRANSACTION_XDR) + + await expect(signTransactionPipeline.execute(mockedInput)).resolves.toEqual(MOCKED_ST_OUTPUT) + expect(mockedSigners[0].sign).toHaveBeenCalledWith(MOCKED_UNSIGNED_TRANSACTION) + expect(mockedSigners[1].sign).not.toHaveBeenCalled() + expect(mockedSigners[2].sign).not.toHaveBeenCalled() + }) + + it('should throw error if try to sign with same signer and requirements multiple times', async () => { + const mockedSigners = MOCKED_SIGNERS.concat( + mockAccountHandler(MOCKED_KEYPAIRS[0].publicKey(), MOCKED_SIGNED_TRANSACTION_XDR), + mockAccountHandler(MOCKED_KEYPAIRS[0].publicKey(), MOCKED_SIGNED_TRANSACTION_XDR), + mockAccountHandler(MOCKED_KEYPAIRS[0].publicKey(), MOCKED_SIGNED_TRANSACTION_XDR) + ) + const mockedSignatureRequirements = MOCKED_SIGNATURE_REQUIREMENTS.concat( + { + publicKey: mockedSigners[0].getPublicKey(), + thresholdLevel: 1, + }, + { + publicKey: mockedSigners[0].getPublicKey(), + thresholdLevel: 1, + }, + { + publicKey: mockedSigners[0].getPublicKey(), + thresholdLevel: 1, + } + ) + const mockedInput = { + ...MOCKED_ST_INPUT, + signers: mockedSigners, + signatureRequirements: mockedSignatureRequirements, + } + jest.spyOn(MOCKED_SIGNER, 'sign').mockRejectedValueOnce('Error signing transaction') + + await expect(signTransactionPipeline.execute(mockedInput)).rejects.toThrow('Failed to sign transaction!') + expect(mockedSigners[0].sign).toHaveBeenCalledWith(MOCKED_UNSIGNED_TRANSACTION) + expect(mockedSigners[1].sign).not.toHaveBeenCalled() + expect(mockedSigners[2].sign).not.toHaveBeenCalled() + }) + + it('should throw error if no signature requirements received', async () => { + const mockedInput = { + ...MOCKED_ST_INPUT, + signatureRequirements: [], + } + + await expect(signTransactionPipeline.execute(mockedInput)).rejects.toThrow('No signature requirements provided!') + }) + + it('should throw error if no signers received', async () => { + const mockedInput = { + ...MOCKED_ST_INPUT, + signers: [], + } + + await expect(signTransactionPipeline.execute(mockedInput)).rejects.toThrow('No signers provided!') + }) + + it('should throw error if signature requirements does not contain signer public key', async () => { + const mockedInput = { + ...MOCKED_ST_INPUT, + signers: [mockAccountHandler('anotherPublicKey')], + } + + await expect(signTransactionPipeline.execute(mockedInput)).rejects.toThrow('The signer was not found!') + }) + + it('should throw error if XDR convertion fails', async () => { + jest.spyOn(MOCKED_SIGNER, 'sign').mockResolvedValueOnce('invalidXDR') + + await expect(signTransactionPipeline.execute(MOCKED_ST_INPUT)).rejects.toThrow('Failed to sign transaction!') + expect(MOCKED_SIGNER.sign).toHaveBeenCalledTimes(1) + expect(MOCKED_SIGNER.getPublicKey).toHaveBeenCalledTimes(2) + }) + + it('should throw error if transaction sign fails', async () => { + ;(MOCKED_SIGNER.sign as unknown as jest.Mock).mockImplementationOnce(() => { + throw new Error('Error signing transaction') + }) + + await expect(signTransactionPipeline.execute(MOCKED_ST_INPUT)).rejects.toThrow('Failed to sign transaction!') + expect(MOCKED_SIGNER.sign).toHaveBeenCalledTimes(1) + expect(MOCKED_SIGNER.getPublicKey).toHaveBeenCalledTimes(2) + }) + + it('should throw error if signer contains signatureSchema', async () => { + const signatureSchema = mockSignatureSchema() + const mockedSigner = mockAccountHandler( + MOCKED_KEYPAIRS[0].publicKey(), + MOCKED_SIGNED_TRANSACTION_XDR, + signatureSchema + ) + const mockedInput = { + ...MOCKED_ST_INPUT, + signers: [mockedSigner], + } + + await expect(signTransactionPipeline.execute(mockedInput)).rejects.toThrow( + 'Multisignature support not implemented yet' + ) + }) + }) +}) diff --git a/src/stellar-plus/test/mocks/transaction-mock.ts b/src/stellar-plus/test/mocks/transaction-mock.ts index 663139e..87ab13f 100644 --- a/src/stellar-plus/test/mocks/transaction-mock.ts +++ b/src/stellar-plus/test/mocks/transaction-mock.ts @@ -1,10 +1,6 @@ -import { Buffer } from 'buffer' +import { FeeBumpHeader, TransactionInvocation } from 'stellar-plus/types' -import { xdr } from '@stellar/stellar-sdk' - -import { FeeBumpHeader, TransactionInvocation, TransactionXdr } from 'stellar-plus/types' - -import { AccountHandler } from '../../account/account-handler/types' +import { AccountHandler, SignatureSchema } from '../../account/account-handler/types' import { EnvelopeHeader } from '../../core/types' export const MockSubmitTransaction = { @@ -28,22 +24,35 @@ export function mockHeader(sourceKey = mockAccount): EnvelopeHeader { } } -export function mockAccountHandler(accountKey = mockAccount): AccountHandler { +export function mockSignatureSchema( + threasholds?: SignatureSchema['threasholds'], + signers?: SignatureSchema['signers'] +): SignatureSchema { return { - sign(_tx: any): TransactionXdr { - return 'success' - }, - signSorobanAuthEntry( - _entry: any, - _validUntilLedgerSeq: number, - _networkPassphrase: string - ): Promise { - return Promise.resolve(xdr.SorobanAuthorizationEntry.fromXDR(Buffer.from('success'))) + threasholds: threasholds || { + low: 1, + medium: 2, + high: 3, }, + signers: signers || [ + { + weight: 1, + publicKey: mockAccount, + }, + ], + } +} - getPublicKey(): string { - return accountKey - }, +export function mockAccountHandler( + accountKey = mockAccount, + outputSignedTransaction?: string, + signatureSchema?: SignatureSchema +): jest.Mocked { + return { + sign: jest.fn().mockReturnValue(outputSignedTransaction ?? 'success'), + signSorobanAuthEntry: jest.fn().mockResolvedValue('success'), + getPublicKey: jest.fn().mockReturnValue(accountKey), + signatureSchema, } } From 72d7b93727ea8694f873e824a06803f6a54445fd Mon Sep 17 00:00:00 2001 From: Bruno Nascimento Date: Wed, 3 Apr 2024 15:18:26 -0300 Subject: [PATCH 18/30] test: add unit test for submit transaction pipeline --- .../pipelines/submit-transaction/index.ts | 2 +- .../submit-transaction/index.unit.test.ts | 136 ++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 src/stellar-plus/core/pipelines/submit-transaction/index.unit.test.ts diff --git a/src/stellar-plus/core/pipelines/submit-transaction/index.ts b/src/stellar-plus/core/pipelines/submit-transaction/index.ts index 7f20f6d..1c6bde7 100644 --- a/src/stellar-plus/core/pipelines/submit-transaction/index.ts +++ b/src/stellar-plus/core/pipelines/submit-transaction/index.ts @@ -8,11 +8,11 @@ import { SubmitTransactionPipelineType, } from 'stellar-plus/core/pipelines/submit-transaction/types' import { extractConveyorBeltErrorMeta } from 'stellar-plus/error/helpers/conveyor-belt' +import { HorizonHandlerClient } from 'stellar-plus/horizon' import { RpcHandler } from 'stellar-plus/rpc/types' import { ConveyorBelt } from 'stellar-plus/utils/pipeline/conveyor-belts' import { PSUError } from './errors' -import { HorizonHandlerClient } from 'stellar-plus/horizon' export class SubmitTransactionPipeline extends ConveyorBelt< SubmitTransactionPipelineInput, diff --git a/src/stellar-plus/core/pipelines/submit-transaction/index.unit.test.ts b/src/stellar-plus/core/pipelines/submit-transaction/index.unit.test.ts new file mode 100644 index 0000000..9c626aa --- /dev/null +++ b/src/stellar-plus/core/pipelines/submit-transaction/index.unit.test.ts @@ -0,0 +1,136 @@ +import { SorobanRpc, TransactionBuilder } from '@stellar/stellar-sdk' + +import { testnet } from 'stellar-plus/constants' +import { SubmitTransactionPipeline } from 'stellar-plus/core/pipelines/submit-transaction' +import { + SubmitTransactionPipelineInput as STInput, + SubmitTransactionPipelineOutput as STOutput, +} from 'stellar-plus/core/pipelines/submit-transaction/types' +import { HorizonHandlerClient } from 'stellar-plus/horizon' +import { DefaultRpcHandler } from 'stellar-plus/rpc' + +const TESTNET_PASSPHRASE = testnet.networkPassphrase +const MOCKED_TRANSACTION_XDR = + 'AAAAAgAAAAC8CrO4sEcs28O8U8KWvl4CpiGpCgRlbEwf2fp21SRe0gAAAGQADg/kAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB1SRe0gAAAEBH30tw2MS4pbpLZ8RbEBTLxF2xalFGJRLfDhgcbildNTgujl6hbNxmw2qltop/SZfe34R4q9v+KVhp5pQ4DmkL' +const MOCKED_TRANSACTION = TransactionBuilder.fromXDR(MOCKED_TRANSACTION_XDR, TESTNET_PASSPHRASE) +const MOCKED_ST_OUTPUT: STOutput = { + response: { + successful: true, + hash: 'hash', + ledger: 1, + envelope_xdr: 'envelope_xdr', + result_xdr: 'result_xdr', + result_meta_xdr: 'result_meta_xdr', + paging_token: 'paging_token', + }, +} +const HORIZON_HANDLER = new HorizonHandlerClient(testnet) +const RPC_HANDLER = new DefaultRpcHandler(testnet) +const MOCKED_ST_INPUT: STInput = { + transaction: MOCKED_TRANSACTION, + networkHandler: HORIZON_HANDLER, +} + +describe('SubmitTransactionPipeline', () => { + let submitTransactionPipeline: SubmitTransactionPipeline + + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should throw error if received handler is neither horizon or rpc', async () => { + submitTransactionPipeline = new SubmitTransactionPipeline() + const MOCKED_INVALID_HANDLER = {} as unknown as HorizonHandlerClient + + await expect( + submitTransactionPipeline.execute({ + ...MOCKED_ST_INPUT, + networkHandler: MOCKED_INVALID_HANDLER, + }) + ).rejects.toThrow('Invalid network handler!') + expect(HORIZON_HANDLER.server.submitTransaction).not.toHaveBeenCalledOnce() + }) + + describe('Submit Transaction with HorizonHandler', () => { + beforeEach(() => { + submitTransactionPipeline = new SubmitTransactionPipeline() + jest.clearAllMocks() + }) + + it('should submit transaction successfully', async () => { + HORIZON_HANDLER.server.submitTransaction = jest.fn().mockResolvedValue(MOCKED_ST_OUTPUT.response) + + await expect(submitTransactionPipeline.execute(MOCKED_ST_INPUT)).resolves.toEqual(MOCKED_ST_OUTPUT) + expect(HORIZON_HANDLER.server.submitTransaction).toHaveBeenCalledWith(MOCKED_TRANSACTION, { + skipMemoRequiredCheck: true, + }) + }) + + it('should throw error if something went wrong', async () => { + HORIZON_HANDLER.server.submitTransaction = jest.fn().mockRejectedValue(MOCKED_ST_OUTPUT.response) + + await expect(submitTransactionPipeline.execute(MOCKED_ST_INPUT)).rejects.toThrow( + 'Transaction submission through Horizon failed!' + ) + expect(HORIZON_HANDLER.server.submitTransaction).toHaveBeenCalledWith(MOCKED_TRANSACTION, { + skipMemoRequiredCheck: true, + }) + }) + + it('should throw error if horizon resolved but was unsuccessful', async () => { + HORIZON_HANDLER.server.submitTransaction = jest.fn().mockResolvedValue({ + ...MOCKED_ST_OUTPUT.response, + successful: false, + }) + + await expect(submitTransactionPipeline.execute(MOCKED_ST_INPUT)).rejects.toThrow( + 'The transaction submitted through Horizon has failed!' + ) + expect(HORIZON_HANDLER.server.submitTransaction).toHaveBeenCalledWith(MOCKED_TRANSACTION, { + skipMemoRequiredCheck: true, + }) + }) + }) + + describe('Submit Transaction with RPCHandler', () => { + const MOCKED_ST_INPUT_RPC: STInput = { + transaction: MOCKED_TRANSACTION, + networkHandler: RPC_HANDLER, + } + const MOCKED_ST_OUTPUT_RPC: STOutput = { + response: { + successful: true, + hash: 'hash', + ledger: 1, + envelope_xdr: 'envelope_xdr', + result_xdr: 'result_xdr', + result_meta_xdr: 'result_meta_xdr', + paging_token: 'paging_token', + status: 'PENDING', + latestLedger: 1, + latestLedgerCloseTime: 1, + } as SorobanRpc.Api.SendTransactionResponse, + } + + beforeEach(() => { + submitTransactionPipeline = new SubmitTransactionPipeline() + jest.clearAllMocks() + }) + + it('should submit transaction successfully', async () => { + RPC_HANDLER.submitTransaction = jest.fn().mockResolvedValue(MOCKED_ST_OUTPUT_RPC.response) + + await expect(submitTransactionPipeline.execute(MOCKED_ST_INPUT_RPC)).resolves.toEqual(MOCKED_ST_OUTPUT_RPC) + expect(RPC_HANDLER.submitTransaction).toHaveBeenCalledWith(MOCKED_TRANSACTION) + }) + + it('should throw error if something went wrong', async () => { + RPC_HANDLER.submitTransaction = jest.fn().mockRejectedValue(MOCKED_ST_OUTPUT_RPC.response) + + await expect(submitTransactionPipeline.execute(MOCKED_ST_INPUT_RPC)).rejects.toThrow( + 'Transaction submission through Soroban RPC failed!' + ) + expect(RPC_HANDLER.submitTransaction).toHaveBeenCalledWith(MOCKED_TRANSACTION) + }) + }) +}) From 0d57031a5f55316da53707bd92a82a67ccdcf005 Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:23:04 -0300 Subject: [PATCH 19/30] Add unit tests soroban auth (#123) * fix: remove validation from contract engine initialization * style: remove comment * feat: modify account handler mocker payload * test: add unit test to soroban auth pipeline --------- Co-authored-by: Bruno Nascimento --- .../core/contract-engine/index.ts | 2 - .../core/contract-engine/index.unit.test.ts | 14 +- .../sign-transaction/index.unit.test.ts | 67 ++- .../core/pipelines/soroban-auth/index.ts | 3 +- .../pipelines/soroban-auth/index.unit.test.ts | 456 ++++++++++++++++++ .../test/mocks/transaction-mock.ts | 22 +- 6 files changed, 525 insertions(+), 39 deletions(-) create mode 100644 src/stellar-plus/core/pipelines/soroban-auth/index.unit.test.ts diff --git a/src/stellar-plus/core/contract-engine/index.ts b/src/stellar-plus/core/contract-engine/index.ts index 609e515..3bed6d3 100644 --- a/src/stellar-plus/core/contract-engine/index.ts +++ b/src/stellar-plus/core/contract-engine/index.ts @@ -105,8 +105,6 @@ export class ContractEngine { this.wasm = contractParameters.wasm this.wasmHash = contractParameters.wasmHash - if (!this.contractId && !this.wasm && !this.wasmHash) throw CEError.contractEngineClassFailedToInitialize() - this.options = { ...options } this.sorobanTransactionPipeline = new SorobanTransactionPipeline(networkConfig, { diff --git a/src/stellar-plus/core/contract-engine/index.unit.test.ts b/src/stellar-plus/core/contract-engine/index.unit.test.ts index 773fc04..937f893 100644 --- a/src/stellar-plus/core/contract-engine/index.unit.test.ts +++ b/src/stellar-plus/core/contract-engine/index.unit.test.ts @@ -42,7 +42,7 @@ const MOCKED_TX_INVOCATION: TransactionInvocation = { signers: [], } -const MOCKED_SOROBAN_INVOKE_ARGS: SorobanInvokeArgs<{}> = { +const MOCKED_SOROBAN_INVOKE_ARGS: SorobanInvokeArgs = { method: tokenMethods.name, methodArgs: {}, ...MOCKED_TX_INVOCATION, @@ -92,17 +92,6 @@ describe('ContractEngine', () => { }) describe('Initialization Errors', () => { - it('should throw error if no wasm file, wasm hash or contract id is provided', () => { - expect(() => { - const contractEngine = new ContractEngine({ - networkConfig: NETWORK_CONFIG, - contractParameters: { - spec: MOCKED_CONTRACT_SPEC, - }, - }) - }).toThrow(CEError.contractEngineClassFailedToInitialize()) - }) - it('should throw error if wasm file is required but is not present', async () => { const contractEngine = new ContractEngine({ networkConfig: NETWORK_CONFIG, @@ -269,6 +258,7 @@ describe('ContractEngine', () => { getLedgerEntries: jest.fn().mockResolvedValue({ entries: [ { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment key: Object.assign(xdr.LedgerKey.contractCode(MOCKED_CONTRACT_CODE_KEY)), xdr: 'xdr', liveUntilLedgerSeq: 1, diff --git a/src/stellar-plus/core/pipelines/sign-transaction/index.unit.test.ts b/src/stellar-plus/core/pipelines/sign-transaction/index.unit.test.ts index 2adebec..17658b3 100644 --- a/src/stellar-plus/core/pipelines/sign-transaction/index.unit.test.ts +++ b/src/stellar-plus/core/pipelines/sign-transaction/index.unit.test.ts @@ -26,7 +26,10 @@ const MOCKED_SIGNATURE_REQUIREMENTS = [ thresholdLevel: 1, }, ] -const MOCKED_SIGNER = mockAccountHandler(MOCKED_KEYPAIRS[0].publicKey(), MOCKED_SIGNED_TRANSACTION_XDR) +const MOCKED_SIGNER = mockAccountHandler({ + accountKey: MOCKED_KEYPAIRS[0].publicKey(), + outputSignedTransaction: MOCKED_SIGNED_TRANSACTION_XDR, +}) const MOCKED_SIGNERS = [MOCKED_SIGNER] const MOCKED_ST_INPUT: STInput = { transaction: MOCKED_UNSIGNED_TRANSACTION, @@ -58,8 +61,14 @@ describe('SignTransactionPipeline', () => { it('should sign transaction successfully with multiple signers', async () => { const mockedSigners = MOCKED_SIGNERS.concat( - mockAccountHandler(MOCKED_KEYPAIRS[1].publicKey(), MOCKED_SIGNED_TRANSACTION_XDR), - mockAccountHandler(MOCKED_KEYPAIRS[2].publicKey(), MOCKED_SIGNED_TRANSACTION_XDR) + mockAccountHandler({ + accountKey: MOCKED_KEYPAIRS[1].publicKey(), + outputSignedTransaction: MOCKED_SIGNED_TRANSACTION_XDR, + }), + mockAccountHandler({ + accountKey: MOCKED_KEYPAIRS[2].publicKey(), + outputSignedTransaction: MOCKED_SIGNED_TRANSACTION_XDR, + }) ) const mockedInput = { ...MOCKED_ST_INPUT, @@ -76,8 +85,14 @@ describe('SignTransactionPipeline', () => { it('should sign transaction successfully with multiple signers and multiple requirements', async () => { const mockedSigners = MOCKED_SIGNERS.concat( - mockAccountHandler(MOCKED_KEYPAIRS[1].publicKey(), MOCKED_SIGNED_TRANSACTION_XDR), - mockAccountHandler(MOCKED_KEYPAIRS[2].publicKey(), MOCKED_SIGNED_TRANSACTION_XDR) + mockAccountHandler({ + accountKey: MOCKED_KEYPAIRS[1].publicKey(), + outputSignedTransaction: MOCKED_SIGNED_TRANSACTION_XDR, + }), + mockAccountHandler({ + accountKey: MOCKED_KEYPAIRS[2].publicKey(), + outputSignedTransaction: MOCKED_SIGNED_TRANSACTION_XDR, + }) ) const mockedSignatureRequirements = MOCKED_SIGNATURE_REQUIREMENTS.concat( { @@ -104,9 +119,18 @@ describe('SignTransactionPipeline', () => { it('should sign transaction successfully with same signer multiple times', async () => { const mockedSigners = MOCKED_SIGNERS.concat( - mockAccountHandler(MOCKED_KEYPAIRS[0].publicKey(), MOCKED_SIGNED_TRANSACTION_XDR), - mockAccountHandler(MOCKED_KEYPAIRS[0].publicKey(), MOCKED_SIGNED_TRANSACTION_XDR), - mockAccountHandler(MOCKED_KEYPAIRS[0].publicKey(), MOCKED_SIGNED_TRANSACTION_XDR) + mockAccountHandler({ + accountKey: MOCKED_KEYPAIRS[0].publicKey(), + outputSignedTransaction: MOCKED_SIGNED_TRANSACTION_XDR, + }), + mockAccountHandler({ + accountKey: MOCKED_KEYPAIRS[0].publicKey(), + outputSignedTransaction: MOCKED_SIGNED_TRANSACTION_XDR, + }), + mockAccountHandler({ + accountKey: MOCKED_KEYPAIRS[0].publicKey(), + outputSignedTransaction: MOCKED_SIGNED_TRANSACTION_XDR, + }) ) const mockedInput = { ...MOCKED_ST_INPUT, @@ -122,9 +146,18 @@ describe('SignTransactionPipeline', () => { it('should throw error if try to sign with same signer and requirements multiple times', async () => { const mockedSigners = MOCKED_SIGNERS.concat( - mockAccountHandler(MOCKED_KEYPAIRS[0].publicKey(), MOCKED_SIGNED_TRANSACTION_XDR), - mockAccountHandler(MOCKED_KEYPAIRS[0].publicKey(), MOCKED_SIGNED_TRANSACTION_XDR), - mockAccountHandler(MOCKED_KEYPAIRS[0].publicKey(), MOCKED_SIGNED_TRANSACTION_XDR) + mockAccountHandler({ + accountKey: MOCKED_KEYPAIRS[0].publicKey(), + outputSignedTransaction: MOCKED_SIGNED_TRANSACTION_XDR, + }), + mockAccountHandler({ + accountKey: MOCKED_KEYPAIRS[0].publicKey(), + outputSignedTransaction: MOCKED_SIGNED_TRANSACTION_XDR, + }), + mockAccountHandler({ + accountKey: MOCKED_KEYPAIRS[0].publicKey(), + outputSignedTransaction: MOCKED_SIGNED_TRANSACTION_XDR, + }) ) const mockedSignatureRequirements = MOCKED_SIGNATURE_REQUIREMENTS.concat( { @@ -174,7 +207,7 @@ describe('SignTransactionPipeline', () => { it('should throw error if signature requirements does not contain signer public key', async () => { const mockedInput = { ...MOCKED_ST_INPUT, - signers: [mockAccountHandler('anotherPublicKey')], + signers: [mockAccountHandler({ accountKey: 'anotherPublicKey' })], } await expect(signTransactionPipeline.execute(mockedInput)).rejects.toThrow('The signer was not found!') @@ -200,11 +233,11 @@ describe('SignTransactionPipeline', () => { it('should throw error if signer contains signatureSchema', async () => { const signatureSchema = mockSignatureSchema() - const mockedSigner = mockAccountHandler( - MOCKED_KEYPAIRS[0].publicKey(), - MOCKED_SIGNED_TRANSACTION_XDR, - signatureSchema - ) + const mockedSigner = mockAccountHandler({ + accountKey: MOCKED_KEYPAIRS[0].publicKey(), + outputSignedTransaction: MOCKED_SIGNED_TRANSACTION_XDR, + signatureSchema: signatureSchema, + }) const mockedInput = { ...MOCKED_ST_INPUT, signers: [mockedSigner], diff --git a/src/stellar-plus/core/pipelines/soroban-auth/index.ts b/src/stellar-plus/core/pipelines/soroban-auth/index.ts index 8e02507..9d7893a 100644 --- a/src/stellar-plus/core/pipelines/soroban-auth/index.ts +++ b/src/stellar-plus/core/pipelines/soroban-auth/index.ts @@ -49,7 +49,8 @@ export class SorobanAuthPipeline extends ConveyorBelt< ...(additionalSorobanAuthToSign || []), // Additional auth entries to sign can come from other simulations and more complex authorization use cases ] - if (authEntriesToSign.length === 0 && !additionalSignedSorobanAuth) return transaction + if (authEntriesToSign.length === 0 && (!additionalSignedSorobanAuth || additionalSignedSorobanAuth.length < 1)) + return transaction if (signers.length === 0) throw PSAError.noSignersProvided(extractConveyorBeltErrorMeta(item, this.getMeta(itemId))) diff --git a/src/stellar-plus/core/pipelines/soroban-auth/index.unit.test.ts b/src/stellar-plus/core/pipelines/soroban-auth/index.unit.test.ts new file mode 100644 index 0000000..a2cc4e9 --- /dev/null +++ b/src/stellar-plus/core/pipelines/soroban-auth/index.unit.test.ts @@ -0,0 +1,456 @@ +import { Account, SorobanDataBuilder, SorobanRpc, Transaction, TransactionBuilder, xdr } from '@stellar/stellar-sdk' + +import { Constants } from 'stellar-plus' +import { SimulateTransactionPipeline } from 'stellar-plus/core/pipelines/simulate-transaction' +import { SorobanAuthPipeline } from 'stellar-plus/core/pipelines/soroban-auth' +import { PSAError } from 'stellar-plus/core/pipelines/soroban-auth/errors' +import { SorobanAuthPipelineInput, SorobanAuthPipelineType } from 'stellar-plus/core/pipelines/soroban-auth/types' +import { ConveyorBeltErrorMeta } from 'stellar-plus/error/helpers/conveyor-belt' +import { DefaultRpcHandler } from 'stellar-plus/rpc/default-handler' +import { mockAccountHandler } from 'stellar-plus/test/mocks/transaction-mock' +import { BeltMetadata } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +const TESTNET_NETWORK_CONFIG = Constants.testnet +const MOCKED_TRANSACTION_OUTPUT = new Transaction( + 'AAAAAgAAAAA/s0szuJKLyO2bQJ0DxXjYA2p8sf8kTBjkhAVTV64DQgAAAGQAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + TESTNET_NETWORK_CONFIG.networkPassphrase +) + +jest.mock('stellar-plus/core/pipelines/simulate-transaction', () => { + return { + SimulateTransactionPipeline: jest.fn().mockImplementation(() => { + return { + execute: jest.fn().mockImplementation(() => { + return Promise.resolve({ assembledTransaction: MOCKED_TRANSACTION_OUTPUT }) + }), + } + }), + } +}) + +jest.mock('stellar-plus/rpc/default-handler', () => { + return { + DefaultRpcHandler: jest.fn().mockImplementation(() => { + return { + getLatestLedger: jest.fn().mockImplementation(() => { + return { + sequence: 0, + } + }), + } + }), + } +}) + +const MOCKED_SIMULATE_TRANSACTION_PIPELINE = SimulateTransactionPipeline as jest.Mock +const MOCKED_DEFAULT_RPC_HANDLER = DefaultRpcHandler as jest.Mock + +const MOCKED_PK_A = 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI' +const MOCKED_ACCOUNT_A = new Account(MOCKED_PK_A, '100') + +const MOCKED_TX_OPTIONS: TransactionBuilder.TransactionBuilderOptions = { + fee: '100', + networkPassphrase: TESTNET_NETWORK_CONFIG.networkPassphrase, + timebounds: { + minTime: 0, + maxTime: 10, + }, +} + +const MOCKED_AUTH_ENTRY_A_XDR = + 'AAAAAQAAAAAAAAAArb8JC0i36Dmfgz6KW5WQ0tnZdAEJokT6E14XqgChUA1ikGR38zUPUgAAAAAAAAABAAAAAAAAAAFk7Ujzh9RWi2IGWZcvksnk6CDy39UadWJfYdD9K0mf2AAAAAh0cmFuc2ZlcgAAAAMAAAASAAAAAAAAAACtvwkLSLfoOZ+DPopblZDS2dl0AQmiRPoTXheqAKFQDQAAABIAAAAAAAAAAPVDIFPI3hOwtWMsV6KpQaDW/2Yg+ouWRCzidXCyoZP5AAAACgAAAAAAAAAAAAAAAAAAAAEAAAAA' +const MOCKED_AUTH_ENTRY_A = xdr.SorobanAuthorizationEntry.fromXDR(MOCKED_AUTH_ENTRY_A_XDR, 'base64') + +const MOCKED_AUTH_ENTRY_A_REQ_SIGNER = 'GCW36CILJC36QOM7QM7IUW4VSDJNTWLUAEE2ERH2CNPBPKQAUFIA2GRV' + +const MOCKED_AUTH_ENTRY_B_XDR = + 'AAAAAQAAAAAAAAAAovR6uAp1T7jRCxGrvh8SBIppUR9ZD2/eCQT9tGO26OdII5B9b2HQOAAAAAAAAAABAAAAAAAAAAHTgPPxxvd0QWWI9GqET9mM/ybmAbehCVOSDLq8gmyohQAAAAh0cmFuc2ZlcgAAAAMAAAASAAAAAAAAAACi9Hq4CnVPuNELEau+HxIEimlRH1kPb94JBP20Y7bo5wAAABIAAAAAAAAAAIgOB9aLzALzAdu7EDOHT7sRsX1lczERWJjuKgPDLKcGAAAACgAAAAAAAAAAAAAAAAAAAAEAAAAA' +const MOCKED_AUTH_ENTRY_B = xdr.SorobanAuthorizationEntry.fromXDR(MOCKED_AUTH_ENTRY_B_XDR, 'base64') + +const MOCKED_AUTH_ENTRY_B_REQ_SIGNER = 'GCRPI6VYBJ2U7OGRBMI2XPQ7CICIU2KRD5MQ6366BECP3NDDW3UOP6HV' + +const MOCKED_AUTH_ENTRY_SOURCE_XDR = + 'AAAAAAAAAAAAAAABTtqfrXuuo4yIBEW0azSfUHab0yKalJuUPxe/LGg6o7YAAAAIdHJhbnNmZXIAAAADAAAAEgAAAAAAAAAA+lIrHIESwBahZaC2Y1qy8V3ofBOcmfRqmYgk2MXmIDsAAAASAAAAAAAAAAC3qK45KNxeE/HNayxHRBU3O0+FQle0MkiVl4yuhRI0fwAAAAoAAAAAAAAAAAAAAAAAAAABAAAAAA==' +const MOCKED_AUTH_ENTRY_SOURCE = xdr.SorobanAuthorizationEntry.fromXDR(MOCKED_AUTH_ENTRY_SOURCE_XDR, 'base64') + +const MOCKED_TRANSACTION_XDR = + 'AAAAAgAAAACPVHnhwyBwtNC/1qxeu9dTtItat/QecxIsa7H316JigQAPQkAADkkGAAAAAgAAAAEAAAAAAAAAAAAAAABmDYF/AAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABZO1I84fUVotiBlmXL5LJ5Ogg8t/VGnViX2HQ/StJn9gAAAAIdHJhbnNmZXIAAAADAAAAEgAAAAAAAAAArb8JC0i36Dmfgz6KW5WQ0tnZdAEJokT6E14XqgChUA0AAAASAAAAAAAAAAD1QyBTyN4TsLVjLFeiqUGg1v9mIPqLlkQs4nVwsqGT+QAAAAoAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA' + +const MOCKED_TRANSACTION = new Transaction(MOCKED_TRANSACTION_XDR, TESTNET_NETWORK_CONFIG.networkPassphrase) + +const MOCKED_SIMULATION_RESPONSE_SUCCESS = { + events: [], + id: 'mocked-id', + latestLedger: 0, + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: '0', + cost: { + cpuInsns: '0', + memBytes: '0', + }, + result: { + auth: [], + xdr: 'mocked-xdr', + retval: xdr.ScVal.scvVoid(), + }, +} as SorobanRpc.Api.SimulateTransactionSuccessResponse + +const MOCKED_RPC = new DefaultRpcHandler(TESTNET_NETWORK_CONFIG) + +const MOCKED_ACCOUNT_HANDLER_A = mockAccountHandler({ accountKey: MOCKED_PK_A }) + +const MOCKED_PIPELINE_ITEM = { + transaction: MOCKED_TRANSACTION, + simulation: MOCKED_SIMULATION_RESPONSE_SUCCESS, + signers: [], + rpcHandler: MOCKED_RPC, +} as SorobanAuthPipelineInput + +const mockConveyorBeltErrorMeta = ( + item: SorobanAuthPipelineInput +): ConveyorBeltErrorMeta => { + return { + item, + meta: { + itemId: 'mocked-id', + beltId: 'mocked-belt-id', + beltType: SorobanAuthPipelineType.id, + }, + } as ConveyorBeltErrorMeta +} + +describe('Soroban Auth Pipeline', () => { + describe('Initialization', () => { + it('should initialize the pipeline', () => { + const pipeline = new SorobanAuthPipeline() + + expect(pipeline).toBeInstanceOf(SorobanAuthPipeline) + }) + }) + + describe('Process', () => { + beforeEach(() => { + jest.clearAllMocks() + jest.useFakeTimers() + }) + + it('should not sign the auth entries if there are no auth entries in the simulation nor additional entries provided', async () => { + const pipeline = new SorobanAuthPipeline() + const mockedItem = { + ...MOCKED_PIPELINE_ITEM, + transaction: MOCKED_TRANSACTION, + simulation: MOCKED_SIMULATION_RESPONSE_SUCCESS, + signers: [MOCKED_ACCOUNT_HANDLER_A], + } + + const output = await pipeline.execute(mockedItem) + + expect(output).toEqual(MOCKED_TRANSACTION) + }) + + it('should not sign the auth entries if there are no auth entries in the simulation and additional entries are provided empty', async () => { + const pipeline = new SorobanAuthPipeline() + const mockedItem = { + ...MOCKED_PIPELINE_ITEM, + transaction: MOCKED_TRANSACTION, + simulation: MOCKED_SIMULATION_RESPONSE_SUCCESS, + signers: [MOCKED_ACCOUNT_HANDLER_A], + additionalSorobanAuthToSign: [], + additionalSignedSorobanAuth: [], + } + + const output = await pipeline.execute(mockedItem) + + expect(output).toEqual(MOCKED_TRANSACTION) + }) + + it('should not sign the auth entries if there are only auth entries for source', async () => { + const pipeline = new SorobanAuthPipeline() + const simulation = { ...MOCKED_SIMULATION_RESPONSE_SUCCESS } as SorobanRpc.Api.SimulateTransactionSuccessResponse + if (simulation.result) simulation.result.auth = [MOCKED_AUTH_ENTRY_SOURCE] + const mockedItem = { + ...MOCKED_PIPELINE_ITEM, + transaction: MOCKED_TRANSACTION, + simulation, + signers: [MOCKED_ACCOUNT_HANDLER_A], + additionalSorobanAuthToSign: [], + additionalSignedSorobanAuth: [], + } + + const output = await pipeline.execute(mockedItem) + + expect(output).toEqual(MOCKED_TRANSACTION) + }) + + it('should not sign the auth entries if there is no result in the simulation', async () => { + const pipeline = new SorobanAuthPipeline() + const simulation = { ...MOCKED_SIMULATION_RESPONSE_SUCCESS } as SorobanRpc.Api.SimulateTransactionSuccessResponse + if (simulation.result) simulation.result = undefined + const mockedSigner = mockAccountHandler({ + accountKey: MOCKED_AUTH_ENTRY_A_REQ_SIGNER, + outputSignedAuthEntry: MOCKED_AUTH_ENTRY_A, + }) + const spyMockSign = jest.spyOn(mockedSigner, 'signSorobanAuthEntry') + const mockedItem = { + ...MOCKED_PIPELINE_ITEM, + transaction: MOCKED_TRANSACTION, + simulation, + signers: [mockedSigner], + additionalSorobanAuthToSign: [], + additionalSignedSorobanAuth: [], + } + + const output = await pipeline.execute(mockedItem) + + expect(output).toEqual(MOCKED_TRANSACTION) + expect(spyMockSign).not.toHaveBeenCalled() + }) + + it('should sign the auth entries if there are auth entries for the transaction and the required signer', async () => { + const pipeline = new SorobanAuthPipeline() + const simulation = { ...MOCKED_SIMULATION_RESPONSE_SUCCESS } as SorobanRpc.Api.SimulateTransactionSuccessResponse + if (simulation.result) simulation.result.auth = [MOCKED_AUTH_ENTRY_A] + const mockedSigner = mockAccountHandler({ + accountKey: MOCKED_AUTH_ENTRY_A_REQ_SIGNER, + outputSignedAuthEntry: MOCKED_AUTH_ENTRY_A, + }) + const spyMockSign = jest.spyOn(mockedSigner, 'signSorobanAuthEntry') + const mockedItem = { + ...MOCKED_PIPELINE_ITEM, + transaction: MOCKED_TRANSACTION, + simulation, + signers: [mockedSigner], + } + + const output = await pipeline.execute(mockedItem) + + expect(MOCKED_SIMULATE_TRANSACTION_PIPELINE).toHaveBeenCalledOnce() + expect(output).toEqual(MOCKED_TRANSACTION_OUTPUT) + expect(spyMockSign).toHaveBeenCalledOnce() + }) + + it('should sign the auth entries if there are auth entries for the transaction only with the required signer', async () => { + const pipeline = new SorobanAuthPipeline() + const simulation = { ...MOCKED_SIMULATION_RESPONSE_SUCCESS } as SorobanRpc.Api.SimulateTransactionSuccessResponse + if (simulation.result) simulation.result.auth = [MOCKED_AUTH_ENTRY_A] + const mockedSignerA = mockAccountHandler({ + accountKey: MOCKED_AUTH_ENTRY_A_REQ_SIGNER, + outputSignedAuthEntry: MOCKED_AUTH_ENTRY_A, + }) + const mockedSignerB = mockAccountHandler({ + accountKey: MOCKED_AUTH_ENTRY_B_REQ_SIGNER, + outputSignedAuthEntry: MOCKED_AUTH_ENTRY_B, + }) + const spyMockSignA = jest.spyOn(mockedSignerA, 'signSorobanAuthEntry') + const spyMockSignB = jest.spyOn(mockedSignerB, 'signSorobanAuthEntry') + const mockedItem = { + ...MOCKED_PIPELINE_ITEM, + transaction: MOCKED_TRANSACTION, + simulation, + signers: [mockedSignerA], + } + + const output = await pipeline.execute(mockedItem) + + expect(MOCKED_SIMULATE_TRANSACTION_PIPELINE).toHaveBeenCalledOnce() + expect(output).toEqual(MOCKED_TRANSACTION_OUTPUT) + expect(spyMockSignA).toHaveBeenCalledOnce() + expect(spyMockSignB).not.toHaveBeenCalled() + }) + + it('should use the transaction timeout to calculate auth timeout', async () => { + const latestLedger = 1000 + MOCKED_DEFAULT_RPC_HANDLER.mockImplementationOnce(() => { + return { + getLatestLedger: jest.fn().mockImplementation(() => { + return { + sequence: latestLedger, + } + }), + } + }) + const currentTime = 1000 + jest.setSystemTime(currentTime) + const timeout = Number(MOCKED_TRANSACTION.timeBounds?.maxTime) - currentTime / 1000 + const expectedExpiration = Number((latestLedger + timeout / 5 + 1).toFixed(0)) + const pipeline = new SorobanAuthPipeline() + const simulation = { ...MOCKED_SIMULATION_RESPONSE_SUCCESS } as SorobanRpc.Api.SimulateTransactionSuccessResponse + if (simulation.result) simulation.result.auth = [MOCKED_AUTH_ENTRY_A] + const mockedSigner = mockAccountHandler({ + accountKey: MOCKED_AUTH_ENTRY_A_REQ_SIGNER, + outputSignedAuthEntry: MOCKED_AUTH_ENTRY_A, + }) + const spyMockSign = jest.spyOn(mockedSigner, 'signSorobanAuthEntry') + const mockedItem = { + ...MOCKED_PIPELINE_ITEM, + transaction: MOCKED_TRANSACTION, + simulation, + signers: [mockedSigner], + rpcHandler: new DefaultRpcHandler(TESTNET_NETWORK_CONFIG), + } + + await pipeline.execute(mockedItem) + + expect(spyMockSign).toHaveBeenCalledWith( + MOCKED_AUTH_ENTRY_A, + expectedExpiration, + TESTNET_NETWORK_CONFIG.networkPassphrase + ) + }) + }) + + it('should use the default timeout to calculate auth timeout when the transaction has no timeout', async () => { + const latestLedger = 500 + MOCKED_DEFAULT_RPC_HANDLER.mockImplementationOnce(() => { + return { + getLatestLedger: jest.fn().mockImplementation(() => { + return { + sequence: latestLedger, + } + }), + } + }) + const currentTime = 1000 + jest.useFakeTimers() + jest.setSystemTime(currentTime) + const mockedTransactionWithoutTimeout = TransactionBuilder.cloneFrom(MOCKED_TRANSACTION, { + fee: '100', + networkPassphrase: TESTNET_NETWORK_CONFIG.networkPassphrase, + timebounds: { + minTime: 0, + maxTime: 0, + }, + }).build() + const defaultValue = 600 + const timeout = Number(defaultValue) + const expectedExpiration = Number((latestLedger + timeout / 5 + 1).toFixed(0)) + const pipeline = new SorobanAuthPipeline() + const simulation = { ...MOCKED_SIMULATION_RESPONSE_SUCCESS } as SorobanRpc.Api.SimulateTransactionSuccessResponse + if (simulation.result) simulation.result.auth = [MOCKED_AUTH_ENTRY_A] + const mockedSigner = mockAccountHandler({ + accountKey: MOCKED_AUTH_ENTRY_A_REQ_SIGNER, + outputSignedAuthEntry: MOCKED_AUTH_ENTRY_A, + }) + const spyMockSign = jest.spyOn(mockedSigner, 'signSorobanAuthEntry') + const mockedItem = { + ...MOCKED_PIPELINE_ITEM, + transaction: mockedTransactionWithoutTimeout, + simulation, + signers: [mockedSigner], + rpcHandler: new DefaultRpcHandler(TESTNET_NETWORK_CONFIG), + } + + await pipeline.execute(mockedItem) + + expect(spyMockSign).toHaveBeenCalledWith( + MOCKED_AUTH_ENTRY_A, + expectedExpiration, + TESTNET_NETWORK_CONFIG.networkPassphrase + ) + }) + + describe('Errors', () => { + it('should throw if there are entries to sign but no signers are provided', async () => { + const pipeline = new SorobanAuthPipeline() + const simulation = { ...MOCKED_SIMULATION_RESPONSE_SUCCESS } as SorobanRpc.Api.SimulateTransactionSuccessResponse + if (simulation.result) simulation.result.auth = [MOCKED_AUTH_ENTRY_A] + const mockedItem = { + ...MOCKED_PIPELINE_ITEM, + transaction: MOCKED_TRANSACTION, + simulation, + signers: [], + additionalSorobanAuthToSign: [], + additionalSignedSorobanAuth: [], + } + + await expect(pipeline.execute(mockedItem)).rejects.toThrow( + PSAError.noSignersProvided(mockConveyorBeltErrorMeta(mockedItem)) + ) + }) + + it('should throw if there are entries to sign but the required signers are not provided', async () => { + const pipeline = new SorobanAuthPipeline() + const simulation = { ...MOCKED_SIMULATION_RESPONSE_SUCCESS } as SorobanRpc.Api.SimulateTransactionSuccessResponse + if (simulation.result) simulation.result.auth = [MOCKED_AUTH_ENTRY_B] + const mockedItem = { + ...MOCKED_PIPELINE_ITEM, + transaction: MOCKED_TRANSACTION, + simulation, + signers: [MOCKED_ACCOUNT_HANDLER_A], + additionalSorobanAuthToSign: [], + additionalSignedSorobanAuth: [], + } + + await expect(pipeline.execute(mockedItem)).rejects.toThrow( + PSAError.signerNotFound( + mockConveyorBeltErrorMeta(mockedItem), + MOCKED_TRANSACTION, + [MOCKED_PK_A], + MOCKED_AUTH_ENTRY_A_REQ_SIGNER, + MOCKED_AUTH_ENTRY_A + ) + ) + }) + + it('should throw error if updateTransaction fails', async () => { + const pipeline = new SorobanAuthPipeline() + const simulation = { ...MOCKED_SIMULATION_RESPONSE_SUCCESS } as SorobanRpc.Api.SimulateTransactionSuccessResponse + if (simulation.result) simulation.result.auth = [MOCKED_AUTH_ENTRY_A] + const mockedSigner = mockAccountHandler({ + accountKey: MOCKED_AUTH_ENTRY_A_REQ_SIGNER, + outputSignedAuthEntry: MOCKED_AUTH_ENTRY_A, + }) + const spyMockSign = jest.spyOn(mockedSigner, 'signSorobanAuthEntry') + const faultyTransaction = new TransactionBuilder(MOCKED_ACCOUNT_A, MOCKED_TX_OPTIONS).build() // Transaction with no operations should cause the updateTransaction to fail + const mockedItem = { + ...MOCKED_PIPELINE_ITEM, + transaction: faultyTransaction, + simulation, + signers: [mockedSigner], + } + + await expect(pipeline.execute(mockedItem)).rejects.toThrow( + PSAError.couldntUpdateTransaction( + new Error('Mocked Error'), + mockConveyorBeltErrorMeta(mockedItem), + MOCKED_TRANSACTION, + [MOCKED_AUTH_ENTRY_A] + ) + ) + expect(spyMockSign).toHaveBeenCalledOnce() + }) + + it('should throw error if simulation fails', async () => { + const pipeline = new SorobanAuthPipeline() + const simulation = { ...MOCKED_SIMULATION_RESPONSE_SUCCESS } as SorobanRpc.Api.SimulateTransactionSuccessResponse + if (simulation.result) simulation.result.auth = [MOCKED_AUTH_ENTRY_A] + const mockedSigner = mockAccountHandler({ + accountKey: MOCKED_AUTH_ENTRY_A_REQ_SIGNER, + outputSignedAuthEntry: MOCKED_AUTH_ENTRY_A, + }) + const mockedItem = { + ...MOCKED_PIPELINE_ITEM, + transaction: MOCKED_TRANSACTION, + simulation, + signers: [mockedSigner], + } + MOCKED_SIMULATE_TRANSACTION_PIPELINE.mockImplementationOnce(() => { + return { + execute: (): void => { + throw new Error('Mocked Error') + }, + } + }) + + await expect(pipeline.execute(mockedItem)).rejects.toThrow( + PSAError.couldntSimulateAuthorizedTransaction( + new Error('Mocked Error'), + mockConveyorBeltErrorMeta(mockedItem), + MOCKED_TRANSACTION, + [MOCKED_AUTH_ENTRY_A] + ) + ) + }) + }) +}) diff --git a/src/stellar-plus/test/mocks/transaction-mock.ts b/src/stellar-plus/test/mocks/transaction-mock.ts index 87ab13f..6542f77 100644 --- a/src/stellar-plus/test/mocks/transaction-mock.ts +++ b/src/stellar-plus/test/mocks/transaction-mock.ts @@ -1,3 +1,5 @@ +import { xdr } from '@stellar/stellar-sdk' + import { FeeBumpHeader, TransactionInvocation } from 'stellar-plus/types' import { AccountHandler, SignatureSchema } from '../../account/account-handler/types' @@ -43,14 +45,20 @@ export function mockSignatureSchema( } } -export function mockAccountHandler( - accountKey = mockAccount, - outputSignedTransaction?: string, +export function mockAccountHandler({ + accountKey = 'mockAccount', + outputSignedTransaction, + outputSignedAuthEntry, + signatureSchema, +}: { + accountKey?: string + outputSignedTransaction?: string + outputSignedAuthEntry?: xdr.SorobanAuthorizationEntry signatureSchema?: SignatureSchema -): jest.Mocked { +}): jest.Mocked { return { sign: jest.fn().mockReturnValue(outputSignedTransaction ?? 'success'), - signSorobanAuthEntry: jest.fn().mockResolvedValue('success'), + signSorobanAuthEntry: jest.fn().mockReturnValue(outputSignedAuthEntry ?? xdr.SorobanAuthorizationEntry), getPublicKey: jest.fn().mockReturnValue(accountKey), signatureSchema, } @@ -58,14 +66,14 @@ export function mockAccountHandler( export function mockFeeBumpHeader(signerKey = mockAccount): FeeBumpHeader { return { - signers: [mockAccountHandler(signerKey)], + signers: [mockAccountHandler({ accountKey: signerKey })], header: mockHeader(signerKey), } } export function mockTransactionInvocation(signerKey = mockAccount): TransactionInvocation { return { - signers: [mockAccountHandler(signerKey)], + signers: [mockAccountHandler({ accountKey: signerKey })], header: mockHeader(signerKey), feeBump: mockFeeBumpHeader(signerKey), } From 882cd855884c80943988330d37264f56d4a08c03 Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Fri, 5 Apr 2024 08:45:41 -0300 Subject: [PATCH 20/30] Complete unit test coverage for account handlers (#125) * test: add unit tests for the default account handler * test: validate error to sign auth entry * feat: add fee bump type to sign payload * test: refactor freighter account handler unit tests --- .../default/index.unit.test.ts | 162 ++++++++ .../freighter/freighter.test.ts | 99 ----- .../account-handler/freighter/index.ts | 5 +- .../freighter/index.unit.test.ts | 371 ++++++++++++++++++ 4 files changed, 535 insertions(+), 102 deletions(-) create mode 100644 src/stellar-plus/account/account-handler/default/index.unit.test.ts delete mode 100644 src/stellar-plus/account/account-handler/freighter/freighter.test.ts create mode 100644 src/stellar-plus/account/account-handler/freighter/index.unit.test.ts diff --git a/src/stellar-plus/account/account-handler/default/index.unit.test.ts b/src/stellar-plus/account/account-handler/default/index.unit.test.ts new file mode 100644 index 0000000..c5ec89c --- /dev/null +++ b/src/stellar-plus/account/account-handler/default/index.unit.test.ts @@ -0,0 +1,162 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { FeeBumpTransaction, Keypair, Transaction, authorizeEntry, xdr } from '@stellar/stellar-sdk' + +import { DefaultAccountHandlerClient } from 'stellar-plus/account/account-handler/default' +import { DAHError } from 'stellar-plus/account/account-handler/default/errors' +import { testnet } from 'stellar-plus/constants' + +jest.mock('@stellar/stellar-sdk', () => { + // The mock doesnt spread the whole originalModule because some internal exported objects cause failures + // so we just unmock the necessary items. + // uncomment and use the following line if you need to check the contents of the module: + // const originalModule: typeof import('@stellar/stellar-sdk') = jest.requireActual('@stellar/stellar-sdk') + const originalModule = jest.requireActual('@stellar/stellar-sdk') + return { + Horizon: originalModule.Horizon, + Keypair: originalModule.Keypair, + Transaction: originalModule.Transaction, + FeeBumpTransaction: originalModule.FeeBumpTransaction, + xdr: originalModule.xdr, + authorizeEntry: jest.fn(), + } +}) + +const MOCKED_AUTHORIZE_ENTRY = authorizeEntry as jest.Mock + +const MOCKED_SOROBAN_AUTH_ENTRY = { + credentials: jest.fn(), + rootInvocation: jest.fn(), + toXDR: jest.fn(), +} as xdr.SorobanAuthorizationEntry + +const TESTNET_CONFIG = testnet + +describe('DefaultAccountHandler', () => { + describe('Initialization', () => { + it('should initialize with just the networkConfig and generate a keypair', () => { + const dah = new DefaultAccountHandlerClient({ networkConfig: TESTNET_CONFIG }) + const spySecretKey = jest.mocked((dah as any).secretKey) + + expect(spySecretKey).toBeDefined() + expect(dah.getPublicKey()).toBe(Keypair.fromSecret(spySecretKey as string).publicKey()) + }) + + it('should initialize with a secret key', () => { + const secretKey = Keypair.random().secret() + const dah = new DefaultAccountHandlerClient({ networkConfig: TESTNET_CONFIG, secretKey }) + + expect(dah.getPublicKey()).toBe(Keypair.fromSecret(secretKey).publicKey()) + }) + + it('should sign a transaction with its secret key', () => { + const keypair = Keypair.random() + const dah = new DefaultAccountHandlerClient({ networkConfig: TESTNET_CONFIG, secretKey: keypair.secret() }) + const mockedXdrResult = 'Mocked XDR Result' + const mockedTx = { + sign: jest.fn().mockReturnValue('Signed'), + toXDR: jest.fn().mockReturnValue(mockedXdrResult), + } as unknown as Transaction + const spySign = jest.spyOn(mockedTx, 'sign') + + const signedTx = dah.sign(mockedTx) + + expect(signedTx).toBe(mockedXdrResult) + expect(spySign).toHaveBeenCalledExactlyOnceWith(keypair) + }) + + it('should sign a fee bummp transaction with its secret key', () => { + const keypair = Keypair.random() + const dah = new DefaultAccountHandlerClient({ networkConfig: TESTNET_CONFIG, secretKey: keypair.secret() }) + const mockedXdrResult = 'Mocked XDR Result' + const mockedTx = { + sign: jest.fn().mockReturnValue('Signed'), + toXDR: jest.fn().mockReturnValue(mockedXdrResult), + } as unknown as FeeBumpTransaction + const spySign = jest.spyOn(mockedTx, 'sign') + + const signedTx = dah.sign(mockedTx) + + expect(signedTx).toBe(mockedXdrResult) + expect(spySign).toHaveBeenCalledExactlyOnceWith(keypair) + }) + + it('should sign a soroban authorization entry with its secret key', async () => { + const keypair = Keypair.random() + MOCKED_AUTHORIZE_ENTRY.mockImplementationOnce(() => MOCKED_SOROBAN_AUTH_ENTRY) + const dah = new DefaultAccountHandlerClient({ networkConfig: TESTNET_CONFIG, secretKey: keypair.secret() }) + + const signedEntry = await dah.signSorobanAuthEntry( + MOCKED_SOROBAN_AUTH_ENTRY, + 123, + TESTNET_CONFIG.networkPassphrase + ) + + expect(signedEntry).toBe(MOCKED_SOROBAN_AUTH_ENTRY) + expect(MOCKED_AUTHORIZE_ENTRY).toHaveBeenCalledExactlyOnceWith( + MOCKED_SOROBAN_AUTH_ENTRY, + keypair, + 123, + TESTNET_CONFIG.networkPassphrase + ) + }) + }) + + describe('Error Handling', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should throw an error if the secret key provided in the constructor is invalid', () => { + const invalidSecretKey = 'Mocked Secret Key' + + expect(() => { + new DefaultAccountHandlerClient({ networkConfig: TESTNET_CONFIG, secretKey: invalidSecretKey }) + }).toThrow(DAHError.failedToLoadSecretKeyError(new Error('Mocked error'))) + }) + + it('should throw an error if the public key cannot be derived from the current secret key', () => { + const invalidSecret = 'Mocked Secret Key' + const dah = new DefaultAccountHandlerClient({ networkConfig: TESTNET_CONFIG }) + jest.replaceProperty(dah as any, 'secretKey', invalidSecret) + + expect(dah.getPublicKey).toThrow(DAHError.failedToLoadSecretKeyError(new Error('Mocked error'))) + }) + + it('should throw an error if the transaction cannot be signed', () => { + const keypair = Keypair.random() + const dah = new DefaultAccountHandlerClient({ networkConfig: TESTNET_CONFIG, secretKey: keypair.secret() }) + const mockedTx = { + sign: jest.fn().mockImplementationOnce(() => { + throw new Error('Mocked error') + }), + toXDR: jest.fn(), + } as unknown as Transaction + const spySign = jest.spyOn(mockedTx, 'sign') + + expect(() => { + dah.sign(mockedTx) + }).toThrow(DAHError.failedToSignTransactionError(new Error('Mocked error'))) + expect(spySign).toHaveBeenCalledExactlyOnceWith(keypair) + }) + + it('should throw an error if the authorizeEntry cannot be signed', async () => { + const keypair = Keypair.random() + MOCKED_AUTHORIZE_ENTRY.mockImplementationOnce(() => { + throw new Error('Mocked error') + }) + const dah = new DefaultAccountHandlerClient({ networkConfig: TESTNET_CONFIG, secretKey: keypair.secret() }) + + await expect( + dah.signSorobanAuthEntry(MOCKED_SOROBAN_AUTH_ENTRY, 123, TESTNET_CONFIG.networkPassphrase) + ).rejects.toThrow( + DAHError.failedToSignAuthorizationEntryError( + new Error('Mocked error'), + 'mocked auth entry xdr', + 123, + TESTNET_CONFIG.networkPassphrase + ) + ) + }) + }) +}) diff --git a/src/stellar-plus/account/account-handler/freighter/freighter.test.ts b/src/stellar-plus/account/account-handler/freighter/freighter.test.ts deleted file mode 100644 index 71e5a1c..0000000 --- a/src/stellar-plus/account/account-handler/freighter/freighter.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import * as freighterApi from '@stellar/freighter-api' - -import { FreighterAccountHandlerClient } from 'stellar-plus/account/account-handler/freighter' -import { testnet } from 'stellar-plus/constants' -import { - mockSignedClassicTransactionXdr, - mockUnsignedClassicTransaction, -} from 'stellar-plus/test/mocks/classic-transaction' -import { NetworkConfig } from 'stellar-plus/types' - -jest.mock('@stellar/freighter-api', () => ({ - getPublicKey: jest.fn(), - isConnected: jest.fn(), - isAllowed: jest.fn(), - setAllowed: jest.fn(), - signTransaction: jest.fn(), - getNetworkDetails: jest.fn(), -})) - -const mockPublicKey = 'GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' -const mockNetwork = testnet as NetworkConfig - -const mockFreighterGetPublicKey = (): void => { - ;(freighterApi.getPublicKey as jest.MockedFunction).mockResolvedValue(mockPublicKey) -} - -const mockFreighterIsConnected = (): void => { - ;(freighterApi.isConnected as jest.MockedFunction).mockResolvedValue(true) -} - -const mockFreighterIsAllowed = (): void => { - ;(freighterApi.isAllowed as jest.MockedFunction).mockResolvedValue(true) -} -const mockFreighterGetNetworkDetailsTestnet = (): void => { - ;(freighterApi.getNetworkDetails as jest.Mock).mockResolvedValue({ networkPassphrase: testnet.networkPassphrase }) -} - -const mockFreighterGetNetworkDetailsWrongNetwork = (): void => { - ;(freighterApi.getNetworkDetails as jest.Mock).mockResolvedValue({ networkPassphrase: 'wrong network' }) -} - -const mockSuccessfullyConnectedFreighter = (): void => { - mockFreighterIsAllowed() - mockFreighterIsConnected() - mockFreighterGetNetworkDetailsTestnet() - mockFreighterGetPublicKey() -} - -describe('FreighterAccountHandlerClient', () => { - let client: FreighterAccountHandlerClient - - beforeEach(() => { - client = new FreighterAccountHandlerClient({ networkConfig: mockNetwork }) - }) - - it('should initialize with provided network', () => { - expect(client).toBeDefined() - }) - - it('should directly load and set the public key', async () => { - mockSuccessfullyConnectedFreighter() - await client.loadPublicKey() - expect(client.getPublicKey()).toBe(mockPublicKey) - }) - - it('should directly load and set the public key when connecting', async () => { - mockSuccessfullyConnectedFreighter() - await client.connect() - expect(client.getPublicKey()).toBe(mockPublicKey) - }) - - it('should connect to Freighter', async () => { - await expect(client.connect()).resolves.not.toThrow() - }) - - it('should disconnect', async () => { - mockSuccessfullyConnectedFreighter() - await client.disconnect() - expect(client.getPublicKey()).toBe('') - }) - - it('should sign a transaction', async () => { - const mockTransaction = mockUnsignedClassicTransaction - ;(freighterApi.signTransaction as jest.MockedFunction).mockResolvedValue( - mockSignedClassicTransactionXdr - ) - await expect(client.sign(mockTransaction)).resolves.toEqual(mockSignedClassicTransactionXdr) - }) - - it('should validate network', async () => { - mockFreighterGetNetworkDetailsTestnet - await expect(client.isNetworkCorrect()).resolves.toBeTruthy() - }) - - it('should validate wrong network', async () => { - mockFreighterGetNetworkDetailsWrongNetwork() - await expect(client.isNetworkCorrect()).rejects.toThrow() - }) -}) diff --git a/src/stellar-plus/account/account-handler/freighter/index.ts b/src/stellar-plus/account/account-handler/freighter/index.ts index 843c1ee..f9ce455 100644 --- a/src/stellar-plus/account/account-handler/freighter/index.ts +++ b/src/stellar-plus/account/account-handler/freighter/index.ts @@ -7,7 +7,7 @@ import { signAuthEntry, signTransaction, } from '@stellar/freighter-api' -import { Transaction, xdr } from '@stellar/stellar-sdk' +import { FeeBumpTransaction, Transaction, xdr } from '@stellar/stellar-sdk' import { FreighterAccHandlerPayload, @@ -103,7 +103,7 @@ export class FreighterAccountHandlerClient extends AccountBaseClient implements * @description - Sign a transaction with Freighter and return the signed transaction. If signerpublicKey is provided, it will be used to specifically request Freighter to sign with that account. * */ - public async sign(tx: Transaction): Promise { + public async sign(tx: Transaction | FeeBumpTransaction): Promise { const isFreighterConnected = await this.isFreighterConnected(true) if (isFreighterConnected) { @@ -226,7 +226,6 @@ export class FreighterAccountHandlerClient extends AccountBaseClient implements */ public async isNetworkCorrect(): Promise { const networkDetails = await getNetworkDetails() - if (networkDetails.networkPassphrase !== this.networkConfig.networkPassphrase) { throw FAHError.connectedToWrongNetworkError(this.networkConfig.name) } diff --git a/src/stellar-plus/account/account-handler/freighter/index.unit.test.ts b/src/stellar-plus/account/account-handler/freighter/index.unit.test.ts new file mode 100644 index 0000000..d15ba07 --- /dev/null +++ b/src/stellar-plus/account/account-handler/freighter/index.unit.test.ts @@ -0,0 +1,371 @@ +import * as freighterApi from '@stellar/freighter-api' +import { FeeBumpTransaction, Transaction, xdr } from '@stellar/stellar-sdk' + +import { FreighterAccountHandlerClient } from 'stellar-plus/account/account-handler/freighter' +import { FAHError } from 'stellar-plus/account/account-handler/freighter/errors' +import { testnet } from 'stellar-plus/constants' + +jest.mock('@stellar/freighter-api', () => ({ + getPublicKey: jest.fn(), + isConnected: jest.fn(), + isAllowed: jest.fn(), + setAllowed: jest.fn().mockResolvedValue(true), + signTransaction: jest.fn(), + getNetworkDetails: jest.fn(), + signAuthEntry: jest.fn(), +})) + +const MOCKED_GET_PUBLIC_KEY = freighterApi.getPublicKey as jest.Mock +const MOCKED_IS_CONNECTED = freighterApi.isConnected as jest.Mock +const MOCKED_IS_ALLOWED = freighterApi.isAllowed as jest.Mock +const MOCKED_SET_ALLOWED = freighterApi.setAllowed as jest.Mock +const MOCKED_SIGN_TRANSACTION = freighterApi.signTransaction as jest.Mock +const MOCKED_GET_NETWORK_DETAILS = freighterApi.getNetworkDetails as jest.Mock +const MOCKED_SIGN_AUTH_ENTRY = freighterApi.signAuthEntry as jest.Mock + +const mockFreighterIsInstalled = (status: boolean): void => { + MOCKED_IS_CONNECTED.mockResolvedValue(status) +} +const mockFreighterIsAllowed = (status: boolean): void => { + MOCKED_IS_ALLOWED.mockResolvedValue(status) +} + +const mockFreighterGetPublicKey = (pk: string): void => { + MOCKED_GET_PUBLIC_KEY.mockResolvedValue(pk) +} + +const mockFreighterGetNetworkDetailsTestnet = (): void => { + MOCKED_GET_NETWORK_DETAILS.mockResolvedValue({ networkPassphrase: testnet.networkPassphrase }) +} + +const mockFreighterGetNetworkDetailsWrongNetwork = (): void => { + MOCKED_GET_NETWORK_DETAILS.mockResolvedValue({ networkPassphrase: 'wrong network' }) +} + +const TESTNET_CONFIG = testnet + +const MOCKED_PK = 'GAUFIAL2LV2OV7EA4NTXZDVPQASGI5Y3EXZV2HQS3UUWMZ7UWJDQURYS' + +describe('FreighterAccountHandlerClient', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + describe('Initialization', () => { + it('should initialize with the networkConfig', () => { + const fah = new FreighterAccountHandlerClient({ networkConfig: TESTNET_CONFIG }) + + expect(fah).toBeDefined() + }) + }) + + describe('Connect', () => { + it('should load and set the public key when connected', async () => { + mockFreighterIsInstalled(true) + mockFreighterIsAllowed(true) + mockFreighterGetNetworkDetailsTestnet() + mockFreighterGetPublicKey(MOCKED_PK) + const fah = new FreighterAccountHandlerClient({ networkConfig: TESTNET_CONFIG }) + + await fah.connect() + + expect(MOCKED_IS_CONNECTED).toHaveBeenCalled() + expect(MOCKED_IS_ALLOWED).toHaveBeenCalled() + expect(MOCKED_GET_PUBLIC_KEY).toHaveBeenCalled() + expect(MOCKED_GET_NETWORK_DETAILS).toHaveBeenCalled() + expect(fah.getPublicKey()).toBe(MOCKED_PK) + }) + + it('should trigger permission if extension is not allowed', async () => { + mockFreighterIsInstalled(true) + mockFreighterIsAllowed(false) + mockFreighterGetNetworkDetailsTestnet() + const fah = new FreighterAccountHandlerClient({ networkConfig: TESTNET_CONFIG }) + + await fah.connect() + + expect(MOCKED_IS_CONNECTED).toHaveBeenCalled() + expect(MOCKED_SET_ALLOWED).toHaveBeenCalled() + expect(MOCKED_GET_PUBLIC_KEY).not.toHaveBeenCalled() + expect(MOCKED_GET_NETWORK_DETAILS).not.toHaveBeenCalled() + expect(fah.getPublicKey()).toBe('') + }) + + it('should trigger permission if extension is not allowed then load the public key and call the provided callback', async () => { + mockFreighterIsInstalled(true) + mockFreighterGetPublicKey(MOCKED_PK) + mockFreighterGetNetworkDetailsTestnet() + MOCKED_IS_ALLOWED.mockResolvedValue(true).mockResolvedValueOnce(false) // will return false once then only true + const fah = new FreighterAccountHandlerClient({ networkConfig: TESTNET_CONFIG }) + const mockedCallBack = jest.fn().mockImplementationOnce((pk: string) => { + // Necessary to break AAA here to ensure assertion only when callback is called + expect(MOCKED_GET_PUBLIC_KEY).toHaveBeenCalled() + expect(pk).toBe(MOCKED_PK) + expect(mockedCallBack).toHaveBeenCalledExactlyOnceWith(MOCKED_PK) + expect(MOCKED_IS_CONNECTED).toHaveBeenCalledTimes(2) + expect(MOCKED_IS_ALLOWED).toHaveBeenCalledTimes(2) + expect(MOCKED_SET_ALLOWED).toHaveBeenCalledOnce() + }) + + await fah.connect(mockedCallBack) + }) + + it('should not load public key if extension is allowed to the wrong network', async () => { + mockFreighterIsInstalled(true) + mockFreighterGetPublicKey(MOCKED_PK) + mockFreighterGetNetworkDetailsWrongNetwork() + mockFreighterIsAllowed(true) + const fah = new FreighterAccountHandlerClient({ networkConfig: TESTNET_CONFIG }) + + await fah.connect() + + expect(MOCKED_IS_CONNECTED).toHaveBeenCalledOnce() + expect(MOCKED_IS_ALLOWED).toHaveBeenCalledOnce() + expect(MOCKED_GET_NETWORK_DETAILS).toHaveBeenCalledOnce() + expect(MOCKED_GET_PUBLIC_KEY).not.toHaveBeenCalled() + expect(fah.getPublicKey()).toBe('') + }) + + describe('Disconnect', () => { + it('should reset the publick key', () => { + const fah = new FreighterAccountHandlerClient({ networkConfig: TESTNET_CONFIG }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + jest.replaceProperty(fah as any, 'publicKey', MOCKED_PK) + + fah.disconnect() + + expect(fah.getPublicKey()).toBe('') + }) + }) + }) + + describe('Load Public Key', () => { + it('should not load the public key if freighter is not installed', async () => { + mockFreighterIsInstalled(false) + const fah = new FreighterAccountHandlerClient({ networkConfig: TESTNET_CONFIG }) + + await fah.loadPublicKey() + + expect(MOCKED_SET_ALLOWED).not.toHaveBeenCalled() + expect(MOCKED_GET_PUBLIC_KEY).not.toHaveBeenCalled() + expect(fah.getPublicKey()).toBe('') + }) + + it('should not load the public key if freighter is installed but not allowed', async () => { + mockFreighterIsInstalled(true) + mockFreighterIsAllowed(false) + const fah = new FreighterAccountHandlerClient({ networkConfig: TESTNET_CONFIG }) + + await fah.loadPublicKey() + + expect(MOCKED_SET_ALLOWED).not.toHaveBeenCalled() + expect(MOCKED_GET_PUBLIC_KEY).not.toHaveBeenCalled() + expect(fah.getPublicKey()).toBe('') + }) + + it('should not load the public key if freighter is connected to the wrong network', async () => { + mockFreighterIsInstalled(true) + mockFreighterIsAllowed(true) + mockFreighterGetNetworkDetailsWrongNetwork() + const fah = new FreighterAccountHandlerClient({ networkConfig: TESTNET_CONFIG }) + + await fah.loadPublicKey() + + expect(MOCKED_SET_ALLOWED).not.toHaveBeenCalled() + expect(MOCKED_GET_PUBLIC_KEY).not.toHaveBeenCalled() + expect(fah.getPublicKey()).toBe('') + }) + + it('should load the public key if freighter is installed and allowed', async () => { + mockFreighterIsInstalled(true) + mockFreighterIsAllowed(true) + mockFreighterGetNetworkDetailsTestnet() + mockFreighterGetPublicKey(MOCKED_PK) + const fah = new FreighterAccountHandlerClient({ networkConfig: TESTNET_CONFIG }) + + await fah.loadPublicKey() + + expect(MOCKED_GET_PUBLIC_KEY).toHaveBeenCalled() + expect(fah.getPublicKey()).toBe(MOCKED_PK) + }) + + it('should trigger permission when enforceConnection is true and then load the public key and trigger the callback', async () => { + mockFreighterIsInstalled(true) + mockFreighterGetPublicKey(MOCKED_PK) + mockFreighterGetNetworkDetailsTestnet() + MOCKED_IS_ALLOWED.mockResolvedValue(true).mockResolvedValueOnce(false) // will return false once then only true + const fah = new FreighterAccountHandlerClient({ networkConfig: TESTNET_CONFIG }) + const mockedCallBack = jest.fn().mockImplementationOnce((pk: string) => { + // Necessary to break AAA here to ensure assertion only when callback is called + expect(MOCKED_GET_PUBLIC_KEY).toHaveBeenCalled() + expect(pk).toBe(MOCKED_PK) + expect(mockedCallBack).toHaveBeenCalledExactlyOnceWith(MOCKED_PK) + expect(MOCKED_IS_CONNECTED).toHaveBeenCalledTimes(2) + expect(MOCKED_IS_ALLOWED).toHaveBeenCalledTimes(2) + expect(MOCKED_SET_ALLOWED).toHaveBeenCalledOnce() + }) + + await fah.loadPublicKey(mockedCallBack, true) + }) + }) + + describe('Core signing features', () => { + it('should sign a transaction with freighter', async () => { + mockFreighterIsInstalled(true) + mockFreighterIsAllowed(true) + mockFreighterGetPublicKey(MOCKED_PK) + mockFreighterGetNetworkDetailsTestnet() + MOCKED_SIGN_TRANSACTION.mockResolvedValue('signedTx') + const mockedTx = { + toXDR: jest.fn().mockReturnValue('mocked xdr'), + } as unknown as Transaction + const fah = new FreighterAccountHandlerClient({ networkConfig: TESTNET_CONFIG }) + await fah.loadPublicKey() + + const signedTx = await fah.sign(mockedTx) + + expect(mockedTx.toXDR).toHaveBeenCalledOnce() + expect(MOCKED_SIGN_TRANSACTION).toHaveBeenCalledExactlyOnceWith('mocked xdr', { + networkPassphrase: TESTNET_CONFIG.networkPassphrase, + accountToSign: MOCKED_PK, + }) + expect(signedTx).toBe('signedTx') + }) + + it('should sign a fee bump transaction with freighter', async () => { + mockFreighterIsInstalled(true) + mockFreighterIsAllowed(true) + mockFreighterGetPublicKey(MOCKED_PK) + mockFreighterGetNetworkDetailsTestnet() + MOCKED_SIGN_TRANSACTION.mockResolvedValue('signedTx') + const mockedTx = { + toXDR: jest.fn().mockReturnValue('mocked xdr'), + } as unknown as FeeBumpTransaction + const fah = new FreighterAccountHandlerClient({ networkConfig: TESTNET_CONFIG }) + await fah.loadPublicKey() + + const signedTx = await fah.sign(mockedTx) + + expect(mockedTx.toXDR).toHaveBeenCalledOnce() + expect(MOCKED_SIGN_TRANSACTION).toHaveBeenCalledExactlyOnceWith('mocked xdr', { + networkPassphrase: TESTNET_CONFIG.networkPassphrase, + accountToSign: MOCKED_PK, + }) + expect(signedTx).toBe('signedTx') + }) + + it('should sign a soroban authorization entry with freighter', async () => { + mockFreighterIsInstalled(true) + mockFreighterIsAllowed(true) + mockFreighterGetPublicKey(MOCKED_PK) + mockFreighterGetNetworkDetailsTestnet() + MOCKED_SIGN_AUTH_ENTRY.mockResolvedValue('signedAuthEntry') + const mockedAuthEntry = { + credentials: jest.fn(), + rootInvocation: jest.fn(), + toXDR: jest.fn().mockReturnValue('mocked xdr'), + } as xdr.SorobanAuthorizationEntry + const spyXdr = jest.spyOn(xdr.SorobanAuthorizationEntry, 'fromXDR').mockImplementationOnce(() => mockedAuthEntry) + const fah = new FreighterAccountHandlerClient({ networkConfig: TESTNET_CONFIG }) + await fah.loadPublicKey() + + const signedAuthEntry = await fah.signSorobanAuthEntry(mockedAuthEntry, 0, TESTNET_CONFIG.networkPassphrase) + + expect(mockedAuthEntry.toXDR).toHaveBeenCalledOnce() + expect(MOCKED_SIGN_AUTH_ENTRY).toHaveBeenCalledExactlyOnceWith('mocked xdr', { + accountToSign: MOCKED_PK, + }) + expect(spyXdr).toHaveBeenCalledExactlyOnceWith('signedAuthEntry', 'base64') + expect(signedAuthEntry).toBe(mockedAuthEntry) + }) + }) + + describe('Errors', () => { + const MOCKED_ERROR = new Error('mocked error') + + it('should throw when failed to load public key', async () => { + mockFreighterIsInstalled(true) + mockFreighterIsAllowed(true) + mockFreighterGetNetworkDetailsTestnet() + MOCKED_GET_PUBLIC_KEY.mockRejectedValue(MOCKED_ERROR) + const fah = new FreighterAccountHandlerClient({ networkConfig: TESTNET_CONFIG }) + + await expect(fah.loadPublicKey()).rejects.toThrow(FAHError.failedToLoadPublicKeyError(MOCKED_ERROR)) + }) + + it('should throw when failed to sign transaction', async () => { + mockFreighterIsInstalled(true) + mockFreighterIsAllowed(true) + mockFreighterGetPublicKey(MOCKED_PK) + mockFreighterGetNetworkDetailsTestnet() + const mockedTx = { + toXDR: jest.fn().mockReturnValue('mocked xdr'), + } as unknown as Transaction + MOCKED_SIGN_TRANSACTION.mockRejectedValue(MOCKED_ERROR) + const fah = new FreighterAccountHandlerClient({ networkConfig: TESTNET_CONFIG }) + await fah.loadPublicKey() + + await expect(fah.sign(mockedTx)).rejects.toThrow(FAHError.failedToSignTransactionError(MOCKED_ERROR)) + }) + + it('should throw when trying to sign a transaction with Freighter when it is not connected', async () => { + mockFreighterIsInstalled(false) + const mockedTx = { + toXDR: jest.fn().mockReturnValue('mocked xdr'), + } as unknown as Transaction + const fah = new FreighterAccountHandlerClient({ networkConfig: TESTNET_CONFIG }) + + await expect(fah.sign(mockedTx)).rejects.toThrow(FAHError.freighterIsNotConnectedError()) + }) + + it('should throw when failed to sign a soroban authorization entry', async () => { + mockFreighterIsInstalled(true) + mockFreighterIsAllowed(true) + mockFreighterGetPublicKey(MOCKED_PK) + mockFreighterGetNetworkDetailsTestnet() + const mockedAuthEntry = { + credentials: jest.fn(), + rootInvocation: jest.fn(), + toXDR: jest.fn().mockReturnValue('mocked xdr'), + } as xdr.SorobanAuthorizationEntry + MOCKED_SIGN_AUTH_ENTRY.mockRejectedValue(MOCKED_ERROR) + const fah = new FreighterAccountHandlerClient({ networkConfig: TESTNET_CONFIG }) + await fah.loadPublicKey() + + await expect(fah.signSorobanAuthEntry(mockedAuthEntry, 0, TESTNET_CONFIG.networkPassphrase)).rejects.toThrow( + FAHError.failedToSignAuthEntryError(MOCKED_ERROR) + ) + }) + + it('should throw when trying to sign a soroban authorization entry with Freighter when it is not connected', async () => { + mockFreighterIsInstalled(false) + const mockedAuthEntry = { + credentials: jest.fn(), + rootInvocation: jest.fn(), + toXDR: jest.fn().mockReturnValue('mocked xdr'), + } as xdr.SorobanAuthorizationEntry + const fah = new FreighterAccountHandlerClient({ networkConfig: TESTNET_CONFIG }) + + await expect(fah.signSorobanAuthEntry(mockedAuthEntry, 0, TESTNET_CONFIG.networkPassphrase)).rejects.toThrow( + FAHError.freighterIsNotConnectedError() + ) + }) + + it('should throw when trying to sign a soroban authorization entry with Freighter when it is connected to a different network than the one requested to sign for', async () => { + mockFreighterIsInstalled(true) + mockFreighterIsAllowed(true) + mockFreighterGetPublicKey(MOCKED_PK) + mockFreighterGetNetworkDetailsTestnet() + const mockedAuthEntry = { + credentials: jest.fn(), + rootInvocation: jest.fn(), + toXDR: jest.fn().mockReturnValue('mocked xdr'), + } as xdr.SorobanAuthorizationEntry + const fah = new FreighterAccountHandlerClient({ networkConfig: TESTNET_CONFIG }) + await fah.loadPublicKey() + + await expect(fah.signSorobanAuthEntry(mockedAuthEntry, 0, 'wrong network')).rejects.toThrow( + FAHError.cannotSignForThisNetwork('wrong network', TESTNET_CONFIG.networkPassphrase) + ) + }) + }) +}) From 10ace5d8ba3bd760dd15e9174b72f14f4586eca1 Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Fri, 5 Apr 2024 10:28:31 -0300 Subject: [PATCH 21/30] Create test-coverage.yml action (#126) * Create test-coverage.yml action * Update test-coverage.yml --- .github/workflows/test-coverage.yml | 51 +++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/test-coverage.yml diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml new file mode 100644 index 0000000..fd865a5 --- /dev/null +++ b/.github/workflows/test-coverage.yml @@ -0,0 +1,51 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Node.js CI + +on: + workflow_dispatch: + workflow_call: + push: + branches: [ "develop", "main" ] + pull_request: + branches: [ "develop", "main" ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 16 + cache: 'npm' + cache-dependency-path: './package-lock.json' + + - name: Install Dependencies + run: npm ci + + - name: Run tests with coverage + run: npm test + + - name: Code Coverage Report + uses: irongut/CodeCoverageSummary@v1.3.0 + with: + filename: ./src/coverage/cobertura-coverage.xml + fail_below_min: false + format: markdown + hide_branch_rate: false + hide_complexity: true + indicators: true + output: both + thresholds: '60 80' + + - name: Add Coverage PR Comment + uses: marocchino/sticky-pull-request-comment@v2 + if: github.event_name == 'pull_request' + with: + recreate: true + path: code-coverage-results.md From 89405b95acd54abdfde4b6e1eec4fc32001f8ee9 Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Fri, 5 Apr 2024 17:06:23 -0300 Subject: [PATCH 22/30] Add unit test for base account handler (#127) * test: add unit tests for the default account handler * test: validate error to sign auth entry * feat: add fee bump type to sign payload * test: refactor freighter account handler unit tests * test: add unit tests for base account handler --- .../account/base/index.unit.test.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/stellar-plus/account/base/index.unit.test.ts diff --git a/src/stellar-plus/account/base/index.unit.test.ts b/src/stellar-plus/account/base/index.unit.test.ts new file mode 100644 index 0000000..de0aab0 --- /dev/null +++ b/src/stellar-plus/account/base/index.unit.test.ts @@ -0,0 +1,37 @@ +import { AccountBaseClient } from 'stellar-plus/account/base' +import { testnet } from 'stellar-plus/constants' + +const TESTNET_CONFIG = testnet + +const MOCKED_PK = 'GAUFIAL2LV2OV7EA4NTXZDVPQASGI5Y3EXZV2HQS3UUWMZ7UWJDQURYS' + +describe('Base Account Handler', () => { + it('should initialize the base account handler with a public key', () => { + const account = new AccountBaseClient({ publicKey: MOCKED_PK }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment + const spyPublicKey = jest.mocked((account as any).publicKey) + + expect(account).toBeDefined() + expect(account).toBeInstanceOf(AccountBaseClient) + expect(spyPublicKey).toBe(MOCKED_PK) + }) + + it('should initialize the base account handler with a network config, enabling helpers', () => { + const account = new AccountBaseClient({ publicKey: MOCKED_PK, networkConfig: TESTNET_CONFIG }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment + const spyAccountDataViewer = jest.mocked((account as any).accountDataViewer) + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment + const spyFriendbot = jest.mocked((account as any).friendbot) + + expect(account).toBeDefined() + expect(account).toBeInstanceOf(AccountBaseClient) + expect(spyAccountDataViewer).toBeDefined() + expect(spyFriendbot).toBeDefined() + }) + + it('should return the public key of the account', () => { + const account = new AccountBaseClient({ publicKey: MOCKED_PK }) + + expect(account.getPublicKey()).toBe(MOCKED_PK) + }) +}) From 4ab013bed4c1d52fe5dc0a87089bb71f994eb0b9 Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Fri, 5 Apr 2024 17:07:18 -0300 Subject: [PATCH 23/30] Add unit test for soroban get transaction pipeline (#124) * refactor: soroban get transaction pipeline constructor * fix: remove old log * test: add unit tests for soroban get transaction pipeline * style: adjust imports --- .../soroban-get-transaction/index.ts | 9 +- .../index.unit.test.ts | 280 ++++++++++++++++++ .../soroban-get-transaction/types.ts | 5 + .../pipelines/soroban-transaction/index.ts | 4 +- .../soroban-transaction/index.unit.test.ts | 9 +- .../error/helpers/result-meta-xdr.ts | 16 +- 6 files changed, 305 insertions(+), 18 deletions(-) create mode 100644 src/stellar-plus/core/pipelines/soroban-get-transaction/index.unit.test.ts diff --git a/src/stellar-plus/core/pipelines/soroban-get-transaction/index.ts b/src/stellar-plus/core/pipelines/soroban-get-transaction/index.ts index c482ddf..da500b9 100644 --- a/src/stellar-plus/core/pipelines/soroban-get-transaction/index.ts +++ b/src/stellar-plus/core/pipelines/soroban-get-transaction/index.ts @@ -6,9 +6,9 @@ import { ConveyorBelt } from 'stellar-plus/utils/pipeline/conveyor-belts' import { SGTError } from './errors' import { SorobanGetTransactionOptions, + SorobanGetTransactionPipelineConstructor, SorobanGetTransactionPipelineInput, SorobanGetTransactionPipelineOutput, - SorobanGetTransactionPipelinePlugin, SorobanGetTransactionPipelineType, } from './types' @@ -19,15 +19,12 @@ export class SorobanGetTransactionPipeline extends ConveyorBelt< > { protected options: SorobanGetTransactionOptions - constructor( - plugins?: SorobanGetTransactionPipelinePlugin[], - options: SorobanGetTransactionOptions = { defaultSecondsToWait: 30, useEnvelopeTimeout: true } - ) { + constructor({ plugins, options }: SorobanGetTransactionPipelineConstructor = {}) { super({ type: SorobanGetTransactionPipelineType.id, plugins: plugins || [], }) - this.options = options + this.options = options || { defaultSecondsToWait: 30, useEnvelopeTimeout: true } } // Waits for the given transaction to be processed by the Soroban server. diff --git a/src/stellar-plus/core/pipelines/soroban-get-transaction/index.unit.test.ts b/src/stellar-plus/core/pipelines/soroban-get-transaction/index.unit.test.ts new file mode 100644 index 0000000..22426cb --- /dev/null +++ b/src/stellar-plus/core/pipelines/soroban-get-transaction/index.unit.test.ts @@ -0,0 +1,280 @@ +import { Account, SorobanRpc, TransactionBuilder, xdr } from '@stellar/stellar-sdk' + +import { Constants } from 'stellar-plus' +import { SorobanGetTransactionPipeline } from 'stellar-plus/core/pipelines/soroban-get-transaction' +import { SGTError } from 'stellar-plus/core/pipelines/soroban-get-transaction/errors' +import { + SorobanGetTransactionOptions, + SorobanGetTransactionPipelineInput, + SorobanGetTransactionPipelinePlugin, + SorobanGetTransactionPipelineType, +} from 'stellar-plus/core/pipelines/soroban-get-transaction/types' +import { ConveyorBeltErrorMeta } from 'stellar-plus/error/helpers/conveyor-belt' +import { DefaultRpcHandler } from 'stellar-plus/rpc/default-handler' +import { BeltMetadata } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +jest.mock('stellar-plus/rpc/default-handler', () => ({ + DefaultRpcHandler: jest.fn().mockImplementation(() => ({ + getTransaction: jest.fn(), + })), +})) + +const MOCKED_RPC_HANDLER = DefaultRpcHandler as jest.Mock + +const TESTNET_CONFIG = Constants.testnet +const MOCKED_PK_A = 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI' +const MOCKED_ACCOUNT_A = new Account(MOCKED_PK_A, '100') + +const MOCKED_TX_OPTIONS: TransactionBuilder.TransactionBuilderOptions = { + fee: '100', + networkPassphrase: TESTNET_CONFIG.networkPassphrase, + timebounds: { + minTime: 0, + maxTime: 0, + }, +} + +const MOCKED_SOROBAN_SUBMISSION = { + status: 'PENDING', + hash: 'mocked-hash', + latestLedger: 0, + latestLedgerCloseTime: 0, +} as SorobanRpc.Api.SendTransactionResponse + +const MOCKED_SUCCESSFUL_RESPONSE = { + status: SorobanRpc.Api.GetTransactionStatus.SUCCESS, +} as SorobanRpc.Api.GetSuccessfulTransactionResponse + +const MOCKED_FAILED_RESPONSE = { + status: SorobanRpc.Api.GetTransactionStatus.FAILED, + resultXdr: xdr.TransactionResult.fromXDR('AAAAAAAAAGT////4AAAAAA==', 'base64'), + resultMetaXdr: xdr.TransactionMeta.fromXDR( + 'AAAAAwAAAAAAAAACAAAAAwAOgggAAAAAAAAAAFBASCSXn/T00voyqd7Oqs2WBynZaF3xrCVh+ffvMXR7AAAAF0h255wADoHgAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAQAOgggAAAAAAAAAAFBASCSXn/T00voyqd7Oqs2WBynZaF3xrCVh+ffvMXR7AAAAF0h255wADoHgAAAAAQAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAAA6CCAAAAABmDqzhAAAAAAAAAAEAAAAAAAAAAAAAAAA=', + 'base64' + ), +} as SorobanRpc.Api.GetFailedTransactionResponse + +const MOCKED_MISSING_RESPONSE = { + status: SorobanRpc.Api.GetTransactionStatus.NOT_FOUND, +} as SorobanRpc.Api.GetMissingTransactionResponse + +const mockConveyorBeltErrorMeta = ( + item: SorobanGetTransactionPipelineInput +): ConveyorBeltErrorMeta => { + return { + item, + meta: { + itemId: 'mocked-id', + beltId: 'mocked-belt-id', + beltType: SorobanGetTransactionPipelineType.id, + }, + } as ConveyorBeltErrorMeta +} + +describe('SorobanGetTransaction', () => { + describe('Initialize', () => { + it('should initialize the pipeline', async () => { + const pipeline = new SorobanGetTransactionPipeline() + + expect(pipeline).toBeDefined() + }) + + it('should initialize the pipeline with the default options', async () => { + const pipeline = new SorobanGetTransactionPipeline() + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment + const mockedOptions = jest.mocked((pipeline as any).options) + + expect(mockedOptions).toBeDefined() + expect((mockedOptions as unknown as SorobanGetTransactionOptions).defaultSecondsToWait).toBe(30) + expect((mockedOptions as unknown as SorobanGetTransactionOptions).useEnvelopeTimeout).toBe(true) + }) + + it('should initialize the pipeline with the given options', async () => { + const customOptions = { + defaultSecondsToWait: 10, + useEnvelopeTimeout: false, + } + const pipeline = new SorobanGetTransactionPipeline({ options: customOptions }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment + const spyOptions = jest.mocked((pipeline as any).options) + + expect(spyOptions).toBeDefined() + expect((spyOptions as unknown as SorobanGetTransactionOptions).defaultSecondsToWait).toBe( + customOptions.defaultSecondsToWait + ) + expect((spyOptions as unknown as SorobanGetTransactionOptions).useEnvelopeTimeout).toBe( + customOptions.useEnvelopeTimeout + ) + }) + + it('should initialize the pipeline with the given plugins', async () => { + const mockedPlugin = jest.fn().mockImplementation(() => ({ + preProcess: jest.fn(), + type: SorobanGetTransactionPipelineType.id, + })) as unknown as SorobanGetTransactionPipelinePlugin + const customPlugins = [mockedPlugin] + const pipeline = new SorobanGetTransactionPipeline({ plugins: customPlugins }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment + const spyPlugins = jest.mocked((pipeline as any).plugins) + + expect(spyPlugins).toBeDefined() + expect(spyPlugins).toBe(customPlugins) + }) + }) + + describe('Core functionalities', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should process the transaction and return if successful', async () => { + MOCKED_RPC_HANDLER.mockImplementationOnce(() => ({ + getTransaction: jest.fn().mockResolvedValue(MOCKED_SUCCESSFUL_RESPONSE), + })) + const mockedRpcHandler = new DefaultRpcHandler(TESTNET_CONFIG) + const mockedTransactionEnvelope = new TransactionBuilder(MOCKED_ACCOUNT_A, MOCKED_TX_OPTIONS).build() + const mockedItem = { + rpcHandler: mockedRpcHandler, + sorobanSubmission: MOCKED_SOROBAN_SUBMISSION, + transactionEnvelope: mockedTransactionEnvelope, + } + const pipeline = new SorobanGetTransactionPipeline() + const spyGetTransaction = jest.spyOn(mockedRpcHandler, 'getTransaction') + + const response = await pipeline.execute(mockedItem) + + expect(response).toBeDefined() + expect(response).toEqual({ response: MOCKED_SUCCESSFUL_RESPONSE }) + expect(spyGetTransaction).toHaveBeenCalledTimes(1) + expect(spyGetTransaction).toHaveBeenCalledWith(MOCKED_SOROBAN_SUBMISSION.hash) + }) + + it('should process the transaction and continue trying if missing', async () => { + MOCKED_RPC_HANDLER.mockImplementationOnce(() => ({ + getTransaction: jest + .fn() + .mockResolvedValue(MOCKED_SUCCESSFUL_RESPONSE) + .mockResolvedValueOnce(MOCKED_MISSING_RESPONSE) + .mockResolvedValueOnce(MOCKED_MISSING_RESPONSE), + })) + const mockedRpcHandler = new DefaultRpcHandler(TESTNET_CONFIG) + const mockedTransactionEnvelope = new TransactionBuilder(MOCKED_ACCOUNT_A, MOCKED_TX_OPTIONS).build() + const mockedItem = { + rpcHandler: mockedRpcHandler, + sorobanSubmission: MOCKED_SOROBAN_SUBMISSION, + transactionEnvelope: mockedTransactionEnvelope, + } + const pipeline = new SorobanGetTransactionPipeline() + const spyGetTransaction = jest.spyOn(mockedRpcHandler, 'getTransaction') + + const response = await pipeline.execute(mockedItem) + + expect(response).toBeDefined() + expect(response).toEqual({ response: MOCKED_SUCCESSFUL_RESPONSE }) + expect(spyGetTransaction).toHaveBeenCalledTimes(3) + expect(spyGetTransaction).toHaveBeenCalledWith(MOCKED_SOROBAN_SUBMISSION.hash) + }) + + it('should get the timeout from the transaction envelope when useEnvelopeTimeout is true', async () => { + MOCKED_RPC_HANDLER.mockImplementationOnce(() => ({ + getTransaction: jest.fn().mockResolvedValue(MOCKED_SUCCESSFUL_RESPONSE), + })) + const mockedRpcHandler = new DefaultRpcHandler(TESTNET_CONFIG) + const timeoutSeconds = 100 + const mockedTransactionEnvelope = new TransactionBuilder(MOCKED_ACCOUNT_A, MOCKED_TX_OPTIONS) + .setTimeout(timeoutSeconds) + .build() + const mockedItem = { + rpcHandler: mockedRpcHandler, + sorobanSubmission: MOCKED_SOROBAN_SUBMISSION, + transactionEnvelope: mockedTransactionEnvelope, + } + const pipeline = new SorobanGetTransactionPipeline({ + options: { defaultSecondsToWait: 30, useEnvelopeTimeout: true }, + }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const spyGetSecondsToWait = jest.spyOn(pipeline as any, 'getSecondsToWait') + + await pipeline.execute(mockedItem) + + expect(spyGetSecondsToWait).toHaveBeenCalledTimes(1) + expect(spyGetSecondsToWait).toHaveReturnedWith(timeoutSeconds) + }) + + it('should get the timeout from the inner transaction envelope when useEnvelopeTimeout is true and transaction is a fee bump', async () => { + MOCKED_RPC_HANDLER.mockImplementationOnce(() => ({ + getTransaction: jest.fn().mockResolvedValue(MOCKED_SUCCESSFUL_RESPONSE), + })) + const mockedRpcHandler = new DefaultRpcHandler(TESTNET_CONFIG) + const timeoutSeconds = 17 + const mockedInnerTransactionEnvelope = new TransactionBuilder(MOCKED_ACCOUNT_A, MOCKED_TX_OPTIONS) + .setTimeout(timeoutSeconds) + .build() + const mockedFeeBumpTransactionEnvelope = TransactionBuilder.buildFeeBumpTransaction( + MOCKED_PK_A, + MOCKED_TX_OPTIONS.fee + 1, // Fee bump fee needs to be higher than ineer transaction fee + mockedInnerTransactionEnvelope, + TESTNET_CONFIG.networkPassphrase + ) + const mockedItem = { + rpcHandler: mockedRpcHandler, + sorobanSubmission: MOCKED_SOROBAN_SUBMISSION, + transactionEnvelope: mockedFeeBumpTransactionEnvelope, + } + const pipeline = new SorobanGetTransactionPipeline({ + options: { defaultSecondsToWait: 30, useEnvelopeTimeout: true }, + }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const spyGetSecondsToWait = jest.spyOn(pipeline as any, 'getSecondsToWait') + + await pipeline.execute(mockedItem) + + expect(spyGetSecondsToWait).toHaveBeenCalledTimes(1) + expect(spyGetSecondsToWait).toHaveReturnedWith(timeoutSeconds) + }) + }) + + describe('Error handling', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should throw an error if the transaction failed', async () => { + MOCKED_RPC_HANDLER.mockImplementationOnce(() => ({ + getTransaction: jest.fn().mockResolvedValue(MOCKED_FAILED_RESPONSE), + })) + const mockedRpcHandler = new DefaultRpcHandler(TESTNET_CONFIG) + const mockedTransactionEnvelope = new TransactionBuilder(MOCKED_ACCOUNT_A, MOCKED_TX_OPTIONS).build() + const mockedItem = { + rpcHandler: mockedRpcHandler, + sorobanSubmission: MOCKED_SOROBAN_SUBMISSION, + transactionEnvelope: mockedTransactionEnvelope, + } + const pipeline = new SorobanGetTransactionPipeline() + + await expect(pipeline.execute(mockedItem)).rejects.toThrow( + SGTError.transactionFailed(mockConveyorBeltErrorMeta(mockedItem), MOCKED_FAILED_RESPONSE) + ) + }) + + it('should throw an error if the transaction is not found', async () => { + MOCKED_RPC_HANDLER.mockImplementationOnce(() => ({ + getTransaction: jest.fn().mockResolvedValue(MOCKED_MISSING_RESPONSE), + })) + const mockedRpcHandler = new DefaultRpcHandler(TESTNET_CONFIG) + const mockedTransactionEnvelope = new TransactionBuilder(MOCKED_ACCOUNT_A, MOCKED_TX_OPTIONS).build() + const mockedItem = { + rpcHandler: mockedRpcHandler, + sorobanSubmission: MOCKED_SOROBAN_SUBMISSION, + transactionEnvelope: mockedTransactionEnvelope, + } + const pipeline = new SorobanGetTransactionPipeline({ + options: { defaultSecondsToWait: 3, useEnvelopeTimeout: false }, + }) + + await expect(pipeline.execute(mockedItem)).rejects.toThrow( + SGTError.transactionNotFound(mockConveyorBeltErrorMeta(mockedItem), 30, MOCKED_SOROBAN_SUBMISSION.hash) + ) + }) + }) +}) diff --git a/src/stellar-plus/core/pipelines/soroban-get-transaction/types.ts b/src/stellar-plus/core/pipelines/soroban-get-transaction/types.ts index 7a26284..75b8841 100644 --- a/src/stellar-plus/core/pipelines/soroban-get-transaction/types.ts +++ b/src/stellar-plus/core/pipelines/soroban-get-transaction/types.ts @@ -3,6 +3,11 @@ import { FeeBumpTransaction, SorobanRpc, Transaction } from '@stellar/stellar-sd import { RpcHandler } from 'stellar-plus/rpc/types' import { BeltPluginType, GenericPlugin } from 'stellar-plus/utils/pipeline/conveyor-belts/types' +export type SorobanGetTransactionPipelineConstructor = { + plugins?: SorobanGetTransactionPipelinePlugin[] + options?: SorobanGetTransactionOptions +} + export type SorobanGetTransactionPipelineInput = { sorobanSubmission: SorobanRpc.Api.SendTransactionResponse rpcHandler: RpcHandler diff --git a/src/stellar-plus/core/pipelines/soroban-transaction/index.ts b/src/stellar-plus/core/pipelines/soroban-transaction/index.ts index bcbfb39..71b3491 100644 --- a/src/stellar-plus/core/pipelines/soroban-transaction/index.ts +++ b/src/stellar-plus/core/pipelines/soroban-transaction/index.ts @@ -198,7 +198,9 @@ export class SorobanTransactionPipeline extends MultiBeltPipeline< 'SorobanGetTransactionPipeline' as SorobanGetTransactionPipelineType ) as SorobanGetTransactionPipelinePlugin[] - const sorobanGetTransactionPipeline = new SorobanGetTransactionPipeline(sorobanGetTransactionPipelinePlugins) + const sorobanGetTransactionPipeline = new SorobanGetTransactionPipeline({ + plugins: sorobanGetTransactionPipelinePlugins, + }) const sorobanGetTransactionResult = await sorobanGetTransactionPipeline.execute( { diff --git a/src/stellar-plus/core/pipelines/soroban-transaction/index.unit.test.ts b/src/stellar-plus/core/pipelines/soroban-transaction/index.unit.test.ts index 0efdb4b..38c860e 100644 --- a/src/stellar-plus/core/pipelines/soroban-transaction/index.unit.test.ts +++ b/src/stellar-plus/core/pipelines/soroban-transaction/index.unit.test.ts @@ -23,6 +23,7 @@ import { SorobanAuthPipeline } from 'stellar-plus/core/pipelines/soroban-auth' import { SorobanAuthPipelinePlugin, SorobanAuthPipelineType } from 'stellar-plus/core/pipelines/soroban-auth/types' import { SorobanGetTransactionPipeline } from 'stellar-plus/core/pipelines/soroban-get-transaction' import { + SorobanGetTransactionPipelineConstructor, SorobanGetTransactionPipelinePlugin, SorobanGetTransactionPipelineType, } from 'stellar-plus/core/pipelines/soroban-get-transaction/types' @@ -279,7 +280,9 @@ describe('Soroban Transaction Pipeline', () => { await pipeline.execute(MOCKED_PIPELINE_ITEM) - expect(MOCKED_SOROBAN_GET_TRANSACTION_PIPELINE).toHaveBeenCalledWith([MOCKED_SOROBAN_GET_TRANSACTION_PLUGIN]) + expect(MOCKED_SOROBAN_GET_TRANSACTION_PIPELINE).toHaveBeenCalledWith({ + plugins: [MOCKED_SOROBAN_GET_TRANSACTION_PLUGIN], + } as SorobanGetTransactionPipelineConstructor) }) }) describe('Core functionalities', () => { @@ -413,7 +416,9 @@ describe('Soroban Transaction Pipeline', () => { await pipeline.execute(MOCKED_ITEM_WITH_PLUGINS) - expect(MOCKED_SOROBAN_GET_TRANSACTION_PIPELINE).toHaveBeenCalledWith([MOCKED_SOROBAN_GET_TRANSACTION_PLUGIN]) + expect(MOCKED_SOROBAN_GET_TRANSACTION_PIPELINE).toHaveBeenCalledWith({ + plugins: [MOCKED_SOROBAN_GET_TRANSACTION_PLUGIN], + } as SorobanGetTransactionPipelineConstructor) }) }) }) diff --git a/src/stellar-plus/error/helpers/result-meta-xdr.ts b/src/stellar-plus/error/helpers/result-meta-xdr.ts index 90a52c4..73f7a01 100644 --- a/src/stellar-plus/error/helpers/result-meta-xdr.ts +++ b/src/stellar-plus/error/helpers/result-meta-xdr.ts @@ -6,24 +6,22 @@ export const extractSorobanResultXdrOpErrorCode = (resultXdr: xdr.TransactionRes return resultXdrObject.result().results()[0].tr().value().switch().name } try { - if (resultXdr.result?.().results !== undefined - && typeof resultXdr.result?.().results === 'function') { + if (resultXdr.result?.().results !== undefined && typeof resultXdr.result?.().results === 'function') { return resultXdr.result?.().results?.()[0].tr?.().value?.().switch?.().name } } catch (error) { - console.log("Xdr don't have results: %s", error) + // "Xdr don't have results" + //TODO: Evaluate if we should treat this in some way } try { - if (resultXdr.result?.().switch !== undefined - && typeof resultXdr.result?.().switch === 'function') { + if (resultXdr.result?.().switch !== undefined && typeof resultXdr.result?.().switch === 'function') { return resultXdr.result?.().switch?.().name } - } - catch (error) { + } catch (error) { console.log('Fail in decode xdr: %s, Error: %s', resultXdr, error) - return "fail_in_decode_xdr" + return 'fail_in_decode_xdr' } - return "not_found" + return 'not_found' } export enum SorobanOpCodes { From 228a50fbdb5cd518c1b2f3671133ec62f395a368 Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Fri, 5 Apr 2024 17:08:43 -0300 Subject: [PATCH 24/30] Add unit tests for the classic asset handler (#128) * refactor: adjust outter imports/exports * test: add unit tests for the classic asset handler --- src/stellar-plus/asset/classic/index.ts | 14 +- .../asset/classic/index.unit.test.ts | 441 ++++++++++++++++++ src/stellar-plus/constants.ts | 15 +- src/stellar-plus/types.ts | 16 +- 4 files changed, 464 insertions(+), 22 deletions(-) create mode 100644 src/stellar-plus/asset/classic/index.unit.test.ts diff --git a/src/stellar-plus/asset/classic/index.ts b/src/stellar-plus/asset/classic/index.ts index cad5560..7b22435 100644 --- a/src/stellar-plus/asset/classic/index.ts +++ b/src/stellar-plus/asset/classic/index.ts @@ -1,6 +1,5 @@ import { Horizon as HorizonNamespace, Operation, Asset as StellarAsset } from '@stellar/stellar-sdk' -import { HorizonHandler } from 'stellar-plus' import { AccountHandler } from 'stellar-plus/account/account-handler/types' import { ClassicAssetHandlerConstructorArgs, @@ -10,6 +9,7 @@ import { AssetTypes } from 'stellar-plus/asset/types' import { ClassicTransactionPipeline } from 'stellar-plus/core/pipelines/classic-transaction' import { ClassicTransactionPipelineOptions } from 'stellar-plus/core/pipelines/classic-transaction/types' import { TransactionInvocation } from 'stellar-plus/core/types' +import { HorizonHandlerClient as HorizonHandler } from 'stellar-plus/horizon' import { CAHError } from './errors' @@ -21,7 +21,7 @@ export class ClassicAssetHandler implements IClassicAssetHandler { private asset: StellarAsset private horizonHandler: HorizonHandler - private classicTrasactionPipeline: ClassicTransactionPipeline + private classicTransactionPipeline: ClassicTransactionPipeline /** * @@ -53,7 +53,7 @@ export class ClassicAssetHandler implements IClassicAssetHandler { this.asset = new StellarAsset(args.code, this.issuerPublicKey) - this.classicTrasactionPipeline = new ClassicTransactionPipeline( + this.classicTransactionPipeline = new ClassicTransactionPipeline( args.networkConfig, args.options?.classicTransactionPipeline as ClassicTransactionPipelineOptions ) @@ -150,7 +150,7 @@ export class ClassicAssetHandler implements IClassicAssetHandler { source: from, }) - await this.classicTrasactionPipeline.execute({ + await this.classicTransactionPipeline.execute({ txInvocation, operations: [transferOp], }) @@ -219,7 +219,7 @@ export class ClassicAssetHandler implements IClassicAssetHandler { source: this.asset.getIssuer(), }) - const result = await this.classicTrasactionPipeline.execute({ + const result = await this.classicTransactionPipeline.execute({ txInvocation: updatedTxInvocation, operations: [mintOp], }) @@ -278,7 +278,7 @@ export class ClassicAssetHandler implements IClassicAssetHandler { source: this.asset.getIssuer(), }) - const result = await this.classicTrasactionPipeline.execute({ + const result = await this.classicTransactionPipeline.execute({ txInvocation: updatedTxInvocation, operations: [addTrustlineOp, mintOp], }) @@ -311,7 +311,7 @@ export class ClassicAssetHandler implements IClassicAssetHandler { asset: this.asset, }) - const result = await this.classicTrasactionPipeline.execute({ + const result = await this.classicTransactionPipeline.execute({ txInvocation, operations: [addTrustlineOp], }) diff --git a/src/stellar-plus/asset/classic/index.unit.test.ts b/src/stellar-plus/asset/classic/index.unit.test.ts new file mode 100644 index 0000000..153ef2e --- /dev/null +++ b/src/stellar-plus/asset/classic/index.unit.test.ts @@ -0,0 +1,441 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +import { ClassicAssetHandler } from 'stellar-plus/asset/classic' +import { CAHError } from 'stellar-plus/asset/classic/errors' +import { testnet } from 'stellar-plus/constants' +import { + BuildTransactionPipelinePlugin, + BuildTransactionPipelineType, +} from 'stellar-plus/core/pipelines/build-transaction/types' +import { + ClassicTransactionPipelinePlugin, + ClassicTransactionPipelineType, +} from 'stellar-plus/core/pipelines/classic-transaction/types' +import { HorizonHandlerClient } from 'stellar-plus/horizon' +import { mockAccountHandler } from 'stellar-plus/test/mocks/transaction-mock' +import { TransactionInvocation } from 'stellar-plus/types' + +jest.mock('@stellar/stellar-sdk', () => { + // The mock doesnt spread the whole originalModule because some internal exported objects cause failures + // so we just unmock the necessary items. + // uncomment and use the following line if you need to check the contents of the module: + // const originalModule: typeof import('@stellar/stellar-sdk') = jest.requireActual('@stellar/stellar-sdk') + const originalModule = jest.requireActual('@stellar/stellar-sdk') + return { + xdr: originalModule.xdr, + Asset: originalModule.Asset, + TransactionBuilder: originalModule.TransactionBuilder, + Operation: { + payment: jest.fn().mockReturnValue('paymentOp'), + changeTrust: jest.fn().mockReturnValue('changeTrustOp'), + }, + } +}) + +jest.mock('stellar-plus/horizon', () => ({ + HorizonHandlerClient: jest.fn().mockImplementation(() => ({ + account: jest.fn(), + loadAccount: jest.fn(), + })), +})) + +const MOCKED_HORIZON_HANDLER = HorizonHandlerClient as jest.Mock + +const TESTNET_CONFIG = testnet +const MOCKED_PK = 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI' +const MOCKED_PK_B = 'GBCBCTQ6YH3XFYDDGARNGYSS2LGTX5CA6P3P2K6ODSRNBKKK7BWMEEVM' + +describe('Classic Asset Handler', () => { + describe('Initialization', () => { + it('should be able to create a new instance with just the asset parameters', () => { + const asset = new ClassicAssetHandler({ + code: 'CAKE', + issuerAccount: MOCKED_PK, + networkConfig: TESTNET_CONFIG, + }) + + expect(asset).toBeDefined() + expect(asset).toBeInstanceOf(ClassicAssetHandler) + expect(asset.code).toBe('CAKE') + expect(asset.issuerPublicKey).toBe(MOCKED_PK) + }) + + it('should be able to create a new instance with the issuer account handler', () => { + const mockedIssuerAccountHandler = mockAccountHandler({ accountKey: MOCKED_PK }) + const asset = new ClassicAssetHandler({ + code: 'CAKE', + issuerAccount: mockedIssuerAccountHandler, + networkConfig: TESTNET_CONFIG, + }) + + expect(asset).toBeDefined() + expect(asset).toBeInstanceOf(ClassicAssetHandler) + expect(asset.code).toBe('CAKE') + expect(asset.issuerPublicKey).toBe(MOCKED_PK) + }) + + it('should initialize the type as credit_alphanum4 based on the code length', () => { + const asset = new ClassicAssetHandler({ + code: 'CAKE', + issuerAccount: MOCKED_PK, + networkConfig: TESTNET_CONFIG, + }) + + expect(asset.type).toBe('credit_alphanum4') + }) + + it('should initialize the type as credit_alphanum12 based on the code length', () => { + const asset = new ClassicAssetHandler({ + code: 'CAKECAKECAKE', + issuerAccount: MOCKED_PK, + networkConfig: TESTNET_CONFIG, + }) + + expect(asset.type).toBe('credit_alphanum12') + }) + + it('should initialize the type as native if the code is XLM', () => { + const asset = new ClassicAssetHandler({ + code: 'XLM', + issuerAccount: MOCKED_PK, + networkConfig: TESTNET_CONFIG, + }) + + expect(asset.type).toBe('native') + }) + + it('should accept options for the Classic Transaction pipeline', () => { + const mockedPlugin = jest.mocked({ + preProcess: jest.fn(), + postProcess: jest.fn(), + type: ClassicTransactionPipelineType.id as ClassicTransactionPipelineType, + }) as unknown as ClassicTransactionPipelinePlugin + + const mockedInnerPlugin = jest.mocked({ + preProcess: jest.fn(), + postProcess: jest.fn(), + type: BuildTransactionPipelineType.id as BuildTransactionPipelineType, + }) as unknown as BuildTransactionPipelinePlugin + + const asset = new ClassicAssetHandler({ + code: 'CAKE', + issuerAccount: MOCKED_PK, + networkConfig: TESTNET_CONFIG, + options: { + classicTransactionPipeline: { + plugins: [mockedPlugin, mockedInnerPlugin], + }, + }, + }) + const spyPipeline = jest.mocked((asset as any).classicTransactionPipeline) + + expect(asset).toBeDefined() + expect(asset).toBeInstanceOf(ClassicAssetHandler) + expect(spyPipeline.plugins).toContain(mockedPlugin) + expect(spyPipeline.plugins).not.toContain(mockedInnerPlugin) + expect(spyPipeline.innerPlugins).toContain(mockedInnerPlugin) + expect(spyPipeline.innerPlugins).not.toContain(mockedPlugin) + }) + }) + + describe('Core Functionalities', () => { + let asset: ClassicAssetHandler + beforeEach(() => { + jest.clearAllMocks() + const mockedIssuerAccountHandler = mockAccountHandler({ accountKey: MOCKED_PK }) + asset = new ClassicAssetHandler({ + code: 'CAKE', + issuerAccount: mockedIssuerAccountHandler, + networkConfig: TESTNET_CONFIG, + }) + }) + + it('should be able to get the asset symbol', async () => { + expect(asset.symbol()).resolves.toBe('CAKE') + }) + + it('should be able to get the decimals', async () => { + expect(asset.decimals()).resolves.toBe(7) // Currently fixed for classic assets + }) + + it('should be able to get the asset name', async () => { + expect(asset.name()).resolves.toBe('CAKE') // Currently defaults to code for classic assets + }) + + it('should be able to get the balance of an account for this asset', async () => { + const mockedBalance = '100.0000000' + MOCKED_HORIZON_HANDLER.mockImplementationOnce(() => ({ + loadAccount: jest.fn().mockImplementation(() => { + return { + balances: [ + { + asset_code: 'CAKE', + asset_issuer: MOCKED_PK, + balance: mockedBalance, + asset_type: 'credit_alphanum4', + }, + { + asset_code: 'CAKECAKECAKE', + asset_issuer: MOCKED_PK, + balance: '250', + asset_type: 'credit_alphanum12', + }, + ], + } + }), + })) + const asset = new ClassicAssetHandler({ + code: 'CAKE', + issuerAccount: MOCKED_PK, + networkConfig: TESTNET_CONFIG, + }) + + expect(asset.balance(MOCKED_PK)).resolves.toBe(Number(mockedBalance)) + }) + + it('should be able to get the balance of an account for this asset if it is native', async () => { + const mockedBalance = '100.0000000' + MOCKED_HORIZON_HANDLER.mockImplementationOnce(() => ({ + loadAccount: jest.fn().mockImplementation(() => { + return { + balances: [ + { + asset_code: 'XLM', + asset_issuer: MOCKED_PK, + balance: mockedBalance, + asset_type: 'native', + }, + { + asset_code: 'CAKECAKECAKE', + asset_issuer: MOCKED_PK, + balance: '250', + asset_type: 'credit_alphanum12', + }, + ], + } + }), + })) + const asset = new ClassicAssetHandler({ + code: 'XLM', + issuerAccount: MOCKED_PK, + networkConfig: TESTNET_CONFIG, + }) + + expect(asset.balance(MOCKED_PK)).resolves.toBe(Number(mockedBalance)) + }) + + it('should return 0 when there is no balance for an account for this asset', async () => { + MOCKED_HORIZON_HANDLER.mockImplementationOnce(() => ({ + loadAccount: jest.fn().mockImplementation(() => { + return { + balances: [ + { + asset_code: 'CAKECAKECAKE', + asset_issuer: MOCKED_PK, + balance: '250', + asset_type: 'credit_alphanum12', + }, + ], + } + }), + })) + const asset = new ClassicAssetHandler({ + code: 'CAKE', + issuerAccount: MOCKED_PK, + networkConfig: TESTNET_CONFIG, + }) + + expect(asset.balance(MOCKED_PK)).resolves.toBe(0) + }) + + it('should be able to perform a transfer operation for a given account', async () => { + const mockedAccountHandler = mockAccountHandler({ accountKey: MOCKED_PK }) + const sender = MOCKED_PK + const receiver = MOCKED_PK_B + const asset = new ClassicAssetHandler({ + code: 'CAKE', + issuerAccount: MOCKED_PK, + networkConfig: TESTNET_CONFIG, + }) + const mockedTxInvocation = { + header: { source: MOCKED_PK, fee: '100', timeout: 100 }, + signers: [mockedAccountHandler], + } + const spyExecute = jest.spyOn((asset as any).classicTransactionPipeline, 'execute').mockResolvedValue({}) + const args = { + from: sender, + to: receiver, + amount: 120, + ...mockedTxInvocation, + } + + await asset.transfer(args) + + expect(spyExecute).toHaveBeenCalledExactlyOnceWith({ + txInvocation: args as TransactionInvocation, + operations: ['paymentOp'], + }) + }) + + it('should be able to perform a burn operation for a given account', async () => { + const mockedAccountHandler = mockAccountHandler({ accountKey: MOCKED_PK }) + const sender = MOCKED_PK + const issuer = MOCKED_PK_B + const asset = new ClassicAssetHandler({ + code: 'CAKE', + issuerAccount: issuer, + networkConfig: TESTNET_CONFIG, + }) + const mockedTxInvocation = { + header: { source: MOCKED_PK, fee: '100', timeout: 100 }, + signers: [mockedAccountHandler], + } + const spyTransfer = jest.spyOn(asset as any, 'transfer').mockResolvedValue({}) + const args = { + from: sender, + amount: 120, + ...mockedTxInvocation, + } + + await asset.burn(args) + + expect(spyTransfer).toHaveBeenCalledExactlyOnceWith({ + ...args, + to: issuer, + }) + }) + + it('should be able to perform a mint operation to a given account, adding the issuer as signer', async () => { + const mockedIssuerAccountHandler = mockAccountHandler({ accountKey: MOCKED_PK }) + const mockedUserAccountHandler = mockAccountHandler({ accountKey: MOCKED_PK_B }) + const asset = new ClassicAssetHandler({ + code: 'CAKE', + issuerAccount: mockedIssuerAccountHandler, + networkConfig: TESTNET_CONFIG, + }) + const mockedTxInvocation = { + header: { source: mockedUserAccountHandler.getPublicKey(), fee: '100', timeout: 100 }, + signers: [mockedUserAccountHandler], + } + const spyExecute = jest.spyOn((asset as any).classicTransactionPipeline, 'execute').mockResolvedValue({}) + const args = { + to: mockedUserAccountHandler.getPublicKey(), + amount: 120, + ...mockedTxInvocation, + } + + await asset.mint(args) + + expect(spyExecute).toHaveBeenCalledExactlyOnceWith({ + txInvocation: { + ...args, + signers: [mockedUserAccountHandler, mockedIssuerAccountHandler], + } as TransactionInvocation, + operations: ['paymentOp'], + }) + }) + + it('should be able to perform a transaction to add trustline for a given account', async () => { + const mockedAccountHandler = mockAccountHandler({ accountKey: MOCKED_PK }) + const asset = new ClassicAssetHandler({ + code: 'CAKE', + issuerAccount: MOCKED_PK, + networkConfig: TESTNET_CONFIG, + }) + const mockedTxInvocation = { + header: { source: MOCKED_PK, fee: '100', timeout: 100 }, + signers: [mockedAccountHandler], + } + const spyExecute = jest.spyOn((asset as any).classicTransactionPipeline, 'execute').mockResolvedValue({}) + const args = { + to: mockedAccountHandler.getPublicKey(), + ...mockedTxInvocation, + } + + await asset.addTrustline(args) + + expect(spyExecute).toHaveBeenCalledExactlyOnceWith({ + txInvocation: args as TransactionInvocation, + operations: ['changeTrustOp'], + }) + }) + + it('should be able to perform a transaction to add trustline and mint operation to a given account, adding the issuer as signer', async () => { + const mockedIssuerAccountHandler = mockAccountHandler({ accountKey: MOCKED_PK }) + const mockedUserAccountHandler = mockAccountHandler({ accountKey: MOCKED_PK_B }) + const asset = new ClassicAssetHandler({ + code: 'CAKE', + issuerAccount: mockedIssuerAccountHandler, + networkConfig: TESTNET_CONFIG, + }) + const mockedTxInvocation = { + header: { source: mockedUserAccountHandler.getPublicKey(), fee: '100', timeout: 100 }, + signers: [mockedUserAccountHandler], + } + const spyExecute = jest.spyOn((asset as any).classicTransactionPipeline, 'execute').mockResolvedValue({}) + const args = { + to: mockedUserAccountHandler.getPublicKey(), + amount: 120, + ...mockedTxInvocation, + } + + await asset.addTrustlineAndMint(args) + + expect(spyExecute).toHaveBeenCalledExactlyOnceWith({ + txInvocation: { + ...args, + signers: [mockedUserAccountHandler, mockedIssuerAccountHandler], + } as TransactionInvocation, + operations: ['changeTrustOp', 'paymentOp'], + }) + }) + }) + + describe('Error Handling', () => { + let asset: ClassicAssetHandler + beforeEach(() => { + jest.clearAllMocks() + const mockedIssuerAccountHandler = mockAccountHandler({ accountKey: MOCKED_PK }) + asset = new ClassicAssetHandler({ + code: 'CAKE', + issuerAccount: mockedIssuerAccountHandler, + networkConfig: TESTNET_CONFIG, + }) + }) + + it('should throw an error if trying to approve an account', async () => { + //Not implemented yet + + expect(asset.approve()).rejects.toThrow('Method not implemented.') + }) + + it('should throw an error if trying to clawback from an account', async () => { + //Not implemented yet + + expect(asset.clawback()).rejects.toThrow('Method not implemented.') + }) + + it('should throw an error if the function invoked require an issuer handler and it is missing', async () => { + const asset = new ClassicAssetHandler({ + code: 'CAKE', + issuerAccount: MOCKED_PK, + networkConfig: TESTNET_CONFIG, + }) + const mockedTxInvocation = { + header: { source: MOCKED_PK_B, fee: '100', timeout: 100 }, + signers: [mockAccountHandler({ accountKey: MOCKED_PK_B })], + } + const spyExecute = jest.spyOn((asset as any).classicTransactionPipeline, 'execute').mockResolvedValue({}) + const args = { + to: MOCKED_PK_B, + amount: 120, + ...mockedTxInvocation, + } + + await expect(asset.addTrustlineAndMint(args)).rejects.toThrow(CAHError.issuerAccountNotDefined()) + expect(spyExecute).not.toHaveBeenCalled() + }) + }) +}) diff --git a/src/stellar-plus/constants.ts b/src/stellar-plus/constants.ts index eba31e6..3f13d9e 100644 --- a/src/stellar-plus/constants.ts +++ b/src/stellar-plus/constants.ts @@ -1,4 +1,17 @@ -import { NetworkConfig, NetworksList } from 'stellar-plus/types' +export type NetworkConfig = { + name: NetworksList + networkPassphrase: string + rpcUrl: string + horizonUrl: string + friendbotUrl?: string +} + +export enum NetworksList { + testnet = 'testnet', + futurenet = 'futurenet', + mainnet = 'mainnet', + custom = 'custom', +} const networksConfig: { [key: string]: NetworkConfig } = { futurenet: { diff --git a/src/stellar-plus/types.ts b/src/stellar-plus/types.ts index 8c7633e..b903d6c 100644 --- a/src/stellar-plus/types.ts +++ b/src/stellar-plus/types.ts @@ -5,6 +5,7 @@ import { Transaction as _Transaction, } from '@stellar/stellar-sdk' +import { NetworkConfig as _NetworkConfig } from 'stellar-plus/constants' import { EnvelopeHeader as _EnvelopeHeader, FeeBumpHeader as _FeeBumpHeader, @@ -17,20 +18,7 @@ export type Transaction = _Transaction export type FeeBumpTransaction = _FeeBumpTransaction -export type NetworkConfig = { - name: NetworksList - networkPassphrase: string - rpcUrl: string - horizonUrl: string - friendbotUrl?: string -} - -export enum NetworksList { - testnet = 'testnet', - futurenet = 'futurenet', - mainnet = 'mainnet', - custom = 'custom', -} +export type NetworkConfig = _NetworkConfig export type u32 = number export type i32 = number From 4823037066d600ef41e105d7658a1929a811683a Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Mon, 8 Apr 2024 14:40:26 -0300 Subject: [PATCH 25/30] feat: remove issuer requirement for classic assets (#129) * refactor: allow for no payload for dah * feat: remove issuer necessity for classic assets to better include native --- .../account/account-handler/default/index.ts | 4 +-- src/stellar-plus/asset/classic/index.ts | 26 +++++++++++++----- .../asset/classic/index.unit.test.ts | 27 ++++++++++++------- src/stellar-plus/asset/classic/types.ts | 4 +-- 4 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/stellar-plus/account/account-handler/default/index.ts b/src/stellar-plus/account/account-handler/default/index.ts index 4ff5084..0cf9b1c 100644 --- a/src/stellar-plus/account/account-handler/default/index.ts +++ b/src/stellar-plus/account/account-handler/default/index.ts @@ -16,8 +16,8 @@ export class DefaultAccountHandlerClient extends AccountBaseClient implements De * @param {NetworkConfig} payload.networkConfig The network to use. * @description - The default account handler is used for handling and creating new accounts by directly manipulating the secret key. */ - constructor(payload: DefaultAccountHandlerPayload) { - const secretKey = payload.secretKey as string + constructor(payload?: DefaultAccountHandlerPayload) { + const secretKey = payload?.secretKey as string try { const keypair = secretKey ? Keypair.fromSecret(secretKey) : Keypair.random() diff --git a/src/stellar-plus/asset/classic/index.ts b/src/stellar-plus/asset/classic/index.ts index 7b22435..d77581f 100644 --- a/src/stellar-plus/asset/classic/index.ts +++ b/src/stellar-plus/asset/classic/index.ts @@ -15,7 +15,7 @@ import { CAHError } from './errors' export class ClassicAssetHandler implements IClassicAssetHandler { public code: string - public issuerPublicKey: string + public issuerPublicKey?: string public type: AssetTypes.native | AssetTypes.credit_alphanum4 | AssetTypes.credit_alphanum12 private issuerAccount?: AccountHandler private asset: StellarAsset @@ -37,18 +37,24 @@ export class ClassicAssetHandler implements IClassicAssetHandler { */ constructor(args: ClassicAssetHandlerConstructorArgs) { this.code = args.code - this.issuerPublicKey = - typeof args.issuerAccount === 'string' ? args.issuerAccount : args.issuerAccount.getPublicKey() - - this.issuerAccount = typeof args.issuerAccount === 'string' ? undefined : args.issuerAccount - this.type = - args.code === 'XLM' + args.code === 'XLM' && !args.issuerAccount ? AssetTypes.native : args.code.length <= 4 ? AssetTypes.credit_alphanum4 : AssetTypes.credit_alphanum12 + // provided Public key for issuer + if (args.issuerAccount && typeof args.issuerAccount === 'string') { + this.issuerPublicKey = args.issuerAccount + } + + // provided Account Handler for issuer + if (args.issuerAccount && typeof args.issuerAccount !== 'string') { + this.issuerAccount = args.issuerAccount + this.issuerPublicKey = args.issuerAccount.getPublicKey() + } + this.horizonHandler = new HorizonHandler(args.networkConfig) this.asset = new StellarAsset(args.code, this.issuerPublicKey) @@ -171,6 +177,12 @@ export class ClassicAssetHandler implements IClassicAssetHandler { * @description - Burns the given amount of the asset from the 'from' account. */ public async burn(args: { from: string; amount: number } & TransactionInvocation): Promise { + if (this.type === AssetTypes.native) { + throw "You can't burn XLM" + } + if (!this.issuerPublicKey) { + throw "Missing issuer public key. Can't burn asset." + } return this.transfer({ ...args, to: this.issuerPublicKey }) } diff --git a/src/stellar-plus/asset/classic/index.unit.test.ts b/src/stellar-plus/asset/classic/index.unit.test.ts index 153ef2e..13942d0 100644 --- a/src/stellar-plus/asset/classic/index.unit.test.ts +++ b/src/stellar-plus/asset/classic/index.unit.test.ts @@ -96,16 +96,25 @@ describe('Classic Asset Handler', () => { expect(asset.type).toBe('credit_alphanum12') }) - it('should initialize the type as native if the code is XLM', () => { + it('should initialize the type as native if the code is XLM as has no issuer', () => { const asset = new ClassicAssetHandler({ code: 'XLM', - issuerAccount: MOCKED_PK, networkConfig: TESTNET_CONFIG, }) expect(asset.type).toBe('native') }) + it('should initialize the type as credit_alphanum4 if the code is XLM as has an issuer', () => { + const asset = new ClassicAssetHandler({ + code: 'XLM', + issuerAccount: MOCKED_PK, + networkConfig: TESTNET_CONFIG, + }) + + expect(asset.type).toBe('credit_alphanum4') + }) + it('should accept options for the Classic Transaction pipeline', () => { const mockedPlugin = jest.mocked({ preProcess: jest.fn(), @@ -203,7 +212,6 @@ describe('Classic Asset Handler', () => { balances: [ { asset_code: 'XLM', - asset_issuer: MOCKED_PK, balance: mockedBalance, asset_type: 'native', }, @@ -219,7 +227,6 @@ describe('Classic Asset Handler', () => { })) const asset = new ClassicAssetHandler({ code: 'XLM', - issuerAccount: MOCKED_PK, networkConfig: TESTNET_CONFIG, }) @@ -260,7 +267,7 @@ describe('Classic Asset Handler', () => { networkConfig: TESTNET_CONFIG, }) const mockedTxInvocation = { - header: { source: MOCKED_PK, fee: '100', timeout: 100 }, + header: { source: MOCKED_PK, fee: '100', timeout: 45 }, signers: [mockedAccountHandler], } const spyExecute = jest.spyOn((asset as any).classicTransactionPipeline, 'execute').mockResolvedValue({}) @@ -289,7 +296,7 @@ describe('Classic Asset Handler', () => { networkConfig: TESTNET_CONFIG, }) const mockedTxInvocation = { - header: { source: MOCKED_PK, fee: '100', timeout: 100 }, + header: { source: MOCKED_PK, fee: '100', timeout: 45 }, signers: [mockedAccountHandler], } const spyTransfer = jest.spyOn(asset as any, 'transfer').mockResolvedValue({}) @@ -316,7 +323,7 @@ describe('Classic Asset Handler', () => { networkConfig: TESTNET_CONFIG, }) const mockedTxInvocation = { - header: { source: mockedUserAccountHandler.getPublicKey(), fee: '100', timeout: 100 }, + header: { source: mockedUserAccountHandler.getPublicKey(), fee: '100', timeout: 45 }, signers: [mockedUserAccountHandler], } const spyExecute = jest.spyOn((asset as any).classicTransactionPipeline, 'execute').mockResolvedValue({}) @@ -345,7 +352,7 @@ describe('Classic Asset Handler', () => { networkConfig: TESTNET_CONFIG, }) const mockedTxInvocation = { - header: { source: MOCKED_PK, fee: '100', timeout: 100 }, + header: { source: MOCKED_PK, fee: '100', timeout: 45 }, signers: [mockedAccountHandler], } const spyExecute = jest.spyOn((asset as any).classicTransactionPipeline, 'execute').mockResolvedValue({}) @@ -371,7 +378,7 @@ describe('Classic Asset Handler', () => { networkConfig: TESTNET_CONFIG, }) const mockedTxInvocation = { - header: { source: mockedUserAccountHandler.getPublicKey(), fee: '100', timeout: 100 }, + header: { source: mockedUserAccountHandler.getPublicKey(), fee: '100', timeout: 45 }, signers: [mockedUserAccountHandler], } const spyExecute = jest.spyOn((asset as any).classicTransactionPipeline, 'execute').mockResolvedValue({}) @@ -424,7 +431,7 @@ describe('Classic Asset Handler', () => { networkConfig: TESTNET_CONFIG, }) const mockedTxInvocation = { - header: { source: MOCKED_PK_B, fee: '100', timeout: 100 }, + header: { source: MOCKED_PK_B, fee: '100', timeout: 45 }, signers: [mockAccountHandler({ accountKey: MOCKED_PK_B })], } const spyExecute = jest.spyOn((asset as any).classicTransactionPipeline, 'execute').mockResolvedValue({}) diff --git a/src/stellar-plus/asset/classic/types.ts b/src/stellar-plus/asset/classic/types.ts index e64a418..d7675da 100644 --- a/src/stellar-plus/asset/classic/types.ts +++ b/src/stellar-plus/asset/classic/types.ts @@ -8,7 +8,7 @@ import { NetworkConfig } from 'stellar-plus/types' export type ClassicAsset = AssetType & { code: string - issuerPublicKey: string + issuerPublicKey?: string type: AssetTypes.native | AssetTypes.credit_alphanum4 | AssetTypes.credit_alphanum12 } @@ -16,7 +16,7 @@ export type ClassicAssetHandler = ClassicAsset & ClassicTokenInterface & Classic export type ClassicAssetHandlerConstructorArgs = { code: string - issuerAccount: string | AccountHandler + issuerAccount?: string | AccountHandler networkConfig: NetworkConfig options?: { From 210cbd70140ec99e57cad860497bdd1b511bebca Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:31:24 -0300 Subject: [PATCH 26/30] test: add unit test to soroban token handler (#131) --- .../asset/soroban-token/index.unit.test.ts | 413 ++++++++++++++++++ 1 file changed, 413 insertions(+) create mode 100644 src/stellar-plus/asset/soroban-token/index.unit.test.ts diff --git a/src/stellar-plus/asset/soroban-token/index.unit.test.ts b/src/stellar-plus/asset/soroban-token/index.unit.test.ts new file mode 100644 index 0000000..a8d1ff9 --- /dev/null +++ b/src/stellar-plus/asset/soroban-token/index.unit.test.ts @@ -0,0 +1,413 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Address, ContractSpec } from '@stellar/stellar-sdk' + +import { SorobanTokenHandler } from 'stellar-plus/asset/soroban-token' +import { spec as DEFAULT_SPEC, methods } from 'stellar-plus/asset/soroban-token/constants' +import { testnet } from 'stellar-plus/constants' +import { SorobanTransactionPipeline } from 'stellar-plus/core/pipelines/soroban-transaction' +import { TransactionInvocation } from 'stellar-plus/types' + +jest.mock('stellar-plus/core/pipelines/soroban-transaction', () => ({ + SorobanTransactionPipeline: jest.fn(), +})) + +const MOCKED_SOROBAN_TRANSACTION_PIPELINE = SorobanTransactionPipeline as jest.Mock +const MOCKED_EXECUTE = jest.fn().mockResolvedValue({}) + +const NETWORK_CONFIG = testnet + +const MOCKED_CONTRACT_ID = 'CBJT4BOMRHYKHZ6HF3QG4YR7Q63BE44G73M4MALDTQ3SQVUZDE7GN35I' +const MOCKED_WASM_HASH = 'eb94566536d7f56c353b4760f6e359eca3631b70d295820fb6de55a796e019ae' +const MOCKED_WASM_FILE = Buffer.from('mockWasm', 'utf-8') + +const MOCKED_TX_INVOCATION: TransactionInvocation = { + header: { + source: 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI', + fee: '100', + timeout: 45, + }, + signers: [], +} + +describe('SorobanToken', () => { + describe('Initialization', () => { + it('should initialize with the contract id', () => { + const token = new SorobanTokenHandler({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + contractId: MOCKED_CONTRACT_ID, + }, + }) + + expect(token.getContractId()).toBe(MOCKED_CONTRACT_ID) + }) + + it('should initialize with a custom spec', () => { + const mockedSpec = new ContractSpec(['AAAAAAAAAAAAAAAEbmFtZQAAAAAAAAABAAAAEA==']) + const token = new SorobanTokenHandler({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + contractId: MOCKED_CONTRACT_ID, + spec: mockedSpec, + }, + }) + const spyTokenSpec = jest.mocked((token as any).spec) + + expect(spyTokenSpec).toBe(mockedSpec) + }) + + it('should initialize with default spec if nothing no contract parameters are provided', () => { + const token = new SorobanTokenHandler({ + networkConfig: NETWORK_CONFIG, + }) + const spyTokenSpec = jest.mocked((token as any).spec) + + expect(spyTokenSpec).toBe(DEFAULT_SPEC) + }) + + it('should initialize with a custom wasm hash', () => { + const token = new SorobanTokenHandler({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + wasmHash: MOCKED_WASM_HASH, + }, + }) + + expect(token.getWasmHash()).toBe(MOCKED_WASM_HASH) + }) + + it('should initialize with a custom wasm', () => { + const token = new SorobanTokenHandler({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + wasm: MOCKED_WASM_FILE, + }, + }) + + expect(token.getWasm()).toBe(MOCKED_WASM_FILE) + }) + }) + + describe('Core invoke methods', () => { + let token: SorobanTokenHandler + let invokeSpy: jest.SpyInstance + + beforeEach(() => { + jest.clearAllMocks() + + MOCKED_SOROBAN_TRANSACTION_PIPELINE.mockImplementation(() => { + return { + execute: MOCKED_EXECUTE, + } + }) + + token = new SorobanTokenHandler({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + contractId: MOCKED_CONTRACT_ID, + }, + }) + + invokeSpy = jest.spyOn(token as any, 'invokeContract') + }) + + afterEach(() => { + expect(MOCKED_EXECUTE).toHaveBeenCalledOnce() + }) + + it('should initialize the contract invoking the initialize method', async () => { + const initializeArgs = { + admin: 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI', + decimal: 7, + name: 'mockedName', + symbol: 'mockedSymbol', + } + + await token.initialize({ ...initializeArgs, ...MOCKED_TX_INVOCATION }) + + expect(invokeSpy).toHaveBeenCalledExactlyOnceWith({ + method: methods.initialize, + methodArgs: { ...initializeArgs, admin: new Address(initializeArgs.admin) }, + ...MOCKED_TX_INVOCATION, + }) + }) + + it('should set a new admin', async () => { + const currentAdmin = 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI' + const newAdmin = 'GBC7ALQTV35BQ3URS5NUHHSWWN3KEXMRGFJ2S4S6VETJ6QA7QCDA64VI' + const setAdminArgs = { + id: currentAdmin, + new_admin: newAdmin, + } + + await token.setAdmin({ ...setAdminArgs, ...MOCKED_TX_INVOCATION }) + + expect(invokeSpy).toHaveBeenCalledExactlyOnceWith({ + method: methods.set_admin, + methodArgs: { id: new Address(currentAdmin), new_admin: new Address(newAdmin) }, + ...MOCKED_TX_INVOCATION, + }) + }) + + // TODO: Review the default SPEC file under constants + // It seems it is not fully compliant with CAP46 + it.skip('should set an account as authorized', async () => { + const accountToAuthorize = 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI' + const setAuthorizedArgs = { + id: accountToAuthorize, + authorize: true, + } + + await token.setAuthorized({ ...setAuthorizedArgs, ...MOCKED_TX_INVOCATION }) + + expect(invokeSpy).toHaveBeenCalledExactlyOnceWith({ + method: methods.set_authorized, + methodArgs: { ...setAuthorizedArgs, id: new Address(accountToAuthorize) }, + ...MOCKED_TX_INVOCATION, + }) + }) + + it('should mint new units', async () => { + const recipient = 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI' + const mintArgs = { + to: recipient, + amount: BigInt(125), + } + + await token.mint({ ...mintArgs, ...MOCKED_TX_INVOCATION }) + + expect(invokeSpy).toHaveBeenCalledExactlyOnceWith({ + method: methods.mint, + methodArgs: { ...mintArgs, to: new Address(recipient) }, + ...MOCKED_TX_INVOCATION, + }) + }) + + it('should burn existing units', async () => { + const from = 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI' + const burnArgs = { + from, + amount: BigInt(131), + } + + await token.burn({ ...burnArgs, ...MOCKED_TX_INVOCATION }) + + expect(invokeSpy).toHaveBeenCalledExactlyOnceWith({ + method: methods.burn, + methodArgs: { ...burnArgs, from: new Address(from) }, + ...MOCKED_TX_INVOCATION, + }) + }) + + // TODO: Review the default SPEC file under constants + // It seems it is not fully compliant with CAP46 + it.skip('should clawback funds from an account', async () => { + const targetAccount = 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI' + const clawbackArgs = { + from: targetAccount, + amount: BigInt(510), + } + + await token.clawback({ ...clawbackArgs, ...MOCKED_TX_INVOCATION }) + + expect(invokeSpy).toHaveBeenCalledExactlyOnceWith({ + method: methods.clawback, + methodArgs: { ...clawbackArgs, from: new Address(targetAccount) }, + ...MOCKED_TX_INVOCATION, + }) + }) + + it('should apprve an account as spender', async () => { + const from = 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI' + const spender = 'GBC7ALQTV35BQ3URS5NUHHSWWN3KEXMRGFJ2S4S6VETJ6QA7QCDA64VI' + const approveArgs = { + from, + spender, + amount: BigInt(510), + expiration_ledger: 10, + } + + await token.approve({ ...approveArgs, ...MOCKED_TX_INVOCATION }) + + expect(invokeSpy).toHaveBeenCalledExactlyOnceWith({ + method: methods.approve, + methodArgs: { ...approveArgs, from: new Address(from), spender: new Address(spender) }, + ...MOCKED_TX_INVOCATION, + }) + }) + + it('should transfer from an account to another', async () => { + const from = 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI' + const to = 'GBC7ALQTV35BQ3URS5NUHHSWWN3KEXMRGFJ2S4S6VETJ6QA7QCDA64VI' + const transferArgs = { + from, + to, + amount: BigInt(11), + } + + await token.transfer({ ...transferArgs, ...MOCKED_TX_INVOCATION }) + + expect(invokeSpy).toHaveBeenCalledExactlyOnceWith({ + method: methods.transfer, + methodArgs: { ...transferArgs, from: new Address(from), to: new Address(to) }, + ...MOCKED_TX_INVOCATION, + }) + }) + + it('should allow a spender to transfer from an account to another', async () => { + const from = 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI' + const to = 'GBC7ALQTV35BQ3URS5NUHHSWWN3KEXMRGFJ2S4S6VETJ6QA7QCDA64VI' + const spender = 'GBDDXMKAGNMZIG6LAVARGB6PHWK2WXBDJDDLD7F4CCTRWZ62BXV7SM2W' + const transferArgs = { + from, + to, + spender, + amount: BigInt(11), + } + + await token.transferFrom({ ...transferArgs, ...MOCKED_TX_INVOCATION }) + + expect(invokeSpy).toHaveBeenCalledExactlyOnceWith({ + method: methods.transfer_from, + methodArgs: { ...transferArgs, from: new Address(from), to: new Address(to), spender: new Address(spender) }, + ...MOCKED_TX_INVOCATION, + }) + }) + + it('should allow a spender to burn from an account', async () => { + const from = 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI' + + const spender = 'GBDDXMKAGNMZIG6LAVARGB6PHWK2WXBDJDDLD7F4CCTRWZ62BXV7SM2W' + const burnFromArgs = { + from, + + spender, + amount: BigInt(11), + } + + await token.burnFrom({ ...burnFromArgs, ...MOCKED_TX_INVOCATION }) + + expect(invokeSpy).toHaveBeenCalledExactlyOnceWith({ + method: methods.burn_from, + methodArgs: { ...burnFromArgs, from: new Address(from), spender: new Address(spender) }, + ...MOCKED_TX_INVOCATION, + }) + }) + }) + + describe('Core read from contract methods', () => { + let token: SorobanTokenHandler + let readSpy: jest.SpyInstance + + beforeEach(() => { + jest.clearAllMocks() + + MOCKED_SOROBAN_TRANSACTION_PIPELINE.mockImplementation(() => { + return { + execute: MOCKED_EXECUTE, + } + }) + + token = new SorobanTokenHandler({ + networkConfig: NETWORK_CONFIG, + contractParameters: { + contractId: MOCKED_CONTRACT_ID, + }, + }) + + readSpy = jest.spyOn(token as any, 'readFromContract') + }) + + afterEach(() => { + expect(MOCKED_EXECUTE).toHaveBeenCalledOnce() + }) + + it('should read the symbol of the token', async () => { + await token.symbol(MOCKED_TX_INVOCATION) + + expect(readSpy).toHaveBeenCalledExactlyOnceWith({ + method: methods.symbol, + methodArgs: {}, + header: MOCKED_TX_INVOCATION.header, + }) + }) + + it('should read the name of the token', async () => { + await token.name(MOCKED_TX_INVOCATION) + + expect(readSpy).toHaveBeenCalledExactlyOnceWith({ + method: methods.name, + methodArgs: {}, + header: MOCKED_TX_INVOCATION.header, + }) + }) + + it('should read the decimals of the token', async () => { + await token.decimals(MOCKED_TX_INVOCATION) + + expect(readSpy).toHaveBeenCalledExactlyOnceWith({ + method: methods.decimals, + methodArgs: {}, + header: MOCKED_TX_INVOCATION.header, + }) + }) + + // TODO: Review the default SPEC file under constants + // It seems it is not fully compliant with CAP46 + it.skip('should read the admin of the token', async () => { + await token.admin(MOCKED_TX_INVOCATION) + + expect(readSpy).toHaveBeenCalledExactlyOnceWith({ + method: methods.admin, + methodArgs: {}, + header: MOCKED_TX_INVOCATION.header, + }) + }) + + it('should read the balance of an account', async () => { + const balanceArgs = { + id: 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI', + } + + await token.balance({ ...balanceArgs, ...MOCKED_TX_INVOCATION }) + + expect(readSpy).toHaveBeenCalledExactlyOnceWith({ + method: methods.balance, + methodArgs: { id: new Address(balanceArgs.id) }, + header: MOCKED_TX_INVOCATION.header, + }) + }) + + // TODO: Review the default SPEC file under constants + // It seems it is not fully compliant with CAP46 + it.skip('should read the spendable balance of an account', async () => { + const spendableBalanceArgs = { + id: 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI', + } + + await token.spendableBalance({ ...spendableBalanceArgs, ...MOCKED_TX_INVOCATION }) + + expect(readSpy).toHaveBeenCalledExactlyOnceWith({ + method: methods.spendable_balance, + methodArgs: { id: new Address(spendableBalanceArgs.id) }, + header: MOCKED_TX_INVOCATION.header, + }) + }) + + it('should read the allowance for a spender an account', async () => { + const allowanceArgs = { + from: 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI', + spender: 'GBDDXMKAGNMZIG6LAVARGB6PHWK2WXBDJDDLD7F4CCTRWZ62BXV7SM2W', + } + + await token.allowance({ ...allowanceArgs, ...MOCKED_TX_INVOCATION }) + + expect(readSpy).toHaveBeenCalledExactlyOnceWith({ + method: methods.allowance, + methodArgs: { from: new Address(allowanceArgs.from), spender: new Address(allowanceArgs.spender) }, + header: MOCKED_TX_INVOCATION.header, + }) + }) + }) +}) From cf4b2e90d5e51338b7a15f9a171e61a2c11378ca Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Wed, 24 Apr 2024 15:31:33 -0300 Subject: [PATCH 27/30] feat: remove helper classes for account (#132) * feat: remove helper classes for account * refactor: remove unused error for base account * test: complete unit coverage for base account errors --- .../account/account-handler/default/index.ts | 4 +- .../account-handler/freighter/index.ts | 6 +- .../account/account-handler/types.ts | 8 +- .../{helpers/friendbot => base}/errors.ts | 78 +++++--- src/stellar-plus/account/base/index.ts | 84 +++++++- .../account/base/index.unit.test.ts | 180 +++++++++++++++--- src/stellar-plus/account/base/types.ts | 20 +- .../helpers/account-data-viewer/index.ts | 39 ---- .../helpers/account-data-viewer/types.ts | 18 -- .../account/helpers/friendbot/index.ts | 48 ----- .../account/helpers/friendbot/types.ts | 9 - src/stellar-plus/account/helpers/index.ts | 17 -- src/stellar-plus/account/helpers/types.ts | 8 - src/stellar-plus/account/index.ts | 2 +- src/stellar-plus/error/types.ts | 4 +- .../test/mocks/transaction-mock.ts | 2 + 16 files changed, 316 insertions(+), 211 deletions(-) rename src/stellar-plus/account/{helpers/friendbot => base}/errors.ts (56%) delete mode 100644 src/stellar-plus/account/helpers/account-data-viewer/index.ts delete mode 100644 src/stellar-plus/account/helpers/account-data-viewer/types.ts delete mode 100644 src/stellar-plus/account/helpers/friendbot/index.ts delete mode 100644 src/stellar-plus/account/helpers/friendbot/types.ts delete mode 100644 src/stellar-plus/account/helpers/index.ts delete mode 100644 src/stellar-plus/account/helpers/types.ts diff --git a/src/stellar-plus/account/account-handler/default/index.ts b/src/stellar-plus/account/account-handler/default/index.ts index 0cf9b1c..501477c 100644 --- a/src/stellar-plus/account/account-handler/default/index.ts +++ b/src/stellar-plus/account/account-handler/default/index.ts @@ -1,12 +1,12 @@ import { FeeBumpTransaction, Keypair, Transaction, authorizeEntry, xdr } from '@stellar/stellar-sdk' import { DefaultAccountHandler, DefaultAccountHandlerPayload } from 'stellar-plus/account/account-handler/default/types' -import { AccountBaseClient } from 'stellar-plus/account/base' +import { AccountBase } from 'stellar-plus/account/base' import { TransactionXdr } from 'stellar-plus/types' import { DAHError } from './errors' -export class DefaultAccountHandlerClient extends AccountBaseClient implements DefaultAccountHandler { +export class DefaultAccountHandlerClient extends AccountBase implements DefaultAccountHandler { protected secretKey: string /** diff --git a/src/stellar-plus/account/account-handler/freighter/index.ts b/src/stellar-plus/account/account-handler/freighter/index.ts index f9ce455..34073c7 100644 --- a/src/stellar-plus/account/account-handler/freighter/index.ts +++ b/src/stellar-plus/account/account-handler/freighter/index.ts @@ -14,13 +14,13 @@ import { FreighterAccountHandler, FreighterCallback, } from 'stellar-plus/account/account-handler/freighter/types' -import { AccountBaseClient } from 'stellar-plus/account/base' +import { AccountBase } from 'stellar-plus/account/base' import { NetworkConfig } from 'stellar-plus/types' import { FAHError } from './errors' -export class FreighterAccountHandlerClient extends AccountBaseClient implements FreighterAccountHandler { - private networkConfig: NetworkConfig +export class FreighterAccountHandlerClient extends AccountBase implements FreighterAccountHandler { + protected networkConfig: NetworkConfig /** * diff --git a/src/stellar-plus/account/account-handler/types.ts b/src/stellar-plus/account/account-handler/types.ts index fdca77f..c26008f 100644 --- a/src/stellar-plus/account/account-handler/types.ts +++ b/src/stellar-plus/account/account-handler/types.ts @@ -1,7 +1,8 @@ import { FeeBumpTransaction, Transaction, xdr } from '@stellar/stellar-sdk' +import { HorizonHandler } from 'stellar-plus' import { AccountBase } from 'stellar-plus/account/base/types' -import { AccountHelpersPayload } from 'stellar-plus/account/helpers/types' +import { NetworkConfig } from 'stellar-plus/constants' import { TransactionXdr } from 'stellar-plus/types' export type AccountHandler = AccountBase & { @@ -15,7 +16,10 @@ export type AccountHandler = AccountBase & { signatureSchema?: SignatureSchema } -export type AccountHandlerPayload = AccountHelpersPayload +export type AccountHandlerPayload = { + networkConfig?: NetworkConfig + horizonHandler?: HorizonHandler +} export type SignatureSchema = { threasholds: { diff --git a/src/stellar-plus/account/helpers/friendbot/errors.ts b/src/stellar-plus/account/base/errors.ts similarity index 56% rename from src/stellar-plus/account/helpers/friendbot/errors.ts rename to src/stellar-plus/account/base/errors.ts index ff5ea92..263860e 100644 --- a/src/stellar-plus/account/helpers/friendbot/errors.ts +++ b/src/stellar-plus/account/base/errors.ts @@ -3,47 +3,40 @@ import { AxiosError } from 'axios' import { StellarPlusError } from 'stellar-plus/error' import { AxiosErrorTypes, extractAxiosErrorInfo } from 'stellar-plus/error/helpers/axios' -export enum FriendbotErrorCodes { - // F0 General - FB001 = 'FB001', - FB002 = 'FB002', - FB003 = 'FB003', - // F1 Account creation - FB100 = 'FB100', - FB101 = 'FB101', - FB102 = 'FB102', - FB103 = 'FB103', +export enum AccountBaseErrorCodes { + // AB0 General + AB001 = 'AB001', + + // AB1 Account creation + AB100 = 'AB100', + AB101 = 'AB101', + AB102 = 'AB102', + AB103 = 'AB103', + + // AB2 Loading from Horizon + AB200 = 'AB200', + AB201 = 'AB201', } const friendbotNotAvailableError = (error?: Error): StellarPlusError => { return new StellarPlusError({ - code: FriendbotErrorCodes.FB001, + code: AccountBaseErrorCodes.AB001, message: 'Friendbot not available!', - source: 'Friendbot', + source: 'AccountBase', details: 'Friendbot is only available in test networks such as the Testnet and Futurenet. Make sure that the Network configuration object contains a valid Friendbot URL.', meta: { error }, }) } -const accountHasNoValidPublicKeyError = (error?: Error): StellarPlusError => { - return new StellarPlusError({ - code: FriendbotErrorCodes.FB002, - message: 'Account has no valid public key!', - source: 'Friendbot', - details: - 'The account has no valid public key. Make sure that this account instance has been initialized correctly and contains a valid public key.', - meta: { error }, - }) -} - const failedToCreateAccountWithFriendbotError = (error?: Error): StellarPlusError => { const axiosError = extractAxiosErrorInfo(error as AxiosError) + if (axiosError.type === AxiosErrorTypes.AxiosRequestError) { return new StellarPlusError({ - code: FriendbotErrorCodes.FB101, + code: AccountBaseErrorCodes.AB101, message: 'Failed request when initializing account with friendbot!', - source: 'Friendbot', + source: 'AccountBase', details: 'The request failed when initializing the account with the friendbot. Make sure that the network is available and that friendbot URL is correct.', meta: { axiosError, error }, @@ -52,9 +45,9 @@ const failedToCreateAccountWithFriendbotError = (error?: Error): StellarPlusErro if (axiosError.type === AxiosErrorTypes.AxiosResponseError) { return new StellarPlusError({ - code: FriendbotErrorCodes.FB102, + code: AccountBaseErrorCodes.AB102, message: 'Failed response when initializing account with friendbot!', - source: 'Friendbot', + source: 'AccountBase', details: 'Received a failed response when initializing the account with the friendbot. Make sure the account has not been already initialized.', meta: { axiosError, error }, @@ -62,16 +55,39 @@ const failedToCreateAccountWithFriendbotError = (error?: Error): StellarPlusErro } return new StellarPlusError({ - code: FriendbotErrorCodes.FB100, + code: AccountBaseErrorCodes.AB100, message: 'Unknown error when initializing account with friendbot!', - source: 'Friendbot', + source: 'AccountBase', details: 'An unexpected error occured during the friendbot invocation to initialize an account.', meta: { axiosError, error }, }) } -export const FBError = { - accountHasNoValidPublicKeyError, +const horizonHandlerNotAvailableError = (error?: Error): StellarPlusError => { + return new StellarPlusError({ + code: AccountBaseErrorCodes.AB200, + message: 'Horizon handler not available!', + source: 'AccountBase', + details: + 'Horizon handler is not available. Make sure that the Horizon handler is correctly initialized by providing an instance of Horizon handler or a network configuration when instancing the account.', + meta: { error }, + }) +} + +const failedToLoadBalances = (error?: Error): StellarPlusError => { + return new StellarPlusError({ + code: AccountBaseErrorCodes.AB201, + message: 'Failed to load balances!', + source: 'AccountBase', + details: + 'Failed to load the account balances from the Horizon server. Make sure that the Horizon handler is correctly initialized and that the account has been correctly initialized.', + meta: { error }, + }) +} + +export const ABError = { failedToCreateAccountWithFriendbotError, friendbotNotAvailableError, + horizonHandlerNotAvailableError, + failedToLoadBalances, } diff --git a/src/stellar-plus/account/base/index.ts b/src/stellar-plus/account/base/index.ts index 56b4b8d..b7a2243 100644 --- a/src/stellar-plus/account/base/index.ts +++ b/src/stellar-plus/account/base/index.ts @@ -1,8 +1,15 @@ -import { AccountBase, AccountBasePayload } from 'stellar-plus/account/base/types' -import { AccountHelpers } from 'stellar-plus/account/helpers' +import { Horizon } from '@stellar/stellar-sdk' +import axios from 'axios' -export class AccountBaseClient extends AccountHelpers implements AccountBase { +import { ABError } from 'stellar-plus/account/base/errors' +import { AccountBasePayload, AccountBase as AccountBaseType } from 'stellar-plus/account/base/types' +import { NetworkConfig } from 'stellar-plus/constants' +import { HorizonHandlerClient as HorizonHandler } from 'stellar-plus/horizon/' + +export class AccountBase implements AccountBaseType { protected publicKey: string + protected networkConfig?: NetworkConfig + protected horizonHandler?: HorizonHandler /** * @@ -13,10 +20,15 @@ export class AccountBaseClient extends AccountHelpers implements AccountBase { * @description - The base account is used for handling accounts with no management actions. */ constructor(payload: AccountBasePayload) { - super(payload) - const { publicKey } = payload as { publicKey: string } + const { publicKey, networkConfig, horizonHandler } = payload this.publicKey = publicKey + this.networkConfig = networkConfig + this.horizonHandler = horizonHandler as HorizonHandler + + if (this.networkConfig && !this.horizonHandler) { + this.horizonHandler = new HorizonHandler(this.networkConfig) + } } /** @@ -27,4 +39,66 @@ export class AccountBaseClient extends AccountHelpers implements AccountBase { getPublicKey(): string { return this.publicKey } + + /** + * + * @returns {void} + * @description - Initialize the account with the friendbot and funds it with 10.000 XLM. + */ + public async initializeWithFriendbot(): Promise { + this.requireTestNetwork() + + try { + await axios.get( + `${this.networkConfig!.friendbotUrl}?addr=${encodeURIComponent(this.publicKey)}` // friendbot URL in networkConfig validated in requireTestNetwork() + ) + + return + } catch (e) { + throw ABError.failedToCreateAccountWithFriendbotError(e as Error) + } + } + + /** + * + * @returns {Horizon.BalanceLine[]} A list of the account's balances. + * @description - The account's balances are retrieved from the Horizon server and provided in a list, including all assets. + */ + public async getBalances(): Promise< + ( + | Horizon.HorizonApi.BalanceLineNative + | Horizon.HorizonApi.BalanceLineAsset<'credit_alphanum4'> + | Horizon.HorizonApi.BalanceLineAsset<'credit_alphanum12'> + | Horizon.HorizonApi.BalanceLineLiquidityPool + )[] + > { + this.requireHorizonHandler() + + try { + const account = await this.horizonHandler!.loadAccount(this.publicKey) // Horizon handler validated in requireHorizonHandler() + return account.balances + } catch (error) { + throw ABError.failedToLoadBalances(error as Error) + } + } + + /** + * + * @description - Throws an error if the network is not a test network. + */ + protected requireTestNetwork(): void { + if (!this.networkConfig?.friendbotUrl) { + throw ABError.friendbotNotAvailableError() + } + } + + /** + * + * @description - Throws an error if the horizon handler is not set + */ + protected requireHorizonHandler(): void { + if (!this.horizonHandler) { + throw ABError.horizonHandlerNotAvailableError() + } + } } diff --git a/src/stellar-plus/account/base/index.unit.test.ts b/src/stellar-plus/account/base/index.unit.test.ts index de0aab0..423621d 100644 --- a/src/stellar-plus/account/base/index.unit.test.ts +++ b/src/stellar-plus/account/base/index.unit.test.ts @@ -1,37 +1,171 @@ -import { AccountBaseClient } from 'stellar-plus/account/base' +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import axios, { AxiosError } from 'axios' + +import { AccountBase } from 'stellar-plus/account/base' +import { ABError } from 'stellar-plus/account/base/errors' import { testnet } from 'stellar-plus/constants' +import { AxiosErrorTypes } from 'stellar-plus/error/helpers/axios' +import { HorizonHandler } from 'stellar-plus/horizon/types' + +jest.mock('axios', () => { + const originalModule = jest.requireActual('axios') + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...originalModule, + get: jest.fn(), + } +}) + +const MOCKED_AXIOS_GET = axios.get as jest.Mock const TESTNET_CONFIG = testnet const MOCKED_PK = 'GAUFIAL2LV2OV7EA4NTXZDVPQASGI5Y3EXZV2HQS3UUWMZ7UWJDQURYS' describe('Base Account Handler', () => { - it('should initialize the base account handler with a public key', () => { - const account = new AccountBaseClient({ publicKey: MOCKED_PK }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment - const spyPublicKey = jest.mocked((account as any).publicKey) - - expect(account).toBeDefined() - expect(account).toBeInstanceOf(AccountBaseClient) - expect(spyPublicKey).toBe(MOCKED_PK) + describe('Initialization', () => { + it('should initialize the base account handler with a public key', () => { + const account = new AccountBase({ publicKey: MOCKED_PK }) + const spyPublicKey = jest.mocked((account as any).publicKey) + + expect(account).toBeDefined() + expect(account).toBeInstanceOf(AccountBase) + expect(spyPublicKey).toBe(MOCKED_PK) + }) + + it('should initialize with optional parameters', () => { + const mockedHorizonHandler = jest.fn() as unknown as HorizonHandler + const account = new AccountBase({ + publicKey: MOCKED_PK, + networkConfig: TESTNET_CONFIG, + horizonHandler: mockedHorizonHandler, + }) + const spyNetworkConfig = jest.mocked((account as any).networkConfig) + const spyHorizonHandler = jest.mocked((account as any).horizonHandler) + + expect(account).toBeDefined() + expect(account).toBeInstanceOf(AccountBase) + expect(spyNetworkConfig).toBe(TESTNET_CONFIG) + expect(spyHorizonHandler).toBe(mockedHorizonHandler) + }) }) - it('should initialize the base account handler with a network config, enabling helpers', () => { - const account = new AccountBaseClient({ publicKey: MOCKED_PK, networkConfig: TESTNET_CONFIG }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment - const spyAccountDataViewer = jest.mocked((account as any).accountDataViewer) - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment - const spyFriendbot = jest.mocked((account as any).friendbot) - - expect(account).toBeDefined() - expect(account).toBeInstanceOf(AccountBaseClient) - expect(spyAccountDataViewer).toBeDefined() - expect(spyFriendbot).toBeDefined() + describe('Core Functionalities', () => { + let account: AccountBase + const mockedLoadAccount = jest.fn() + + beforeEach(() => { + jest.clearAllMocks() + account = new AccountBase({ + publicKey: MOCKED_PK, + networkConfig: TESTNET_CONFIG, + horizonHandler: jest.mocked({ + loadAccount: mockedLoadAccount, + } as unknown as HorizonHandler), + }) + }) + + it('should return the public key of the account', () => { + expect(account.getPublicKey()).toBe(MOCKED_PK) + }) + + it('should initialize the account with the friendbot', async () => { + MOCKED_AXIOS_GET.mockResolvedValue({ data: 'Success' }) + + await account.initializeWithFriendbot() + + expect(MOCKED_AXIOS_GET).toHaveBeenCalledExactlyOnceWith( + `${TESTNET_CONFIG.friendbotUrl}?addr=${encodeURIComponent(MOCKED_PK)}` + ) + }) + + it('should load the account balances', async () => { + const mockedBalances = [ + { + asset_type: 'native', + balance: '10.0000000', + }, + ] + mockedLoadAccount.mockResolvedValue({ balances: mockedBalances }) + + const balances = await account.getBalances() + + expect(balances).toBe(mockedBalances) + expect(mockedLoadAccount).toHaveBeenCalledExactlyOnceWith(MOCKED_PK) + }) }) - it('should return the public key of the account', () => { - const account = new AccountBaseClient({ publicKey: MOCKED_PK }) + describe('Error Handling', () => { + let account: AccountBase + const mockedLoadAccount = jest.fn() + + beforeEach(() => { + jest.clearAllMocks() + account = new AccountBase({ + publicKey: MOCKED_PK, + networkConfig: TESTNET_CONFIG, + horizonHandler: jest.mocked({ + loadAccount: mockedLoadAccount, + } as unknown as HorizonHandler), + }) + }) + + it('should throw an error if friendbot fails to initialize the account', async () => { + const mockedError = new Error('Failed to initialize with friendbot') + MOCKED_AXIOS_GET.mockRejectedValue(mockedError) + + await expect(account.initializeWithFriendbot()).rejects.toThrow( + ABError.failedToCreateAccountWithFriendbotError(mockedError) + ) + }) + + it('should throw an error if the request to friendbot fails', async () => { + const mockedAxiosError = jest.mocked({ + message: 'Failed request', + type: AxiosErrorTypes.AxiosRequestError, + request: {}, + }) as unknown as AxiosError + + MOCKED_AXIOS_GET.mockRejectedValue(mockedAxiosError) + + await expect(account.initializeWithFriendbot()).rejects.toThrow( + ABError.failedToCreateAccountWithFriendbotError(mockedAxiosError) + ) + }) + + it('should throw an error if the response from friendbot fails', async () => { + const mockedAxiosError = jest.mocked({ + message: 'Failed response', + type: AxiosErrorTypes.AxiosResponseError, + response: {}, + }) as unknown as AxiosError + + MOCKED_AXIOS_GET.mockRejectedValue(mockedAxiosError) + + await expect(account.initializeWithFriendbot()).rejects.toThrow( + ABError.failedToCreateAccountWithFriendbotError(mockedAxiosError) + ) + }) + + it('should throw an error if the account balances cannot be loaded', async () => { + const mockedError = new Error('Failed to load account balances') + mockedLoadAccount.mockRejectedValue(mockedError) + + await expect(account.getBalances()).rejects.toThrow(ABError.failedToLoadBalances(mockedError)) + expect(mockedLoadAccount).toHaveBeenCalledExactlyOnceWith(MOCKED_PK) + }) + + it('should throw an error if the friendbot is not available', async () => { + account = new AccountBase({ publicKey: MOCKED_PK }) + + await expect(account.initializeWithFriendbot()).rejects.toThrow(ABError.friendbotNotAvailableError()) + }) + + it('should throw an error if the horizon handler is not available', async () => { + account = new AccountBase({ publicKey: MOCKED_PK }) - expect(account.getPublicKey()).toBe(MOCKED_PK) + await expect(account.getBalances()).rejects.toThrow(ABError.horizonHandlerNotAvailableError()) + }) }) }) diff --git a/src/stellar-plus/account/base/types.ts b/src/stellar-plus/account/base/types.ts index c79e5ea..6b2e4ce 100644 --- a/src/stellar-plus/account/base/types.ts +++ b/src/stellar-plus/account/base/types.ts @@ -1,9 +1,23 @@ -import { AccountHelpers, AccountHelpersPayload } from 'stellar-plus/account/helpers/types' +import { Horizon } from '@stellar/stellar-sdk' -export type AccountBase = AccountHelpers & { +import { NetworkConfig } from 'stellar-plus/constants' +import { HorizonHandler } from 'stellar-plus/horizon/types' + +export type AccountBase = { getPublicKey(): string + initializeWithFriendbot(): Promise + getBalances(): Promise< + ( + | Horizon.HorizonApi.BalanceLineNative + | Horizon.HorizonApi.BalanceLineAsset<'credit_alphanum4'> + | Horizon.HorizonApi.BalanceLineAsset<'credit_alphanum12'> + | Horizon.HorizonApi.BalanceLineLiquidityPool + )[] + > } -export type AccountBasePayload = AccountHelpersPayload & { +export type AccountBasePayload = { publicKey: string + networkConfig?: NetworkConfig + horizonHandler?: HorizonHandler } diff --git a/src/stellar-plus/account/helpers/account-data-viewer/index.ts b/src/stellar-plus/account/helpers/account-data-viewer/index.ts deleted file mode 100644 index 85b5ac0..0000000 --- a/src/stellar-plus/account/helpers/account-data-viewer/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Horizon } from '@stellar/stellar-sdk' - -import { AccountHelpers } from 'stellar-plus/account/helpers' -import { AccountDataViewer } from 'stellar-plus/account/helpers/account-data-viewer/types' -import { HorizonHandlerClient } from 'stellar-plus/horizon/index' -import { HorizonHandler } from 'stellar-plus/horizon/types' -import { NetworkConfig } from 'stellar-plus/types' - -export class AccountDataViewerClient implements AccountDataViewer { - private networkConfig: NetworkConfig - private horizonHandler: HorizonHandler - private parent: AccountHelpers - constructor(networkConfig: NetworkConfig, parent: AccountHelpers) { - this.networkConfig = networkConfig - this.horizonHandler = new HorizonHandlerClient(this.networkConfig) as HorizonHandler - this.parent = parent - } - - /** - * - * @returns {Horizon.BalanceLine[]} A list of the account's balances. - * @description - The account's balances are retrieved from the Horizon server and provided in a list, including all assets. - */ - public async getBalances(): Promise< - ( - | Horizon.HorizonApi.BalanceLineNative - | Horizon.HorizonApi.BalanceLineAsset<'credit_alphanum4'> - | Horizon.HorizonApi.BalanceLineAsset<'credit_alphanum12'> - | Horizon.HorizonApi.BalanceLineLiquidityPool - )[] - > { - if ('publicKey' in this.parent && this.parent.publicKey && this.parent.publicKey !== '') { - const account = await this.horizonHandler.loadAccount(this.parent.publicKey as string) - return account.balances - } - - throw new Error('Account has no valid public key!') - } -} diff --git a/src/stellar-plus/account/helpers/account-data-viewer/types.ts b/src/stellar-plus/account/helpers/account-data-viewer/types.ts deleted file mode 100644 index 66a4e13..0000000 --- a/src/stellar-plus/account/helpers/account-data-viewer/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Horizon } from '@stellar/stellar-sdk' - -import { NetworkConfig } from 'stellar-plus/types' - -export type AccountDataViewer = { - getBalances(): Promise< - ( - | Horizon.HorizonApi.BalanceLineNative - | Horizon.HorizonApi.BalanceLineAsset<'credit_alphanum4'> - | Horizon.HorizonApi.BalanceLineAsset<'credit_alphanum12'> - | Horizon.HorizonApi.BalanceLineLiquidityPool - )[] - > -} - -export type AccountDataViewerConstructor = { - networkConfig?: NetworkConfig -} diff --git a/src/stellar-plus/account/helpers/friendbot/index.ts b/src/stellar-plus/account/helpers/friendbot/index.ts deleted file mode 100644 index 62caf2d..0000000 --- a/src/stellar-plus/account/helpers/friendbot/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -import axios from 'axios' - -import { AccountHelpers } from 'stellar-plus/account/helpers' -import { Friendbot } from 'stellar-plus/account/helpers/friendbot/types' -import { NetworkConfig } from 'stellar-plus/types' - -import { FBError } from './errors' - -export class FriendbotClient implements Friendbot { - private networkConfig: NetworkConfig - private parent: AccountHelpers - constructor(networkConfig: NetworkConfig, parent: AccountHelpers) { - this.networkConfig = networkConfig - this.parent = parent - } - - /** - * - * @returns {void} - * @description - Initialize the account with the friendbot and funds it with 10.000 XLM. - */ - public async initialize(): Promise { - this.requireTestNetwork() - - if ('publicKey' in this.parent && this.parent.publicKey && this.parent.publicKey !== '') { - try { - await axios.get( - `${this.networkConfig.friendbotUrl}?addr=${encodeURIComponent(this.parent.publicKey as string)}` - ) - - return - } catch (e) { - throw FBError.failedToCreateAccountWithFriendbotError(e as Error) - } - } - throw FBError.accountHasNoValidPublicKeyError() - } - - /** - * - * @description - Throws an error if the network is not a test network. - */ - private requireTestNetwork(): void { - if (!this.networkConfig.friendbotUrl) { - throw FBError.friendbotNotAvailableError() - } - } -} diff --git a/src/stellar-plus/account/helpers/friendbot/types.ts b/src/stellar-plus/account/helpers/friendbot/types.ts deleted file mode 100644 index 2a14cb2..0000000 --- a/src/stellar-plus/account/helpers/friendbot/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { NetworkConfig } from 'stellar-plus/types' - -export type Friendbot = { - initialize(): Promise -} - -export type FriendbotConstructor = { - networkConfig?: NetworkConfig -} diff --git a/src/stellar-plus/account/helpers/index.ts b/src/stellar-plus/account/helpers/index.ts deleted file mode 100644 index 1e38e85..0000000 --- a/src/stellar-plus/account/helpers/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { AccountDataViewerClient } from 'stellar-plus/account/helpers/account-data-viewer' -import { AccountDataViewer } from 'stellar-plus/account/helpers/account-data-viewer/types' -import { FriendbotClient } from 'stellar-plus/account/helpers/friendbot' -import { Friendbot } from 'stellar-plus/account/helpers/friendbot/types' -import { AccountHelpersPayload, AccountHelpers as AccountHelpersType } from 'stellar-plus/account/helpers/types' - -export class AccountHelpers implements AccountHelpersType { - public accountDataViewer?: AccountDataViewer - public friendbot?: Friendbot - - constructor(payload: AccountHelpersPayload) { - if ('networkConfig' in payload && payload.networkConfig) { - this.accountDataViewer = new AccountDataViewerClient(payload.networkConfig, this) - this.friendbot = new FriendbotClient(payload.networkConfig, this) - } - } -} diff --git a/src/stellar-plus/account/helpers/types.ts b/src/stellar-plus/account/helpers/types.ts deleted file mode 100644 index 50734d8..0000000 --- a/src/stellar-plus/account/helpers/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { AccountDataViewer, AccountDataViewerConstructor } from 'stellar-plus/account/helpers/account-data-viewer/types' -import { Friendbot } from 'stellar-plus/account/helpers/friendbot/types' - -export type AccountHelpersPayload = AccountDataViewerConstructor -export type AccountHelpers = { - accountDataViewer?: AccountDataViewer - friendbot?: Friendbot -} diff --git a/src/stellar-plus/account/index.ts b/src/stellar-plus/account/index.ts index 658da24..7beb26e 100644 --- a/src/stellar-plus/account/index.ts +++ b/src/stellar-plus/account/index.ts @@ -1,7 +1,7 @@ import { DefaultAccountHandlerClient as DefaultAccountHandler } from 'stellar-plus/account/account-handler/default' import { FreighterAccountHandlerClient as FreighterAccountHandler } from 'stellar-plus/account/account-handler/freighter' import { AccountHandler as _AccountHandler } from 'stellar-plus/account/account-handler/types' -import { AccountBaseClient as Base } from 'stellar-plus/account/base' +import { AccountBase as Base } from 'stellar-plus/account/base' import { AccountBase } from 'stellar-plus/account/base/types' export { Base, FreighterAccountHandler, DefaultAccountHandler } diff --git a/src/stellar-plus/error/types.ts b/src/stellar-plus/error/types.ts index 5358646..c1c6724 100644 --- a/src/stellar-plus/error/types.ts +++ b/src/stellar-plus/error/types.ts @@ -1,6 +1,6 @@ import { DefaultAccountHandlerErrorCodes } from 'stellar-plus/account/account-handler/default/errors' import { FreighterAccountHandlerErrorCodes } from 'stellar-plus/account/account-handler/freighter/errors' -import { FriendbotErrorCodes } from 'stellar-plus/account/helpers/friendbot/errors' +import { AccountBaseErrorCodes } from 'stellar-plus/account/base/errors' import { ClassicAssetHandlerErrorCodes } from 'stellar-plus/asset/classic/errors' import { ChannelAccountsErrorCodes } from 'stellar-plus/channel-accounts/errors' import { ContractEngineErrorCodes } from 'stellar-plus/core/contract-engine/errors' @@ -30,7 +30,7 @@ export type StellarPlusErrorObject = { export type ErrorCodes = | GeneralErrorCodes - | FriendbotErrorCodes + | AccountBaseErrorCodes | ContractEngineErrorCodes | ChannelAccountsErrorCodes | ErrorCodesPipelineFeeBump diff --git a/src/stellar-plus/test/mocks/transaction-mock.ts b/src/stellar-plus/test/mocks/transaction-mock.ts index 6542f77..5f2a1a4 100644 --- a/src/stellar-plus/test/mocks/transaction-mock.ts +++ b/src/stellar-plus/test/mocks/transaction-mock.ts @@ -61,6 +61,8 @@ export function mockAccountHandler({ signSorobanAuthEntry: jest.fn().mockReturnValue(outputSignedAuthEntry ?? xdr.SorobanAuthorizationEntry), getPublicKey: jest.fn().mockReturnValue(accountKey), signatureSchema, + getBalances: jest.fn().mockReturnValue([]), + initializeWithFriendbot: jest.fn(), } } From c008ca6be0a012229eff1220839ebb79468eac24 Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Thu, 25 Apr 2024 09:42:24 -0300 Subject: [PATCH 28/30] Standardize default network configs and custom (#133) * feat: standardize network config options as functions under network * test: adjust tests for new network config format * feat: require rpc url for default rpc handler to initialize * feat: require horizon url to initialize default horizon handler * test: remove local testing from coverage * feat: remove soroban client * feat: convert horizon load account error to sp error * feat: add allowhttp to network config * test: add unit tests to default horizon handler * test: add default rpc handler unit tests * fix: account changes with network new config format * test: add network configuration unit tests * refactor: allowhttp object Co-authored-by: Caio Teixeira --------- Co-authored-by: Caio Teixeira --- src/jest.config.js | 1 + .../account/account-handler/default/index.ts | 3 +- .../default/index.unit.test.ts | 4 +- .../account-handler/freighter/index.ts | 3 +- .../freighter/index.unit.test.ts | 6 +- .../account-handler/freighter/types.ts | 2 +- .../account/account-handler/types.ts | 2 +- src/stellar-plus/account/base/index.ts | 2 +- .../account/base/index.unit.test.ts | 4 +- src/stellar-plus/account/base/types.ts | 2 +- .../asset/classic/index.unit.test.ts | 4 +- .../asset/soroban-token/index.unit.test.ts | 4 +- .../core/contract-engine/errors.ts | 7 +- .../core/contract-engine/index.unit.test.ts | 4 +- .../build-transaction/index.unit.test.ts | 4 +- .../index.unit.test.ts | 4 +- .../classic-transaction/index.unit.test.ts | 4 +- .../sign-transaction/index.unit.test.ts | 4 +- .../simulate-transaction/index.unit.test.ts | 4 +- .../pipelines/soroban-auth/index.unit.test.ts | 4 +- .../index.unit.test.ts | 4 +- .../soroban-transaction/index.unit.test.ts | 4 +- .../submit-transaction/index.unit.test.ts | 8 +- src/stellar-plus/error/types.ts | 17 ++- src/stellar-plus/horizon/errors.ts | 31 ++++ src/stellar-plus/horizon/index.ts | 17 ++- src/stellar-plus/horizon/index.unit.test.ts | 53 +++++++ src/stellar-plus/index.ts | 3 +- .../{constants.ts => network/index.ts} | 54 ++++--- src/stellar-plus/network/index.unit.test.ts | 59 ++++++++ .../rpc/default-handler/errors.ts | 20 +++ src/stellar-plus/rpc/default-handler/index.ts | 10 +- .../rpc/default-handler/index.unit.test.ts | 135 ++++++++++++++++++ src/stellar-plus/soroban/index.ts | 20 --- src/stellar-plus/soroban/types.ts | 5 - src/stellar-plus/test/mocks/constants.ts | 4 +- src/stellar-plus/types.ts | 2 +- 37 files changed, 410 insertions(+), 108 deletions(-) create mode 100644 src/stellar-plus/horizon/errors.ts create mode 100644 src/stellar-plus/horizon/index.unit.test.ts rename src/stellar-plus/{constants.ts => network/index.ts} (60%) create mode 100644 src/stellar-plus/network/index.unit.test.ts create mode 100644 src/stellar-plus/rpc/default-handler/errors.ts create mode 100644 src/stellar-plus/rpc/default-handler/index.unit.test.ts delete mode 100644 src/stellar-plus/soroban/index.ts delete mode 100644 src/stellar-plus/soroban/types.ts diff --git a/src/jest.config.js b/src/jest.config.js index 4d255ff..534887f 100644 --- a/src/jest.config.js +++ b/src/jest.config.js @@ -12,6 +12,7 @@ module.exports = { '!/node_modules/', '!/coverage/', ], + coveragePathIgnorePatterns: ['/index-test.ts'], setupFilesAfterEnv: ['./setup-tests.ts'], modulePathIgnorePatterns: ['/dist/'], moduleDirectories: ['node_modules', 'src'], diff --git a/src/stellar-plus/account/account-handler/default/index.ts b/src/stellar-plus/account/account-handler/default/index.ts index 501477c..4bc4666 100644 --- a/src/stellar-plus/account/account-handler/default/index.ts +++ b/src/stellar-plus/account/account-handler/default/index.ts @@ -1,11 +1,10 @@ import { FeeBumpTransaction, Keypair, Transaction, authorizeEntry, xdr } from '@stellar/stellar-sdk' +import { DAHError } from 'stellar-plus/account/account-handler/default/errors' import { DefaultAccountHandler, DefaultAccountHandlerPayload } from 'stellar-plus/account/account-handler/default/types' import { AccountBase } from 'stellar-plus/account/base' import { TransactionXdr } from 'stellar-plus/types' -import { DAHError } from './errors' - export class DefaultAccountHandlerClient extends AccountBase implements DefaultAccountHandler { protected secretKey: string diff --git a/src/stellar-plus/account/account-handler/default/index.unit.test.ts b/src/stellar-plus/account/account-handler/default/index.unit.test.ts index c5ec89c..3272f70 100644 --- a/src/stellar-plus/account/account-handler/default/index.unit.test.ts +++ b/src/stellar-plus/account/account-handler/default/index.unit.test.ts @@ -4,7 +4,7 @@ import { FeeBumpTransaction, Keypair, Transaction, authorizeEntry, xdr } from '@ import { DefaultAccountHandlerClient } from 'stellar-plus/account/account-handler/default' import { DAHError } from 'stellar-plus/account/account-handler/default/errors' -import { testnet } from 'stellar-plus/constants' +import { TestNet } from 'stellar-plus/network' jest.mock('@stellar/stellar-sdk', () => { // The mock doesnt spread the whole originalModule because some internal exported objects cause failures @@ -30,7 +30,7 @@ const MOCKED_SOROBAN_AUTH_ENTRY = { toXDR: jest.fn(), } as xdr.SorobanAuthorizationEntry -const TESTNET_CONFIG = testnet +const TESTNET_CONFIG = TestNet() describe('DefaultAccountHandler', () => { describe('Initialization', () => { diff --git a/src/stellar-plus/account/account-handler/freighter/index.ts b/src/stellar-plus/account/account-handler/freighter/index.ts index 34073c7..9b05b68 100644 --- a/src/stellar-plus/account/account-handler/freighter/index.ts +++ b/src/stellar-plus/account/account-handler/freighter/index.ts @@ -9,6 +9,7 @@ import { } from '@stellar/freighter-api' import { FeeBumpTransaction, Transaction, xdr } from '@stellar/stellar-sdk' +import { FAHError } from 'stellar-plus/account/account-handler/freighter/errors' import { FreighterAccHandlerPayload, FreighterAccountHandler, @@ -17,8 +18,6 @@ import { import { AccountBase } from 'stellar-plus/account/base' import { NetworkConfig } from 'stellar-plus/types' -import { FAHError } from './errors' - export class FreighterAccountHandlerClient extends AccountBase implements FreighterAccountHandler { protected networkConfig: NetworkConfig diff --git a/src/stellar-plus/account/account-handler/freighter/index.unit.test.ts b/src/stellar-plus/account/account-handler/freighter/index.unit.test.ts index d15ba07..950988d 100644 --- a/src/stellar-plus/account/account-handler/freighter/index.unit.test.ts +++ b/src/stellar-plus/account/account-handler/freighter/index.unit.test.ts @@ -3,7 +3,7 @@ import { FeeBumpTransaction, Transaction, xdr } from '@stellar/stellar-sdk' import { FreighterAccountHandlerClient } from 'stellar-plus/account/account-handler/freighter' import { FAHError } from 'stellar-plus/account/account-handler/freighter/errors' -import { testnet } from 'stellar-plus/constants' +import { TestNet } from 'stellar-plus/network' jest.mock('@stellar/freighter-api', () => ({ getPublicKey: jest.fn(), @@ -35,14 +35,14 @@ const mockFreighterGetPublicKey = (pk: string): void => { } const mockFreighterGetNetworkDetailsTestnet = (): void => { - MOCKED_GET_NETWORK_DETAILS.mockResolvedValue({ networkPassphrase: testnet.networkPassphrase }) + MOCKED_GET_NETWORK_DETAILS.mockResolvedValue({ networkPassphrase: TestNet().networkPassphrase }) } const mockFreighterGetNetworkDetailsWrongNetwork = (): void => { MOCKED_GET_NETWORK_DETAILS.mockResolvedValue({ networkPassphrase: 'wrong network' }) } -const TESTNET_CONFIG = testnet +const TESTNET_CONFIG = TestNet() const MOCKED_PK = 'GAUFIAL2LV2OV7EA4NTXZDVPQASGI5Y3EXZV2HQS3UUWMZ7UWJDQURYS' diff --git a/src/stellar-plus/account/account-handler/freighter/types.ts b/src/stellar-plus/account/account-handler/freighter/types.ts index f9289d8..93216a6 100644 --- a/src/stellar-plus/account/account-handler/freighter/types.ts +++ b/src/stellar-plus/account/account-handler/freighter/types.ts @@ -1,5 +1,5 @@ import { AccountHandler, AccountHandlerPayload } from 'stellar-plus/account/account-handler/types' -import { NetworkConfig } from 'stellar-plus/types' +import { NetworkConfig } from 'stellar-plus/network' export type FreighterAccountHandler = AccountHandler & { connect(onPublicKeyReceived: FreighterCallback): Promise diff --git a/src/stellar-plus/account/account-handler/types.ts b/src/stellar-plus/account/account-handler/types.ts index c26008f..84c0ded 100644 --- a/src/stellar-plus/account/account-handler/types.ts +++ b/src/stellar-plus/account/account-handler/types.ts @@ -2,7 +2,7 @@ import { FeeBumpTransaction, Transaction, xdr } from '@stellar/stellar-sdk' import { HorizonHandler } from 'stellar-plus' import { AccountBase } from 'stellar-plus/account/base/types' -import { NetworkConfig } from 'stellar-plus/constants' +import { NetworkConfig } from 'stellar-plus/network' import { TransactionXdr } from 'stellar-plus/types' export type AccountHandler = AccountBase & { diff --git a/src/stellar-plus/account/base/index.ts b/src/stellar-plus/account/base/index.ts index b7a2243..8a46a90 100644 --- a/src/stellar-plus/account/base/index.ts +++ b/src/stellar-plus/account/base/index.ts @@ -3,8 +3,8 @@ import axios from 'axios' import { ABError } from 'stellar-plus/account/base/errors' import { AccountBasePayload, AccountBase as AccountBaseType } from 'stellar-plus/account/base/types' -import { NetworkConfig } from 'stellar-plus/constants' import { HorizonHandlerClient as HorizonHandler } from 'stellar-plus/horizon/' +import { NetworkConfig } from 'stellar-plus/network' export class AccountBase implements AccountBaseType { protected publicKey: string diff --git a/src/stellar-plus/account/base/index.unit.test.ts b/src/stellar-plus/account/base/index.unit.test.ts index 423621d..9dddb9f 100644 --- a/src/stellar-plus/account/base/index.unit.test.ts +++ b/src/stellar-plus/account/base/index.unit.test.ts @@ -4,9 +4,9 @@ import axios, { AxiosError } from 'axios' import { AccountBase } from 'stellar-plus/account/base' import { ABError } from 'stellar-plus/account/base/errors' -import { testnet } from 'stellar-plus/constants' import { AxiosErrorTypes } from 'stellar-plus/error/helpers/axios' import { HorizonHandler } from 'stellar-plus/horizon/types' +import { TestNet } from 'stellar-plus/network' jest.mock('axios', () => { const originalModule = jest.requireActual('axios') @@ -19,7 +19,7 @@ jest.mock('axios', () => { const MOCKED_AXIOS_GET = axios.get as jest.Mock -const TESTNET_CONFIG = testnet +const TESTNET_CONFIG = TestNet() const MOCKED_PK = 'GAUFIAL2LV2OV7EA4NTXZDVPQASGI5Y3EXZV2HQS3UUWMZ7UWJDQURYS' diff --git a/src/stellar-plus/account/base/types.ts b/src/stellar-plus/account/base/types.ts index 6b2e4ce..64d9b4c 100644 --- a/src/stellar-plus/account/base/types.ts +++ b/src/stellar-plus/account/base/types.ts @@ -1,7 +1,7 @@ import { Horizon } from '@stellar/stellar-sdk' -import { NetworkConfig } from 'stellar-plus/constants' import { HorizonHandler } from 'stellar-plus/horizon/types' +import { NetworkConfig } from 'stellar-plus/network' export type AccountBase = { getPublicKey(): string diff --git a/src/stellar-plus/asset/classic/index.unit.test.ts b/src/stellar-plus/asset/classic/index.unit.test.ts index 13942d0..29dc21d 100644 --- a/src/stellar-plus/asset/classic/index.unit.test.ts +++ b/src/stellar-plus/asset/classic/index.unit.test.ts @@ -4,7 +4,6 @@ import { ClassicAssetHandler } from 'stellar-plus/asset/classic' import { CAHError } from 'stellar-plus/asset/classic/errors' -import { testnet } from 'stellar-plus/constants' import { BuildTransactionPipelinePlugin, BuildTransactionPipelineType, @@ -14,6 +13,7 @@ import { ClassicTransactionPipelineType, } from 'stellar-plus/core/pipelines/classic-transaction/types' import { HorizonHandlerClient } from 'stellar-plus/horizon' +import { TestNet } from 'stellar-plus/network' import { mockAccountHandler } from 'stellar-plus/test/mocks/transaction-mock' import { TransactionInvocation } from 'stellar-plus/types' @@ -43,7 +43,7 @@ jest.mock('stellar-plus/horizon', () => ({ const MOCKED_HORIZON_HANDLER = HorizonHandlerClient as jest.Mock -const TESTNET_CONFIG = testnet +const TESTNET_CONFIG = TestNet() const MOCKED_PK = 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI' const MOCKED_PK_B = 'GBCBCTQ6YH3XFYDDGARNGYSS2LGTX5CA6P3P2K6ODSRNBKKK7BWMEEVM' diff --git a/src/stellar-plus/asset/soroban-token/index.unit.test.ts b/src/stellar-plus/asset/soroban-token/index.unit.test.ts index a8d1ff9..726de62 100644 --- a/src/stellar-plus/asset/soroban-token/index.unit.test.ts +++ b/src/stellar-plus/asset/soroban-token/index.unit.test.ts @@ -4,8 +4,8 @@ import { Address, ContractSpec } from '@stellar/stellar-sdk' import { SorobanTokenHandler } from 'stellar-plus/asset/soroban-token' import { spec as DEFAULT_SPEC, methods } from 'stellar-plus/asset/soroban-token/constants' -import { testnet } from 'stellar-plus/constants' import { SorobanTransactionPipeline } from 'stellar-plus/core/pipelines/soroban-transaction' +import { TestNet } from 'stellar-plus/network' import { TransactionInvocation } from 'stellar-plus/types' jest.mock('stellar-plus/core/pipelines/soroban-transaction', () => ({ @@ -15,7 +15,7 @@ jest.mock('stellar-plus/core/pipelines/soroban-transaction', () => ({ const MOCKED_SOROBAN_TRANSACTION_PIPELINE = SorobanTransactionPipeline as jest.Mock const MOCKED_EXECUTE = jest.fn().mockResolvedValue({}) -const NETWORK_CONFIG = testnet +const NETWORK_CONFIG = TestNet() const MOCKED_CONTRACT_ID = 'CBJT4BOMRHYKHZ6HF3QG4YR7Q63BE44G73M4MALDTQ3SQVUZDE7GN35I' const MOCKED_WASM_HASH = 'eb94566536d7f56c353b4760f6e359eca3631b70d295820fb6de55a796e019ae' diff --git a/src/stellar-plus/core/contract-engine/errors.ts b/src/stellar-plus/core/contract-engine/errors.ts index d9f041d..40c03a2 100644 --- a/src/stellar-plus/core/contract-engine/errors.ts +++ b/src/stellar-plus/core/contract-engine/errors.ts @@ -1,11 +1,6 @@ import { SorobanRpc } from '@stellar/stellar-sdk' import { StellarPlusError } from 'stellar-plus/error' -import { - extractSimulationBaseData, - extractSimulationErrorData, - extractSimulationRestoreData, -} from 'stellar-plus/error/helpers/soroban-rpc' export enum ContractEngineErrorCodes { // CE0 General @@ -113,7 +108,7 @@ const contractCodeMissingLiveUntilLedgerSeq = ( }) } -const contractEngineClassFailedToInitialize = () => { +const contractEngineClassFailedToInitialize = (): StellarPlusError => { return new StellarPlusError({ code: ContractEngineErrorCodes.CE009, message: 'Contract engine class failed to initialize!', diff --git a/src/stellar-plus/core/contract-engine/index.unit.test.ts b/src/stellar-plus/core/contract-engine/index.unit.test.ts index 937f893..ece57f7 100644 --- a/src/stellar-plus/core/contract-engine/index.unit.test.ts +++ b/src/stellar-plus/core/contract-engine/index.unit.test.ts @@ -1,9 +1,9 @@ import { Asset, Contract, SorobanRpc, xdr } from '@stellar/stellar-sdk' -import { Constants } from 'stellar-plus' import { methods as tokenMethods, spec as tokenSpec } from 'stellar-plus/asset/soroban-token/constants' import { SorobanTransactionPipeline } from 'stellar-plus/core/pipelines/soroban-transaction' import { StellarPlusError } from 'stellar-plus/error' +import { TestNet } from 'stellar-plus/network' import { DefaultRpcHandler } from 'stellar-plus/rpc' import { TransactionInvocation } from 'stellar-plus/types' @@ -32,7 +32,7 @@ const MOCKED_STELLAR_ASSET = Asset.native() const MOCKED_CONTRACT_CODE_KEY = new xdr.LedgerKeyContractCode({ hash: Buffer.from(MOCKED_WASM_HASH, 'hex'), }) -const NETWORK_CONFIG = Constants.testnet +const NETWORK_CONFIG = TestNet() const MOCKED_TX_INVOCATION: TransactionInvocation = { header: { source: 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI', diff --git a/src/stellar-plus/core/pipelines/build-transaction/index.unit.test.ts b/src/stellar-plus/core/pipelines/build-transaction/index.unit.test.ts index aaa3400..5b6e5b6 100644 --- a/src/stellar-plus/core/pipelines/build-transaction/index.unit.test.ts +++ b/src/stellar-plus/core/pipelines/build-transaction/index.unit.test.ts @@ -2,12 +2,12 @@ import { Asset, Operation } from '@stellar/stellar-base' import { Horizon, TransactionBuilder } from '@stellar/stellar-sdk' import { AccountResponse } from '@stellar/stellar-sdk/lib/horizon' -import { testnet } from 'stellar-plus/constants' import { BuildTransactionPipelineInput as BTInput, BuildTransactionPipelineOutput as BTOutput, } from 'stellar-plus/core/pipelines/build-transaction/types' import { HorizonHandler } from 'stellar-plus/horizon/types' +import { TestNet } from 'stellar-plus/network' import { BuildTransactionPipeline } from '.' @@ -33,7 +33,7 @@ jest.mock('@stellar/stellar-sdk', () => ({ export function createMockedHorizonHandler(): jest.Mocked { return { loadAccount: jest.fn().mockResolvedValue({} as AccountResponse), - server: new Horizon.Server(testnet.horizonUrl), + server: new Horizon.Server(TestNet().horizonUrl as string), } } diff --git a/src/stellar-plus/core/pipelines/classic-sign-requirements/index.unit.test.ts b/src/stellar-plus/core/pipelines/classic-sign-requirements/index.unit.test.ts index c19d877..2bbb3ae 100644 --- a/src/stellar-plus/core/pipelines/classic-sign-requirements/index.unit.test.ts +++ b/src/stellar-plus/core/pipelines/classic-sign-requirements/index.unit.test.ts @@ -1,8 +1,8 @@ import { Account, Asset, Claimant, Operation, TransactionBuilder, xdr } from '@stellar/stellar-sdk' -import { Constants } from 'stellar-plus' import { SignatureRequirement, SignatureThreshold } from 'stellar-plus/core/types' import { ConveyorBeltErrorMeta } from 'stellar-plus/error/helpers/conveyor-belt' +import { TestNet } from 'stellar-plus/network' import { BeltMetadata } from 'stellar-plus/utils/pipeline/conveyor-belts/types' import { CSRError } from './errors' @@ -16,7 +16,7 @@ const MOCKED_PK_C = 'GCPXAF4S5MBXA3DRNBA7XYP55S6F3UN2ZJRAS72BXEJMD7JVMGIGCKNA' const MOCKED_ACCOUNT_A = new Account(MOCKED_PK_A, '100') -const TESTNET_PASSPHRASE = Constants.testnet.networkPassphrase +const TESTNET_PASSPHRASE = TestNet().networkPassphrase const MOCKED_FEE = '100' const MOCKED_BUMP_FEE = '101' diff --git a/src/stellar-plus/core/pipelines/classic-transaction/index.unit.test.ts b/src/stellar-plus/core/pipelines/classic-transaction/index.unit.test.ts index dffd8b5..117ba9d 100644 --- a/src/stellar-plus/core/pipelines/classic-transaction/index.unit.test.ts +++ b/src/stellar-plus/core/pipelines/classic-transaction/index.unit.test.ts @@ -1,4 +1,3 @@ -import { Constants } from 'stellar-plus' import { BuildTransactionPipeline } from 'stellar-plus/core/pipelines/build-transaction' import { BuildTransactionPipelinePlugin, @@ -25,6 +24,7 @@ import { SubmitTransactionPipelinePlugin, SubmitTransactionPipelineType, } from 'stellar-plus/core/pipelines/submit-transaction/types' +import { TestNet } from 'stellar-plus/network' import { TransactionInvocation } from 'stellar-plus/types' import { ClassicChannelAccountsPlugin } from 'stellar-plus/utils/pipeline/plugins/classic-transaction/channel-accounts' import { DebugPlugin } from 'stellar-plus/utils/pipeline/plugins/generic/debug' @@ -84,7 +84,7 @@ const MOCKED_SUBMIT_TRANSACTION_PLUGIN = jest.mocked({ type: 'SubmitTransactionPipeline' as SubmitTransactionPipelineType, }) as unknown as SubmitTransactionPipelinePlugin -const TESTNET_NETWORK_CONFIG = Constants.testnet +const TESTNET_NETWORK_CONFIG = TestNet() const MOCKED_TX_INVOCATION: TransactionInvocation = { header: { diff --git a/src/stellar-plus/core/pipelines/sign-transaction/index.unit.test.ts b/src/stellar-plus/core/pipelines/sign-transaction/index.unit.test.ts index 17658b3..1f64641 100644 --- a/src/stellar-plus/core/pipelines/sign-transaction/index.unit.test.ts +++ b/src/stellar-plus/core/pipelines/sign-transaction/index.unit.test.ts @@ -1,11 +1,11 @@ import { Keypair, TransactionBuilder } from '@stellar/stellar-sdk' -import { testnet } from 'stellar-plus/constants' import { SignTransactionPipeline } from 'stellar-plus/core/pipelines/sign-transaction' import { SignTransactionPipelineInput as STInput, SignTransactionPipelineOutput as STOutput, } from 'stellar-plus/core/pipelines/sign-transaction/types' +import { TestNet } from 'stellar-plus/network' import { mockAccountHandler, mockSignatureSchema } from 'stellar-plus/test/mocks/transaction-mock' const MOCKED_KEYPAIRS = [ @@ -13,7 +13,7 @@ const MOCKED_KEYPAIRS = [ Keypair.fromSecret('SA2WW3DO6AVJQO5V4MU64DSDL34FRXVIQXIUMKS7JMAENCCI3ORMQVLA'), Keypair.fromSecret('SCHH7OAC6MC4NF3TG2JML56WJT5U7ZE355USOKGXZCQ2FCJZEX62OEKR'), ] -const TESTNET_PASSPHRASE = testnet.networkPassphrase +const TESTNET_PASSPHRASE = TestNet().networkPassphrase const MOCKED_UNSGINED_TRANSACTION_XDR = 'AAAAAgAAAAC8CrO4sEcs28O8U8KWvl4CpiGpCgRlbEwf2fp21SRe0gAAAGQADg/kAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' const MOCKED_SIGNED_TRANSACTION_XDR = diff --git a/src/stellar-plus/core/pipelines/simulate-transaction/index.unit.test.ts b/src/stellar-plus/core/pipelines/simulate-transaction/index.unit.test.ts index 4e9f7f2..67fe95b 100644 --- a/src/stellar-plus/core/pipelines/simulate-transaction/index.unit.test.ts +++ b/src/stellar-plus/core/pipelines/simulate-transaction/index.unit.test.ts @@ -1,6 +1,5 @@ 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 { @@ -8,6 +7,7 @@ import { SimulateTransactionPipelineType, } from 'stellar-plus/core/pipelines/simulate-transaction/types' import { ConveyorBeltErrorMeta } from 'stellar-plus/error/helpers/conveyor-belt' +import { TestNet } from 'stellar-plus/network' import { RpcHandler } from 'stellar-plus/rpc/types' import { BeltMetadata } from 'stellar-plus/utils/pipeline/conveyor-belts/types' @@ -50,7 +50,7 @@ const MOCKED_INVALID_SIMULATION_RESPONSE = const MOCKED_PK_A = 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI' const MOCKED_ACCOUNT_A = new Account(MOCKED_PK_A, '100') -const TESTNET_PASSPHRASE = Constants.testnet.networkPassphrase +const TESTNET_PASSPHRASE = TestNet().networkPassphrase const MOCKED_FEE = '100' const MOCKED_TX_OPTIONS: TransactionBuilder.TransactionBuilderOptions = { fee: MOCKED_FEE, diff --git a/src/stellar-plus/core/pipelines/soroban-auth/index.unit.test.ts b/src/stellar-plus/core/pipelines/soroban-auth/index.unit.test.ts index a2cc4e9..80cf1ff 100644 --- a/src/stellar-plus/core/pipelines/soroban-auth/index.unit.test.ts +++ b/src/stellar-plus/core/pipelines/soroban-auth/index.unit.test.ts @@ -1,16 +1,16 @@ import { Account, SorobanDataBuilder, SorobanRpc, Transaction, TransactionBuilder, xdr } from '@stellar/stellar-sdk' -import { Constants } from 'stellar-plus' import { SimulateTransactionPipeline } from 'stellar-plus/core/pipelines/simulate-transaction' import { SorobanAuthPipeline } from 'stellar-plus/core/pipelines/soroban-auth' import { PSAError } from 'stellar-plus/core/pipelines/soroban-auth/errors' import { SorobanAuthPipelineInput, SorobanAuthPipelineType } from 'stellar-plus/core/pipelines/soroban-auth/types' import { ConveyorBeltErrorMeta } from 'stellar-plus/error/helpers/conveyor-belt' +import { TestNet } from 'stellar-plus/network' import { DefaultRpcHandler } from 'stellar-plus/rpc/default-handler' import { mockAccountHandler } from 'stellar-plus/test/mocks/transaction-mock' import { BeltMetadata } from 'stellar-plus/utils/pipeline/conveyor-belts/types' -const TESTNET_NETWORK_CONFIG = Constants.testnet +const TESTNET_NETWORK_CONFIG = TestNet() const MOCKED_TRANSACTION_OUTPUT = new Transaction( 'AAAAAgAAAAA/s0szuJKLyO2bQJ0DxXjYA2p8sf8kTBjkhAVTV64DQgAAAGQAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', TESTNET_NETWORK_CONFIG.networkPassphrase diff --git a/src/stellar-plus/core/pipelines/soroban-get-transaction/index.unit.test.ts b/src/stellar-plus/core/pipelines/soroban-get-transaction/index.unit.test.ts index 22426cb..dca89d8 100644 --- a/src/stellar-plus/core/pipelines/soroban-get-transaction/index.unit.test.ts +++ b/src/stellar-plus/core/pipelines/soroban-get-transaction/index.unit.test.ts @@ -1,6 +1,5 @@ import { Account, SorobanRpc, TransactionBuilder, xdr } from '@stellar/stellar-sdk' -import { Constants } from 'stellar-plus' import { SorobanGetTransactionPipeline } from 'stellar-plus/core/pipelines/soroban-get-transaction' import { SGTError } from 'stellar-plus/core/pipelines/soroban-get-transaction/errors' import { @@ -10,6 +9,7 @@ import { SorobanGetTransactionPipelineType, } from 'stellar-plus/core/pipelines/soroban-get-transaction/types' import { ConveyorBeltErrorMeta } from 'stellar-plus/error/helpers/conveyor-belt' +import { TestNet } from 'stellar-plus/network' import { DefaultRpcHandler } from 'stellar-plus/rpc/default-handler' import { BeltMetadata } from 'stellar-plus/utils/pipeline/conveyor-belts/types' @@ -21,7 +21,7 @@ jest.mock('stellar-plus/rpc/default-handler', () => ({ const MOCKED_RPC_HANDLER = DefaultRpcHandler as jest.Mock -const TESTNET_CONFIG = Constants.testnet +const TESTNET_CONFIG = TestNet() const MOCKED_PK_A = 'GACF23GKVFTU77K6W6PWSVN7YBM63UHDULILIEXJO6FR4YKMJ7FW3DTI' const MOCKED_ACCOUNT_A = new Account(MOCKED_PK_A, '100') diff --git a/src/stellar-plus/core/pipelines/soroban-transaction/index.unit.test.ts b/src/stellar-plus/core/pipelines/soroban-transaction/index.unit.test.ts index 38c860e..39b9ec9 100644 --- a/src/stellar-plus/core/pipelines/soroban-transaction/index.unit.test.ts +++ b/src/stellar-plus/core/pipelines/soroban-transaction/index.unit.test.ts @@ -1,4 +1,3 @@ -import { Constants } from 'stellar-plus' import { BuildTransactionPipeline } from 'stellar-plus/core/pipelines/build-transaction' import { BuildTransactionPipelinePlugin, @@ -38,6 +37,7 @@ import { SubmitTransactionPipelinePlugin, SubmitTransactionPipelineType, } from 'stellar-plus/core/pipelines/submit-transaction/types' +import { TestNet } from 'stellar-plus/network' import { TransactionInvocation } from 'stellar-plus/types' import { DebugPlugin } from 'stellar-plus/utils/pipeline/plugins/generic/debug' import { SorobanChannelAccountsPlugin } from 'stellar-plus/utils/pipeline/plugins/soroban-transaction/channel-accounts' @@ -137,7 +137,7 @@ const MOCKED_SOROBAN_GET_TRANSACTION_PLUGIN = jest.mocked({ type: 'SorobanGetTransactionPipeline' as SorobanGetTransactionPipelineType, }) as unknown as SorobanGetTransactionPipelinePlugin -const TESTNET_NETWORK_CONFIG = Constants.testnet +const TESTNET_NETWORK_CONFIG = TestNet() const MOCKED_TX_INVOCATION: TransactionInvocation = { header: { diff --git a/src/stellar-plus/core/pipelines/submit-transaction/index.unit.test.ts b/src/stellar-plus/core/pipelines/submit-transaction/index.unit.test.ts index 9c626aa..946ba70 100644 --- a/src/stellar-plus/core/pipelines/submit-transaction/index.unit.test.ts +++ b/src/stellar-plus/core/pipelines/submit-transaction/index.unit.test.ts @@ -1,15 +1,15 @@ import { SorobanRpc, TransactionBuilder } from '@stellar/stellar-sdk' -import { testnet } from 'stellar-plus/constants' import { SubmitTransactionPipeline } from 'stellar-plus/core/pipelines/submit-transaction' import { SubmitTransactionPipelineInput as STInput, SubmitTransactionPipelineOutput as STOutput, } from 'stellar-plus/core/pipelines/submit-transaction/types' import { HorizonHandlerClient } from 'stellar-plus/horizon' +import { TestNet } from 'stellar-plus/network' import { DefaultRpcHandler } from 'stellar-plus/rpc' -const TESTNET_PASSPHRASE = testnet.networkPassphrase +const TESTNET_PASSPHRASE = TestNet().networkPassphrase const MOCKED_TRANSACTION_XDR = 'AAAAAgAAAAC8CrO4sEcs28O8U8KWvl4CpiGpCgRlbEwf2fp21SRe0gAAAGQADg/kAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB1SRe0gAAAEBH30tw2MS4pbpLZ8RbEBTLxF2xalFGJRLfDhgcbildNTgujl6hbNxmw2qltop/SZfe34R4q9v+KVhp5pQ4DmkL' const MOCKED_TRANSACTION = TransactionBuilder.fromXDR(MOCKED_TRANSACTION_XDR, TESTNET_PASSPHRASE) @@ -24,8 +24,8 @@ const MOCKED_ST_OUTPUT: STOutput = { paging_token: 'paging_token', }, } -const HORIZON_HANDLER = new HorizonHandlerClient(testnet) -const RPC_HANDLER = new DefaultRpcHandler(testnet) +const HORIZON_HANDLER = new HorizonHandlerClient(TestNet()) +const RPC_HANDLER = new DefaultRpcHandler(TestNet()) const MOCKED_ST_INPUT: STInput = { transaction: MOCKED_TRANSACTION, networkHandler: HORIZON_HANDLER, diff --git a/src/stellar-plus/error/types.ts b/src/stellar-plus/error/types.ts index c1c6724..567ad3e 100644 --- a/src/stellar-plus/error/types.ts +++ b/src/stellar-plus/error/types.ts @@ -12,13 +12,18 @@ import { ErrorCodesPipelineSimulateTransaction } from 'stellar-plus/core/pipelin import { ErrorCodesPipelineSorobanAuth } from 'stellar-plus/core/pipelines/soroban-auth/errors' import { ErrorCodesPipelineSorobanGetTransaction } from 'stellar-plus/core/pipelines/soroban-get-transaction/errors' import { ErrorCodesPipelineSubmitTransaction } from 'stellar-plus/core/pipelines/submit-transaction/errors' +import { AxiosErrorInfo } from 'stellar-plus/error/helpers/axios' +import { SubmitTransactionMetaInfo, TransactionDiagnostic } from 'stellar-plus/error/helpers/horizon' +import { + GetTransactionErrorInfo, + SendTransactionErrorInfo, + SimulationErrorInfo, +} from 'stellar-plus/error/helpers/soroban-rpc' +import { TransactionData, TransactionInvocationMeta } from 'stellar-plus/error/helpers/transaction' +import { DefaultHorizonHandlerErrorCodes } from 'stellar-plus/horizon/errors' +import { DefaultRpcHandlerErrorCodes } from 'stellar-plus/rpc/default-handler/errors' import { ValidationCloudRpcHandlerErrorCodes } from 'stellar-plus/rpc/validation-cloud-handler/errors' -import { AxiosErrorInfo } from './helpers/axios' -import { SubmitTransactionMetaInfo, TransactionDiagnostic } from './helpers/horizon' -import { GetTransactionErrorInfo, SendTransactionErrorInfo, SimulationErrorInfo } from './helpers/soroban-rpc' -import { TransactionData, TransactionInvocationMeta } from './helpers/transaction' - export type StellarPlusErrorObject = { code: ErrorCodes message: string @@ -34,9 +39,11 @@ export type ErrorCodes = | ContractEngineErrorCodes | ChannelAccountsErrorCodes | ErrorCodesPipelineFeeBump + | DefaultRpcHandlerErrorCodes | ClassicAssetHandlerErrorCodes | ErrorCodesPipelineSorobanAuth | DefaultAccountHandlerErrorCodes + | DefaultHorizonHandlerErrorCodes | ErrorCodesPipelineSignTransaction | FreighterAccountHandlerErrorCodes | ValidationCloudRpcHandlerErrorCodes diff --git a/src/stellar-plus/horizon/errors.ts b/src/stellar-plus/horizon/errors.ts new file mode 100644 index 0000000..5e45079 --- /dev/null +++ b/src/stellar-plus/horizon/errors.ts @@ -0,0 +1,31 @@ +import { StellarPlusError } from 'stellar-plus/error' + +export enum DefaultHorizonHandlerErrorCodes { + // CE0 General + DHH001 = 'DHH001', + DHH002 = 'DHH002', +} + +const missingHorizonUrl = (): StellarPlusError => { + return new StellarPlusError({ + code: DefaultHorizonHandlerErrorCodes.DHH001, + message: 'Missing Horizon Url!', + source: 'Default Horizon Handler', + details: + 'The Default Horizon Handler requires an Horizon Url to be defined in the network configuration. Review the network configuration object provided and make sure it has url to connect directly with the Horizon API.', + }) +} + +const failedToLoadAccount = (): StellarPlusError => { + return new StellarPlusError({ + code: DefaultHorizonHandlerErrorCodes.DHH002, + message: 'Failed to load account from Horizon server.', + source: 'Default Horizon Handler', + details: + 'Failed to load account from Horizon server. An unexpected error occurred while trying to load the account from the Horizon server.', + }) +} +export const DHHError = { + missingHorizonUrl, + failedToLoadAccount, +} diff --git a/src/stellar-plus/horizon/index.ts b/src/stellar-plus/horizon/index.ts index 28c5047..1ff72af 100644 --- a/src/stellar-plus/horizon/index.ts +++ b/src/stellar-plus/horizon/index.ts @@ -1,6 +1,6 @@ import { Horizon } from '@stellar/stellar-sdk' -import { StellarPlusError } from 'stellar-plus/error' +import { DHHError } from 'stellar-plus/horizon/errors' import { HorizonHandler } from 'stellar-plus/horizon/types' import { NetworkConfig } from 'stellar-plus/types' @@ -17,7 +17,14 @@ export class HorizonHandlerClient implements HorizonHandler { */ constructor(networkConfig: NetworkConfig) { this.networkConfig = networkConfig - this.server = new Horizon.Server(this.networkConfig.horizonUrl) + + if (!this.networkConfig.horizonUrl) { + throw DHHError.missingHorizonUrl() + } + + const serverOpts = { allowHttp: networkConfig.allowHttp } + + this.server = new Horizon.Server(this.networkConfig.horizonUrl, serverOpts) } /** @@ -32,11 +39,7 @@ export class HorizonHandlerClient implements HorizonHandler { try { return await this.server.loadAccount(accountId) } catch (error) { - throw StellarPlusError.unexpectedError({ - error: error as Error, - message: 'Failed to load account from Horizon server.', - source: 'HorizonHandlerClient', - }) + throw DHHError.failedToLoadAccount() } } } diff --git a/src/stellar-plus/horizon/index.unit.test.ts b/src/stellar-plus/horizon/index.unit.test.ts new file mode 100644 index 0000000..5d7641e --- /dev/null +++ b/src/stellar-plus/horizon/index.unit.test.ts @@ -0,0 +1,53 @@ +import { DHHError } from 'stellar-plus/horizon/errors' +import { HorizonHandlerClient } from 'stellar-plus/horizon/index' +import { CustomNet, TestNet } from 'stellar-plus/network' + +// jest.mock('@stellar/stellar-sdk', () => ({ +// Horizon: jest.fn(), +// })) + +// const MOCKED_HORIZON = Horizon as unknown as jest.Mock + +const NETWORK_CONFIG = TestNet() + +describe('Default Horizon Handler', () => { + describe('Constructor', () => { + it('should throw an error if the network c]onfiguration is missing the Horizon URL', () => { + const netWorkConfigWithoutHorizonUrl = CustomNet({ ...NETWORK_CONFIG, horizonUrl: undefined }) + + expect(() => new HorizonHandlerClient(netWorkConfigWithoutHorizonUrl)).toThrow(DHHError.missingHorizonUrl()) + }) + it('should create a new Horizon server instance', () => { + const horizonHandler = new HorizonHandlerClient(NETWORK_CONFIG) + + expect(horizonHandler).toBeDefined() + expect(horizonHandler.server).toBeDefined() + }) + + it('should create a new Horizon server instance with allowHttp option when enabled in the network', () => { + const networkConfigWithHttp = CustomNet({ ...NETWORK_CONFIG, allowHttp: true }) + const horizonHandler = new HorizonHandlerClient(networkConfigWithHttp) + + expect(horizonHandler).toBeDefined() + expect(horizonHandler.server).toBeDefined() + }) + }) + + describe('loadAccount', () => { + it('should throw an error if the account fails to load', async () => { + const horizonHandler = new HorizonHandlerClient(NETWORK_CONFIG) + horizonHandler.server.loadAccount = jest.fn().mockRejectedValue(new Error('Failed to load account')) + + await expect(horizonHandler.loadAccount('mock_account')).rejects.toThrow(DHHError.failedToLoadAccount()) + }) + + it('should return the account response from horizon', async () => { + const horizonHandler = new HorizonHandlerClient(NETWORK_CONFIG) + horizonHandler.server.loadAccount = jest.fn().mockResolvedValue({ id: 'mock_account' }) + + const response = await horizonHandler.loadAccount('mock_account') + + expect(response).toEqual({ id: 'mock_account' }) + }) + }) +}) diff --git a/src/stellar-plus/index.ts b/src/stellar-plus/index.ts index 73baa53..65ad512 100644 --- a/src/stellar-plus/index.ts +++ b/src/stellar-plus/index.ts @@ -6,9 +6,8 @@ import { plugins } from './utils/pipeline/plugins' export * as Account from 'stellar-plus/account/index' export * as Asset from 'stellar-plus/asset/index' -export * as Constants from 'stellar-plus/constants' +export * as Network from 'stellar-plus/network' export { HorizonHandlerClient as HorizonHandler } from 'stellar-plus/horizon/index' -export { SorobanHandlerClient as SorobanHandler } from 'stellar-plus/soroban/index' export { Core } from 'stellar-plus/core/index' diff --git a/src/stellar-plus/constants.ts b/src/stellar-plus/network/index.ts similarity index 60% rename from src/stellar-plus/constants.ts rename to src/stellar-plus/network/index.ts index 3f13d9e..6c03f90 100644 --- a/src/stellar-plus/constants.ts +++ b/src/stellar-plus/network/index.ts @@ -1,9 +1,10 @@ export type NetworkConfig = { name: NetworksList networkPassphrase: string - rpcUrl: string - horizonUrl: string + rpcUrl?: string + horizonUrl?: string friendbotUrl?: string + allowHttp?: boolean } export enum NetworksList { @@ -13,31 +14,48 @@ export enum NetworksList { custom = 'custom', } -const networksConfig: { [key: string]: NetworkConfig } = { - futurenet: { +export const TestNet = (): NetworkConfig => { + return { + name: NetworksList.testnet, + networkPassphrase: 'Test SDF Network ; September 2015', + rpcUrl: 'https://soroban-testnet.stellar.org:443', + friendbotUrl: 'https://friendbot.stellar.org', + horizonUrl: 'https://horizon-testnet.stellar.org', + allowHttp: false, + } +} + +export const FutureNet = (): NetworkConfig => { + return { name: NetworksList.futurenet, networkPassphrase: 'Test SDF Future Network ; October 2022', rpcUrl: 'https://rpc-futurenet.stellar.org:443', friendbotUrl: 'https://friendbot-futurenet.stellar.org', horizonUrl: 'https://horizon-futurenet.stellar.org', - }, + allowHttp: false, + } +} - testnet: { - name: NetworksList.testnet, - networkPassphrase: 'Test SDF Network ; September 2015', - rpcUrl: 'https://soroban-testnet.stellar.org:443', - friendbotUrl: 'https://friendbot.stellar.org', - horizonUrl: 'https://horizon-testnet.stellar.org', - }, - mainnet: { +export const MainNet = (): NetworkConfig => { + return { name: NetworksList.mainnet, networkPassphrase: 'Public Global Stellar Network ; September 2015', rpcUrl: '', horizonUrl: 'https://horizon.stellar.org', - }, + allowHttp: false, + } +} +export type CustomNetworkPayload = { + networkPassphrase: string + rpcUrl?: string + horizonUrl?: string + friendbotUrl?: string + allowHttp?: boolean } -const testnet: NetworkConfig = networksConfig.testnet -const futurenet: NetworkConfig = networksConfig.futurenet -const mainnet: NetworkConfig = networksConfig.mainnet -export { testnet, futurenet, mainnet } +export const CustomNet = (payload: CustomNetworkPayload): NetworkConfig => { + return { + name: NetworksList.custom, + ...payload, + } +} diff --git a/src/stellar-plus/network/index.unit.test.ts b/src/stellar-plus/network/index.unit.test.ts new file mode 100644 index 0000000..3bf138a --- /dev/null +++ b/src/stellar-plus/network/index.unit.test.ts @@ -0,0 +1,59 @@ +import { CustomNet, FutureNet, MainNet, NetworksList, TestNet } from '.' + +describe('Network', () => { + describe('Default Network Configurations', () => { + it('should return the TestNet configuration', () => { + const testNet = TestNet() + expect(testNet).toEqual({ + name: NetworksList.testnet, + networkPassphrase: 'Test SDF Network ; September 2015', + rpcUrl: 'https://soroban-testnet.stellar.org:443', + friendbotUrl: 'https://friendbot.stellar.org', + horizonUrl: 'https://horizon-testnet.stellar.org', + allowHttp: false, + }) + }) + it('should return the FutureNet configuration', () => { + const futureNet = FutureNet() + expect(futureNet).toEqual({ + name: NetworksList.futurenet, + networkPassphrase: 'Test SDF Future Network ; October 2022', + rpcUrl: 'https://rpc-futurenet.stellar.org:443', + friendbotUrl: 'https://friendbot-futurenet.stellar.org', + horizonUrl: 'https://horizon-futurenet.stellar.org', + allowHttp: false, + }) + }) + it('should return the MainNet configuration', () => { + const mainNet = MainNet() + expect(mainNet).toEqual({ + name: NetworksList.mainnet, + networkPassphrase: 'Public Global Stellar Network ; September 2015', + rpcUrl: '', + horizonUrl: 'https://horizon.stellar.org', + allowHttp: false, + }) + }) + }) + + describe('Custom Network Configuration', () => { + it('should return a custom network configuration', () => { + const customNet = CustomNet({ + networkPassphrase: 'Custom Network', + rpcUrl: 'https://rpc.custom.com', + friendbotUrl: 'https://friendbot.custom.com', + horizonUrl: 'https://horizon.custom.com', + allowHttp: true, + }) + + expect(customNet).toEqual({ + name: NetworksList.custom, + networkPassphrase: 'Custom Network', + rpcUrl: 'https://rpc.custom.com', + friendbotUrl: 'https://friendbot.custom.com', + horizonUrl: 'https://horizon.custom.com', + allowHttp: true, + }) + }) + }) +}) diff --git a/src/stellar-plus/rpc/default-handler/errors.ts b/src/stellar-plus/rpc/default-handler/errors.ts new file mode 100644 index 0000000..8b13bad --- /dev/null +++ b/src/stellar-plus/rpc/default-handler/errors.ts @@ -0,0 +1,20 @@ +import { StellarPlusError } from 'stellar-plus/error' + +export enum DefaultRpcHandlerErrorCodes { + // CE0 General + DRH001 = 'DRH001', +} + +const missingRpcUrl = (): StellarPlusError => { + return new StellarPlusError({ + code: DefaultRpcHandlerErrorCodes.DRH001, + message: 'Missing RPC Url!', + source: 'Default RPC Handler', + details: + 'The Default RPC Handler requires an RPC Url to be defined in the network configuration. Review the network configuration object provided and make sure it has url to connect directly with the RPC server.', + }) +} + +export const DRHError = { + missingRpcUrl, +} diff --git a/src/stellar-plus/rpc/default-handler/index.ts b/src/stellar-plus/rpc/default-handler/index.ts index 2a7643f..a1740b2 100644 --- a/src/stellar-plus/rpc/default-handler/index.ts +++ b/src/stellar-plus/rpc/default-handler/index.ts @@ -1,5 +1,6 @@ import { FeeBumpTransaction, SorobanRpc, Transaction, xdr } from '@stellar/stellar-sdk' +import { DRHError } from 'stellar-plus/rpc/default-handler/errors' import { RpcHandler } from 'stellar-plus/rpc/types' import { NetworkConfig } from 'stellar-plus/types' @@ -18,7 +19,14 @@ export class DefaultRpcHandler implements RpcHandler { */ constructor(networkConfig: NetworkConfig) { this.networkConfig = networkConfig - this.server = new SorobanRpc.Server(this.networkConfig.rpcUrl) + + if (!this.networkConfig.rpcUrl) { + throw DRHError.missingRpcUrl() + } + + const serverOpts = networkConfig.allowHttp ? { allowHttp: true } : {} + + this.server = new SorobanRpc.Server(this.networkConfig.rpcUrl, serverOpts) } /** diff --git a/src/stellar-plus/rpc/default-handler/index.unit.test.ts b/src/stellar-plus/rpc/default-handler/index.unit.test.ts new file mode 100644 index 0000000..b1cff6c --- /dev/null +++ b/src/stellar-plus/rpc/default-handler/index.unit.test.ts @@ -0,0 +1,135 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { SorobanRpc, Transaction } from '@stellar/stellar-sdk' + +import { CustomNet, TestNet } from 'stellar-plus/network' +import { DRHError } from 'stellar-plus/rpc/default-handler/errors' +import { DefaultRpcHandler } from 'stellar-plus/rpc/default-handler/index' + +const NETWORK_CONFIG = TestNet() + +describe('Default RPC Handler', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + describe('constructor', () => { + it('should throw an error if the network configuration is missing the RPC URL', () => { + const netWorkConfigWithoutRpcUrl = CustomNet({ ...NETWORK_CONFIG, rpcUrl: undefined }) + + expect(() => new DefaultRpcHandler(netWorkConfigWithoutRpcUrl)).toThrow(DRHError.missingRpcUrl()) + }) + + it('should create a new Soroban server instance', () => { + const rpcHandler = new DefaultRpcHandler(NETWORK_CONFIG) + + expect(rpcHandler).toBeDefined() + expect((rpcHandler as any).server).toBeDefined() + }) + + it('should create a new Soroban server instance with allowHttp option when enabled in the network', () => { + const networkConfigWithHttp = CustomNet({ ...NETWORK_CONFIG, allowHttp: true }) + const rpcHandler = new DefaultRpcHandler(networkConfigWithHttp) + + expect(rpcHandler).toBeDefined() + expect((rpcHandler as any).server).toBeDefined() + }) + }) + + describe('getTransaction', () => { + it('should return the transaction response from Soroban', async () => { + const rpcHandler = new DefaultRpcHandler(NETWORK_CONFIG) + ;(rpcHandler as any).server.getTransaction = jest.fn().mockResolvedValue({ id: 'mock_transaction' }) + + const response = await rpcHandler.getTransaction('mock_transaction') + + expect(response).toEqual({ id: 'mock_transaction' }) + }) + }) + + describe('simulateTransaction', () => { + it('should return the transaction simulation response from Soroban', async () => { + const rpcHandler = new DefaultRpcHandler(NETWORK_CONFIG) + ;(rpcHandler as any).server.simulateTransaction = jest.fn().mockResolvedValue({ id: 'mock_simulation' }) + + const response = await rpcHandler.simulateTransaction(jest.fn() as unknown as Transaction) + + expect(response).toEqual({ id: 'mock_simulation' }) + }) + }) + + describe('prepareTransaction', () => { + it('should return the transaction preparation response from Soroban', async () => { + const rpcHandler = new DefaultRpcHandler(NETWORK_CONFIG) + ;(rpcHandler as any).server.prepareTransaction = jest.fn().mockResolvedValue({ id: 'mock_preparation' }) + + const response = await rpcHandler.prepareTransaction(jest.fn() as unknown as Transaction) + + expect(response).toEqual({ id: 'mock_preparation' }) + }) + }) + + describe('submitTransaction', () => { + it('should return the transaction submission response from Soroban', async () => { + const rpcHandler = new DefaultRpcHandler(NETWORK_CONFIG) + ;(rpcHandler as any).server.sendTransaction = jest.fn().mockResolvedValue({ id: 'mock_submission' }) + + const response = await rpcHandler.submitTransaction(jest.fn() as unknown as Transaction) + + expect(response).toEqual({ id: 'mock_submission' }) + }) + }) + + describe('getLatestLedger', () => { + it('should return the latest ledger response from Soroban', async () => { + const rpcHandler = new DefaultRpcHandler(NETWORK_CONFIG) + ;(rpcHandler as any).server.getLatestLedger = jest.fn().mockResolvedValue({ id: 'mock_ledger' }) + + const response = await rpcHandler.getLatestLedger() + + expect(response).toEqual({ id: 'mock_ledger' }) + }) + }) + + describe('getHealth', () => { + it('should return the health response from Soroban', async () => { + const rpcHandler = new DefaultRpcHandler(NETWORK_CONFIG) + ;(rpcHandler as any).server.getHealth = jest.fn().mockResolvedValue({ id: 'mock_health' }) + + const response = await rpcHandler.getHealth() + + expect(response).toEqual({ id: 'mock_health' }) + }) + }) + + describe('getNetwork', () => { + it('should return the network response from Soroban', async () => { + const rpcHandler = new DefaultRpcHandler(NETWORK_CONFIG) + ;(rpcHandler as any).server.getNetwork = jest.fn().mockResolvedValue({ id: 'mock_network' }) + + const response = await rpcHandler.getNetwork() + + expect(response).toEqual({ id: 'mock_network' }) + }) + }) + + describe('getEvents', () => { + it('should return the events response from Soroban', async () => { + const rpcHandler = new DefaultRpcHandler(NETWORK_CONFIG) + ;(rpcHandler as any).server.getEvents = jest.fn().mockResolvedValue({ id: 'mock_events' }) + + const response = await rpcHandler.getEvents(jest.fn() as unknown as SorobanRpc.Server.GetEventsRequest) + + expect(response).toEqual({ id: 'mock_events' }) + }) + }) + + describe('getLedgerEntries', () => { + it('should return the ledger entries response from Soroban', async () => { + const rpcHandler = new DefaultRpcHandler(NETWORK_CONFIG) + ;(rpcHandler as any).server.getLedgerEntries = jest.fn().mockResolvedValue({ id: 'mock_ledger_entries' }) + + const response = await rpcHandler.getLedgerEntries() + + expect(response).toEqual({ id: 'mock_ledger_entries' }) + }) + }) +}) diff --git a/src/stellar-plus/soroban/index.ts b/src/stellar-plus/soroban/index.ts deleted file mode 100644 index 0193bdc..0000000 --- a/src/stellar-plus/soroban/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { SorobanRpc } from '@stellar/stellar-sdk' - -import { SorobanHandler } from 'stellar-plus/soroban/types' -import { NetworkConfig } from 'stellar-plus/types' -export class SorobanHandlerClient implements SorobanHandler { - private networkConfig: NetworkConfig - public server: SorobanRpc.Server - - /** - * - * @param {NetworkConfig} networkConfig - The network to use. - * - * @description - The soroban handler is used for interacting with the Soroban server. - * - */ - constructor(networkConfig: NetworkConfig) { - this.networkConfig = networkConfig - this.server = new SorobanRpc.Server(this.networkConfig.rpcUrl) - } -} diff --git a/src/stellar-plus/soroban/types.ts b/src/stellar-plus/soroban/types.ts deleted file mode 100644 index 1276d0c..0000000 --- a/src/stellar-plus/soroban/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { SorobanRpc } from '@stellar/stellar-sdk' - -export type SorobanHandler = { - server: SorobanRpc.Server -} diff --git a/src/stellar-plus/test/mocks/constants.ts b/src/stellar-plus/test/mocks/constants.ts index 543c606..bd1534e 100644 --- a/src/stellar-plus/test/mocks/constants.ts +++ b/src/stellar-plus/test/mocks/constants.ts @@ -1,6 +1,6 @@ -import { testnet } from 'stellar-plus/constants' +import { TestNet } from 'stellar-plus/network' export const ACCOUNT_A_PK = 'GAVVRSYGWDE3N24SJRCMCXOAOXVPM7YPTEUSOVXEN344Q45UZ6DMQSR2' export const ACCOUNT_A_SK = 'SDZTDX3J7PGZW6PKIPODAFQ7SRBCKTQGSG7YD7QMJQDKOSN6ZZTJOSMT' export const NETWORK_PASSPHRASE = 'Test SDF Network ; September 2015' -export const NETWORK = testnet +export const NETWORK = TestNet() diff --git a/src/stellar-plus/types.ts b/src/stellar-plus/types.ts index b903d6c..e39d637 100644 --- a/src/stellar-plus/types.ts +++ b/src/stellar-plus/types.ts @@ -5,12 +5,12 @@ import { Transaction as _Transaction, } from '@stellar/stellar-sdk' -import { NetworkConfig as _NetworkConfig } from 'stellar-plus/constants' import { EnvelopeHeader as _EnvelopeHeader, FeeBumpHeader as _FeeBumpHeader, TransactionInvocation as _TransactionInvocation, } from 'stellar-plus/core/types' +import { NetworkConfig as _NetworkConfig } from 'stellar-plus/network' export type TransactionXdr = string From 20abb097ccdc059adbb9fa88f3f80ba5d2cc220e Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Thu, 25 Apr 2024 16:26:49 -0300 Subject: [PATCH 29/30] packaging: bump version to v0.8.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3f3bcb7..31dcd14 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stellar-plus", - "version": "0.7.0", + "version": "0.8.0", "description": "beta version of stellar-plus, an all-in-one sdk for the Stellar blockchain", "main": "./lib/index.js", "types": "./lib/index.d.ts", From 5bf176a082bfd6c99253a2b432c21511d986aa13 Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Thu, 25 Apr 2024 16:50:42 -0300 Subject: [PATCH 30/30] Fix merge issues (#137) * feat: standardize network config options as functions under network * fix: duplicate imports --- src/stellar-plus/core/contract-engine/errors.ts | 1 - src/stellar-plus/core/contract-engine/index.ts | 1 - src/stellar-plus/core/contract-engine/index.unit.test.ts | 3 --- src/stellar-plus/core/pipelines/submit-transaction/index.ts | 1 - 4 files changed, 6 deletions(-) diff --git a/src/stellar-plus/core/contract-engine/errors.ts b/src/stellar-plus/core/contract-engine/errors.ts index e06a612..40c03a2 100644 --- a/src/stellar-plus/core/contract-engine/errors.ts +++ b/src/stellar-plus/core/contract-engine/errors.ts @@ -108,7 +108,6 @@ const contractCodeMissingLiveUntilLedgerSeq = ( }) } - const contractEngineClassFailedToInitialize = (): StellarPlusError => { return new StellarPlusError({ code: ContractEngineErrorCodes.CE009, diff --git a/src/stellar-plus/core/contract-engine/index.ts b/src/stellar-plus/core/contract-engine/index.ts index fd91632..3bed6d3 100644 --- a/src/stellar-plus/core/contract-engine/index.ts +++ b/src/stellar-plus/core/contract-engine/index.ts @@ -105,7 +105,6 @@ export class ContractEngine { this.wasm = contractParameters.wasm this.wasmHash = contractParameters.wasmHash - this.options = { ...options } this.sorobanTransactionPipeline = new SorobanTransactionPipeline(networkConfig, { diff --git a/src/stellar-plus/core/contract-engine/index.unit.test.ts b/src/stellar-plus/core/contract-engine/index.unit.test.ts index 3842f90..c8ff462 100644 --- a/src/stellar-plus/core/contract-engine/index.unit.test.ts +++ b/src/stellar-plus/core/contract-engine/index.unit.test.ts @@ -12,7 +12,6 @@ import { SorobanInvokeArgs } from './types' import { ContractEngine } from '../contract-engine' import { ContractIdOutput, ContractWasmHashOutput } from '../pipelines/soroban-get-transaction/types' - jest.mock('stellar-plus/core/pipelines/soroban-transaction', () => ({ SorobanTransactionPipeline: jest.fn(), })) @@ -94,7 +93,6 @@ describe('ContractEngine', () => { }) describe('Initialization Errors', () => { - it('should throw error if wasm file is required but is not present', async () => { const contractEngine = new ContractEngine({ networkConfig: NETWORK_CONFIG, @@ -261,7 +259,6 @@ describe('ContractEngine', () => { getLedgerEntries: jest.fn().mockResolvedValue({ entries: [ { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment key: Object.assign(xdr.LedgerKey.contractCode(MOCKED_CONTRACT_CODE_KEY)), diff --git a/src/stellar-plus/core/pipelines/submit-transaction/index.ts b/src/stellar-plus/core/pipelines/submit-transaction/index.ts index 288cde2..1c6bde7 100644 --- a/src/stellar-plus/core/pipelines/submit-transaction/index.ts +++ b/src/stellar-plus/core/pipelines/submit-transaction/index.ts @@ -13,7 +13,6 @@ import { RpcHandler } from 'stellar-plus/rpc/types' import { ConveyorBelt } from 'stellar-plus/utils/pipeline/conveyor-belts' import { PSUError } from './errors' -import { HorizonHandlerClient } from 'stellar-plus/horizon' export class SubmitTransactionPipeline extends ConveyorBelt< SubmitTransactionPipelineInput,