diff --git a/package-lock.json b/package-lock.json index b922e5e..240c500 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,24 @@ { "name": "stellar-plus", - "version": "0.5.3", + "version": "0.5.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stellar-plus", - "version": "0.5.3", + "version": "0.4.1", "license": "ISC", "dependencies": { "@stellar/freighter-api": "^1.7.1", "@stellar/stellar-sdk": "^11.1.0", "axios": "^1.6.2", - "stellar-base": "^10.0.1" + "stellar-sdk": "^11.2.1", + "uuid": "^9.0.1" }, "devDependencies": { "@types/jest": "^29.5.10", "@types/node": "^20.10.0", + "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.13.2", "@typescript-eslint/parser": "^6.13.2", "eslint": "^8.55.0", @@ -1533,6 +1535,12 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "node_modules/@types/uuid": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz", + "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==", + "dev": true + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -2006,11 +2014,11 @@ } }, "node_modules/axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.4", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -3770,9 +3778,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", @@ -6688,9 +6696,9 @@ } }, "node_modules/sodium-native": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.0.4.tgz", - "integrity": "sha512-faqOKw4WQKK7r/ybn6Lqo1F9+L5T6NlBJJYvpxbZPetpWylUVqz449mvlwIBKBqxEHbWakWuOlUt8J3Qpc4sWw==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.0.6.tgz", + "integrity": "sha512-uYsyycwcz9kYDwpXxJmL2YZosynsxcP6RPySbARVJdC9uNDa2CMjzJ7/WsMMvThKgvAYsBWdZc7L/WSVj9lTcA==", "hasInstallScript": true, "optional": true, "dependencies": { @@ -6775,20 +6783,36 @@ "node": ">=8" } }, - "node_modules/stellar-base": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/stellar-base/-/stellar-base-10.0.1.tgz", - "integrity": "sha512-SL7nzip0Vq5rFWAqodjGN7a1xe4rGfw5fU1CT7N0S4XQOIBC+vLRH5C1KD0XSjITDAk+F4HX6yLpbNORRX3/Zw==", + "node_modules/stellar-sdk": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/stellar-sdk/-/stellar-sdk-11.2.1.tgz", + "integrity": "sha512-wrHf9NMewDX6gZT4CTPfPIHsYlVl7CzIMEv1AVTPezB4VwUR6HX7wy8lpvdbmxe/zgHxJZ+YWnrxb2DG60Kpxg==", + "dependencies": { + "@stellar/stellar-base": "^10.0.2", + "axios": "^1.6.5", + "bignumber.js": "^9.1.2", + "eventsource": "^2.0.2", + "randombytes": "^2.1.0", + "toml": "^3.0.0", + "typescript": "^5.3.3", + "urijs": "^1.19.1" + } + }, + "node_modules/stellar-sdk/node_modules/@stellar/stellar-base": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-10.0.2.tgz", + "integrity": "sha512-SK7WgnSc2xn4vYRRLsjbA6fstyzZfFKUTg/PTrl1Cc5RbhbBSLXVKqd2hw0gAjZ8vHpEMmZvfZgBJnTAYUbrPA==", "dependencies": { "@stellar/js-xdr": "^3.0.1", "base32.js": "^0.1.0", "bignumber.js": "^9.1.2", "buffer": "^6.0.3", "sha.js": "^2.3.6", - "tweetnacl": "^1.0.3" + "tweetnacl": "^1.0.3", + "typescript": "^5.3.3" }, "optionalDependencies": { - "sodium-native": "^4.0.1" + "sodium-native": "^4.0.5" } }, "node_modules/string-length": { @@ -7293,7 +7317,6 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7382,6 +7405,18 @@ "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==" }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/package.json b/package.json index 430e978..7156443 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stellar-plus", - "version": "0.5.7", + "version": "0.6.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", @@ -27,6 +27,7 @@ "devDependencies": { "@types/jest": "^29.5.10", "@types/node": "^20.10.0", + "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.13.2", "@typescript-eslint/parser": "^6.13.2", "eslint": "^8.55.0", @@ -50,8 +51,8 @@ }, "dependencies": { "@stellar/freighter-api": "^1.7.1", - "@stellar/stellar-sdk": "^11.1.0", "axios": "^1.6.2", - "stellar-base": "^10.0.1" + "@stellar/stellar-sdk": "^11.2.1", + "uuid": "^9.0.1" } } diff --git a/src/stellar-plus/account/account-handler/default/default.test.ts b/src/stellar-plus/account/account-handler/default/default.test.ts deleted file mode 100644 index b20ae95..0000000 --- a/src/stellar-plus/account/account-handler/default/default.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Transaction } from '@stellar/stellar-sdk' - -import { Constants } from 'stellar-plus' -import { DefaultAccountHandler } from 'stellar-plus/account' -import { SimpleKeyPairMock, mockedSimpleKeyPair } from 'stellar-plus/test/mocks/accounts' -import { - mockSignedClassicTransactionXdr, - mockUnsignedClassicTransaction, -} from 'stellar-plus/test/mocks/classic-transaction' - -describe('DefaultAccountHandler', () => { - let mockKeypair: SimpleKeyPairMock - let mockUnsignedTx: Transaction - let mockSignedTxXdr: string - - const mockedNetwork = Constants.testnet - - beforeEach(() => { - mockKeypair = mockedSimpleKeyPair - mockUnsignedTx = mockUnsignedClassicTransaction - mockSignedTxXdr = mockSignedClassicTransactionXdr - }) - - it('should create an instance with a provided secret key', () => { - const secretKey = mockKeypair.secretKey - const client = new DefaultAccountHandler({ secretKey }) - expect(client.getPublicKey()).toBe(mockKeypair.publicKey) - }) - - it('should create an instance with a random secret key', () => { - const client = new DefaultAccountHandler({ network: mockedNetwork }) - // Check if the public key starts with 'G' - expect(client.getPublicKey()).toMatch(/^G/) - // Check the length of the public key (assuming the length is known, e.g., 56) - expect(client.getPublicKey()).toHaveLength(56) - // Use a regular expression to validate the format (this regex is just an example) - const publicKeyRegex = /^G[A-Z2-7]{55}$/ - expect(client.getPublicKey()).toMatch(publicKeyRegex) - }) - - it('should return the public key of the account', () => { - const secretKey = mockKeypair.secretKey - const client = new DefaultAccountHandler({ secretKey }) - const publicKey = client.getPublicKey() - expect(publicKey).toBe(mockKeypair.publicKey) - }) - - it("should sign a transaction with the account's secret key", () => { - const secretKey = mockKeypair.secretKey - const client = new DefaultAccountHandler({ secretKey }) - const signedTx = client.sign(mockUnsignedTx) - expect(signedTx).toEqual(mockSignedTxXdr) - }) -}) diff --git a/src/stellar-plus/account/account-handler/default/index.ts b/src/stellar-plus/account/account-handler/default/index.ts index a11c0bb..b2e29bb 100644 --- a/src/stellar-plus/account/account-handler/default/index.ts +++ b/src/stellar-plus/account/account-handler/default/index.ts @@ -13,7 +13,7 @@ export class DefaultAccountHandlerClient extends AccountBaseClient implements De * * @args payload - The payload for the account handler. Additional parameters may be provided to enable different helpers. * @param {string=} payload.secretKey The secret key of the account. If not provided, a new random account will be created. - * @param {Network} payload.network The network to use. + * @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) { diff --git a/src/stellar-plus/account/account-handler/freighter/freighter.test.ts b/src/stellar-plus/account/account-handler/freighter/freighter.test.ts index ae28546..71e5a1c 100644 --- a/src/stellar-plus/account/account-handler/freighter/freighter.test.ts +++ b/src/stellar-plus/account/account-handler/freighter/freighter.test.ts @@ -6,7 +6,7 @@ import { mockSignedClassicTransactionXdr, mockUnsignedClassicTransaction, } from 'stellar-plus/test/mocks/classic-transaction' -import { Network } from 'stellar-plus/types' +import { NetworkConfig } from 'stellar-plus/types' jest.mock('@stellar/freighter-api', () => ({ getPublicKey: jest.fn(), @@ -18,7 +18,7 @@ jest.mock('@stellar/freighter-api', () => ({ })) const mockPublicKey = 'GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' -const mockNetwork = testnet as Network +const mockNetwork = testnet as NetworkConfig const mockFreighterGetPublicKey = (): void => { ;(freighterApi.getPublicKey as jest.MockedFunction).mockResolvedValue(mockPublicKey) @@ -50,7 +50,7 @@ describe('FreighterAccountHandlerClient', () => { let client: FreighterAccountHandlerClient beforeEach(() => { - client = new FreighterAccountHandlerClient({ network: mockNetwork }) + client = new FreighterAccountHandlerClient({ networkConfig: mockNetwork }) }) it('should initialize with provided network', () => { diff --git a/src/stellar-plus/account/account-handler/freighter/index.ts b/src/stellar-plus/account/account-handler/freighter/index.ts index b672be3..484286f 100644 --- a/src/stellar-plus/account/account-handler/freighter/index.ts +++ b/src/stellar-plus/account/account-handler/freighter/index.ts @@ -14,27 +14,27 @@ import { FreighterCallback, } from 'stellar-plus/account/account-handler/freighter/types' import { AccountBaseClient } from 'stellar-plus/account/base' -import { Network } from 'stellar-plus/types' +import { NetworkConfig } from 'stellar-plus/types' import { FAHError } from './errors' export class FreighterAccountHandlerClient extends AccountBaseClient implements FreighterAccountHandler { - private network: Network + private networkConfig: NetworkConfig /** * * @args payload - The payload for the Freighter account handler. Additional parameters may be provided to enable different helpers. * - * @param {Network} payload.network The network to use. + * @param {NetworkConfig} payload.networkConfig The network to use. * * @description - The Freighter account handler is used for handling and creating new accounts by integrating with the browser extension Freighter App. */ constructor(payload: FreighterAccHandlerPayload) { - const { network } = payload as { network: Network } + const { networkConfig } = payload as { networkConfig: NetworkConfig } const publicKey = '' super({ ...payload, publicKey }) - this.network = network + this.networkConfig = networkConfig this.publicKey = '' } @@ -111,7 +111,7 @@ export class FreighterAccountHandlerClient extends AccountBaseClient implements const txXDR = tx.toXDR() const signedTx = await signTransaction(txXDR, { - networkPassphrase: this.network.networkPassphrase, + networkPassphrase: this.networkConfig.networkPassphrase, accountToSign: this.publicKey, }) return signedTx @@ -197,8 +197,8 @@ export class FreighterAccountHandlerClient extends AccountBaseClient implements public async isNetworkCorrect(): Promise { const networkDetails = await getNetworkDetails() - if (networkDetails.networkPassphrase !== this.network.networkPassphrase) { - throw FAHError.connectedToWrongNetworkError(this.network.name) + if (networkDetails.networkPassphrase !== this.networkConfig.networkPassphrase) { + throw FAHError.connectedToWrongNetworkError(this.networkConfig.name) } return true } diff --git a/src/stellar-plus/account/account-handler/freighter/types.ts b/src/stellar-plus/account/account-handler/freighter/types.ts index 15fbf2a..f9289d8 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 { Network } from 'stellar-plus/types' +import { NetworkConfig } from 'stellar-plus/types' export type FreighterAccountHandler = AccountHandler & { connect(onPublicKeyReceived: FreighterCallback): Promise @@ -12,7 +12,7 @@ export type FreighterAccountHandler = AccountHandler & { } export type FreighterAccHandlerPayload = AccountHandlerPayload & { - network: Network + networkConfig: NetworkConfig } export type FreighterCallback = (pk: string) => Promise diff --git a/src/stellar-plus/account/account-handler/types.d.ts b/src/stellar-plus/account/account-handler/types.ts similarity index 66% rename from src/stellar-plus/account/account-handler/types.d.ts rename to src/stellar-plus/account/account-handler/types.ts index e4dedb9..3bc1da5 100644 --- a/src/stellar-plus/account/account-handler/types.d.ts +++ b/src/stellar-plus/account/account-handler/types.ts @@ -5,7 +5,21 @@ import { AccountHelpersPayload } from 'stellar-plus/account/helpers/types' import { TransactionXdr } from 'stellar-plus/types' export type AccountHandler = AccountBase & { + getPublicKey(): string sign(tx: Transaction | FeeBumpTransaction): Promise | TransactionXdr + signatureSchema?: SignatureSchema } export type AccountHandlerPayload = AccountHelpersPayload + +export type SignatureSchema = { + threasholds: { + low: number + medium: number + high: number + } + signers: { + weight: number + publicKey: string + }[] +} diff --git a/src/stellar-plus/account/base/index.ts b/src/stellar-plus/account/base/index.ts index 3bead58..5808918 100644 --- a/src/stellar-plus/account/base/index.ts +++ b/src/stellar-plus/account/base/index.ts @@ -1,16 +1,15 @@ import { AccountBase, AccountBasePayload } from 'stellar-plus/account/base/types' import { AccountHelpers } from 'stellar-plus/account/helpers' -import { TransactionProcessor } from 'stellar-plus/core/classic-transaction-processor' export class AccountBaseClient extends AccountHelpers implements AccountBase { protected publicKey: string // helpers: AccountHelpers - private transactionProcessor?: TransactionProcessor + /** * * @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 {Network=} payload.network The network to use. + * @param {NetworkConfig=} payload.networkConfig The network to use. * * @description - The base account is used for handling accounts with no management actions. */ diff --git a/src/stellar-plus/account/helpers/account-data-viewer/index.ts b/src/stellar-plus/account/helpers/account-data-viewer/index.ts index 2cf33e1..85b5ac0 100644 --- a/src/stellar-plus/account/helpers/account-data-viewer/index.ts +++ b/src/stellar-plus/account/helpers/account-data-viewer/index.ts @@ -4,15 +4,15 @@ 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 { Network } from 'stellar-plus/types' +import { NetworkConfig } from 'stellar-plus/types' export class AccountDataViewerClient implements AccountDataViewer { - private network: Network + private networkConfig: NetworkConfig private horizonHandler: HorizonHandler private parent: AccountHelpers - constructor(network: Network, parent: AccountHelpers) { - this.network = network - this.horizonHandler = new HorizonHandlerClient(this.network) as HorizonHandler + constructor(networkConfig: NetworkConfig, parent: AccountHelpers) { + this.networkConfig = networkConfig + this.horizonHandler = new HorizonHandlerClient(this.networkConfig) as HorizonHandler this.parent = parent } diff --git a/src/stellar-plus/account/helpers/account-data-viewer/types.ts b/src/stellar-plus/account/helpers/account-data-viewer/types.ts index 473cac3..0a2717b 100644 --- a/src/stellar-plus/account/helpers/account-data-viewer/types.ts +++ b/src/stellar-plus/account/helpers/account-data-viewer/types.ts @@ -1,6 +1,6 @@ import { Horizon } from '@stellar/stellar-sdk' -import { Network } from 'stellar-plus/types' +import { NetworkConfig } from 'stellar-plus/types' // // Allows for account data to be fetched from the network @@ -19,5 +19,5 @@ export type AccountDataViewer = { } export type AccountDataViewerConstructor = { - network?: Network + networkConfig?: NetworkConfig } diff --git a/src/stellar-plus/account/helpers/friendbot/index.ts b/src/stellar-plus/account/helpers/friendbot/index.ts index 74a4872..62caf2d 100644 --- a/src/stellar-plus/account/helpers/friendbot/index.ts +++ b/src/stellar-plus/account/helpers/friendbot/index.ts @@ -2,15 +2,15 @@ import axios from 'axios' import { AccountHelpers } from 'stellar-plus/account/helpers' import { Friendbot } from 'stellar-plus/account/helpers/friendbot/types' -import { Network } from 'stellar-plus/types' +import { NetworkConfig } from 'stellar-plus/types' import { FBError } from './errors' export class FriendbotClient implements Friendbot { - private network: Network + private networkConfig: NetworkConfig private parent: AccountHelpers - constructor(network: Network, parent: AccountHelpers) { - this.network = network + constructor(networkConfig: NetworkConfig, parent: AccountHelpers) { + this.networkConfig = networkConfig this.parent = parent } @@ -24,7 +24,9 @@ export class FriendbotClient implements Friendbot { if ('publicKey' in this.parent && this.parent.publicKey && this.parent.publicKey !== '') { try { - await axios.get(`${this.network.friendbotUrl}?addr=${encodeURIComponent(this.parent.publicKey as string)}`) + await axios.get( + `${this.networkConfig.friendbotUrl}?addr=${encodeURIComponent(this.parent.publicKey as string)}` + ) return } catch (e) { @@ -39,7 +41,7 @@ export class FriendbotClient implements Friendbot { * @description - Throws an error if the network is not a test network. */ private requireTestNetwork(): void { - if (!this.network.friendbotUrl) { + 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 index f08e25c..559dfca 100644 --- a/src/stellar-plus/account/helpers/friendbot/types.ts +++ b/src/stellar-plus/account/helpers/friendbot/types.ts @@ -1,4 +1,4 @@ -import { Network } from 'stellar-plus/types' +import { NetworkConfig } from 'stellar-plus/types' // // Allows for accounts to be initialized with Friendbot @@ -8,5 +8,5 @@ export type Friendbot = { } export type FriendbotConstructor = { - network?: Network + networkConfig?: NetworkConfig } diff --git a/src/stellar-plus/account/helpers/index.ts b/src/stellar-plus/account/helpers/index.ts index e5e991b..1e38e85 100644 --- a/src/stellar-plus/account/helpers/index.ts +++ b/src/stellar-plus/account/helpers/index.ts @@ -9,9 +9,9 @@ export class AccountHelpers implements AccountHelpersType { public friendbot?: Friendbot constructor(payload: AccountHelpersPayload) { - if ('network' in payload && payload.network) { - this.accountDataViewer = new AccountDataViewerClient(payload.network, this) - this.friendbot = new FriendbotClient(payload.network, this) + 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/asset/classic/index.ts b/src/stellar-plus/asset/classic/index.ts index 00ecd1d..1e1b218 100644 --- a/src/stellar-plus/asset/classic/index.ts +++ b/src/stellar-plus/asset/classic/index.ts @@ -1,29 +1,33 @@ 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, ClassicAssetHandler as IClassicAssetHandler, } from 'stellar-plus/asset/classic/types' import { AssetTypes } from 'stellar-plus/asset/types' -import { TransactionProcessor } from 'stellar-plus/core/classic-transaction-processor' +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 { i128 } from 'stellar-plus/types' import { CAHError } from './errors' -export class ClassicAssetHandler extends TransactionProcessor implements IClassicAssetHandler { +export class ClassicAssetHandler implements IClassicAssetHandler { public code: string public issuerPublicKey: string public type: AssetTypes.native | AssetTypes.credit_alphanum4 | AssetTypes.credit_alphanum12 private issuerAccount?: AccountHandler private asset: StellarAsset + private horizonHandler: HorizonHandler + + private classicTrasactionPipeline: ClassicTransactionPipeline /** * * @param {string} code - The asset code. * @param {string} issuerPublicKey - The public key of the asset issuer. - * @param {Network} network - The network to use. + * @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. * @@ -32,9 +36,13 @@ export class ClassicAssetHandler extends TransactionProcessor implements IClassi * */ constructor(args: ClassicAssetHandlerConstructorArgs) { - super({ ...args }) + // super({ ...args }) this.code = args.code - this.issuerPublicKey = args.issuerPublicKey + 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' ? AssetTypes.native @@ -42,8 +50,14 @@ export class ClassicAssetHandler extends TransactionProcessor implements IClassi ? AssetTypes.credit_alphanum4 : AssetTypes.credit_alphanum12 - this.asset = new StellarAsset(args.code, args.issuerPublicKey) - this.issuerAccount = args.issuerAccount + this.horizonHandler = new HorizonHandler(args.networkConfig) + + this.asset = new StellarAsset(args.code, this.issuerPublicKey) + + this.classicTrasactionPipeline = new ClassicTransactionPipeline( + args.networkConfig, + args.options?.classicTransactionPipeline as ClassicTransactionPipelineOptions + ) } //========================================== @@ -115,9 +129,9 @@ export class ClassicAssetHandler extends TransactionProcessor implements IClassi return balanceLine[0] ? Number(balanceLine[0].balance) : 0 } - public async spendable_balance(): Promise { - throw new Error('Method not implemented.') - } + // public async spendable_balance(): Promise { + // throw new Error('Method not implemented.') + // } /** * @@ -133,31 +147,21 @@ export class ClassicAssetHandler extends TransactionProcessor implements IClassi * @description - Transfers the given amount of the asset from the 'from' account to the 'to' account. */ public async transfer(args: { from: string; to: string; amount: number } & TransactionInvocation): Promise { - const { from, to, amount, header, signers, feeBump } = args + const { from, to, amount } = args - const txInvocation = { - header, - signers, - feeBump, - } - const { envelope, updatedTxInvocation } = await this.transactionSubmitter.createEnvelope(txInvocation) - - // const { updatedHeader, updatedSigners, updatedFeeBump } = updatedTxInvocation + const txInvocation = args as TransactionInvocation - const tx = envelope - .addOperation( - Operation.payment({ - destination: to, - asset: this.asset, - amount: amount.toString(), - source: from, - }) - ) - .setTimeout(updatedTxInvocation.header.timeout) - .build() + const transferOp = Operation.payment({ + destination: to, + asset: this.asset, + amount: amount.toString(), + source: from, + }) - this.verifySigners([from], updatedTxInvocation.signers) - await this.processTransaction(tx, updatedTxInvocation.signers, updatedTxInvocation.feeBump) + await this.classicTrasactionPipeline.execute({ + txInvocation, + operations: [transferOp], + }) return } @@ -189,6 +193,11 @@ export class ClassicAssetHandler extends TransactionProcessor implements IClassi //========================================== // Management Methods - Require Admin / Issuer account //========================================== + // These methods make use of this.requireIssuerAccount() + // to enforce the issuer account to be set. The issue account + // is then injected as a signer in the transaction invocation + // when needed. + // // // @@ -223,25 +232,25 @@ export class ClassicAssetHandler extends TransactionProcessor implements IClassi const { to, amount } = args - const { envelope, updatedTxInvocation } = await this.transactionSubmitter.createEnvelope({ ...args }) - - const tx = envelope - .addOperation( - Operation.payment({ - destination: to, - asset: this.asset, - amount: amount.toString(), - source: this.asset.getIssuer(), - }) - ) - .setTimeout(updatedTxInvocation.header.timeout) - .build() + const txInvocation = args as TransactionInvocation + const updatedTxInvocation = { + ...txInvocation, + signers: [...txInvocation.signers, this.issuerAccount!], // Adds the issuer account as a signer. Issue account initialization is already verified by requireIssuerAccount(). + } - const signersWithIssuer = [...updatedTxInvocation.signers, this.issuerAccount!] + const mintOp = Operation.payment({ + destination: to, + asset: this.asset, + amount: amount.toString(), + source: this.asset.getIssuer(), + }) - this.verifySigners([this.asset.getIssuer()], signersWithIssuer) + const result = await this.classicTrasactionPipeline.execute({ + txInvocation: updatedTxInvocation, + operations: [mintOp], + }) - return await this.processTransaction(tx, signersWithIssuer, updatedTxInvocation.feeBump) + return result.response } public async clawback(): Promise { @@ -277,32 +286,63 @@ export class ClassicAssetHandler extends TransactionProcessor implements IClassi const { to, amount } = args - const { envelope, updatedTxInvocation } = await this.transactionSubmitter.createEnvelope({ ...args }) - - const { header, signers, feeBump } = updatedTxInvocation - - const tx = envelope - .addOperation( - Operation.changeTrust({ - source: to, - asset: this.asset, - }) - ) - .addOperation( - Operation.payment({ - destination: to, - asset: this.asset, - amount: amount.toString(), - source: this.asset.getIssuer(), - }) - ) - .setTimeout(header.timeout) - .build() - - const signersWithIssuer = [...signers, this.issuerAccount!] - this.verifySigners([to, this.asset.getIssuer()], signersWithIssuer) - - return await this.processTransaction(tx, signersWithIssuer, feeBump) + const txInvocation = args as TransactionInvocation + const updatedTxInvocation = { + ...txInvocation, + signers: [...txInvocation.signers, this.issuerAccount!], // Adds the issuer account as a signer. Issue account initialization is already verified by requireIssuerAccount(). + } + + const addTrustlineOp = Operation.changeTrust({ + source: to, + asset: this.asset, + }) + + const mintOp = Operation.payment({ + destination: to, + asset: this.asset, + amount: amount.toString(), + source: this.asset.getIssuer(), + }) + + const result = await this.classicTrasactionPipeline.execute({ + txInvocation: updatedTxInvocation, + operations: [addTrustlineOp, mintOp], + }) + + return result.response + } + + /** + * + * @param {string} to - The account id to add the trustline. + * @param {TransactionInvocation} txInvocation - The transaction invocation object. + * + * @requires - The 'to' account to be set as a signer in the transaction invocation. + * + * @description - Adds the trustline for the asset to the 'to' account. + * + * @returns {HorizonNamespace.SubmitTransactionResponse} The response from the Horizon server. + */ + public async addTrustline( + args: { + to: string + } & TransactionInvocation + ): Promise { + const { to } = args + + const txInvocation = args as TransactionInvocation + + const addTrustlineOp = Operation.changeTrust({ + source: to, + asset: this.asset, + }) + + const result = await this.classicTrasactionPipeline.execute({ + txInvocation, + operations: [addTrustlineOp], + }) + + return result.response } //========================================== diff --git a/src/stellar-plus/asset/classic/types.ts b/src/stellar-plus/asset/classic/types.ts index 650b7ae..7a32a90 100644 --- a/src/stellar-plus/asset/classic/types.ts +++ b/src/stellar-plus/asset/classic/types.ts @@ -2,9 +2,9 @@ import { HorizonApi } from '@stellar/stellar-sdk/lib/horizon' import { AccountHandler } from 'stellar-plus/account/account-handler/types' import { AssetType, AssetTypes } from 'stellar-plus/asset/types' -import { TransactionSubmitter } from 'stellar-plus/core/transaction-submitter/classic/types' +import { ClassicTransactionPipelineOptions } from 'stellar-plus/core/pipelines/classic-transaction/types' import { TransactionInvocation } from 'stellar-plus/core/types' -import { Network } from 'stellar-plus/types' +import { NetworkConfig } from 'stellar-plus/types' export type ClassicAsset = AssetType & { code: string @@ -16,10 +16,12 @@ export type ClassicAssetHandler = ClassicAsset & ClassicTokenInterface & Classic export type ClassicAssetHandlerConstructorArgs = { code: string - issuerPublicKey: string - network: Network - issuerAccount?: AccountHandler - transactionSubmitter?: TransactionSubmitter + issuerAccount: string | AccountHandler + networkConfig: NetworkConfig + + options?: { + classicTransactionPipeline?: ClassicTransactionPipelineOptions + } } export type ClassicTokenInterface = ClassicTokenInterfaceManagement & ClassicTokenInterfaceUser diff --git a/src/stellar-plus/asset/soroban-token/index.ts b/src/stellar-plus/asset/soroban-token/index.ts index 23c9c3a..fce35d2 100644 --- a/src/stellar-plus/asset/soroban-token/index.ts +++ b/src/stellar-plus/asset/soroban-token/index.ts @@ -1,6 +1,6 @@ -import { Address } from '@stellar/stellar-sdk' +import { Address, ContractSpec } from '@stellar/stellar-sdk' -import { methods, spec } from 'stellar-plus/asset/soroban-token/constants' +import { spec as defaultSpec, methods } from 'stellar-plus/asset/soroban-token/constants' import { SorobanTokenHandlerConstructorArgs, SorobanTokenInterface } from 'stellar-plus/asset/soroban-token/types' import { AssetTypes } from 'stellar-plus/asset/types' import { ContractEngine } from 'stellar-plus/core/contract-engine' @@ -13,7 +13,7 @@ export class SorobanTokenHandler extends ContractEngine implements SorobanTokenI /** * * @args args - * @param {Network} args.network - Network to connect to + * @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 @@ -26,7 +26,10 @@ export class SorobanTokenHandler extends ContractEngine implements SorobanTokenI constructor(args: SorobanTokenHandlerConstructorArgs) { super({ ...args, - spec: args.spec || spec, + contractParameters: { + ...args.contractParameters, + spec: args.contractParameters?.spec || (defaultSpec as ContractSpec), + }, }) } diff --git a/src/stellar-plus/asset/soroban-token/types.ts b/src/stellar-plus/asset/soroban-token/types.ts index b9bc268..f45ee28 100644 --- a/src/stellar-plus/asset/soroban-token/types.ts +++ b/src/stellar-plus/asset/soroban-token/types.ts @@ -3,16 +3,16 @@ import { ContractSpec } from '@stellar/stellar-sdk' import { AssetType } from 'stellar-plus/asset/types' import { Options } from 'stellar-plus/core/contract-engine/types' import { SorobanSimulationInvocation, TransactionInvocation } from 'stellar-plus/core/types' -import { RpcHandler } from 'stellar-plus/rpc/types' -import { Network, i128, u32 } from 'stellar-plus/types' +import { NetworkConfig, i128, u32 } from 'stellar-plus/types' export type SorobanTokenHandlerConstructorArgs = { - network: Network - spec?: ContractSpec //optional when compared to ContractEngine - contractId?: string - rpcHandler?: RpcHandler - wasm?: Buffer - wasmHash?: string + networkConfig: NetworkConfig + contractParameters?: { + spec?: ContractSpec + contractId?: string + wasm?: Buffer + wasmHash?: string + } options?: Options } diff --git a/src/stellar-plus/asset/stellar-asset-contract/index.ts b/src/stellar-plus/asset/stellar-asset-contract/index.ts index a984647..9fff1f8 100644 --- a/src/stellar-plus/asset/stellar-asset-contract/index.ts +++ b/src/stellar-plus/asset/stellar-asset-contract/index.ts @@ -3,12 +3,11 @@ import { Asset } from '@stellar/stellar-sdk' import { ClassicAssetHandler } from 'stellar-plus/asset/classic' import { ClassicAssetHandlerConstructorArgs } from 'stellar-plus/asset/classic/types' 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' -import { SorobanTokenHandlerConstructorArgs } from '../soroban-token/types' - export class SACHandler implements SACHandlerType { public type: AssetTypes = AssetTypes.SAC @@ -18,7 +17,7 @@ export class SACHandler implements SACHandlerType { /** * * @param args - The constructor arguments. - * @param {Network} args.network - The network to connect to. + * @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. @@ -40,7 +39,7 @@ export class SACHandler implements SACHandlerType { * @example - Initialize the Stellar Asset Contract handler and wrapping a classic asset with it: * * ```typescript - * const issuer = new StellarPlus.Account.DefaultAccountHandler({ network }) + * const issuer = new StellarPlus.Account.DefaultAccountHandler({ networkConfig }) * await issuer.friendbot?.initialize() * * const issuerInvocation: TransactionInvocation = { diff --git a/src/stellar-plus/asset/stellar-asset-contract/types.ts b/src/stellar-plus/asset/stellar-asset-contract/types.ts index f19e767..ab84214 100644 --- a/src/stellar-plus/asset/stellar-asset-contract/types.ts +++ b/src/stellar-plus/asset/stellar-asset-contract/types.ts @@ -1,12 +1,25 @@ +import { ContractSpec } from '@stellar/stellar-sdk' + import { ClassicAssetHandler } from 'stellar-plus/asset/classic' import { ClassicAssetHandlerConstructorArgs } from 'stellar-plus/asset/classic/types' import { SorobanTokenHandler } from 'stellar-plus/asset/soroban-token' -import { SorobanTokenHandlerConstructorArgs } from 'stellar-plus/asset/soroban-token/types' import { AssetType } from 'stellar-plus/asset/types' +import { Options } from 'stellar-plus/core/contract-engine/types' +import { NetworkConfig } from 'stellar-plus/types' export type SACHandler = AssetType & { classicHandler: ClassicAssetHandler sorobanTokenHandler: SorobanTokenHandler } -export type SACConstructorArgs = ClassicAssetHandlerConstructorArgs & SorobanTokenHandlerConstructorArgs +export type SACConstructorArgs = ClassicAssetHandlerConstructorArgs & { + networkConfig: NetworkConfig + contractParameters?: { + spec?: ContractSpec + contractId?: string + wasm?: Buffer + wasmHash?: string + } + options?: Options +} +// } diff --git a/src/stellar-plus/channel-accounts/channel-accounts-util.test.ts b/src/stellar-plus/channel-accounts/channel-accounts-util.test.ts deleted file mode 100644 index 2f2b12d..0000000 --- a/src/stellar-plus/channel-accounts/channel-accounts-util.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* eslint-disable import/order */ - -import { TransactionProcessor } from 'stellar-plus/core/classic-transaction-processor' -import MockTransactionProcessor from 'stellar-plus/core/classic-transaction-processor/mocks' - -jest.mock('stellar-plus/core/classic-transaction-processor', () => { - return { TransactionProcessor: MockTransactionProcessor as unknown as TransactionProcessor } -}) - -import { Operation } from '@stellar/stellar-sdk' - -import { DefaultAccountHandlerClient as DefaultAccountHandler } from 'stellar-plus/account/account-handler/default' -import { ChannelAccounts } from 'stellar-plus/channel-accounts' -import { testnet } from 'stellar-plus/constants' -import { TransactionInvocation } from 'stellar-plus/core/types' -import { Network } from 'stellar-plus/types' - -describe('ChannelAccounts Util', () => { - const mockNetwork: Network = testnet - let mockSponsor: DefaultAccountHandler - let mockTxInvocation: TransactionInvocation - - beforeEach(() => { - mockSponsor = new DefaultAccountHandler({ network: mockNetwork }) - mockTxInvocation = { - header: { - source: mockSponsor.getPublicKey(), - fee: '100', - timeout: 30, - }, - signers: [mockSponsor], - } - }) - - // Tests for openChannels - describe('openChannels', () => { - it('should open the correct number of channels', async () => { - // Given - const numberOfChannels = 5 - - jest.spyOn(Operation, 'beginSponsoringFutureReserves') - jest.spyOn(Operation, 'createAccount') - jest.spyOn(Operation, 'endSponsoringFutureReserves') - - // When - const channels = await ChannelAccounts.openChannels({ - numberOfChannels, - sponsor: mockSponsor, - network: mockNetwork, - txInvocation: mockTxInvocation, - }) - - // Then - expect(channels).toHaveLength(numberOfChannels) - expect(channels[0]).toBeInstanceOf(DefaultAccountHandler) - - expect(Operation.beginSponsoringFutureReserves).toHaveBeenCalledTimes(numberOfChannels) - expect(Operation.createAccount).toHaveBeenCalledTimes(numberOfChannels) - expect(Operation.endSponsoringFutureReserves).toHaveBeenCalledTimes(numberOfChannels) - }) - }) - - it('should throw error for invalid number of channels - under minimum', async () => { - // Given - const numberOfChannels = 0 - - // When - const openChannels = ChannelAccounts.openChannels({ - numberOfChannels, - sponsor: mockSponsor, - network: mockNetwork, - txInvocation: mockTxInvocation, - }) - - // Then - await expect(openChannels).rejects.toThrowError('Invalid number of channels to create') - }) - - it('should throw error for invalid number of channels - above maximum', async () => { - // Given - const numberOfChannels = 100 - - // When - const openChannels = ChannelAccounts.openChannels({ - numberOfChannels, - sponsor: mockSponsor, - network: mockNetwork, - txInvocation: mockTxInvocation, - }) - - // Then - await expect(openChannels).rejects.toThrowError('Invalid number of channels to create') - }) - - // Tests for closeChannels - describe('closeChannels', () => { - it('should close the channels and merge them into the sponsor', async () => { - // Given - const numberOfChannels = 5 - const channels = await ChannelAccounts.openChannels({ - numberOfChannels, - sponsor: mockSponsor, - network: mockNetwork, - txInvocation: mockTxInvocation, - }) - - jest.spyOn(Operation, 'accountMerge') - - // When - await ChannelAccounts.closeChannels({ - channels, - sponsor: mockSponsor, - network: mockNetwork, - txInvocation: mockTxInvocation, - }) - - // Then - expect(Operation.accountMerge).toHaveBeenCalledTimes(numberOfChannels) - }) - }) -}) diff --git a/src/stellar-plus/channel-accounts/index.ts b/src/stellar-plus/channel-accounts/index.ts index 5e09fd9..95cc29c 100644 --- a/src/stellar-plus/channel-accounts/index.ts +++ b/src/stellar-plus/channel-accounts/index.ts @@ -2,18 +2,18 @@ import { xdr as ClassicXdrNamespace, Operation } from '@stellar/stellar-sdk' import { DefaultAccountHandlerClient as DefaultAccountHandler } from 'stellar-plus/account/account-handler/default' import { AccountHandler } from 'stellar-plus/account/account-handler/types' -import { TransactionProcessor } from 'stellar-plus/core/classic-transaction-processor' +import { CHAError } from 'stellar-plus/channel-accounts/errors' +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 { Network } from 'stellar-plus/types' - -import { CHAError } from './errors' +import { NetworkConfig } from 'stellar-plus/types' export class ChannelAccounts { /** * @args {} The arguments for opening channels. * @param {number} numberOfChannels The number of channels to open. * @param {AccountHandler} sponsor The account that will sponsor the channels. - * @param {Network} network The network to use. + * @param {NetworkConfig} networkConfig The network to use. * @param {TransactionInvocation} txInvocation: The transaction invocation settings to use when building the transaction envelope. * * @description - Opens the given number of channels and returns the list of channel accounts. The accounts will be funded with 0 balance and sponsored by the sponsor account. @@ -23,12 +23,13 @@ export class ChannelAccounts { public static async openChannels(args: { numberOfChannels: number sponsor: AccountHandler - network: Network txInvocation: TransactionInvocation + networkConfig: NetworkConfig + transactionPipelineOptions?: ClassicTransactionPipelineOptions }): Promise { - const { numberOfChannels, sponsor, network, txInvocation } = args + const { numberOfChannels, sponsor, transactionPipelineOptions, txInvocation, networkConfig } = args - const txProcessor = new TransactionProcessor({ network }) + const classicTransactionPipeline = new ClassicTransactionPipeline(networkConfig, transactionPipelineOptions) if (numberOfChannels <= 0 || numberOfChannels > 15) { throw CHAError.invalidNumberOfChannelsToCreate(0, 15) @@ -37,7 +38,7 @@ export class ChannelAccounts { const operations: ClassicXdrNamespace.Operation[] = [] for (let i = 0; i < numberOfChannels; i++) { - const channel = new DefaultAccountHandler({ network }) + const channel = new DefaultAccountHandler({ networkConfig: networkConfig }) channels.push(channel) operations.push( @@ -55,19 +56,15 @@ export class ChannelAccounts { ) } - const verifiedSourceTxInvocation: TransactionInvocation = { - ...(this.verifyTxInvocationWithSponsor(txInvocation, sponsor) as TransactionInvocation), + const updatedTxInvocation = { + ...txInvocation, + signers: [...txInvocation.signers, ...channels, sponsor], } - verifiedSourceTxInvocation.signers.push(...channels) - - const { builtTx, updatedTxInvocation } = await txProcessor.buildCustomTransaction( + await classicTransactionPipeline.execute({ + txInvocation: updatedTxInvocation, operations, - verifiedSourceTxInvocation - ) - - // console.log("TxInvocation: ", updatedTxInvocation); - await txProcessor.processTransaction(builtTx, updatedTxInvocation.signers) + }) return channels } @@ -76,7 +73,7 @@ export class ChannelAccounts { * @args {} The arguments for closing channels. * @param {DefaultAccountHandler[]} channels The list of channels to close. * @param {DefaultAccountHandler} sponsor The account that was the sponsor for the channels. - * @param {Network} network The network to use. + * @param {NetworkConfig} networkConfig The network to use. * @param {TransactionInvocation }xInvocation The transaction invocation settings to use when building the transaction envelope. * * @description - Closes the given channels and merges the balances into the sponsor account. @@ -86,16 +83,17 @@ export class ChannelAccounts { public static async closeChannels(args: { channels: DefaultAccountHandler[] sponsor: AccountHandler - network: Network txInvocation: TransactionInvocation + networkConfig: NetworkConfig + transactionPipelineOptions?: ClassicTransactionPipelineOptions }): Promise { - const { channels, sponsor, network, txInvocation } = args - const txProcessor = new TransactionProcessor({ network }) - const operations: ClassicXdrNamespace.Operation[] = [] + const { channels, sponsor, networkConfig, txInvocation, transactionPipelineOptions } = args + + const classicTransactionPipeline = new ClassicTransactionPipeline(networkConfig, transactionPipelineOptions) + const operations: ClassicXdrNamespace.Operation[] = [] for (let i = 0; i < channels.length; i++) { const channel = channels[i] - operations.push( Operation.accountMerge({ source: channel.getPublicKey(), @@ -103,36 +101,15 @@ export class ChannelAccounts { }) ) } - const verifiedTxInvocation = this.verifyTxInvocationWithSponsor(txInvocation, sponsor) - - verifiedTxInvocation.signers = [...verifiedTxInvocation.signers, ...channels] - const { builtTx, updatedTxInvocation } = await txProcessor.buildCustomTransaction(operations, verifiedTxInvocation) - - // console.log("TxInvocation: ", updatedTxInvocation); - await txProcessor.processTransaction(builtTx, updatedTxInvocation.signers) - } - - /** - * - * - * @param {TransactionInvocation} txInvocation The transaction invocation settings to use when building the transaction envelope. - * @param {DefaultAccountHandler} sponsor The account that will sponsor the channels. - * - * @description - Verifies that the transaction invocation has the sponsor as a signer if the source is not the sponsor. - * - * @returns {TransactionInvocation} The updated transaction invocation. - */ - private static verifyTxInvocationWithSponsor( - txInvocation: TransactionInvocation, - sponsor: AccountHandler - ): TransactionInvocation { - return { + const updatedTxInvocation = { ...txInvocation, - signers: - txInvocation.header.source === sponsor.getPublicKey() - ? [...txInvocation.signers] - : [...txInvocation.signers, sponsor], + signers: [...txInvocation.signers, ...channels, sponsor], } + + await classicTransactionPipeline.execute({ + txInvocation: updatedTxInvocation, + operations, + }) } } diff --git a/src/stellar-plus/constants.ts b/src/stellar-plus/constants.ts index 6a60e8a..eba31e6 100644 --- a/src/stellar-plus/constants.ts +++ b/src/stellar-plus/constants.ts @@ -1,6 +1,6 @@ -import { Network, NetworksList } from 'stellar-plus/types' +import { NetworkConfig, NetworksList } from 'stellar-plus/types' -const networksConfig: { [key: string]: Network } = { +const networksConfig: { [key: string]: NetworkConfig } = { futurenet: { name: NetworksList.futurenet, networkPassphrase: 'Test SDF Future Network ; October 2022', @@ -23,8 +23,8 @@ const networksConfig: { [key: string]: Network } = { horizonUrl: 'https://horizon.stellar.org', }, } -const testnet: Network = networksConfig.testnet -const futurenet: Network = networksConfig.futurenet -const mainnet: Network = networksConfig.mainnet +const testnet: NetworkConfig = networksConfig.testnet +const futurenet: NetworkConfig = networksConfig.futurenet +const mainnet: NetworkConfig = networksConfig.mainnet export { testnet, futurenet, mainnet } diff --git a/src/stellar-plus/core/classic-transaction-processor/errors.ts b/src/stellar-plus/core/classic-transaction-processor/errors.ts deleted file mode 100644 index 28771d3..0000000 --- a/src/stellar-plus/core/classic-transaction-processor/errors.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { StellarPlusError } from 'stellar-plus/error' -import { TransactionInvocationMeta, extractTransactionInvocationMeta } from 'stellar-plus/error/helpers/transaction' - -import { FeeBumpHeader } from '../types' - -export enum ClassicTransactionProcessorErrorCodes { - // CTP0 General - CTP001 = 'CTP001', - CTP002 = 'CTP002', - CTP003 = 'CTP003', -} - -const wrappingFeeBumpWithFeeBump = (error?: Error): StellarPlusError => { - return new StellarPlusError({ - code: ClassicTransactionProcessorErrorCodes.CTP001, - message: 'Failed to wrap fee bump!', - source: 'ClassicTransactionProcessor', - details: - 'Cannot wrap a fee bump transaction with another fee bump transaction. Make sure that the inner transaction is a normal transaction envelope.', - meta: { error }, - }) -} - -const missingSignerPublicKey = (publicKey: string): StellarPlusError => { - return new StellarPlusError({ - code: ClassicTransactionProcessorErrorCodes.CTP002, - message: 'Missing signer public key!', - source: 'ClassicTransactionProcessor', - details: `Missing signer public key: ${publicKey}. Make sure to include a AccountHandler for this account as a signer in the Transaction Invocation object.`, - }) -} - -const failedToWrapFeeBump = (error: Error, feeBump: FeeBumpHeader): StellarPlusError => { - return new StellarPlusError({ - code: ClassicTransactionProcessorErrorCodes.CTP003, - message: 'Failed to wrap fee bump!', - source: 'ClassicTransactionProcessor', - details: `Failed to wrap fee bump! A problem occurred while wrapping the fee bump transaction! Check the meta property for more details.`, - meta: { - message: error.message, - error, - transactionInvocation: extractTransactionInvocationMeta(feeBump, true) as TransactionInvocationMeta, - }, - }) -} - -export const CTPError = { - wrappingFeeBumpWithFeeBump, - missingSignerPublicKey, - failedToWrapFeeBump, -} diff --git a/src/stellar-plus/core/classic-transaction-processor/index.ts b/src/stellar-plus/core/classic-transaction-processor/index.ts deleted file mode 100644 index 441941a..0000000 --- a/src/stellar-plus/core/classic-transaction-processor/index.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { - FeeBumpTransaction, - Horizon as HorizonNamespace, - Transaction, - TransactionBuilder, - xdr as xdrNamespace, -} from '@stellar/stellar-sdk' - -import { AccountHandler } from 'stellar-plus/account/account-handler/types' -import { TransactionProcessorConstructor } from 'stellar-plus/core/classic-transaction-processor/types' -import { DefaultTransactionSubmitter } from 'stellar-plus/core/transaction-submitter/classic/default' -import { TransactionSubmitter } from 'stellar-plus/core/transaction-submitter/classic/types' -import { FeeBumpHeader, TransactionInvocation } from 'stellar-plus/core/types' -import { HorizonHandlerClient } from 'stellar-plus/horizon/index' -import { HorizonHandler } from 'stellar-plus/horizon/types' -import { Network, TransactionXdr } from 'stellar-plus/types' - -import { CTPError } from './errors' - -export class TransactionProcessor { - protected horizonHandler: HorizonHandler - protected network: Network - protected transactionSubmitter: TransactionSubmitter - - /** - * - * @param {Network} network - * @param {TransactionSubmitter=} transactionSubmitter - */ - constructor(args: TransactionProcessorConstructor) { - this.network = args.network - this.horizonHandler = new HorizonHandlerClient(args.network) - this.transactionSubmitter = args.transactionSubmitter || new DefaultTransactionSubmitter(args.network) - } - - /** - * - * @param {Transaction} envelope - * @param {AccountHandler[]} signers - * - * @description - Signs the given transaction envelope with the provided signers. - * - * @returns {TransactionXdr} The signed transaction in xdr format. - */ - protected async signEnvelope( - envelope: Transaction | FeeBumpTransaction, - signers: AccountHandler[] - ): Promise { - let signedXDR = envelope.toXDR() - for (const signer of signers) { - signedXDR = await signer.sign(TransactionBuilder.fromXDR(signedXDR, this.network.networkPassphrase)) - } - return signedXDR - } - - /** - * - * @param {TransactionXdr} envelopeXdr - * @param {FeeBumpHeader} feeBump - * - * @description - Wraps the given transaction envelope with the provided fee bump header. - * - * @returns {FeeBumpTransaction} The fee bump transaction. - */ - protected async wrapFeeBump(envelopeXdr: TransactionXdr, feeBump: FeeBumpHeader): Promise { - const innerTx = TransactionBuilder.fromXDR(envelopeXdr, this.network.networkPassphrase) - - if (innerTx instanceof FeeBumpTransaction) { - throw CTPError.wrappingFeeBumpWithFeeBump() - } - try { - const feeBumpTx = TransactionBuilder.buildFeeBumpTransaction( - feeBump.header.source, - feeBump.header.fee, - innerTx, - this.network.networkPassphrase - ) - - const signedFeeBumpXDR = await this.signEnvelope(feeBumpTx, feeBump.signers) - - return TransactionBuilder.fromXDR(signedFeeBumpXDR, this.network.networkPassphrase) as FeeBumpTransaction - } catch (error) { - throw CTPError.failedToWrapFeeBump(error as Error, feeBump) - } - } - - /** - * - * @param {Transaction} envelope - * @param {AccountHandler[]} signers - * @param {FeeBumpHeader=} feeBump - * - * @description - Processes the given transaction envelope with the provided signers and fee bump header, submitting to the network. - * - * @returns {Promise} The horizon response from the transaction submission. - */ - public async processTransaction( - envelope: Transaction, - signers: AccountHandler[], - feeBump?: FeeBumpHeader - ): Promise { - const signedInnerTransaction = await this.signEnvelope(envelope, signers) - const finalEnvelope = feeBump - ? await this.wrapFeeBump(signedInnerTransaction, feeBump) - : TransactionBuilder.fromXDR(signedInnerTransaction, this.network.networkPassphrase) - const horizonResponse = (await this.transactionSubmitter.submit( - finalEnvelope - )) as HorizonNamespace.HorizonApi.SubmitTransactionResponse - const processedTransaction = this.transactionSubmitter.postProcessTransaction( - horizonResponse - ) as HorizonNamespace.HorizonApi.SubmitTransactionResponse - return processedTransaction - } - - /** - * - * @param {string[]} publicKeys - * @param {AccountHandler[]} signers - * - * @description - Verifies that all public keys are present in the signers array. Throws an error if any are missing. - * - * @returns {void} - */ - protected verifySigners(publicKeys: string[], signers: AccountHandler[]): void { - publicKeys.forEach((publicKey) => { - if (!signers.find((signer) => signer.getPublicKey() === publicKey)) { - throw CTPError.missingSignerPublicKey(publicKey) - } - }) - } - - /** - * - * @param {xdrNamespace.Operation[]} operations Array of operations to add to the transaction. - * @param {TransactionInvocation} txInvocation The transaction invocation settings to use when building the transaction envelope. - * - * @description - Builds a custom transaction with the provided operations and transaction invocation. - * - * @returns {Promise<{builtTx: ClassicTransaction, updatedTxInvocation: TransactionInvocation}>} The built transaction and updated transaction invocation. - */ - public async buildCustomTransaction( - operations: xdrNamespace.Operation[], - txInvocation: TransactionInvocation - ): Promise<{ - builtTx: Transaction - updatedTxInvocation: TransactionInvocation - }> { - const { envelope, updatedTxInvocation } = await this.transactionSubmitter.createEnvelope(txInvocation) - - const { header } = updatedTxInvocation - - let tx: TransactionBuilder = envelope - - for (const operation of operations) { - tx = envelope.addOperation(operation) - } - - const builtTx = tx.setTimeout(header.timeout).build() - - return { builtTx, updatedTxInvocation } - } -} diff --git a/src/stellar-plus/core/classic-transaction-processor/mocks/index.ts b/src/stellar-plus/core/classic-transaction-processor/mocks/index.ts deleted file mode 100644 index 4162100..0000000 --- a/src/stellar-plus/core/classic-transaction-processor/mocks/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Transaction, xdr as xdrNamespace } from '@stellar/stellar-sdk' -import { HorizonApi } from '@stellar/stellar-sdk/lib/horizon' - -import { TransactionInvocation } from 'stellar-plus/core/types' -import { mockUnsignedClassicTransaction } from 'stellar-plus/test/mocks/classic-transaction' - -export default class MockTransactionProcessor { - public processTransaction = jest.fn(async (): Promise => { - return Promise.resolve({ - hash: 'mock hash', - ledger: 12345, - successful: true, - envelope_xdr: 'mock envelope xdr', - result_xdr: 'mock result xdr', - result_meta_xdr: 'mock result meta xdr', - paging_token: 'mock paging token', - }) - }) - - public buildCustomTransaction = jest.fn( - async ( - operations: xdrNamespace.Operation[], - txInvocation: TransactionInvocation - ): Promise<{ - builtTx: Transaction - updatedTxInvocation: TransactionInvocation - }> => { - return Promise.resolve({ - builtTx: mockUnsignedClassicTransaction as Transaction, - updatedTxInvocation: txInvocation, - }) - } - ) -} diff --git a/src/stellar-plus/core/classic-transaction-processor/types.ts b/src/stellar-plus/core/classic-transaction-processor/types.ts deleted file mode 100644 index 5f9e8d3..0000000 --- a/src/stellar-plus/core/classic-transaction-processor/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { TransactionSubmitter } from 'stellar-plus/core/transaction-submitter/classic/types' -import { Network } from 'stellar-plus/types' - -export type TransactionProcessorConstructor = { - network: Network - transactionSubmitter?: TransactionSubmitter -} diff --git a/src/stellar-plus/core/contract-engine/contract-engine.test.ts b/src/stellar-plus/core/contract-engine/contract-engine.test.ts deleted file mode 100644 index 01d2b4c..0000000 --- a/src/stellar-plus/core/contract-engine/contract-engine.test.ts +++ /dev/null @@ -1,469 +0,0 @@ -import { Buffer } from 'buffer' - -import { Asset } from '@stellar/stellar-sdk' - -import { spec } from 'stellar-plus/asset/soroban-token/constants' -import { testnet } from 'stellar-plus/constants' -import { ContractEngine } from 'stellar-plus/core/contract-engine' -import { CEError } from 'stellar-plus/core/contract-engine/errors' -import { ContractEngineConstructorArgs, TransactionResources } from 'stellar-plus/core/contract-engine/types' -import { TransactionInvocation } from 'stellar-plus/core/types' -import { mockUnsignedClassicTransaction } from 'stellar-plus/test/mocks/classic-transaction' -import { mockTransactionInvocation } from 'stellar-plus/test/mocks/transaction-mock' - -import { SorobanInvokeArgs, SorobanSimulateArgs } from '../soroban-transaction-processor/types' - -const mockCEConstructorBaseArgs: ContractEngineConstructorArgs = { - network: testnet, - spec: spec, -} - -class TestableContractEngine extends ContractEngine { - public invokeContractTest(args: SorobanInvokeArgs): Promise { - return this.invokeContract(args) - } - public readFromContractTest(args: SorobanSimulateArgs): Promise { - return this.readFromContract(args) - } -} - -const mockTransactionResources: TransactionResources = { - cpuInstructions: 10, - ram: 10, - minResourceFee: 10, - ledgerReadBytes: 10, - ledgerWriteBytes: 10, - ledgerEntryReads: 10, - ledgerEntryWrites: 10, - eventSize: 10, - returnValueSize: 10, - transactionSize: 10, -} - -describe('ContractEngine', () => { - let mockTxInvocation: TransactionInvocation - - beforeEach(() => { - mockTxInvocation = mockTransactionInvocation() - }) - - afterEach(() => { - jest.clearAllMocks() - }) - - describe('Constructor', () => { - it('should correctly initialize the contract engine', () => { - const mockContractEngineWithWasm: ContractEngine = new ContractEngine({ - ...mockCEConstructorBaseArgs, - wasm: Buffer.from('mock-wasm'), - }) - expect(mockContractEngineWithWasm.getWasm()).toEqual(Buffer.from('mock-wasm')) - - const mockContractEngineWithWasmHash: ContractEngine = new ContractEngine({ - ...mockCEConstructorBaseArgs, - wasmHash: 'mock-wasm-hash', - }) - expect(mockContractEngineWithWasmHash.getWasmHash()).toEqual('mock-wasm-hash') - - const mockContractEngineWithContractId: ContractEngine = new ContractEngine({ - ...mockCEConstructorBaseArgs, - contractId: 'mock-contract-id', - }) - - expect(mockContractEngineWithContractId.getContractId()).toEqual('mock-contract-id') - }) - - it('should throw an error if values are missing in getters', () => { - const mockContractEngineWithoutValues: ContractEngine = new ContractEngine({ - ...mockCEConstructorBaseArgs, - }) - expect(() => mockContractEngineWithoutValues.getWasm()).toThrowError(CEError.missingWasm()) - expect(() => mockContractEngineWithoutValues.getWasmHash()).toThrowError(CEError.missingWasmHash()) - expect(() => mockContractEngineWithoutValues.getContractId()).toThrowError(CEError.missingContractId()) - }) - }) - - describe('uploadWasm', () => { - it('should upload WASM and update wasmHash', async () => { - const mockContractEngineWithWasm: ContractEngine = new ContractEngine({ - ...mockCEConstructorBaseArgs, - wasm: Buffer.from('mock-wasm'), - }) - - // Allowing this instance of 'any' to be able to mock the internal method - // uploadContractWasm which is protected and and would required a more complex approach - // to mock it. - // - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithWasm as any, 'buildUploadContractWasmTransaction') - .mockResolvedValueOnce(mockUnsignedClassicTransaction) - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithWasm as any, 'processBuiltTransaction') - .mockResolvedValueOnce({ response: 'mock-success-response', transactionResources: mockTransactionResources }) - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithWasm as any, 'extractWasmHashFromUploadWasmResponse') - .mockReturnValue('mock-wasm-hash') - - await mockContractEngineWithWasm.uploadWasm(mockTxInvocation) - expect(mockContractEngineWithWasm.getWasmHash()).toEqual('mock-wasm-hash') - }) - - it('should throw an error if wasm is missing', async () => { - const mockContractEngineWithoutWasm: ContractEngine = new ContractEngine({ - ...mockCEConstructorBaseArgs, - }) - - await expect(mockContractEngineWithoutWasm.uploadWasm(mockTxInvocation)).rejects.toThrowError( - CEError.missingWasm() - ) - }) - }) - - describe('deploy contract', () => { - it('should deploy a new instance of the contract and update contract id', async () => { - const mockContractEngineWithWasmHash: ContractEngine = new ContractEngine({ - ...mockCEConstructorBaseArgs, - wasmHash: 'mock-wasm-hash', - }) - - // Allowing this instance of 'any' to be able to mock the internal method - // deployContract which is protected and and would required a more complex approach - // to mock it. - // - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithWasmHash as any, 'buildDeployContractTransaction') - .mockResolvedValueOnce(mockUnsignedClassicTransaction) - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithWasmHash as any, 'processBuiltTransaction') - .mockResolvedValueOnce({ response: 'mock-success-response', transactionResources: mockTransactionResources }) - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithWasmHash as any, 'extractContractIdFromDeployContractResponse') - .mockReturnValue('mock-contract-id') - - await mockContractEngineWithWasmHash.deploy(mockTxInvocation) - expect(mockContractEngineWithWasmHash.getContractId()).toEqual('mock-contract-id') - }) - - it('should throw an error if wasm hash is missing', async () => { - const mockContractEngineWithoutWasmHash: ContractEngine = new ContractEngine({ - ...mockCEConstructorBaseArgs, - }) - - await expect(mockContractEngineWithoutWasmHash.deploy(mockTxInvocation)).rejects.toThrowError( - CEError.missingWasmHash() - ) - }) - }) - - describe('wrap asset contract', () => { - it('should wrap a classic asset and update the contract id', async () => { - const mockContractEngineWithoutContractId: ContractEngine = new ContractEngine({ - ...mockCEConstructorBaseArgs, - }) - - // Allowing this instance of 'any' to be able to mock the internal method - // wrapClassicAsset which is protected and and would required a more complex approach - // to mock it. - // - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithoutContractId as any, 'buildWrapClassicAssetTransaction') - .mockResolvedValueOnce(mockUnsignedClassicTransaction) - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithoutContractId as any, 'processBuiltTransaction') - .mockResolvedValueOnce({ response: 'mock-success-response', transactionResources: mockTransactionResources }) - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithoutContractId as any, 'extractContractIdFromWrapClassicAssetResponse') - .mockReturnValue('mock-contract-id') - - const mockAsset = new Asset('mockAsset', 'GBKIJOMRI2BRGEJ3CCME2E3Q6B6LOGQLL4KLDBLYDKVSHGY7CAROYISV') - - await mockContractEngineWithoutContractId.wrapAndDeployClassicAsset({ asset: mockAsset, ...mockTxInvocation }) - expect(mockContractEngineWithoutContractId.getContractId()).toEqual('mock-contract-id') - }) - - it('should throw an error if contract already has a contract Id', async () => { - const mockContractEngineWithContractId: ContractEngine = new ContractEngine({ - ...mockCEConstructorBaseArgs, - contractId: 'mock-contract-id', - }) - - const mockAsset = new Asset('mockAsset', 'GBKIJOMRI2BRGEJ3CCME2E3Q6B6LOGQLL4KLDBLYDKVSHGY7CAROYISV') - await expect( - mockContractEngineWithContractId.wrapAndDeployClassicAsset({ asset: mockAsset, ...mockTxInvocation }) - ).rejects.toThrowError(CEError.contractIdAlreadySet()) - }) - }) - - describe('invokeContract', () => { - it('should invoke a contract method and return the output', async () => { - const mockContractEngineWithContractId: TestableContractEngine = new TestableContractEngine({ - ...mockCEConstructorBaseArgs, - contractId: 'mock-contract-id', - }) - - // Allowing these instances of 'any' to be able to mock the internal methods - // which are protected and and would required a more complex approach - // to mock. - // - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'buildTransaction') - .mockResolvedValueOnce(mockUnsignedClassicTransaction) - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'extractOutputFromProcessedInvocation') - .mockResolvedValueOnce('mock-output') - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'processBuiltTransaction') - .mockResolvedValueOnce({ response: 'mock-success-response', transactionResources: mockTransactionResources }) - - expect( - await mockContractEngineWithContractId.invokeContractTest({ - method: 'mock-method', - methodArgs: { arg: 'mock-method-args' }, - ...mockTxInvocation, - }) - ).toEqual('mock-output') - }) - - it('should throw an error if contract id is missing', async () => { - const mockContractEngineWithoutContractId: TestableContractEngine = new TestableContractEngine({ - ...mockCEConstructorBaseArgs, - }) - - await expect( - mockContractEngineWithoutContractId.invokeContractTest({ - method: 'mock-method', - methodArgs: { arg: 'mock-method-args' }, - ...mockTxInvocation, - }) - ).rejects.toThrowError(CEError.missingContractId()) - }) - }) - - describe('readFromContract', () => { - it('should read from a contract method and return the output', async () => { - const mockContractEngineWithContractId: TestableContractEngine = new TestableContractEngine({ - ...mockCEConstructorBaseArgs, - contractId: 'mock-contract-id', - }) - - // Allowing these instances of 'any' to be able to mock the internal methods - // which are protected and and would required a more complex approach - // to mock. - // - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'buildTransaction') - .mockResolvedValueOnce(mockUnsignedClassicTransaction) - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'simulateTransaction') - .mockResolvedValueOnce('mock-output-unprocessed') - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'verifySimulationResponse') - .mockResolvedValueOnce('mock-output-verified') - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - - .spyOn(mockContractEngineWithContractId as any, 'extractOutputFromSimulation') - .mockResolvedValueOnce('mock-output') - - expect( - await mockContractEngineWithContractId.readFromContractTest({ - method: 'mock-method', - methodArgs: { arg: 'mock-method-args' }, - ...mockTxInvocation, - }) - ).toEqual('mock-output') - }) - - it('should throw an error if contract id is missing', async () => { - const mockContractEngineWithoutContractId: TestableContractEngine = new TestableContractEngine({ - ...mockCEConstructorBaseArgs, - }) - - await expect( - mockContractEngineWithoutContractId.readFromContractTest({ - method: 'mock-method', - methodArgs: { arg: 'mock-method-args' }, - ...mockTxInvocation, - }) - ).rejects.toThrowError(CEError.missingContractId()) - }) - }) - - describe('options', () => { - it('should invoke costHandler if provided when debug is true and return the transaction costs - invoke contract', async () => { - const mockCostHandler = jest.fn() - - const mockContractEngineWithContractId: TestableContractEngine = new TestableContractEngine({ - ...mockCEConstructorBaseArgs, - contractId: 'mock-contract-id', - options: { - debug: true, - costHandler: mockCostHandler, - }, - }) - - // Allowing these instances of 'any' to be able to mock the internal methods - // which are protected and and would required a more complex approach - // to mock. - // - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'buildTransaction') - .mockResolvedValueOnce(mockUnsignedClassicTransaction) - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'extractOutputFromProcessedInvocation') - .mockResolvedValueOnce('mock-output') - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'processBuiltTransaction') - .mockResolvedValueOnce({ response: 'mock-success-response', transactionResources: mockTransactionResources }) - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'extractFeeCharged') - .mockResolvedValueOnce(10) - - await mockContractEngineWithContractId.invokeContractTest({ - method: 'mock-method', - methodArgs: { arg: 'mock-method-args' }, - ...mockTxInvocation, - }) - - expect(mockCostHandler).toHaveBeenCalledTimes(1) - expect(mockCostHandler).toHaveBeenCalledWith( - 'mock-method', - mockTransactionResources, - expect.any(Number), - expect.any(Number) - ) - }) - - it('should not invoke costHandler if provided when debug is false - invoke contract', async () => { - const mockCostHandler = jest.fn() - - const mockContractEngineWithContractId: TestableContractEngine = new TestableContractEngine({ - ...mockCEConstructorBaseArgs, - contractId: 'mock-contract-id', - options: { - debug: false, - costHandler: mockCostHandler, - }, - }) - - // Allowing these instances of 'any' to be able to mock the internal methods - // which are protected and and would required a more complex approach - // to mock. - // - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'buildTransaction') - .mockResolvedValueOnce(mockUnsignedClassicTransaction) - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'extractOutputFromProcessedInvocation') - .mockResolvedValueOnce('mock-output') - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'processBuiltTransaction') - .mockResolvedValueOnce({ response: 'mock-success-response', transactionResources: mockTransactionResources }) - - await mockContractEngineWithContractId.invokeContractTest({ - method: 'mock-method', - methodArgs: { arg: 'mock-method-args' }, - ...mockTxInvocation, - }) - - expect(mockCostHandler).toHaveBeenCalledTimes(0) - expect(mockCostHandler).not.toHaveBeenCalledWith('mock-method', mockTransactionResources, expect.any(Number)) - }) - - it('should invoke costHandler if provided when debug is true and return the transaction costs - read from contract', async () => { - const mockCostHandler = jest.fn() - - const mockContractEngineWithContractId: TestableContractEngine = new TestableContractEngine({ - ...mockCEConstructorBaseArgs, - contractId: 'mock-contract-id', - options: { - debug: true, - costHandler: mockCostHandler, - }, - }) - - // Allowing these instances of 'any' to be able to mock the internal methods - // which are protected and and would required a more complex approach - // to mock. - // - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'buildTransaction') - .mockResolvedValueOnce(mockUnsignedClassicTransaction) - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'simulateTransaction') - .mockResolvedValueOnce('mock-output-unprocessed') - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'verifySimulationResponse') - .mockResolvedValueOnce('mock-output-verified') - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'extractOutputFromSimulation') - .mockResolvedValueOnce('mock-output') - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'parseTransactionResources') - .mockResolvedValueOnce(mockTransactionResources) - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'extractFeeCharged') - .mockResolvedValueOnce(10) - - await mockContractEngineWithContractId.readFromContractTest({ - method: 'mock-method', - methodArgs: { arg: 'mock-method-args' }, - ...mockTxInvocation, - }) - - expect(mockCostHandler).toHaveBeenCalledTimes(1) - expect(mockCostHandler).toHaveBeenCalledWith( - 'mock-method', - mockTransactionResources, - expect.any(Number), - expect.any(Number) - ) - }) - - it('should not invoke costHandler if provided when debug is false - read from contract', async () => { - const mockCostHandler = jest.fn() - - const mockContractEngineWithContractId: TestableContractEngine = new TestableContractEngine({ - ...mockCEConstructorBaseArgs, - contractId: 'mock-contract-id', - options: { - debug: false, - costHandler: mockCostHandler, - }, - }) - - // Allowing these instances of 'any' to be able to mock the internal methods - // which are protected and and would required a more complex approach - // to mock. - // - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'buildTransaction') - .mockResolvedValueOnce(mockUnsignedClassicTransaction) - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'simulateTransaction') - .mockResolvedValueOnce('mock-output-unprocessed') - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'verifySimulationResponse') - .mockResolvedValueOnce('mock-output-verified') - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'extractOutputFromSimulation') - .mockResolvedValueOnce('mock-output') - jest // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(mockContractEngineWithContractId as any, 'parseTransactionResources') - .mockResolvedValueOnce(mockTransactionResources) - - await mockContractEngineWithContractId.readFromContractTest({ - method: 'mock-method', - methodArgs: { arg: 'mock-method-args' }, - ...mockTxInvocation, - }) - - expect(mockCostHandler).toHaveBeenCalledTimes(0) - expect(mockCostHandler).not.toHaveBeenCalledWith( - 'mock-method', - mockTransactionResources, - expect.any(Number), - expect.any(Number) - ) - }) - }) -}) diff --git a/src/stellar-plus/core/contract-engine/errors.ts b/src/stellar-plus/core/contract-engine/errors.ts index 79bb939..b5dd4f7 100644 --- a/src/stellar-plus/core/contract-engine/errors.ts +++ b/src/stellar-plus/core/contract-engine/errors.ts @@ -83,7 +83,7 @@ const contractInstanceNotFound = (ledgerEntries: SorobanRpc.Api.GetLedgerEntries } const contractInstanceMissingLiveUntilLedgerSeq = ( - ledgerEntries: SorobanRpc.Api.GetLedgerEntriesResponse + ledgerEntries?: SorobanRpc.Api.GetLedgerEntriesResponse ): StellarPlusError => { return new StellarPlusError({ code: ContractEngineErrorCodes.CE006, @@ -107,7 +107,7 @@ const contractCodeNotFound = (ledgerEntries: SorobanRpc.Api.GetLedgerEntriesResp } const contractCodeMissingLiveUntilLedgerSeq = ( - ledgerEntries: SorobanRpc.Api.GetLedgerEntriesResponse + ledgerEntries?: SorobanRpc.Api.GetLedgerEntriesResponse ): StellarPlusError => { return new StellarPlusError({ code: ContractEngineErrorCodes.CE008, diff --git a/src/stellar-plus/core/contract-engine/index.ts b/src/stellar-plus/core/contract-engine/index.ts index ca399ca..7e208cb 100644 --- a/src/stellar-plus/core/contract-engine/index.ts +++ b/src/stellar-plus/core/contract-engine/index.ts @@ -1,35 +1,62 @@ import { Buffer } from 'buffer' -import { Contract, ContractSpec, SorobanRpc as SorobanRpcNamespace, Transaction, xdr } from '@stellar/stellar-sdk' - -import { ContractEngineConstructorArgs, Options, TransactionResources } from 'stellar-plus/core/contract-engine/types' -import { SorobanTransactionProcessor } from 'stellar-plus/core/soroban-transaction-processor' import { + Address, + Contract, + ContractSpec, + Operation, + OperationOptions, + SorobanDataBuilder, + SorobanRpc as SorobanRpcNamespace, + xdr, +} from '@stellar/stellar-sdk' + +import { CEError } from 'stellar-plus/core/contract-engine/errors' +import { + ContractEngineConstructorArgs, + Options, RestoreFootprintArgs, SorobanInvokeArgs, SorobanSimulateArgs, WrapClassicAssetArgs, -} from 'stellar-plus/core/soroban-transaction-processor/types' + isRestoreFootprintWithLedgerKeys, +} from 'stellar-plus/core/contract-engine/types' +import { SimulatedInvocationOutput } from 'stellar-plus/core/pipelines/simulate-transaction/types' +import { ContractIdOutput, ContractWasmHashOutput } from 'stellar-plus/core/pipelines/soroban-get-transaction/types' +import { SorobanTransactionPipeline } from 'stellar-plus/core/pipelines/soroban-transaction' import { TransactionInvocation } from 'stellar-plus/core/types' import { StellarPlusError } from 'stellar-plus/error' -import { StellarPlusErrorObject } from 'stellar-plus/error/types' +import { DefaultRpcHandler } from 'stellar-plus/rpc' +import { RpcHandler } from 'stellar-plus/rpc/types' +import { NetworkConfig } from 'stellar-plus/types' +import { generateRandomSalt } from 'stellar-plus/utils/functions' +import { InjectPreprocessParameterPlugin } from 'stellar-plus/utils/pipeline/plugins/generic/inject-preprocess-parameter' +import { ExtractInvocationOutputFromSimulationPlugin } from 'stellar-plus/utils/pipeline/plugins/simulate-transaction/extract-invocation-output' +import { ExtractContractIdPlugin } from 'stellar-plus/utils/pipeline/plugins/soroban-get-transaction/extract-contract-id' +import { ExtractInvocationOutputPlugin } from 'stellar-plus/utils/pipeline/plugins/soroban-get-transaction/extract-invocation-output' +import { ExtractWasmHashPlugin } from 'stellar-plus/utils/pipeline/plugins/soroban-get-transaction/extract-wasm-hash' -import { CEError, ContractEngineErrorCodes } from './errors' +import { + BuildTransactionPipelineInput, + BuildTransactionPipelineOutput, + BuildTransactionPipelineType, +} from '../pipelines/build-transaction/types' -export class ContractEngine extends SorobanTransactionProcessor { +export class ContractEngine { private spec: ContractSpec private contractId?: string private wasm?: Buffer private wasmHash?: string + private networkConfig: NetworkConfig + private rpcHandler: RpcHandler - private options: Options = { - debug: false, - costHandler: defaultCostHandler, - } + private sorobanTransactionPipeline: SorobanTransactionPipeline + + private options: Options /** * - * @param {Network} network - The network to use. + * @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. @@ -68,12 +95,21 @@ export class ContractEngine extends SorobanTransactionProcessor { * ``` */ constructor(args: ContractEngineConstructorArgs) { - super(args.network, args.rpcHandler) - this.spec = args.spec - this.contractId = args.contractId - this.wasm = args.wasm - this.wasmHash = args.wasmHash - this.options = { ...this.options, ...args.options } + const { networkConfig, contractParameters, options } = args as ContractEngineConstructorArgs + this.networkConfig = networkConfig + this.rpcHandler = options?.sorobanTransactionPipeline?.customRpcHandler + ? options.sorobanTransactionPipeline.customRpcHandler + : new DefaultRpcHandler(this.networkConfig) + this.spec = contractParameters.spec + this.contractId = contractParameters.contractId + this.wasm = contractParameters.wasm + this.wasmHash = contractParameters.wasmHash + this.options = { ...options } + + this.sorobanTransactionPipeline = new SorobanTransactionPipeline(networkConfig, { + customRpcHandler: options?.sorobanTransactionPipeline?.customRpcHandler, + ...this.options.sorobanTransactionPipeline, + }) } public getContractId(): string { @@ -96,6 +132,10 @@ export class ContractEngine extends SorobanTransactionProcessor { return new Contract(this.contractId!).getFootprint() } + public getRpcHandler(): RpcHandler { + return this.rpcHandler + } + /** * * @param {void} args - No arguments. @@ -106,23 +146,11 @@ export class ContractEngine extends SorobanTransactionProcessor { */ public async getContractInstanceLiveUntilLedgerSeq(): Promise { - this.requireContractId() - - const footprint = this.getContractFootprint() + const contractInstance = await this.getContractInstanceLedgerEntry() - const ledgerEntries = (await this.getRpcHandler().getLedgerEntries( - footprint - )) as SorobanRpcNamespace.Api.GetLedgerEntriesResponse - - const contractInstance = ledgerEntries.entries.find((entry) => entry.key.switch().name === 'contractData') - - if (!contractInstance) { - throw CEError.contractInstanceNotFound(ledgerEntries) - } if (!contractInstance.liveUntilLedgerSeq) { - throw CEError.contractInstanceMissingLiveUntilLedgerSeq(ledgerEntries) + throw CEError.contractInstanceMissingLiveUntilLedgerSeq() } - return contractInstance.liveUntilLedgerSeq } @@ -136,19 +164,10 @@ export class ContractEngine extends SorobanTransactionProcessor { * * */ public async getContractCodeLiveUntilLedgerSeq(): Promise { - this.requireWasmHash() - - const ledgerEntries = (await this.getRpcHandler().getLedgerEntries( - xdr.LedgerKey.contractCode(new xdr.LedgerKeyContractCode({ hash: Buffer.from(this.getWasmHash(), 'hex') })) - )) as SorobanRpcNamespace.Api.GetLedgerEntriesResponse + const contractCode = await this.getContractCodeLedgerEntry() - const contractCode = ledgerEntries.entries.find((entry) => entry.key.switch().name === 'contractCode') - - if (!contractCode) { - throw CEError.contractCodeNotFound(ledgerEntries) - } if (!contractCode.liveUntilLedgerSeq) { - throw CEError.contractCodeMissingLiveUntilLedgerSeq(ledgerEntries) + throw CEError.contractCodeMissingLiveUntilLedgerSeq() } return contractCode.liveUntilLedgerSeq @@ -181,22 +200,9 @@ export class ContractEngine extends SorobanTransactionProcessor { protected async readFromContract(args: SorobanSimulateArgs): Promise { this.requireContractId() - const startTime = Date.now() + const output = (await this.invokeContract(args, true)) as SimulatedInvocationOutput - const builtTx = (await this.buildTransaction(args, this.spec, this.contractId!)) as Transaction // Contract Id verified in requireContractId - const simulatedTransaction = await this.simulateTransaction(builtTx) - - const successfulSimulation = await this.verifySimulationResponse(simulatedTransaction) - - const costs = this.options.debug ? await this.parseTransactionResources(successfulSimulation) : {} - - const output = this.extractOutputFromSimulation(successfulSimulation, args.method) - - if (this.options.debug) { - this.options.costHandler?.(args.method, costs, Date.now() - startTime, 0) - } - - return output + return output.value } /** @@ -229,153 +235,37 @@ export class ContractEngine extends SorobanTransactionProcessor { * console.log(output) // 3 * ``` */ - protected async invokeContract(args: SorobanInvokeArgs): Promise { - this.requireContractId() - - const startTime = Date.now() - - const builtTx = await this.buildTransaction(args, this.spec, this.contractId!) // Contract Id verified in requireContractId - - const txInvocation = { ...args } as TransactionInvocation - - const { response, transactionResources } = await this.processBuiltTransaction({ - builtTx, - updatedTxInvocation: txInvocation, - }) - - const output = this.extractOutputFromProcessedInvocation(response, args.method) - - if (this.options.debug) { - const feeCharged = await this.extractFeeCharged(response) - this.options.costHandler?.( - args.method, - transactionResources as TransactionResources, - Date.now() - startTime, - feeCharged - ) - } - - return output - } - - /** - * - * @param {SorobanRpcNamespace.Api.SimulateTransactionSuccessResponse} simulatedTransaction - The simulated transaction response to parse. - * - * @returns {Promise} The parsed transaction costs. - * - * @description - Parses the transaction costs from the simulated transaction response. - * - */ - private async parseTransactionResources( - simulatedTransaction: SorobanRpcNamespace.Api.SimulateTransactionSuccessResponse - ): Promise { - const calculateEventSize = (event: xdr.DiagnosticEvent): number => { - if (event.event()?.type().name === 'diagnostic') { - return 0 - } - return event.toXDR().length - } - - const sorobanTransactionData = simulatedTransaction.transactionData.build() - const events = simulatedTransaction.events?.map((event) => calculateEventSize(event)) - const returnValueSize = simulatedTransaction.result?.retval.toXDR().length - const transactionDataSize = sorobanTransactionData.toXDR().length - const eventsSize = events?.reduce((accumulator, currentValue) => accumulator + currentValue, 0) - - return { - // cpuInstructions: Number(simulatedTransaction.cost?.cpuInsns), - cpuInstructions: Number(sorobanTransactionData?.resources().instructions()), - ram: Number(simulatedTransaction.cost?.memBytes), - minResourceFee: Number(simulatedTransaction.minResourceFee), - ledgerReadBytes: sorobanTransactionData?.resources().readBytes(), - ledgerWriteBytes: sorobanTransactionData?.resources().writeBytes(), - ledgerEntryReads: sorobanTransactionData?.resources().footprint().readOnly().length, - ledgerEntryWrites: sorobanTransactionData?.resources().footprint().readWrite().length, - eventSize: eventsSize, - returnValueSize: returnValueSize, - transactionSize: transactionDataSize, - } - } - - private async extractFeeCharged(tx: SorobanRpcNamespace.Api.GetSuccessfulTransactionResponse): Promise { - return Number(tx.resultXdr.feeCharged()) - } - - private async extractOutputFromSimulation( - simulated: SorobanRpcNamespace.Api.SimulateTransactionSuccessResponse, - method: string - ): Promise { - if (!simulated.result) { - throw CEError.simulationMissingResult(simulated) - } - - const output = this.spec.funcResToNative(method, simulated.result.retval) as unknown - return output - } - - private async extractOutputFromProcessedInvocation( - response: SorobanRpcNamespace.Api.GetSuccessfulTransactionResponse, - method: string + protected async invokeContract( + args: SorobanInvokeArgs | SorobanSimulateArgs, + simulateOnly: boolean = false ): Promise { - // console.log('Response: ', response) - const invocationResultMetaXdr = response.resultMetaXdr - const output = this.spec.funcResToNative( - method, - invocationResultMetaXdr.v3().sorobanMeta()?.returnValue().toXDR('base64') as string - ) as unknown - return output - } - - private async verifySimulationResponse( - simulated: SorobanRpcNamespace.Api.SimulateTransactionResponse - ): Promise { - if (SorobanRpcNamespace.Api.isSimulationError(simulated)) { - throw CEError.simulationFailed(simulated) - } - - // Simulated transactions with restore status are simulated as if - // the restore was done already. This means that the simulation - // result will come as successful. Therefore, we need to restore - // the footprint and proceed as if it was successful. - // Here, if no auto restor is set, we throw an error as the - // execution cannot proceed. - if (SorobanRpcNamespace.Api.isSimulationRestore(simulated)) { - throw CEError.transactionNeedsRestore(simulated) - // if (this.options.restoreTxInvocation) { - // await this.autoRestoreFootprintFromFromSimulation(simulated, originalTx) - // // Bump sequence number if same account is used for restore - // } else { - - // } - } + this.requireContractId() - if (SorobanRpcNamespace.Api.isSimulationSuccess(simulated) && simulated.result) { - return simulated as SorobanRpcNamespace.Api.SimulateTransactionSuccessResponse - } + const { method, methodArgs } = args + const txInvocation = { ...(args as SorobanInvokeArgs) } as TransactionInvocation - if (SorobanRpcNamespace.Api.isSimulationSuccess(simulated) && !simulated.result) { - throw CEError.simulationMissingResult(simulated) - } + const encodedArgs = this.spec.funcArgsToScVals(method, methodArgs) - throw CEError.couldntVerifyTransactionSimulation(simulated) - } + const contract = new Contract(this.contractId!) // Contract Id verified in requireContractId + const contractCallOperation = contract.call(method, ...encodedArgs) - private async autoRestoreFootprintFromFromSimulation( - simulation: SorobanRpcNamespace.Api.SimulateTransactionRestoreResponse, - originalTx: Transaction - ): Promise { - if (!this.options.restoreTxInvocation) { - throw CEError.restoreOptionNotSet(simulation) - } - const restorePreamble = simulation.restorePreamble + const executionPlugins = [ + ...(simulateOnly + ? [new ExtractInvocationOutputFromSimulationPlugin(this.spec, method)] + : [new ExtractInvocationOutputPlugin(this.spec, method)]), + ...(txInvocation.executionPlugins || []), + ] - await this.restoreFootprint({ restorePreamble, ...this.options.restoreTxInvocation } as RestoreFootprintArgs) + const result = await this.sorobanTransactionPipeline.execute({ + txInvocation, + operations: [contractCallOperation], + options: { + executionPlugins, + simulateOnly, + }, + }) - if (originalTx.source === this.options.restoreTxInvocation.header.source) { - originalTx.sequence = (BigInt(originalTx.sequence) + BigInt(1)).toString() - } - return originalTx + return result.output } //========================================== @@ -384,59 +274,6 @@ export class ContractEngine extends SorobanTransactionProcessor { // // - private async processBuiltTransaction(args: { - builtTx: Transaction - updatedTxInvocation: TransactionInvocation - }): Promise<{ - response: SorobanRpcNamespace.Api.GetSuccessfulTransactionResponse - transactionResources?: TransactionResources - }> { - const { updatedTxInvocation } = args - - let { builtTx } = args - - const simulatedTransaction = await this.simulateTransaction(builtTx) - - let successfulSimulation: SorobanRpcNamespace.Api.SimulateTransactionSuccessResponse - - try { - successfulSimulation = await this.verifySimulationResponse(simulatedTransaction) - } catch (error: unknown) { - const spError = error as StellarPlusErrorObject - if (spError.code === ContractEngineErrorCodes.CE102) { - // Transaction needs Restore - if (this.options.restoreTxInvocation) { - builtTx = await this.autoRestoreFootprintFromFromSimulation( - simulatedTransaction as SorobanRpcNamespace.Api.SimulateTransactionRestoreResponse, - builtTx - ) - - successfulSimulation = simulatedTransaction as SorobanRpcNamespace.Api.SimulateTransactionSuccessResponse - } else { - throw CEError.restoreOptionNotSet( - simulatedTransaction as SorobanRpcNamespace.Api.SimulateTransactionRestoreResponse - ) - } - } else { - throw error - } - } - - const transactionResources = this.options.debug ? await this.parseTransactionResources(successfulSimulation) : {} - - const assembledTransaction = await this.assembleTransaction(builtTx, successfulSimulation) - - return { - response: await this.processSorobanTransaction( - assembledTransaction, - updatedTxInvocation.signers, - updatedTxInvocation.feeBump, - updatedTxInvocation.header.timeout - ), - transactionResources, - } - } - /** * @param {TransactionInvocation} txInvocation - The transaction invocation object to use in this transaction. * @@ -448,26 +285,18 @@ export class ContractEngine extends SorobanTransactionProcessor { public async uploadWasm(txInvocation: TransactionInvocation): Promise { this.requireWasm() - const startTime = Date.now() - const builtTransactionObjectToProcess = await this.buildUploadContractWasmTransaction({ - wasm: this.wasm!, // Wasm verified in requireWasm - ...txInvocation, - }) - try { - const { response, transactionResources } = await this.processBuiltTransaction(builtTransactionObjectToProcess) - // Not using the root returnValue parameter because it may not be available depending on the rpcHandler. - const wasmHash = this.extractWasmHashFromUploadWasmResponse(response) - this.wasmHash = wasmHash - - if (this.options.debug) { - this.options.costHandler?.( - 'uploadWasm', - transactionResources as TransactionResources, - Date.now() - startTime, - await this.extractFeeCharged(response) - ) - } + const uploadOperation = Operation.uploadContractWasm({ wasm: this.wasm! }) // Wasm verified in requireWasm + + const result = await this.sorobanTransactionPipeline.execute({ + txInvocation, + operations: [uploadOperation], + options: { + executionPlugins: [new ExtractWasmHashPlugin()], + }, + }) + + this.wasmHash = (result.output as ContractWasmHashOutput).wasmHash } catch (error) { throw CEError.failedToUploadWasm(error as StellarPlusError) } @@ -483,28 +312,23 @@ export class ContractEngine extends SorobanTransactionProcessor { * */ public async deploy(txInvocation: TransactionInvocation): Promise { this.requireWasmHash() - const startTime = Date.now() - - const builtTransactionObjectToProcess = await this.buildDeployContractTransaction({ - wasmHash: this.wasmHash!, // Wasm hash verified in requireWasmHash - ...txInvocation, - }) try { - const { response, transactionResources } = await this.processBuiltTransaction(builtTransactionObjectToProcess) - // Not using the root returnValue parameter because it may not be available depending on the rpcHandler. - const contractId = this.extractContractIdFromDeployContractResponse(response) - - this.contractId = contractId - - if (this.options.debug) { - this.options.costHandler?.( - 'deployWasm', - transactionResources as TransactionResources, - Date.now() - startTime, - await this.extractFeeCharged(response) - ) - } + const deployOperation = Operation.createCustomContract({ + address: new Address(txInvocation.header.source), + wasmHash: Buffer.from(this.wasmHash!, 'hex'), // Wasm hash verified in requireWasmHash + salt: generateRandomSalt(), + } as OperationOptions.CreateCustomContract) + + const result = await this.sorobanTransactionPipeline.execute({ + txInvocation, + operations: [deployOperation], + options: { + executionPlugins: [new ExtractContractIdPlugin()], + }, + }) + + this.contractId = (result.output as ContractIdOutput).contractId } catch (error) { throw CEError.failedToDeployContract(error as StellarPlusError) } @@ -512,42 +336,33 @@ export class ContractEngine extends SorobanTransactionProcessor { public async wrapAndDeployClassicAsset(args: WrapClassicAssetArgs): Promise { this.requireNoContractId() - const startTime = Date.now() - - const builtTransactionObjectToProcess = await this.buildWrapClassicAssetTransaction(args) try { - const { response, transactionResources } = await this.processBuiltTransaction(builtTransactionObjectToProcess) - // Not using the root returnValue parameter because it may not be available depending on the rpcHandler. - const contractId = this.extractContractIdFromWrapClassicAssetResponse(response) - - this.contractId = contractId - - if (this.options.debug) { - this.options.costHandler?.( - 'wrapSAC', - transactionResources as TransactionResources, - Date.now() - startTime, - await this.extractFeeCharged(response) - ) - } + const txInvocation = args as TransactionInvocation + + const wrapOperation = Operation.createStellarAssetContract({ + asset: args.asset, + } as OperationOptions.CreateStellarAssetContract) + + const result = await this.sorobanTransactionPipeline.execute({ + txInvocation, + operations: [wrapOperation], + options: { + executionPlugins: [new ExtractContractIdPlugin()], + }, + }) + + this.contractId = (result.output as ContractIdOutput).contractId } catch (error) { throw CEError.failedToWrapAsset(error as StellarPlusError) } } - /** - * - * @param {TransactionInvocation} txInvocation - The transaction invocation object to use in this transaction. - * - * @returns {Promise} - The output of the invocation. - * - * @description - Restores the contract instance footprint. - */ - public async restoreContractFootprint(txInvocation: TransactionInvocation): Promise { - const footprint = this.getContractFootprint() - - return await this.restoreFootprint({ ...txInvocation, keys: [footprint] }) // Contract Id verified in requireContractId + public async restoreContractInstance(args: TransactionInvocation): Promise { + return await this.restore({ + keys: [(await this.getContractInstanceLedgerEntry()).key], + ...(args as TransactionInvocation), + }) } /** @@ -558,20 +373,11 @@ export class ContractEngine extends SorobanTransactionProcessor { * * @description - Restores the contract code. */ - public async restoreContractCode(txInvocation: TransactionInvocation): Promise { - this.requireWasmHash() - - const ledgerEntries = (await this.getRpcHandler().getLedgerEntries( - xdr.LedgerKey.contractCode(new xdr.LedgerKeyContractCode({ hash: Buffer.from(this.getWasmHash(), 'hex') })) - )) as SorobanRpcNamespace.Api.GetLedgerEntriesResponse - - const contractCode = ledgerEntries.entries.find((entry) => entry.key.switch().name === 'contractCode') - - if (!contractCode) { - throw CEError.contractCodeNotFound(ledgerEntries) - } - - return await this.restoreFootprint({ ...txInvocation, keys: [contractCode.key] }) // Contract Id verified in requireContractId + public async restoreContractCode(args: TransactionInvocation): Promise { + return await this.restore({ + keys: [(await this.getContractCodeLedgerEntry()).key], + ...(args as TransactionInvocation), + }) } //========================================== @@ -601,16 +407,83 @@ export class ContractEngine extends SorobanTransactionProcessor { throw CEError.missingWasmHash() } } -} -function defaultCostHandler( - methodName: string, - costs: TransactionResources, - elapsedTime: number, - feeCharged: number -): void { - console.log('Debugging method: ', methodName) - console.log(costs) - console.log('Fee charged: ', feeCharged) - console.log('Elapsed time: ', elapsedTime) + protected async getContractCodeLedgerEntry(): Promise { + this.requireWasmHash() + + const ledgerEntries = (await this.getRpcHandler().getLedgerEntries( + xdr.LedgerKey.contractCode(new xdr.LedgerKeyContractCode({ hash: Buffer.from(this.getWasmHash(), 'hex') })) + )) as SorobanRpcNamespace.Api.GetLedgerEntriesResponse + + const contractCode = ledgerEntries.entries.find((entry) => entry.key.switch().name === 'contractCode') + + if (!contractCode) { + throw CEError.contractCodeNotFound(ledgerEntries) + } + return contractCode as SorobanRpcNamespace.Api.LedgerEntryResult + } + + protected async getContractInstanceLedgerEntry(): Promise { + this.requireWasmHash() + + const footprint = this.getContractFootprint() + + const ledgerEntries = (await this.getRpcHandler().getLedgerEntries( + footprint + )) as SorobanRpcNamespace.Api.GetLedgerEntriesResponse + + const contractInstance = ledgerEntries.entries.find((entry) => entry.key.switch().name === 'contractData') + + if (!contractInstance) { + throw CEError.contractInstanceNotFound(ledgerEntries) + } + + return contractInstance as SorobanRpcNamespace.Api.LedgerEntryResult + } + + /** + * @args {RestoreFootprintArgs} args - The arguments for the invocation. + * @param {EnvelopeHeader} args.header - The header for the transaction. + * @param {AccountHandler[]} args.signers - The signers for the transaction. + * @param {FeeBumpHeader=} args.feeBump - The fee bump header for the transaction. This is optional. + * + * Option 1: Provide the keys directly. + * @param {xdr.LedgerKey[]} args.keys - The keys to restore. + * Option 2: Provide the restore preamble. + * @param { RestoreFootprintWithRestorePreamble} args.restorePreamble - The restore preamble. + * @param {string} args.restorePreamble.minResourceFee - The minimum resource fee. + * @param {SorobanDataBuilder} args.restorePreamble.transactionData - The transaction data. + * + * @returns {Promise} + * + * @description - Execute a transaction to restore a given footprint. + */ + protected async restore(args: RestoreFootprintArgs): Promise { + const txInvocation = args as TransactionInvocation + const sorobanData = isRestoreFootprintWithLedgerKeys(args) + ? new SorobanDataBuilder().setReadWrite(args.keys).build() + : args.restorePreamble.transactionData.build() + + const options: OperationOptions.RestoreFootprint = {} + + const injectionParameter = { sorobanData: sorobanData } + + const restoreFootprintOperation = Operation.restoreFootprint(options) + await this.sorobanTransactionPipeline.execute({ + txInvocation, + operations: [restoreFootprintOperation], + options: { + executionPlugins: [ + new InjectPreprocessParameterPlugin< + BuildTransactionPipelineInput, + BuildTransactionPipelineOutput, + BuildTransactionPipelineType, + typeof injectionParameter + >(injectionParameter, BuildTransactionPipelineType.id, 'preProcess'), + ], + }, + }) + + return + } } diff --git a/src/stellar-plus/core/contract-engine/types.ts b/src/stellar-plus/core/contract-engine/types.ts index 538179b..b553459 100644 --- a/src/stellar-plus/core/contract-engine/types.ts +++ b/src/stellar-plus/core/contract-engine/types.ts @@ -1,23 +1,24 @@ -import { ContractSpec } from '@stellar/stellar-sdk' +import { SorobanDataBuilder, Asset as StellarAsset, ContractSpec as _ContractSpec, xdr } from '@stellar/stellar-sdk' -import { TransactionInvocation } from 'stellar-plus/core/types' -import { RpcHandler } from 'stellar-plus/rpc/types' -import { Network } from 'stellar-plus/types' +import { AccountHandler } from 'stellar-plus/account' +import { SorobanTransactionPipelineOptions } from 'stellar-plus/core/pipelines/soroban-transaction/types' +import { EnvelopeHeader, FeeBumpHeader, NetworkConfig, TransactionInvocation } from 'stellar-plus/types' + +export type ContractSpec = _ContractSpec export type ContractEngineConstructorArgs = { - network: Network - spec: ContractSpec - contractId?: string - rpcHandler?: RpcHandler - wasm?: Buffer - wasmHash?: string + networkConfig: NetworkConfig + contractParameters: { + spec: ContractSpec + contractId?: string + wasm?: Buffer + wasmHash?: string + } options?: Options } export type Options = { - debug?: boolean - costHandler?: (methodName: string, costs: TransactionResources, elapsedTime: number, feeCharged: number) => void - restoreTxInvocation?: TransactionInvocation + sorobanTransactionPipeline?: SorobanTransactionPipelineOptions } export type TransactionResources = { @@ -32,3 +33,57 @@ export type TransactionResources = { returnValueSize?: number transactionSize?: number } + +export type SorobanInvokeArgs = SorobanSimulateArgs & { + signers: AccountHandler[] + feeBump?: FeeBumpHeader +} + +export type SorobanSimulateArgs = { + method: string + methodArgs: T + header: EnvelopeHeader +} + +export type SorobanUploadArgs = TransactionInvocation & { + wasm: Buffer +} + +export type SorobanDeployArgs = TransactionInvocation & { + wasmHash: string +} + +export type WrapClassicAssetArgs = TransactionInvocation & { + asset: StellarAsset +} + +export type ExtendFootprintTTLArgs = TransactionInvocation & { + extendTo: number + footprint: xdr.LedgerFootprint +} + +export type RestoreFootprintArgs = TransactionInvocation & + (RestoreFootprintWithLedgerKeys | RestoreFootprintWithRestorePreamble) + +export type RestoreFootprintWithLedgerKeys = { + keys: xdr.LedgerKey[] +} + +export type RestoreFootprintWithRestorePreamble = { + restorePreamble: { + minResourceFee: string + transactionData: SorobanDataBuilder + } +} + +export function isRestoreFootprintWithLedgerKeys( + args: RestoreFootprintArgs +): args is RestoreFootprintWithLedgerKeys & TransactionInvocation { + return 'keys' in args +} + +export function isRestoreFootprintWithRestorePreamble( + args: RestoreFootprintArgs +): args is RestoreFootprintWithRestorePreamble & TransactionInvocation { + return 'restorePreamble' in args +} diff --git a/src/stellar-plus/core/index.ts b/src/stellar-plus/core/index.ts index 310f873..f52d763 100644 --- a/src/stellar-plus/core/index.ts +++ b/src/stellar-plus/core/index.ts @@ -1,15 +1,25 @@ -import { TransactionProcessor } from 'stellar-plus/core/classic-transaction-processor' -import { SorobanTransactionProcessor } from 'stellar-plus/core/soroban-transaction-processor' -import { ChannelAccountsTransactionSubmitter } from 'stellar-plus/core/transaction-submitter/classic/channel-accounts-submitter' -import { DefaultTransactionSubmitter } from 'stellar-plus/core/transaction-submitter/classic/default' +import { ContractEngine } from './contract-engine' +import { BuildTransactionPipeline } from './pipelines/build-transaction' +import { ClassicSignRequirementsPipeline } from './pipelines/classic-sign-requirements' +import { ClassicTransactionPipeline } from './pipelines/classic-transaction' +import { FeeBumpPipeline } from './pipelines/fee-bump' +import { SignTransactionPipeline } from './pipelines/sign-transaction' +import { SimulateTransactionPipeline } from './pipelines/simulate-transaction' +import { SorobanGetTransactionPipeline } from './pipelines/soroban-get-transaction' +import { SorobanTransactionPipeline } from './pipelines/soroban-transaction' +import { SubmitTransactionPipeline } from './pipelines/submit-transaction' export const Core = { - Classic: { - DefaultTransactionSubmitter, - ChannelAccountsTransactionSubmitter, - TransactionProcessor, - }, - Soroban: { - SorobanTransactionProcessor, + Pipelines: { + BuildTransaction: BuildTransactionPipeline, + ClassicSignRequirements: ClassicSignRequirementsPipeline, + ClassicTransaction: ClassicTransactionPipeline, + FeeBump: FeeBumpPipeline, + SignTransaction: SignTransactionPipeline, + SimulateTransaction: SimulateTransactionPipeline, + SorobanGetTransaction: SorobanGetTransactionPipeline, + SorobanTransaction: SorobanTransactionPipeline, + SubmitTransaction: SubmitTransactionPipeline, }, + ContractEngine: ContractEngine, } diff --git a/src/stellar-plus/core/pipelines/build-transaction/errors.ts b/src/stellar-plus/core/pipelines/build-transaction/errors.ts new file mode 100644 index 0000000..7f9d95c --- /dev/null +++ b/src/stellar-plus/core/pipelines/build-transaction/errors.ts @@ -0,0 +1,103 @@ +import { StellarPlusError } from 'stellar-plus/error' +import { ConveyorBeltErrorMeta } from 'stellar-plus/error/helpers/conveyor-belt' +import { BeltMetadata } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +import { BuildTransactionPipelineInput } from './types' + +export enum ErrorCodesPipelineBuildTransaction { + // PBT0 General + PBT001 = 'PBT001', + PBT002 = 'PBT002', + PBT003 = 'PBT003', + PBT004 = 'PBT004', + PBT005 = 'PBT005', +} + +const couldntLoadAccount = ( + error: Error, + conveyorBeltErrorMeta: ConveyorBeltErrorMeta +): StellarPlusError => { + return new StellarPlusError({ + code: ErrorCodesPipelineBuildTransaction.PBT001, + message: 'Could not load account!', + source: 'PipelineBuildTransaction', + details: "An issue occurred while loading the account's data. Refer to the meta section for more details.", + meta: { + error, + conveyorBeltErrorMeta, + }, + }) +} + +const couldntCreateTransactionBuilder = ( + error: Error, + conveyorBeltErrorMeta: ConveyorBeltErrorMeta +): StellarPlusError => { + return new StellarPlusError({ + code: ErrorCodesPipelineBuildTransaction.PBT002, + message: 'Could not create transaction builder!', + source: 'PipelineBuildTransaction', + details: 'An issue occurred while creating the transaction builder. Refer to the meta section for more details.', + meta: { + error, + conveyorBeltErrorMeta, + }, + }) +} + +const couldntAddOperations = ( + error: Error, + conveyorBeltErrorMeta: ConveyorBeltErrorMeta +): StellarPlusError => { + return new StellarPlusError({ + code: ErrorCodesPipelineBuildTransaction.PBT003, + message: 'Could not add operations!', + source: 'PipelineBuildTransaction', + details: + 'An issue occurred while adding the operations to the transaction builder. Refer to the meta section for more details.', + meta: { + error, + conveyorBeltErrorMeta, + }, + }) +} + +const couldntBuildTransaction = ( + error: Error, + conveyorBeltErrorMeta: ConveyorBeltErrorMeta +): StellarPlusError => { + return new StellarPlusError({ + code: ErrorCodesPipelineBuildTransaction.PBT004, + message: 'Could not build transaction!', + source: 'PipelineBuildTransaction', + details: "An issue occurred while building the transaction's envelope. Refer to the meta section for more details.", + meta: { + error, + conveyorBeltErrorMeta, + }, + }) +} + +const couldntSetSorobanData = ( + error: Error, + conveyorBeltErrorMeta: ConveyorBeltErrorMeta +): StellarPlusError => { + return new StellarPlusError({ + code: ErrorCodesPipelineBuildTransaction.PBT005, + message: 'Could not set Soroban data!', + source: 'PipelineBuildTransaction', + details: 'An issue occurred while setting the Soroban data. Refer to the meta section for more details.', + meta: { + error, + conveyorBeltErrorMeta, + }, + }) +} + +export const PBTError = { + couldntLoadAccount, + couldntCreateTransactionBuilder, + couldntAddOperations, + couldntBuildTransaction, + couldntSetSorobanData, +} diff --git a/src/stellar-plus/core/pipelines/build-transaction/index.ts b/src/stellar-plus/core/pipelines/build-transaction/index.ts new file mode 100644 index 0000000..b9ff381 --- /dev/null +++ b/src/stellar-plus/core/pipelines/build-transaction/index.ts @@ -0,0 +1,68 @@ +import { Account, TransactionBuilder } from '@stellar/stellar-sdk' + +import { PBTError } from 'stellar-plus/core/pipelines/build-transaction/errors' +import { + BuildTransactionPipelineInput as BTInput, + BuildTransactionPipelineOutput as BTOutput, + BuildTransactionPipelinePlugin as BTPluginType, + BuildTransactionPipelineType as BTType, + BuildTransactionPipelineType, +} from 'stellar-plus/core/pipelines/build-transaction/types' +import { extractConveyorBeltErrorMeta } from 'stellar-plus/error/helpers/conveyor-belt' +import { ConveyorBelt } from 'stellar-plus/utils/pipeline/conveyor-belts' + +export class BuildTransactionPipeline extends ConveyorBelt { + constructor(plugins?: BTPluginType[]) { + super({ + type: BuildTransactionPipelineType.id, + plugins: plugins || [], + }) + } + + protected async process(item: BTInput, itemId: string): Promise { + const { header, horizonHandler, operations, networkPassphrase, sorobanData }: BTInput = item + let sourceAccount: Account + + try { + sourceAccount = (await horizonHandler.loadAccount(header.source)) as Account + } catch (e) { + throw PBTError.couldntLoadAccount(e as Error, extractConveyorBeltErrorMeta(item, this.getMeta(itemId))) + } + + let txEnvelope: TransactionBuilder + try { + txEnvelope = new TransactionBuilder(sourceAccount, { + fee: header.fee, + networkPassphrase: networkPassphrase, + }) + } catch (e) { + throw PBTError.couldntCreateTransactionBuilder( + e as Error, + extractConveyorBeltErrorMeta(item, this.getMeta(itemId)) + ) + } + + if (sorobanData) { + try { + txEnvelope.setSorobanData(sorobanData) + } catch (e) { + throw PBTError.couldntSetSorobanData(e as Error, extractConveyorBeltErrorMeta(item, this.getMeta(itemId))) + } + } + + try { + for (const operation of operations) { + txEnvelope.addOperation(operation) + } + txEnvelope.setTimeout(header.timeout) + } catch (e) { + throw PBTError.couldntAddOperations(e as Error, extractConveyorBeltErrorMeta(item, this.getMeta(itemId))) + } + + try { + return txEnvelope.build() as BTOutput + } catch (e) { + throw PBTError.couldntBuildTransaction(e as Error, extractConveyorBeltErrorMeta(item, this.getMeta(itemId))) + } + } +} diff --git a/src/stellar-plus/core/pipelines/build-transaction/types.ts b/src/stellar-plus/core/pipelines/build-transaction/types.ts new file mode 100644 index 0000000..7f97c28 --- /dev/null +++ b/src/stellar-plus/core/pipelines/build-transaction/types.ts @@ -0,0 +1,26 @@ +import { xdr } from '@stellar/stellar-sdk' + +import { HorizonHandler } from 'stellar-plus' +import { EnvelopeHeader, Transaction } from 'stellar-plus/types' +import { BeltPluginType, GenericPlugin } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +export type BuildTransactionPipelineInput = { + header: EnvelopeHeader + horizonHandler: HorizonHandler + operations: xdr.Operation[] + networkPassphrase: string + sorobanData?: string | xdr.SorobanTransactionData +} + +export type BuildTransactionPipelineOutput = Transaction + +// export type BuildTransactionPipelineType = 'BuildTransactionPipeline' +export enum BuildTransactionPipelineType { + id = 'BuildTransactionPipeline', +} + +export type BuildTransactionPipelinePlugin = BeltPluginType< + BuildTransactionPipelineInput, + BuildTransactionPipelineOutput, + BuildTransactionPipelineType | GenericPlugin +> diff --git a/src/stellar-plus/core/pipelines/classic-sign-requirements/errors.ts b/src/stellar-plus/core/pipelines/classic-sign-requirements/errors.ts new file mode 100644 index 0000000..15ba7f8 --- /dev/null +++ b/src/stellar-plus/core/pipelines/classic-sign-requirements/errors.ts @@ -0,0 +1,36 @@ +import { FeeBumpTransaction, 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 { ClassicSignRequirementsPipelineInput } from './types' + +export enum ErrorCodesPipelineClassicSignRequirements { + // PBT0 General + CSR001 = 'CSR001', +} + +const processFailed = ( + error: Error, + conveyorBeltErrorMeta: ConveyorBeltErrorMeta, + transaction: Transaction | FeeBumpTransaction +): StellarPlusError => { + return new StellarPlusError({ + code: ErrorCodesPipelineClassicSignRequirements.CSR001, + message: 'An issue occurred while processing the classic sign requirements.', + source: 'PipelineClassicSignRequirements', + details: + "An issue occurred while processing the classic sign requirements. Review the transaction object and the operations' parameters.", + meta: { + error, + conveyorBeltErrorMeta, + transactionData: extractTransactionData(transaction), + }, + }) +} + +export const CSRError = { + processFailed, +} diff --git a/src/stellar-plus/core/pipelines/classic-sign-requirements/index.ts b/src/stellar-plus/core/pipelines/classic-sign-requirements/index.ts new file mode 100644 index 0000000..a207992 --- /dev/null +++ b/src/stellar-plus/core/pipelines/classic-sign-requirements/index.ts @@ -0,0 +1,255 @@ +import { FeeBumpTransaction, Operation, Transaction, xdr } from '@stellar/stellar-sdk' + +import { + ClassicSignRequirementsPipelineInput, + ClassicSignRequirementsPipelineOutput, + ClassicSignRequirementsPipelinePlugin, + ClassicSignRequirementsPipelineType, +} from 'stellar-plus/core/pipelines/classic-sign-requirements/types' +import { SignatureRequirement, SignatureThreshold } from 'stellar-plus/core/types' +import { extractConveyorBeltErrorMeta } from 'stellar-plus/error/helpers/conveyor-belt' +import { ConveyorBelt } from 'stellar-plus/utils/pipeline/conveyor-belts' + +import { CSRError } from './errors' + +export class ClassicSignRequirementsPipeline extends ConveyorBelt< + ClassicSignRequirementsPipelineInput, + ClassicSignRequirementsPipelineOutput, + ClassicSignRequirementsPipelineType +> { + constructor(plugins?: ClassicSignRequirementsPipelinePlugin[]) { + super({ + type: ClassicSignRequirementsPipelineType.id, + plugins: plugins || [], + }) + } + + protected async process( + item: ClassicSignRequirementsPipelineInput, + _itemId: string + ): Promise { + const transaction = item + + try { + const operations = transaction instanceof FeeBumpTransaction ? [] : (transaction as Transaction).operations + const sourceRequirement = this.getSignatureThresholdForSource(transaction) + + const operationRequirements = + transaction instanceof FeeBumpTransaction ? [] : this.getOperationsSignatureRequirements(operations) + + const bundledRequirements = this.bundleSignatureRequirements(operationRequirements, sourceRequirement) + + return bundledRequirements + } catch (e) { + throw CSRError.processFailed(e as Error, extractConveyorBeltErrorMeta(item, this.getMeta(_itemId)), transaction) + } + } + + private getOperationsSignatureRequirements(operations: Operation[]): SignatureRequirement[] { + const requirements: SignatureRequirement[] = [] + + for (const op of operations) { + const signerRequirements = this.getRequiredSignatureThresholdForClassicOperation(op) + if (signerRequirements) requirements.push(signerRequirements) + } + + return requirements + } + + private getSignatureThresholdForSource(transaction: Transaction | FeeBumpTransaction): SignatureRequirement { + if ((transaction as FeeBumpTransaction).innerTransaction) { + const fbTx = transaction as FeeBumpTransaction + return { + publicKey: fbTx.feeSource, + thresholdLevel: SignatureThreshold.low, + } + } + + const tx = transaction as Transaction + return { + publicKey: tx.source, + thresholdLevel: SignatureThreshold.medium, + } + } + + private getRequiredSignatureThresholdForClassicOperation(operation: Operation): SignatureRequirement | void { + const setSourceSigner = (source?: string): string => { + return source ? source : 'source' + } + + let thresholdLevel = SignatureThreshold.medium + + switch (operation.type) { + case xdr.OperationType.createAccount().name: + return { + publicKey: setSourceSigner(operation.source), + thresholdLevel, + } + case xdr.OperationType.payment().name: + return { + publicKey: setSourceSigner(operation.source), + thresholdLevel, + } + case xdr.OperationType.pathPaymentStrictSend().name: + return { + publicKey: setSourceSigner(operation.source), + thresholdLevel, + } + + case xdr.OperationType.pathPaymentStrictReceive().name: + return { + publicKey: setSourceSigner(operation.source), + thresholdLevel, + } + case xdr.OperationType.manageSellOffer().name: + return { + publicKey: setSourceSigner(operation.source), + thresholdLevel, + } + + case xdr.OperationType.manageBuyOffer().name: + return { + publicKey: setSourceSigner(operation.source), + thresholdLevel, + } + + case xdr.OperationType.createPassiveSellOffer().name: + return { + publicKey: setSourceSigner(operation.source), + thresholdLevel, + } + + 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 + ) { + thresholdLevel = SignatureThreshold.high + } + + return { + publicKey: setSourceSigner(operation.source), + thresholdLevel, + } + + case xdr.OperationType.changeTrust().name: + return { + publicKey: setSourceSigner(operation.source), + thresholdLevel, + } + + case xdr.OperationType.allowTrust().name: + return { + publicKey: setSourceSigner(operation.source), + thresholdLevel: SignatureThreshold.low, + } + + case xdr.OperationType.accountMerge().name: + return { + publicKey: setSourceSigner(operation.source), + thresholdLevel, + } + + case xdr.OperationType.manageData().name: + return { + publicKey: setSourceSigner(operation.source), + thresholdLevel, + } + + case xdr.OperationType.bumpSequence().name: + return { + publicKey: setSourceSigner(operation.source), + thresholdLevel: SignatureThreshold.low, + } + + case xdr.OperationType.createClaimableBalance().name: + return { + publicKey: setSourceSigner(operation.source), + thresholdLevel, + } + + case xdr.OperationType.claimClaimableBalance().name: + return { + publicKey: setSourceSigner(operation.source), + thresholdLevel, + } + + case xdr.OperationType.beginSponsoringFutureReserves().name: + return { + publicKey: setSourceSigner(operation.source), + thresholdLevel, + } + + case xdr.OperationType.endSponsoringFutureReserves().name: + return { + publicKey: setSourceSigner(operation.source), + thresholdLevel, + } + + case xdr.OperationType.revokeSponsorship().name: + return { + publicKey: setSourceSigner(operation.source), + thresholdLevel, + } + + case xdr.OperationType.clawback().name: + return { + publicKey: setSourceSigner(operation.source), + thresholdLevel, + } + + case xdr.OperationType.clawbackClaimableBalance().name: + return { + publicKey: setSourceSigner(operation.source), + thresholdLevel, + } + + case xdr.OperationType.setTrustLineFlags().name: + return { + publicKey: setSourceSigner(operation.source), + thresholdLevel: SignatureThreshold.low, + } + + case xdr.OperationType.liquidityPoolDeposit().name: + return { + publicKey: setSourceSigner(operation.source), + thresholdLevel, + } + + case xdr.OperationType.liquidityPoolWithdraw().name: + return { + publicKey: setSourceSigner(operation.source), + thresholdLevel, + } + } + } + + private bundleSignatureRequirements( + operationRequirements: SignatureRequirement[], + sourceRequirement: SignatureRequirement + ): SignatureRequirement[] { + const bundledRequirements: SignatureRequirement[] = [sourceRequirement] + + for (const requirement of operationRequirements) { + const publicKey = requirement.publicKey === 'source' ? sourceRequirement.publicKey : requirement.publicKey + + const index = bundledRequirements.findIndex((r) => r.publicKey === publicKey) + if (index === -1) { + bundledRequirements.push({ + publicKey, + thresholdLevel: requirement.thresholdLevel, + }) + } else { + bundledRequirements[index].thresholdLevel = Math.max( + bundledRequirements[index].thresholdLevel, + requirement.thresholdLevel + ) + } + } + + return bundledRequirements + } +} diff --git a/src/stellar-plus/core/pipelines/classic-sign-requirements/types.ts b/src/stellar-plus/core/pipelines/classic-sign-requirements/types.ts new file mode 100644 index 0000000..4f7b971 --- /dev/null +++ b/src/stellar-plus/core/pipelines/classic-sign-requirements/types.ts @@ -0,0 +1,19 @@ +import { FeeBumpTransaction, Transaction } from '@stellar/stellar-sdk' + +import { SignatureRequirement } from 'stellar-plus/core/types' +import { BeltPluginType, GenericPlugin } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +export type ClassicSignRequirementsPipelineInput = Transaction | FeeBumpTransaction + +export type ClassicSignRequirementsPipelineOutput = SignatureRequirement[] + +// export type ClassicSignRequirementsPipelineType = 'ClassicSignRequirementsPipeline' +export enum ClassicSignRequirementsPipelineType { + id = 'ClassicSignRequirementsPipeline', +} + +export type ClassicSignRequirementsPipelinePlugin = BeltPluginType< + ClassicSignRequirementsPipelineInput, + ClassicSignRequirementsPipelineOutput, + ClassicSignRequirementsPipelineType | GenericPlugin +> diff --git a/src/stellar-plus/core/pipelines/classic-transaction/index.ts b/src/stellar-plus/core/pipelines/classic-transaction/index.ts new file mode 100644 index 0000000..f7a18a3 --- /dev/null +++ b/src/stellar-plus/core/pipelines/classic-transaction/index.ts @@ -0,0 +1,132 @@ +import { BuildTransactionPipeline } from 'stellar-plus/core/pipelines/build-transaction' +import { ClassicSignRequirementsPipeline } from 'stellar-plus/core/pipelines/classic-sign-requirements' +import { + ClassicTransactionPipelineInput, + ClassicTransactionPipelineOptions, + ClassicTransactionPipelineOutput, + ClassicTransactionPipelinePlugin, + ClassicTransactionPipelineType, + SupportedInnerPlugins, +} 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 { HorizonHandlerClient } from 'stellar-plus/horizon' +import { NetworkConfig } from 'stellar-plus/types' +import { MultiBeltPipeline } from 'stellar-plus/utils/pipeline/multi-belt-pipeline' +import { MultiBeltPipelineOptions } from 'stellar-plus/utils/pipeline/multi-belt-pipeline/types' + +import { BuildTransactionPipelinePlugin, BuildTransactionPipelineType } from '../build-transaction/types' +import { + ClassicSignRequirementsPipelinePlugin, + ClassicSignRequirementsPipelineType, +} from '../classic-sign-requirements/types' + +export class ClassicTransactionPipeline extends MultiBeltPipeline< + ClassicTransactionPipelineInput, + ClassicTransactionPipelineOutput, + ClassicTransactionPipelineType, + SupportedInnerPlugins +> { + private horizonHandler: HorizonHandlerClient + private networkConfig: NetworkConfig + + constructor(networkConfig: NetworkConfig, options?: ClassicTransactionPipelineOptions) { + const internalConstructorArgs = { + beltType: ClassicTransactionPipelineType.id, + plugins: (options?.plugins as ClassicTransactionPipelinePlugin[]) || [], + } as MultiBeltPipelineOptions< + ClassicTransactionPipelineInput, + ClassicTransactionPipelineOutput, + ClassicTransactionPipelineType, + SupportedInnerPlugins + > + + super({ + ...internalConstructorArgs, + ...{ type: ClassicTransactionPipelineType.id }, + }) + + this.networkConfig = networkConfig + this.horizonHandler = new HorizonHandlerClient(this.networkConfig) + } + + protected async process( + item: ClassicTransactionPipelineInput, + itemId: string + ): Promise { + const { txInvocation, operations, options }: ClassicTransactionPipelineInput = item + const executionPlugins = [] + if (options?.executionPlugins) executionPlugins.push(...options.executionPlugins) + + // ======================= Build Transaction ========================== + + const buildTransactionPipelinePlugins = this.getInnerPluginsByType( + executionPlugins, + 'BuildTransactionPipeline' as BuildTransactionPipelineType + ) as BuildTransactionPipelinePlugin[] + + const builTransactionPipeline = new BuildTransactionPipeline(buildTransactionPipelinePlugins) + + const builtTx = await builTransactionPipeline.execute( + { + header: txInvocation.header, + horizonHandler: this.horizonHandler, + operations, + networkPassphrase: this.networkConfig.networkPassphrase, + }, + itemId + ) + + // ======================= Calculate classic requirements ========================== + const classicSignRequirementsPipelinePlugins = this.getInnerPluginsByType( + executionPlugins, + 'ClassicSignRequirementsPipeline' as ClassicSignRequirementsPipelineType + ) as ClassicSignRequirementsPipelinePlugin[] + + const classicSignRequirementsPipeline = new ClassicSignRequirementsPipeline(classicSignRequirementsPipelinePlugins) + + const classicSignatureRequirements = await classicSignRequirementsPipeline.execute(builtTx, itemId) + + // ======================= Sign Transaction ========================== + const signTransactionPipelinePlugins = this.getInnerPluginsByType( + executionPlugins, + 'SignTransactionPipeline' as SignTransactionPipelineType + ) as SignTransactionPipelinePlugin[] + + const signTransactionPipeline = new SignTransactionPipeline(signTransactionPipelinePlugins) + const signedTransaction = await signTransactionPipeline.execute( + { + transaction: builtTx, + signatureRequirements: classicSignatureRequirements, + signers: txInvocation.signers, + }, + itemId + ) + + // ======================= Submit Transaction ========================== + const submitTransactionPipelinePlugins = this.getInnerPluginsByType( + executionPlugins, + 'SubmitTransactionPipeline' as SubmitTransactionPipelineType + ) as SubmitTransactionPipelinePlugin[] + + const submitTransactionPipeline = new SubmitTransactionPipeline(submitTransactionPipelinePlugins) + + const submissionResult = await submitTransactionPipeline.execute( + { + transaction: signedTransaction, + networkHandler: this.horizonHandler, + }, + itemId + ) + + return submissionResult as ClassicTransactionPipelineOutput + } +} diff --git a/src/stellar-plus/core/pipelines/classic-transaction/types.ts b/src/stellar-plus/core/pipelines/classic-transaction/types.ts new file mode 100644 index 0000000..0fee55c --- /dev/null +++ b/src/stellar-plus/core/pipelines/classic-transaction/types.ts @@ -0,0 +1,48 @@ +import { xdr } from '@stellar/stellar-sdk' +import { HorizonApi } from '@stellar/stellar-sdk/lib/horizon' + +import { BuildTransactionPipelinePlugin } from 'stellar-plus/core/pipelines/build-transaction/types' +import { ClassicSignRequirementsPipelinePlugin } from 'stellar-plus/core/pipelines/classic-sign-requirements/types' +import { SignTransactionPipelinePlugin } from 'stellar-plus/core/pipelines/sign-transaction/types' +import { SubmitTransactionPipelinePlugin } from 'stellar-plus/core/pipelines/submit-transaction/types' +import { TransactionInvocation } from 'stellar-plus/types' +import { ConveyorBelt } from 'stellar-plus/utils/pipeline/conveyor-belts' +import { BeltPluginType, GenericPlugin } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +export enum ClassicTransactionPipelineType { + id = 'ClassicTransactionPipeline', +} + +export type ClassicTransactionPipelineInput = { + txInvocation: TransactionInvocation + operations: xdr.Operation[] + options?: { + executionPlugins?: SupportedInnerPlugins[] + } +} + +export type ClassicTransactionPipelineOutput = { response: HorizonApi.SubmitTransactionResponse } + +export type SupportedInnerPlugins = + | BuildTransactionPipelinePlugin + | ClassicSignRequirementsPipelinePlugin + | SignTransactionPipelinePlugin + | SubmitTransactionPipelinePlugin + +export type ClassicTransactionPipeline = ConveyorBelt< + ClassicTransactionPipelineInput, + ClassicTransactionPipelineOutput, + ClassicTransactionPipelineType +> + +export type ClassicTransactionPipelinePlugin = ClassicTransactionPipelineMainPlugin | SupportedInnerPlugins + +export type ClassicTransactionPipelineMainPlugin = BeltPluginType< + ClassicTransactionPipelineInput, + ClassicTransactionPipelineOutput, + ClassicTransactionPipelineType | GenericPlugin +> + +export type ClassicTransactionPipelineOptions = { + plugins?: ClassicTransactionPipelinePlugin[] +} diff --git a/src/stellar-plus/core/pipelines/fee-bump/index.ts b/src/stellar-plus/core/pipelines/fee-bump/index.ts new file mode 100644 index 0000000..aafe1fb --- /dev/null +++ b/src/stellar-plus/core/pipelines/fee-bump/index.ts @@ -0,0 +1,41 @@ +import { FeeBumpTransaction, Transaction, TransactionBuilder } from '@stellar/stellar-sdk' + +import { + FeeBumpPipelineInput, + FeeBumpPipelineOutput, + FeeBumpPipelinePlugin, + FeeBumpPipelineType, +} from 'stellar-plus/core/pipelines/fee-bump/types' +import { ConveyorBelt } from 'stellar-plus/utils/pipeline/conveyor-belts' + +export class FeeBumpPipeline extends ConveyorBelt { + constructor(plugins?: FeeBumpPipelinePlugin[]) { + super({ + type: FeeBumpPipelineType.id, + plugins: plugins || [], + }) + } + + protected async process(item: FeeBumpPipelineInput, _itemId: string): Promise { + const { innerTransaction, feeBumpHeader }: FeeBumpPipelineInput = item + + const networkPassphrase = innerTransaction.networkPassphrase + + try { + // The conversion to and from XDR seems unnecessary, but it's the only way to + // get the fee bump transaction to be properly signed. + // Not fully sure if a bug in the SDK or the way we build the transaction yet + // Needs further investigation. + const feeBumpTransaction = TransactionBuilder.buildFeeBumpTransaction( + feeBumpHeader.header.source, + feeBumpHeader.header.fee, + TransactionBuilder.fromXDR(innerTransaction.toXDR(), networkPassphrase) as Transaction, + networkPassphrase + ) as FeeBumpTransaction + + return feeBumpTransaction + } catch (error) { + throw new Error(`Error building fee bump transaction: ${(error as Error).message}`) + } + } +} diff --git a/src/stellar-plus/core/pipelines/fee-bump/types.ts b/src/stellar-plus/core/pipelines/fee-bump/types.ts new file mode 100644 index 0000000..5c67ded --- /dev/null +++ b/src/stellar-plus/core/pipelines/fee-bump/types.ts @@ -0,0 +1,22 @@ +import { FeeBumpTransaction, Transaction } from '@stellar/stellar-sdk' + +import { FeeBumpHeader } from 'stellar-plus/types' +import { BeltPluginType, GenericPlugin } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +export type FeeBumpPipelineInput = { + innerTransaction: Transaction + feeBumpHeader: FeeBumpHeader +} + +export type FeeBumpPipelineOutput = FeeBumpTransaction + +// export type FeeBumpPipelineType = 'FeeBumpPipeline' +export enum FeeBumpPipelineType { + id = 'FeeBumpPipeline', +} + +export type FeeBumpPipelinePlugin = BeltPluginType< + FeeBumpPipelineInput, + FeeBumpPipelineOutput, + FeeBumpPipelineType | GenericPlugin +> diff --git a/src/stellar-plus/core/pipelines/sign-transaction/index.ts b/src/stellar-plus/core/pipelines/sign-transaction/index.ts new file mode 100644 index 0000000..27a9862 --- /dev/null +++ b/src/stellar-plus/core/pipelines/sign-transaction/index.ts @@ -0,0 +1,61 @@ +import { FeeBumpTransaction, Transaction, TransactionBuilder } from '@stellar/stellar-sdk' + +import { AccountHandler } from 'stellar-plus/account' +import { + SignTransactionPipelineInput, + SignTransactionPipelineOutput, + SignTransactionPipelinePlugin, + SignTransactionPipelineType, +} from 'stellar-plus/core/pipelines/sign-transaction/types' +import { SignatureRequirement } from 'stellar-plus/core/types' +import { ConveyorBelt } from 'stellar-plus/utils/pipeline/conveyor-belts' + +export class SignTransactionPipeline extends ConveyorBelt< + SignTransactionPipelineInput, + SignTransactionPipelineOutput, + SignTransactionPipelineType +> { + constructor(plugins?: SignTransactionPipelinePlugin[]) { + super({ + type: SignTransactionPipelineType.id, + plugins: plugins || [], + }) + } + + protected async process(item: SignTransactionPipelineInput, _itemId: string): Promise { + const { transaction, signatureRequirements, signers }: SignTransactionPipelineInput = item + + const signedTransaction = await this.signTransaction(transaction, signatureRequirements, signers) + + return signedTransaction + } + + private async signTransaction( + transaction: Transaction | FeeBumpTransaction, + requirements: SignatureRequirement[], + signers: AccountHandler[] + ): Promise { + const passphrase = transaction.networkPassphrase + let signedTransaction = transaction + + if (signers.length === 0) { + throw new Error('No signers provided') + } + + for (const requirement of requirements) { + const signer = signers.find((s) => s.getPublicKey() === requirement.publicKey) as AccountHandler + + if (!signer) throw new Error(`Signer not found: ${requirement.publicKey}`) + + if (!signer.signatureSchema) { + signedTransaction = TransactionBuilder.fromXDR( + await signer.sign(signedTransaction), + passphrase + ) as typeof transaction + } else { + throw new Error('Multisignature support not implemented yet') + } + } + return signedTransaction + } +} diff --git a/src/stellar-plus/core/pipelines/sign-transaction/types.ts b/src/stellar-plus/core/pipelines/sign-transaction/types.ts new file mode 100644 index 0000000..feb5e71 --- /dev/null +++ b/src/stellar-plus/core/pipelines/sign-transaction/types.ts @@ -0,0 +1,24 @@ +import { FeeBumpTransaction, Transaction } from '@stellar/stellar-sdk' + +import { AccountHandler } from 'stellar-plus/account' +import { SignatureRequirement } from 'stellar-plus/core/types' +import { BeltPluginType, GenericPlugin } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +export type SignTransactionPipelineInput = { + transaction: Transaction | FeeBumpTransaction + signatureRequirements: SignatureRequirement[] + signers: AccountHandler[] +} + +export type SignTransactionPipelineOutput = Transaction | FeeBumpTransaction + +// export type SignTransactionPipelineType = 'SignTransactionPipeline' +export enum SignTransactionPipelineType { + id = 'SignTransactionPipeline', +} + +export type SignTransactionPipelinePlugin = BeltPluginType< + SignTransactionPipelineInput, + SignTransactionPipelineOutput, + SignTransactionPipelineType | GenericPlugin +> diff --git a/src/stellar-plus/core/pipelines/simulate-transaction/errors.ts b/src/stellar-plus/core/pipelines/simulate-transaction/errors.ts new file mode 100644 index 0000000..08c35ef --- /dev/null +++ b/src/stellar-plus/core/pipelines/simulate-transaction/errors.ts @@ -0,0 +1,108 @@ +import { SorobanRpc } from '@stellar/stellar-sdk' + +import { StellarPlusError } from 'stellar-plus/error' +import { ConveyorBeltErrorMeta } from 'stellar-plus/error/helpers/conveyor-belt' +import { BeltMetadata } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +import { SimulateTransactionPipelineInput } from './types' + +export enum ErrorCodesPipelineSimulateTransaction { + // PSI0 General + PSI001 = 'PSI001', + PSI002 = 'PSI002', + PSI003 = 'PSI003', + PSI004 = 'PSI004', + + //PSI1 Restore + PSI100 = 'PSI100', +} + +const failedToSimulateTransaction = ( + error: Error | StellarPlusError, + conveyorBeltErrorMeta: ConveyorBeltErrorMeta +): StellarPlusError => { + return new StellarPlusError({ + code: ErrorCodesPipelineSimulateTransaction.PSI001, + message: 'Failed to simulate!', + source: 'PipelineSimulateTransaction', + details: 'An issue occurred while simulating the transaction. Refer to the meta section for more details.', + meta: { + error, + conveyorBeltErrorMeta, + }, + }) +} + +const simulationFailed = ( + failedSimulation: SorobanRpc.Api.SimulateTransactionErrorResponse, + conveyorBeltErrorMeta: ConveyorBeltErrorMeta +): StellarPlusError => { + return new StellarPlusError({ + code: ErrorCodesPipelineSimulateTransaction.PSI002, + message: 'Failed to simulate!', + source: 'PipelineSimulateTransaction', + details: `The simulated transaction status is not success. This indicates the transaction won't succeed if processed by the network. Refer to the meta section for more details and review the transaction parameters.`, + meta: { + message: failedSimulation.error, + conveyorBeltErrorMeta, + sorobanSimulationData: failedSimulation, + }, + }) +} + +const simulationMissingResult = ( + simulationResponse: SorobanRpc.Api.SimulateTransactionSuccessResponse, + conveyorBeltErrorMeta: ConveyorBeltErrorMeta +): StellarPlusError => { + return new StellarPlusError({ + code: ErrorCodesPipelineSimulateTransaction.PSI003, + message: 'Simulation missing result!', + source: 'PipelineSimulateTransaction', + details: `The simulated transaction status is success but the result is missing. Refer to the meta section for more details and review the transaction parameters.`, + meta: { + conveyorBeltErrorMeta, + sorobanSimulationData: simulationResponse, + }, + }) +} + +const simulationResultCouldNotBeVerified = ( + simulationResponse: SorobanRpc.Api.SimulateTransactionSuccessResponse, + conveyorBeltErrorMeta: ConveyorBeltErrorMeta +): StellarPlusError => { + return new StellarPlusError({ + code: ErrorCodesPipelineSimulateTransaction.PSI004, + message: 'Simulation result could not be verified!', + source: 'PipelineSimulateTransaction', + details: `The simulated transaction status is success but the result could not be verified. Refer to the meta section for more details and review the transaction parameters.`, + meta: { + conveyorBeltErrorMeta, + sorobanSimulationData: simulationResponse, + }, + }) +} + +const transactionNeedsRestore = ( + simulationResponse: SorobanRpc.Api.SimulateTransactionRestoreResponse, + conveyorBeltErrorMeta: ConveyorBeltErrorMeta +): StellarPlusError => { + return new StellarPlusError({ + code: ErrorCodesPipelineSimulateTransaction.PSI100, + message: 'Transaction needs restore!', + source: 'PipelineSimulateTransaction', + details: `The simulated transaction status is restore. This indicates the transaction needs to be restored. Refer to the meta section for more details and review the transaction parameter.`, + + meta: { + conveyorBeltErrorMeta, + sorobanSimulationData: simulationResponse, + }, + }) +} + +export const PSIError = { + failedToSimulateTransaction, + simulationFailed, + transactionNeedsRestore, + simulationMissingResult, + simulationResultCouldNotBeVerified, +} diff --git a/src/stellar-plus/core/pipelines/simulate-transaction/index.ts b/src/stellar-plus/core/pipelines/simulate-transaction/index.ts new file mode 100644 index 0000000..f68813f --- /dev/null +++ b/src/stellar-plus/core/pipelines/simulate-transaction/index.ts @@ -0,0 +1,74 @@ +import { SorobanRpc, Transaction } from '@stellar/stellar-sdk' + +import { + SimulateTransactionPipelineInput, + SimulateTransactionPipelineOutput, + SimulateTransactionPipelinePlugin, + SimulateTransactionPipelineType, +} from 'stellar-plus/core/pipelines/simulate-transaction/types' +import { extractConveyorBeltErrorMeta } from 'stellar-plus/error/helpers/conveyor-belt' +import { ConveyorBelt } from 'stellar-plus/utils/pipeline/conveyor-belts' + +import { PSIError } from './errors' + +export class SimulateTransactionPipeline extends ConveyorBelt< + SimulateTransactionPipelineInput, + SimulateTransactionPipelineOutput, + SimulateTransactionPipelineType +> { + constructor(plugins?: SimulateTransactionPipelinePlugin[]) { + super({ + type: SimulateTransactionPipelineType.id, + plugins: plugins || [], + }) + } + + protected async process( + item: SimulateTransactionPipelineInput, + itemId: string + ): Promise { + const { transaction, rpcHandler }: SimulateTransactionPipelineInput = item as SimulateTransactionPipelineInput + + let simulationResponse: SorobanRpc.Api.SimulateTransactionResponse + + try { + simulationResponse = await rpcHandler.simulateTransaction(transaction) + } catch (e) { + throw PSIError.failedToSimulateTransaction(e as Error, extractConveyorBeltErrorMeta(item, this.getMeta(itemId))) + } + + if (SorobanRpc.Api.isSimulationError(simulationResponse)) { + throw PSIError.simulationFailed(simulationResponse, extractConveyorBeltErrorMeta(item, this.getMeta(itemId))) + } + + if (SorobanRpc.Api.isSimulationRestore(simulationResponse) && simulationResponse.result) { + return { + response: simulationResponse as SorobanRpc.Api.SimulateTransactionRestoreResponse, + assembledTransaction: this.assembleTransaction(transaction, simulationResponse), + } as SimulateTransactionPipelineOutput + } + + if (SorobanRpc.Api.isSimulationSuccess(simulationResponse)) { + return { + response: simulationResponse as SorobanRpc.Api.SimulateTransactionSuccessResponse, + assembledTransaction: this.assembleTransaction(transaction, simulationResponse), + } as SimulateTransactionPipelineOutput + } + + throw PSIError.simulationResultCouldNotBeVerified( + simulationResponse, + extractConveyorBeltErrorMeta(item, this.getMeta(itemId)) + ) + } + + private assembleTransaction( + transaction: Transaction, + simulationResponse: SorobanRpc.Api.SimulateTransactionSuccessResponse + ): Transaction { + try { + return SorobanRpc.assembleTransaction(transaction, simulationResponse).build() + } catch (e) { + throw new Error('assembleTransaction failed') + } + } +} diff --git a/src/stellar-plus/core/pipelines/simulate-transaction/types.ts b/src/stellar-plus/core/pipelines/simulate-transaction/types.ts new file mode 100644 index 0000000..cc4a4fe --- /dev/null +++ b/src/stellar-plus/core/pipelines/simulate-transaction/types.ts @@ -0,0 +1,35 @@ +import { SorobanRpc, Transaction } from '@stellar/stellar-sdk' + +import { TransactionResources } from 'stellar-plus/core/contract-engine/types' +import { RpcHandler } from 'stellar-plus/rpc/types' +import { BeltPluginType, GenericPlugin } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +export type SimulateTransactionPipelineInput = { + transaction: Transaction + rpcHandler: RpcHandler +} + +export type SimulateTransactionPipelineOutput = { + response: SorobanRpc.Api.SimulateTransactionSuccessResponse | SorobanRpc.Api.SimulateTransactionRestoreResponse + output?: SimulatedInvocationOutput & ResourcesOutput + assembledTransaction: Transaction +} + +// export type SimulateTransactionPipelineType = 'SimulateTransactionPipeline' +export enum SimulateTransactionPipelineType { + id = 'SimulateTransactionPipeline', +} + +export type SimulateTransactionPipelinePlugin = BeltPluginType< + SimulateTransactionPipelineInput, + SimulateTransactionPipelineOutput, + SimulateTransactionPipelineType | GenericPlugin +> + +export type SimulatedInvocationOutput = { + value?: unknown +} + +export type ResourcesOutput = { + resources?: TransactionResources +} diff --git a/src/stellar-plus/core/pipelines/soroban-get-transaction/errors.ts b/src/stellar-plus/core/pipelines/soroban-get-transaction/errors.ts new file mode 100644 index 0000000..32ea1df --- /dev/null +++ b/src/stellar-plus/core/pipelines/soroban-get-transaction/errors.ts @@ -0,0 +1,53 @@ +import { SorobanRpc } from '@stellar/stellar-sdk' + +import { StellarPlusError } from 'stellar-plus/error' +import { ConveyorBeltErrorMeta } from 'stellar-plus/error/helpers/conveyor-belt' +import { extractGetTransactionData } from 'stellar-plus/error/helpers/soroban-rpc' +import { BeltMetadata } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +import { SorobanGetTransactionPipelineInput } from './types' + +export enum ErrorCodesPipelineSorobanGetTransaction { + //General + SGT001 = 'SGT001', + SGT002 = 'SGT002', +} + +const transactionFailed = ( + conveyorBeltErrorMeta: ConveyorBeltErrorMeta, + response: SorobanRpc.Api.GetFailedTransactionResponse +): StellarPlusError => { + return new StellarPlusError({ + code: ErrorCodesPipelineSorobanGetTransaction.SGT001, + message: 'The transaction failed!', + source: 'PipelineSorobanGetTransaction', + details: + 'The transaction was submitted to the network but failed to be processed. This indicates an issue with the transaction that makes it invalid. Please check the details of the transaction and try again.', + meta: { + conveyorBeltErrorMeta, + sorobanGetTransactionData: extractGetTransactionData(response), + }, + }) +} + +const transactionNotFound = ( + conveyorBeltErrorMeta: ConveyorBeltErrorMeta, + timeout: number, + hash: string +): StellarPlusError => { + return new StellarPlusError({ + code: ErrorCodesPipelineSorobanGetTransaction.SGT002, + message: 'Transaction not found!', + source: 'PipelineSorobanGetTransaction', + details: `The transaction was not found in the Soroban server. This indicates that the transaction was not fully processed when the timeout(${timeout}s) was reachead.`, + meta: { + conveyorBeltErrorMeta, + transactionHash: hash, + }, + }) +} + +export const SGTError = { + transactionFailed, + transactionNotFound, +} diff --git a/src/stellar-plus/core/pipelines/soroban-get-transaction/index.ts b/src/stellar-plus/core/pipelines/soroban-get-transaction/index.ts new file mode 100644 index 0000000..c482ddf --- /dev/null +++ b/src/stellar-plus/core/pipelines/soroban-get-transaction/index.ts @@ -0,0 +1,90 @@ +import { FeeBumpTransaction, SorobanRpc, Transaction } from '@stellar/stellar-sdk' + +import { extractConveyorBeltErrorMeta } from 'stellar-plus/error/helpers/conveyor-belt' +import { ConveyorBelt } from 'stellar-plus/utils/pipeline/conveyor-belts' + +import { SGTError } from './errors' +import { + SorobanGetTransactionOptions, + SorobanGetTransactionPipelineInput, + SorobanGetTransactionPipelineOutput, + SorobanGetTransactionPipelinePlugin, + SorobanGetTransactionPipelineType, +} from './types' + +export class SorobanGetTransactionPipeline extends ConveyorBelt< + SorobanGetTransactionPipelineInput, + SorobanGetTransactionPipelineOutput, + SorobanGetTransactionPipelineType +> { + protected options: SorobanGetTransactionOptions + + constructor( + plugins?: SorobanGetTransactionPipelinePlugin[], + options: SorobanGetTransactionOptions = { defaultSecondsToWait: 30, useEnvelopeTimeout: true } + ) { + super({ + type: SorobanGetTransactionPipelineType.id, + plugins: plugins || [], + }) + this.options = options + } + + // Waits for the given transaction to be processed by the Soroban server. + // Soroban transactions are processed asynchronously, so this method will wait for the transaction to be processed. + // If the transaction is not processed within the given timeout, it will throw an error. + // If the transaction is processed, it will return the response from the Soroban server. + // If the transaction fails, it will throw an error. + protected async process( + item: SorobanGetTransactionPipelineInput, + itemId: string + ): Promise { + const { sorobanSubmission, transactionEnvelope, rpcHandler }: SorobanGetTransactionPipelineInput = item + const { hash } = sorobanSubmission + + const secondsToWait = this.getSecondsToWait(transactionEnvelope) + const waitUntil = Date.now() + secondsToWait * 1000 + const initialWaitTime = 1000 //1 second + + let currentWaitTime = initialWaitTime + + let updatedTransaction = await rpcHandler.getTransaction(hash) + while (Date.now() < waitUntil && updatedTransaction.status === SorobanRpc.Api.GetTransactionStatus.NOT_FOUND) { + await new Promise((resolve) => setTimeout(resolve, currentWaitTime)) + updatedTransaction = await rpcHandler.getTransaction(hash) + currentWaitTime *= 2 // Exponential backoff + } + + if (updatedTransaction.status === SorobanRpc.Api.GetTransactionStatus.SUCCESS) { + return { response: updatedTransaction as SorobanRpc.Api.GetSuccessfulTransactionResponse } + } + + if (updatedTransaction.status === SorobanRpc.Api.GetTransactionStatus.FAILED) { + throw SGTError.transactionFailed( + extractConveyorBeltErrorMeta(item, this.getMeta(itemId)), + updatedTransaction as SorobanRpc.Api.GetFailedTransactionResponse + ) + } + throw SGTError.transactionNotFound(extractConveyorBeltErrorMeta(item, this.getMeta(itemId)), secondsToWait, hash) + } + private getSecondsToWait(transactionEnvelope?: Transaction | FeeBumpTransaction): number { + let secondsToWait = this.options.defaultSecondsToWait + + if (this.options.useEnvelopeTimeout && transactionEnvelope) { + const txTimeout = (transactionEnvelope as FeeBumpTransaction).innerTransaction + ? this.getTransactionTimeoutInSeconds((transactionEnvelope as FeeBumpTransaction).innerTransaction) + : this.getTransactionTimeoutInSeconds(transactionEnvelope as Transaction) + + if (txTimeout > 0) { + secondsToWait = txTimeout + } + } + + return secondsToWait + } + + private getTransactionTimeoutInSeconds(transactionEnvelope: Transaction): number { + const txTimeout = Number(transactionEnvelope.timeBounds?.maxTime) ?? 0 + return txTimeout > 0 ? txTimeout - Math.floor(Date.now() / 1000) : 0 + } +} diff --git a/src/stellar-plus/core/pipelines/soroban-get-transaction/types.ts b/src/stellar-plus/core/pipelines/soroban-get-transaction/types.ts new file mode 100644 index 0000000..820b36b --- /dev/null +++ b/src/stellar-plus/core/pipelines/soroban-get-transaction/types.ts @@ -0,0 +1,46 @@ +import { FeeBumpTransaction, SorobanRpc, Transaction } from '@stellar/stellar-sdk' + +import { RpcHandler } from 'stellar-plus/rpc/types' +import { BeltPluginType, GenericPlugin } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +export type SorobanGetTransactionPipelineInput = { + sorobanSubmission: SorobanRpc.Api.SendTransactionResponse + rpcHandler: RpcHandler + transactionEnvelope?: Transaction | FeeBumpTransaction +} + +export type SorobanGetTransactionPipelineOutput = { + response: SorobanRpc.Api.GetSuccessfulTransactionResponse + output?: ContractIdOutput & ContractWasmHashOutput & ContractInvocationOutput & FeeChargedOutput +} + +// export type SorobanGetTransactionPipelineType = 'SorobanGetTransactionPipeline' +export enum SorobanGetTransactionPipelineType { + id = 'SorobanGetTransactionPipeline', +} + +export type SorobanGetTransactionPipelinePlugin = BeltPluginType< + SorobanGetTransactionPipelineInput, + SorobanGetTransactionPipelineOutput, + SorobanGetTransactionPipelineType | GenericPlugin +> + +export type SorobanGetTransactionOptions = { + defaultSecondsToWait: number // Defines the default number of seconds to wait before checking the status of a transaction + useEnvelopeTimeout: boolean // If true, the pipeline will use the timeout defined in the transaction envelope whenever available +} + +export type ContractIdOutput = { + contractId?: string +} +export type ContractWasmHashOutput = { + wasmHash?: string +} + +export type ContractInvocationOutput = { + value?: OutputType +} + +export type FeeChargedOutput = { + feeCharged?: string +} diff --git a/src/stellar-plus/core/pipelines/soroban-transaction/index.ts b/src/stellar-plus/core/pipelines/soroban-transaction/index.ts new file mode 100644 index 0000000..8cf8c95 --- /dev/null +++ b/src/stellar-plus/core/pipelines/soroban-transaction/index.ts @@ -0,0 +1,196 @@ +import { SorobanRpc } from '@stellar/stellar-sdk' + +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 { SorobanGetTransactionPipeline } from 'stellar-plus/core/pipelines/soroban-get-transaction' +import { + SorobanGetTransactionPipelinePlugin, + SorobanGetTransactionPipelineType, +} from 'stellar-plus/core/pipelines/soroban-get-transaction/types' +import { + SorobanTransactionPipelineInput, + SorobanTransactionPipelineOptions, + SorobanTransactionPipelineOutput, + SorobanTransactionPipelinePlugin, + SorobanTransactionPipelineType, + SupportedInnerPlugins, + TransactionExecutionOutput, + TransactionSimulationOutput, +} 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 { HorizonHandlerClient } from 'stellar-plus/horizon' +import { DefaultRpcHandler } from 'stellar-plus/rpc' +import { RpcHandler } from 'stellar-plus/rpc/types' +import { NetworkConfig } from 'stellar-plus/types' + +import { MultiBeltPipeline } from '../../../utils/pipeline/multi-belt-pipeline' //'stellar-plus/utils/pipeline/multi-belt-pipeline' +import { MultiBeltPipelineOptions } from '../../../utils/pipeline/multi-belt-pipeline/types' + +export class SorobanTransactionPipeline extends MultiBeltPipeline< + SorobanTransactionPipelineInput, + SorobanTransactionPipelineOutput, + SorobanTransactionPipelineType, + SupportedInnerPlugins +> { + private rpcHandler: RpcHandler + private horizonHandler: HorizonHandlerClient + private networkConfig: NetworkConfig + + constructor(networkConfig: NetworkConfig, options?: SorobanTransactionPipelineOptions) { + const internalConstructorArgs = { + beltType: SorobanTransactionPipelineType.id, + plugins: (options?.plugins as SorobanTransactionPipelinePlugin[]) || [], + } as MultiBeltPipelineOptions< + SorobanTransactionPipelineInput, + SorobanTransactionPipelineOutput, + SorobanTransactionPipelineType, + SupportedInnerPlugins + > + + super({ + ...internalConstructorArgs, + ...{ type: SorobanTransactionPipelineType.id }, + }) + + this.networkConfig = networkConfig + this.horizonHandler = new HorizonHandlerClient(this.networkConfig) + this.rpcHandler = options?.customRpcHandler || new DefaultRpcHandler(this.networkConfig) + } + + protected async process( + item: SorobanTransactionPipelineInput, + itemId: string + ): Promise { + const { txInvocation, operations, options }: SorobanTransactionPipelineInput = item + const executionPlugins = [] + if (options?.executionPlugins) executionPlugins.push(...options.executionPlugins) + + // ======================= Build Transaction ========================== + + const buildTransactionPipelinePlugins = this.getInnerPluginsByType( + executionPlugins, + 'BuildTransactionPipeline' as BuildTransactionPipelineType + ) as BuildTransactionPipelinePlugin[] + + const builTransactionPipeline = new BuildTransactionPipeline(buildTransactionPipelinePlugins) + + const builtTx = await builTransactionPipeline.execute( + { + header: txInvocation.header, + horizonHandler: this.horizonHandler, + operations, + networkPassphrase: this.networkConfig.networkPassphrase, + }, + itemId + ) + + // ======================= Simulate Transaction ========================== + const simulateTransactionPipelinePlugins = this.getInnerPluginsByType( + executionPlugins, + 'SimulateTransactionPipeline' as SimulateTransactionPipelineType + ) as SimulateTransactionPipelinePlugin[] + + const simulateTransactionPipeline = new SimulateTransactionPipeline(simulateTransactionPipelinePlugins) + + const successfulSimulation = await simulateTransactionPipeline.execute( + { + transaction: builtTx, + rpcHandler: this.rpcHandler, + }, + itemId + ) + + if (options?.simulateOnly) { + return successfulSimulation as TransactionSimulationOutput + } + + // ======================= Assemble ========================== + // const assembledTransaction = SorobanRpc.assembleTransaction(builtTx, successfulSimulation.response).build() + const assembledTransaction = successfulSimulation.assembledTransaction + + // Soroban signature belt + + // ======================= Calculate classic requirements ========================== + const classicSignRequirementsPipelinePlugins = this.getInnerPluginsByType( + executionPlugins, + 'ClassicSignRequirementsPipeline' as ClassicSignRequirementsPipelineType + ) as ClassicSignRequirementsPipelinePlugin[] + + const classicSignRequirementsPipeline = new ClassicSignRequirementsPipeline(classicSignRequirementsPipelinePlugins) + + const classicSignatureRequirements = await classicSignRequirementsPipeline.execute(assembledTransaction, itemId) + + // ======================= Sign Transaction ========================== + const signTransactionPipelinePlugins = this.getInnerPluginsByType( + executionPlugins, + 'SignTransactionPipeline' as SignTransactionPipelineType + ) as SignTransactionPipelinePlugin[] + + const signTransactionPipeline = new SignTransactionPipeline(signTransactionPipelinePlugins) + const signedTransaction = await signTransactionPipeline.execute( + { + transaction: assembledTransaction, + signatureRequirements: classicSignatureRequirements, + signers: txInvocation.signers, + }, + itemId + ) + + // ======================= Submit Transaction ========================== + const submitTransactionPipelinePlugins = this.getInnerPluginsByType( + executionPlugins, + 'SubmitTransactionPipeline' as SubmitTransactionPipelineType + ) as SubmitTransactionPipelinePlugin[] + + const submitTransactionPipeline = new SubmitTransactionPipeline(submitTransactionPipelinePlugins) + + const submissionResult = await submitTransactionPipeline.execute( + { + transaction: signedTransaction, + networkHandler: this.rpcHandler, + }, + itemId + ) + + // ======================= Submission Follow Up ========================== + const sorobanGetTransactionPipelinePlugins = this.getInnerPluginsByType( + executionPlugins, + 'SorobanGetTransactionPipeline' as SorobanGetTransactionPipelineType + ) as SorobanGetTransactionPipelinePlugin[] + + const sorobanGetTransactionPipeline = new SorobanGetTransactionPipeline(sorobanGetTransactionPipelinePlugins) + + const sorobanGetTransactionResult = await sorobanGetTransactionPipeline.execute( + { + sorobanSubmission: submissionResult.response as SorobanRpc.Api.SendTransactionResponse, + rpcHandler: this.rpcHandler, + transactionEnvelope: signedTransaction, + }, + itemId + ) + + return sorobanGetTransactionResult as TransactionExecutionOutput + } +} diff --git a/src/stellar-plus/core/pipelines/soroban-transaction/types.ts b/src/stellar-plus/core/pipelines/soroban-transaction/types.ts new file mode 100644 index 0000000..0d8c255 --- /dev/null +++ b/src/stellar-plus/core/pipelines/soroban-transaction/types.ts @@ -0,0 +1,62 @@ +import { xdr } from '@stellar/stellar-sdk' + +import { BuildTransactionPipelinePlugin } from 'stellar-plus/core/pipelines/build-transaction/types' +import { ClassicSignRequirementsPipelinePlugin } from 'stellar-plus/core/pipelines/classic-sign-requirements/types' +import { SignTransactionPipelinePlugin } from 'stellar-plus/core/pipelines/sign-transaction/types' +import { + SimulateTransactionPipelineOutput, + SimulateTransactionPipelinePlugin, +} from 'stellar-plus/core/pipelines/simulate-transaction/types' +import { + SorobanGetTransactionPipelineOutput, + SorobanGetTransactionPipelinePlugin, +} from 'stellar-plus/core/pipelines/soroban-get-transaction/types' +import { SubmitTransactionPipelinePlugin } from 'stellar-plus/core/pipelines/submit-transaction/types' +import { RpcHandler } from 'stellar-plus/rpc/types' +import { TransactionInvocation } from 'stellar-plus/types' +import { ConveyorBelt } from 'stellar-plus/utils/pipeline/conveyor-belts' +import { BeltPluginType, GenericPlugin } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +export enum SorobanTransactionPipelineType { + id = 'SorobanTransactionPipeline', +} + +export type SorobanTransactionPipelineInput = { + txInvocation: TransactionInvocation + operations: xdr.Operation[] + options?: { + executionPlugins?: SupportedInnerPlugins[] + simulateOnly?: boolean + } +} + +export type TransactionSimulationOutput = SimulateTransactionPipelineOutput +export type TransactionExecutionOutput = SorobanGetTransactionPipelineOutput +export type SorobanTransactionPipelineOutput = TransactionSimulationOutput | TransactionExecutionOutput + +export type SupportedInnerPlugins = + | BuildTransactionPipelinePlugin + | SimulateTransactionPipelinePlugin + | ClassicSignRequirementsPipelinePlugin + | SignTransactionPipelinePlugin + | SubmitTransactionPipelinePlugin + | SorobanGetTransactionPipelinePlugin + +export type SorobanTransactionPipeline = ConveyorBelt< + SorobanTransactionPipelineInput, + SorobanTransactionPipelineOutput, + SorobanTransactionPipelineType +> + +export type SorobanTransactionPipelinePlugin = SorobanTransactionPipelineMainPlugin | SupportedInnerPlugins + +export type SorobanTransactionPipelineMainPlugin = BeltPluginType< + SorobanTransactionPipelineInput, + SorobanTransactionPipelineOutput, + SorobanTransactionPipelineType | GenericPlugin +> + +export type SorobanTransactionPipelineOptions = { + plugins?: SorobanTransactionPipelinePlugin[] + customRpcHandler?: RpcHandler +} diff --git a/src/stellar-plus/core/pipelines/submit-transaction/index.ts b/src/stellar-plus/core/pipelines/submit-transaction/index.ts new file mode 100644 index 0000000..990e25b --- /dev/null +++ b/src/stellar-plus/core/pipelines/submit-transaction/index.ts @@ -0,0 +1,69 @@ +import { FeeBumpTransaction, Transaction } from '@stellar/stellar-sdk' +import { HorizonApi } from '@stellar/stellar-sdk/lib/horizon' + +import { HorizonHandler } from 'stellar-plus' +import { + SubmitTransactionPipelineInput, + SubmitTransactionPipelineOutput, + SubmitTransactionPipelinePlugin, + SubmitTransactionPipelineType, +} from 'stellar-plus/core/pipelines/submit-transaction/types' +import { RpcHandler } from 'stellar-plus/rpc/types' +import { ConveyorBelt } from 'stellar-plus/utils/pipeline/conveyor-belts' + +export class SubmitTransactionPipeline extends ConveyorBelt< + SubmitTransactionPipelineInput, + SubmitTransactionPipelineOutput, + SubmitTransactionPipelineType +> { + constructor(plugins?: SubmitTransactionPipelinePlugin[]) { + super({ + type: SubmitTransactionPipelineType.id, + plugins: plugins || [], + }) + } + + protected async process( + item: SubmitTransactionPipelineInput, + _itemId: string + ): Promise { + const { transaction, networkHandler }: SubmitTransactionPipelineInput = item + + if (networkHandler instanceof HorizonHandler) { + return await this.submitTransactionThroughHorizon(transaction, networkHandler) + } + + if (networkHandler.type && networkHandler.type === 'RpcHandler') { + return await this.submitTransactionThroughRpc(transaction, networkHandler) + } + + throw new Error('Invalid network handler') + } + + private async submitTransactionThroughHorizon( + transaction: Transaction | FeeBumpTransaction, + horizonHandler: HorizonHandler + ): Promise { + try { + const response = (await horizonHandler.server.submitTransaction(transaction, { + skipMemoRequiredCheck: true, // Not skipping memo required check causes an error when submitting fee bump transactions + })) as HorizonApi.SubmitTransactionResponse + return { response } + } catch (error) { + throw new Error(`Error submitting transaction through horizon: ${(error as Error).message}`) + } + } + + private async submitTransactionThroughRpc( + transaction: Transaction | FeeBumpTransaction, + rpcHandler: RpcHandler + ): Promise { + try { + const response = await rpcHandler.submitTransaction(transaction) + + return { response } + } catch (error) { + throw new Error(`Error submitting transaction through rpc: ${(error as Error).message}`) + } + } +} diff --git a/src/stellar-plus/core/pipelines/submit-transaction/types.ts b/src/stellar-plus/core/pipelines/submit-transaction/types.ts new file mode 100644 index 0000000..1f07f42 --- /dev/null +++ b/src/stellar-plus/core/pipelines/submit-transaction/types.ts @@ -0,0 +1,25 @@ +import { FeeBumpTransaction, SorobanRpc, Transaction } from '@stellar/stellar-sdk' +import { HorizonApi } from '@stellar/stellar-sdk/lib/horizon' + +import { HorizonHandler } from 'stellar-plus' +import { RpcHandler } from 'stellar-plus/rpc/types' +import { BeltPluginType, GenericPlugin } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +export type SubmitTransactionPipelineInput = { + transaction: Transaction | FeeBumpTransaction + networkHandler: RpcHandler | HorizonHandler +} + +export type SubmitTransactionPipelineOutput = { + response: HorizonApi.SubmitTransactionResponse | SorobanRpc.Api.SendTransactionResponse +} + +export enum SubmitTransactionPipelineType { + id = 'SubmitTransactionPipeline', +} + +export type SubmitTransactionPipelinePlugin = BeltPluginType< + SubmitTransactionPipelineInput, + SubmitTransactionPipelineOutput, + SubmitTransactionPipelineType | GenericPlugin +> diff --git a/src/stellar-plus/core/soroban-transaction-processor/errors.ts b/src/stellar-plus/core/soroban-transaction-processor/errors.ts deleted file mode 100644 index b1c1db2..0000000 --- a/src/stellar-plus/core/soroban-transaction-processor/errors.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { FeeBumpTransaction, SorobanRpc, Transaction } from '@stellar/stellar-sdk' - -import { StellarPlusError } from 'stellar-plus/error' -import { SorobanOpCodes } from 'stellar-plus/error/helpers/result-meta-xdr' -import { - GetTransactionFailedErrorInfo, - extractGetTransactionData, - extractSendTransactionErrorData, - extractSimulationBaseData, -} from 'stellar-plus/error/helpers/soroban-rpc' -import { extractTransactionData, extractTransactionInvocationMeta } from 'stellar-plus/error/helpers/transaction' - -import { EnvelopeHeader } from '../types' - -export enum SorobanTransactionProcessorErrorCodes { - // STP0 General - STP001 = 'STP001', - STP002 = 'STP002', - STP003 = 'STP003', - STP004 = 'STP004', - STP005 = 'STP005', - STP006 = 'STP006', - STP007 = 'STP007', - STP008 = 'STP008', - STP009 = 'STP009', - STP010 = 'STP010', - STP011 = 'STP011', - STP012 = 'STP012', - - // STP1 Transaction Operation Error codes - STP100 = 'STP100', - STP101 = 'STP101', -} - -const failedToBuildTransaction = (error: Error, header: EnvelopeHeader): StellarPlusError => { - return new StellarPlusError({ - code: SorobanTransactionProcessorErrorCodes.STP001, - message: 'Failed to build transaction!', - source: 'SorobanTransactionProcessor', - details: - 'Failed to build transaction! The transaction could not be built. Make sure that the transaction is valid and that the account has been initialized correctly.', - meta: { message: error.message, transactionInvocation: extractTransactionInvocationMeta({ header }, true) }, - }) -} - -const failedToSimulateTransaction = (error: Error, tx: Transaction | FeeBumpTransaction): StellarPlusError => { - return new StellarPlusError({ - code: SorobanTransactionProcessorErrorCodes.STP002, - message: 'Failed to simulate transaction!', - source: 'SorobanTransactionProcessor', - details: 'The transaction could not be simulated. Review the transaction envelope and make sure that it is valid.', - meta: { message: error.message, transactionData: extractTransactionData(tx), transactionXDR: tx.toXDR(), error }, - }) -} - -const failedToAssembleTransaction = ( - error: Error, - tx: Transaction | FeeBumpTransaction, - simulatedTransaction: SorobanRpc.Api.SimulateTransactionResponse -): StellarPlusError => { - return new StellarPlusError({ - code: SorobanTransactionProcessorErrorCodes.STP003, - message: 'Failed to assemble transaction!', - source: 'SorobanTransactionProcessor', - details: 'The transaction could not be assembled. Review the transaction envelope and make sure that it is valid.', - meta: { - message: error.message, - sorobanSimulationData: extractSimulationBaseData(simulatedTransaction), - transactionData: extractTransactionData(tx), - transactionXDR: tx.toXDR(), - error, - }, - }) -} - -const failedToSubmitTransaction = (error: Error, tx: Transaction | FeeBumpTransaction): StellarPlusError => { - return new StellarPlusError({ - code: SorobanTransactionProcessorErrorCodes.STP004, - message: 'Failed to submit transaction!', - source: 'SorobanTransactionProcessor', - details: 'The transaction could not be submitted. Review the transaction envelope and make sure that it is valid.', - meta: { message: error.message, transactionData: extractTransactionData(tx), transactionXDR: tx.toXDR(), error }, - }) -} - -const failedToSubmitTransactionWithResponse = (response: SorobanRpc.Api.SendTransactionResponse): StellarPlusError => { - return new StellarPlusError({ - code: SorobanTransactionProcessorErrorCodes.STP004, - message: 'Submitted transaction failed!', - source: 'SorobanTransactionProcessor', - details: - 'The transaction could not be submitted.. Review the transaction envelope and make sure that it is valid. Also review the error message for further information about the failure.', - meta: { sorobanSendTransactionData: extractSendTransactionErrorData(response) }, - }) -} - -const failedToVerifyTransactionSubmission = (response: SorobanRpc.Api.SendTransactionResponse): StellarPlusError => { - return new StellarPlusError({ - code: SorobanTransactionProcessorErrorCodes.STP005, - message: 'Submitted transaction could not be verified!', - source: 'SorobanTransactionProcessor', - details: - 'The submitted transaction could not be verified after submission. Review the transaction envelope and make sure that it is valid. Also review the error message for further information about the failure.', - meta: { sorobanSendTransactionData: extractSendTransactionErrorData(response) }, - }) -} - -const transactionSubmittedFailed = (response: SorobanRpc.Api.GetFailedTransactionResponse): StellarPlusError => { - const sorobanGetTransactionData = extractGetTransactionData(response) - - if ((sorobanGetTransactionData as GetTransactionFailedErrorInfo).opCode) { - return verifyOpErrorCode(sorobanGetTransactionData as GetTransactionFailedErrorInfo) - } - - return new StellarPlusError({ - code: SorobanTransactionProcessorErrorCodes.STP006, - message: 'Transaction failed!', - source: 'SorobanTransactionProcessor', - details: `The transaction submitted failed. Review the transaction envelope and make sure that it is valid. Also review the error message for further information about the failure.`, - meta: { - sorobanGetTransactionData: extractGetTransactionData(response), - }, - }) -} - -const verifyOpErrorCode = (sorobanGetTransactionData: GetTransactionFailedErrorInfo): StellarPlusError => { - const opErrorCode = sorobanGetTransactionData.opCode - - if (opErrorCode === SorobanOpCodes.invokeHostFunctionEntryArchived) { - return new StellarPlusError({ - code: SorobanTransactionProcessorErrorCodes.STP101, - message: 'Transaction failed! Entry archived!', - source: 'SorobanTransactionProcessor', - details: `The transaction submitted failed with operation error code ${opErrorCode} . The entry is archived and needs to be restored. Refer to the Soroban documentation for further information about restoring footpring. `, - meta: { - sorobanGetTransactionData, - }, - }) - } - - return new StellarPlusError({ - code: SorobanTransactionProcessorErrorCodes.STP100, - message: 'Transaction failed!', - source: 'SorobanTransactionProcessor', - details: `The transaction submitted failed. Unknown soroban operation error code: ${opErrorCode} `, - meta: { - sorobanGetTransactionData, - }, - }) -} - -const transactionSubmittedNotFound = ( - response: SorobanRpc.Api.GetTransactionResponse, - waitTimeout: number, - transactionHash: string -): StellarPlusError => { - return new StellarPlusError({ - code: SorobanTransactionProcessorErrorCodes.STP007, - message: 'Transaction not found!', - source: 'SorobanTransactionProcessor', - details: `The transaction submitted was not found within the waiting period of ${waitTimeout} ms. Althought the transaction was sent for processing, the subsequent attempts to verify the transaction status didn't succeed to locate it. Review the error message for further information about the failure.`, - meta: { - transactionHash, - sorobanGetTransactionData: extractGetTransactionData(response), - }, - }) -} - -const failedToUploadWasm = (error: StellarPlusError): StellarPlusError => { - return new StellarPlusError({ - code: SorobanTransactionProcessorErrorCodes.STP008, - message: 'Failed to upload wasm!', - source: 'SorobanTransactionProcessor', - 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 }, - }) -} - -const failedToDeployContract = (error: StellarPlusError): StellarPlusError => { - return new StellarPlusError({ - code: SorobanTransactionProcessorErrorCodes.STP009, - message: 'Failed to deploy contract!', - source: 'SorobanTransactionProcessor', - 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 }, - }) -} - -const failedToWrapAsset = (error: StellarPlusError): StellarPlusError => { - return new StellarPlusError({ - code: SorobanTransactionProcessorErrorCodes.STP010, - message: 'Failed to wrap asset!', - source: 'SorobanTransactionProcessor', - 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 }, - }) -} - -const failedToRestoreFootprintWithError = (error: StellarPlusError, transaction: Transaction): StellarPlusError => { - return new StellarPlusError({ - code: SorobanTransactionProcessorErrorCodes.STP011, - message: 'Failed to restore footprint!', - source: 'SorobanTransactionProcessor', - details: - 'The footprint could not be restored. Review the meta error to identify the underlying cause for this issue.', - meta: { - message: error.message, - error: error, - transactionData: extractTransactionData(transaction), - transactionXDR: transaction.toXDR(), - }, - }) -} - -const failedToRestoreFootprintWithResponse = ( - response: SorobanRpc.Api.GetTransactionResponse, - transaction: Transaction -): StellarPlusError => { - return new StellarPlusError({ - code: SorobanTransactionProcessorErrorCodes.STP012, - message: 'Failed to restore footprint!', - source: 'SorobanTransactionProcessor', - details: - 'The footprint could not be restored. Review the meta error to identify the underlying cause for this issue.', - meta: { - sorobanGetTransactionData: extractGetTransactionData(response), - transactionData: extractTransactionData(transaction), - transactionXDR: transaction.toXDR(), - }, - }) -} - -export const STPError = { - failedToBuildTransaction, - failedToSimulateTransaction, - failedToAssembleTransaction, - failedToSubmitTransaction, - failedToSubmitTransactionWithResponse, - failedToVerifyTransactionSubmission, - transactionSubmittedFailed, - transactionSubmittedNotFound, - failedToUploadWasm, - failedToDeployContract, - failedToWrapAsset, - failedToRestoreFootprintWithError, - failedToRestoreFootprintWithResponse, -} diff --git a/src/stellar-plus/core/soroban-transaction-processor/index.ts b/src/stellar-plus/core/soroban-transaction-processor/index.ts deleted file mode 100644 index b04fbba..0000000 --- a/src/stellar-plus/core/soroban-transaction-processor/index.ts +++ /dev/null @@ -1,523 +0,0 @@ -import { Buffer } from 'buffer' - -import { - Address, - Contract, - ContractSpec, - FeeBumpTransaction, - Operation, - OperationOptions, - SorobanDataBuilder, - SorobanRpc as SorobanRpcNamespace, - Account as StellarAccount, - Transaction, - TransactionBuilder, - xdr, -} from '@stellar/stellar-sdk' - -import { AccountHandler } from 'stellar-plus/account/account-handler/types' -import { TransactionProcessor } from 'stellar-plus/core/classic-transaction-processor' -import { - RestoreFootprintArgs, - SorobanDeployArgs, - SorobanSimulateArgs, - SorobanUploadArgs, - WrapClassicAssetArgs, - isRestoreFootprintWithLedgerKeys, -} from 'stellar-plus/core/soroban-transaction-processor/types' -import { FeeBumpHeader, TransactionInvocation } from 'stellar-plus/core/types' -import { StellarPlusError } from 'stellar-plus/error' -import { DefaultRpcHandler } from 'stellar-plus/rpc/default-handler' -import { RpcHandler } from 'stellar-plus/rpc/types' -import { Network, TransactionXdr } from 'stellar-plus/types' -import { generateRandomSalt } from 'stellar-plus/utils/functions' - -import { STPError } from './errors' - -export class SorobanTransactionProcessor extends TransactionProcessor { - private rpcHandler: RpcHandler - - /** - * - * @param {Network} network - The network to use. - * @param {RpcHandler=} rpcHandler - The rpc handler to use. - * - * @description - The Soroban transaction processor is used for handling Soroban transactions and submitting them to the network. - * - */ - constructor(network: Network, rpcHandler?: RpcHandler) { - super({ network }) - this.rpcHandler = rpcHandler || new DefaultRpcHandler(network) - } - - /** - * @args {SorobanSimulateArgs} args - The arguments for the invocation. - * @param {string} args.method - The method to invoke as it is identified in the contract. - * @param {object} args.methodArgs - The arguments for the method invocation. - * @param {EnvelopeHeader} args.header - The header for the transaction. - * - * @arg {ContractSpec} spec - The contract specification. - * @arg {string} contractId - The contract id. - * - * @description - Builds a Soroban transaction envelope. - * - * @returns {Promise} The Soroban transaction envelope. - */ - protected async buildTransaction( - args: SorobanSimulateArgs, - spec: ContractSpec, - contractId: string - ): Promise { - const { method, methodArgs, header } = args - - const encodedArgs = spec.funcArgsToScVals(method, methodArgs) - - try { - const sourceAccount = (await this.horizonHandler.loadAccount(header.source)) as StellarAccount - const contract = new Contract(contractId) - const txEnvelope = new TransactionBuilder(sourceAccount, { - fee: header.fee, - networkPassphrase: this.network.networkPassphrase, - }) - .addOperation(contract.call(method, ...encodedArgs)) - .setTimeout(header.timeout) - .build() - - return txEnvelope - } catch (e) { - throw STPError.failedToBuildTransaction(e as Error, header) - } - } - - /** - * - * @param {Transaction} tx - The transaction to simulate. - * - * @description - Simulates the given transaction. - * - * @returns {Promise} The simulation response. - */ - protected async simulateTransaction(tx: Transaction): Promise { - try { - const response = await this.rpcHandler.simulateTransaction(tx) - return response - } catch (e) { - throw STPError.failedToSimulateTransaction(e as Error, tx) - } - } - - protected async assembleTransaction( - rawTransaction: Transaction, - simulatedTransaction: SorobanRpcNamespace.Api.SimulateTransactionResponse - ): Promise { - try { - const response = SorobanRpcNamespace.assembleTransaction(rawTransaction, simulatedTransaction) - return response.build() - } catch (e) { - throw STPError.failedToAssembleTransaction(e as Error, rawTransaction, simulatedTransaction) - } - } - - /** - * - * @param {Transaction | FeeBumpTransaction} tx - The transaction to submit. - * - * @description - Submits the given transaction to the network. - * - * @returns {Promise} The response from the Soroban server. - */ - protected async submitTransaction( - tx: Transaction | FeeBumpTransaction - ): Promise { - // console.log('Submitting transaction: ', tx.toXDR()) - try { - const response = await this.rpcHandler.submitTransaction(tx) - return response - } catch (e) { - throw STPError.failedToSubmitTransaction(e as Error, tx) - } - } - - /** - * - * @param {Transaction} envelope - The prepared transaction envelope to sign. - * @param {AccountHandler[]} signers - The signers to sign the transaction with. - * @param {FeeBumpHeader=} feeBump - The fee bump header to use. - * - * @description - Signs the given transaction envelope with the provided signers and submits it to the network. - * - * @returns {Promise} The response from the Soroban server. - */ - protected async processSorobanTransaction( - envelope: Transaction, - signers: AccountHandler[], - feeBump?: FeeBumpHeader, - secondsToWait?: number - ): Promise { - const signedInnerTransaction = await this.signEnvelope(envelope, signers) - - const finalEnvelope = feeBump - ? ((await this.wrapSorobanFeeBump(signedInnerTransaction, feeBump)) as FeeBumpTransaction) - : (TransactionBuilder.fromXDR(signedInnerTransaction, this.network.networkPassphrase) as Transaction) - - const rpcResponse = await this.submitTransaction(finalEnvelope) - const processedTransaction = await this.postProcessSorobanSubmission(rpcResponse, secondsToWait) - - return processedTransaction - } - - /** - * - * @param {SorobanRpcNamespace.SendTransactionResponse} response - The response from the Soroban server. - * - * @description - Processes the given Soroban transaction submission response. - * - * @returns {Promise} The response from the Soroban server. - */ - protected async postProcessSorobanSubmission( - response: SorobanRpcNamespace.Api.SendTransactionResponse, - secondsToWait?: number - ): Promise { - if (response.status === 'ERROR') { - throw STPError.failedToSubmitTransactionWithResponse(response) - } - - if (response.status === 'PENDING' || response.status === 'TRY_AGAIN_LATER') { - // console.log('Waiting for Transaction!: ') - return await this.waitForTransaction(response.hash, secondsToWait ? secondsToWait : 15) // Arbitrary 15 seconds timeout default - } - - throw STPError.failedToVerifyTransactionSubmission(response) - } - - /** - * - * @param {string} transactionHash - The hash of the transaction to wait for. - * @param {number} secondsToWait - The number of seconds to wait before timing out. Defaults to 15 seconds. - * - * - * @description - Waits for the given transaction to be processed by the Soroban server. - * Soroban transactions are processed asynchronously, so this method will wait for the transaction to be processed. - * If the transaction is not processed within the given timeout, it will throw an error. - * If the transaction is processed, it will return the response from the Soroban server. - * If the transaction fails, it will throw an error. - * @returns {Promise} The response from the Soroban server. - */ - protected async waitForTransaction( - transactionHash: string, - secondsToWait: number - ): Promise { - const timeout = secondsToWait * 1000 - const waitUntil = Date.now() + timeout - const initialWaitTime = 1000 - let waitTime = initialWaitTime - - let updatedTransaction = await this.rpcHandler.getTransaction(transactionHash) - while ( - Date.now() < waitUntil && - updatedTransaction.status === SorobanRpcNamespace.Api.GetTransactionStatus.NOT_FOUND - ) { - await new Promise((resolve) => setTimeout(resolve, waitTime)) - updatedTransaction = await this.rpcHandler.getTransaction(transactionHash) - waitTime *= 2 // Exponential backoff - } - - if (updatedTransaction.status === SorobanRpcNamespace.Api.GetTransactionStatus.SUCCESS) { - return updatedTransaction as SorobanRpcNamespace.Api.GetSuccessfulTransactionResponse - } - - if (updatedTransaction.status === SorobanRpcNamespace.Api.GetTransactionStatus.FAILED) { - // const failedTransaction = updatedTransaction as SorobanRpcNamespace.GetFailedTransactionResponse - // console.log("Details!: ", JSON.stringify(failedTransaction)); - throw STPError.transactionSubmittedFailed(updatedTransaction) - } - - throw STPError.transactionSubmittedNotFound(updatedTransaction, timeout, transactionHash) - } - - protected postProcessTransaction( - response: SorobanRpcNamespace.Api.GetSuccessfulTransactionResponse - ): SorobanRpcNamespace.Api.GetSuccessfulTransactionResponse { - //TODO: implement - - return response - } - - /** - * - * @param {Transaction} envelopeXdr - The inner transaction envelope to wrap. - * @param {FeeBumpHeader} feeBump - The fee bump header to use. - * - * @description - Wraps the given transaction envelope with the provided fee bump header. - * - * @returns {Promise} The wrapped transaction envelope. - */ - protected async wrapSorobanFeeBump(envelopeXdr: TransactionXdr, feeBump: FeeBumpHeader): Promise { - const tx = TransactionBuilder.fromXDR(envelopeXdr, this.network.networkPassphrase) as Transaction - - const feeBumpTx = TransactionBuilder.buildFeeBumpTransaction( - feeBump.header.source, - feeBump.header.fee, - tx, - this.network.networkPassphrase - ) - - const signedFeeBumpXDR = await this.signEnvelope(feeBumpTx, feeBump.signers) - - return TransactionBuilder.fromXDR(signedFeeBumpXDR, this.network.networkPassphrase) as FeeBumpTransaction - } - - /** - * - * @args {SorobanUploadArgs} args - The arguments for the invocation. - * @param {Buffer} args.wasm - The Buffer of the wasm file to upload. - * @param {EnvelopeHeader} args.header - The header for the transaction. - * @param {AccountHandler[]} args.signers - The signers for the transaction. - * @param {FeeBumpHeader=} args.feeBump - The fee bump header for the transaction. This is optional. - * @returns {Promise} The wasm hash of the uploaded wasm. - * - * @description - Builds a transaction to upload a wasm file to the Soroban server. - */ - protected async buildUploadContractWasmTransaction(args: SorobanUploadArgs): Promise<{ - builtTx: Transaction - updatedTxInvocation: TransactionInvocation - }> { - const { wasm, header, signers, feeBump } = args - - const txInvocation = { - signers, - header, - feeBump, - } - - const uploadOperation = [Operation.uploadContractWasm({ wasm })] - return await this.buildCustomTransaction(uploadOperation, txInvocation) - } - - /** - * - * @args {SorobanUploadArgs} args - The arguments for the invocation. - * @param {Buffer} args.wasm - The Buffer of the wasm file to upload. - * @param {EnvelopeHeader} args.header - The header for the transaction. - * @param {AccountHandler[]} args.signers - The signers for the transaction. - * @param {FeeBumpHeader=} args.feeBump - The fee bump header for the transaction. This is optional. - * @returns {Promise} The wasm hash of the uploaded wasm. - * - * @description - Uploads a wasm file to the Soroban server and returns the wasm hash. This hash can be used to deploy new instances of the contract. - */ - protected async uploadContractWasm(args: SorobanUploadArgs): Promise { - const { builtTx, updatedTxInvocation } = await this.buildUploadContractWasmTransaction(args) - - const simulatedTransaction = await this.simulateTransaction(builtTx) - const assembledTransaction = await this.assembleTransaction(builtTx, simulatedTransaction) - - try { - const output = await this.processSorobanTransaction( - assembledTransaction, - updatedTxInvocation.signers, - updatedTxInvocation.feeBump - ) - - // Not using the root returnValue parameter because it may not be available depending on the rpcHandler. - return this.extractWasmHashFromUploadWasmResponse(output) - } catch (error) { - throw STPError.failedToUploadWasm(error as StellarPlusError) - } - } - - protected extractWasmHashFromUploadWasmResponse( - response: SorobanRpcNamespace.Api.GetSuccessfulTransactionResponse - ): string { - return (response.resultMetaXdr.v3().sorobanMeta()?.returnValue().value() as Buffer).toString('hex') as string - } - - protected async buildDeployContractTransaction( - args: SorobanDeployArgs - ): Promise<{ builtTx: Transaction; updatedTxInvocation: TransactionInvocation }> { - const { wasmHash, header, signers, feeBump } = args - - const txInvocation = { - signers, - header, - feeBump, - } - - const options: OperationOptions.CreateCustomContract = { - address: new Address(header.source), - wasmHash: Buffer.from(wasmHash, 'hex'), - salt: generateRandomSalt(), - } - - const deployOperation = [Operation.createCustomContract(options)] - - return await this.buildCustomTransaction(deployOperation, txInvocation) - } - - /** - * - * @args {SorobanDeployArgs} args - The arguments for the invocation. - * @param {string} args.wasmHash - The wasm hash of the contract to deploy. - * @param {EnvelopeHeader} args.header - The header for the transaction. - * @param {AccountHandler[]} args.signers - The signers for the transaction. - * @param {FeeBumpHeader=} args.feeBump - The fee bump header for the transaction. This is optional. - * @returns {Promise} The contract Id of the deployed contract instance. - * - * @description - Deploys a new instance of the contract to the Soroban server and returns the contract id of the deployed contract instance. - */ - protected async deployContract(args: SorobanDeployArgs): Promise { - const { builtTx, updatedTxInvocation } = await this.buildDeployContractTransaction(args) - - const simulatedTransaction = await this.simulateTransaction(builtTx) - const assembledTransaction = await this.assembleTransaction(builtTx, simulatedTransaction) - - try { - const output = await this.processSorobanTransaction( - assembledTransaction, - updatedTxInvocation.signers, - updatedTxInvocation.feeBump - ) - // Not using the root returnValue parameter because it may not be available depending on the rpcHandler. - return this.extractContractIdFromDeployContractResponse(output) - } catch (error) { - throw STPError.failedToDeployContract(error as StellarPlusError) - } - } - protected extractContractIdFromDeployContractResponse( - response: SorobanRpcNamespace.Api.GetSuccessfulTransactionResponse - ): string { - return Address.fromScAddress( - response.resultMetaXdr.v3().sorobanMeta()?.returnValue().address() as xdr.ScAddress - ).toString() as string - } - - /** - * @args {WrapClassicAssetArgs} args - The arguments for the invocation. - * @param {Asset} args.asset - The asset to wrap. - * @param {EnvelopeHeader} args.header - The header for the transaction. - * @param {AccountHandler[]} args.signers - The signers for the transaction. - * @param {FeeBumpHeader=} args.feeBump - The fee bump header for the transaction. This is optional. - * - * @returns {Promise<{ builtTx: Transaction; updatedTxInvocation: TransactionInvocation }>} The built transaction and updated transaction invocation. - * - * @description - Builds a transaction to wrap a classic asset on the Stellar network. - * */ - protected async buildWrapClassicAssetTransaction( - args: WrapClassicAssetArgs - ): Promise<{ builtTx: Transaction; updatedTxInvocation: TransactionInvocation }> { - const { asset, header, signers, feeBump } = args - - const txInvocation = { - signers, - header, - feeBump, - } - - const options: OperationOptions.CreateStellarAssetContract = { - asset, - } - - const wrapOperation = [Operation.createStellarAssetContract(options)] - - return await this.buildCustomTransaction(wrapOperation, txInvocation) - } - - /** - * @args {WrapClassicAssetArgs} args - The arguments for the invocation. - * @param {Asset} args.asset - The asset to wrap. - * @param {EnvelopeHeader} args.header - The header for the transaction. - * @param {AccountHandler[]} args.signers - The signers for the transaction. - * @param {FeeBumpHeader=} args.feeBump - The fee bump header for the transaction. This is optional. - * @returns {Promise} The address of the wrapped asset contract. - * @description - Wraps a classic asset on the Stellar network and returns the address of the wrapped asset contract. - * - **/ - protected async wrapClassicAsset(args: WrapClassicAssetArgs): Promise { - const { builtTx, updatedTxInvocation } = await this.buildWrapClassicAssetTransaction(args) - - const simulatedTransaction = await this.simulateTransaction(builtTx) - const assembledTransaction = await this.assembleTransaction(builtTx, simulatedTransaction) - - try { - const output = await this.processSorobanTransaction( - assembledTransaction, - updatedTxInvocation.signers, - updatedTxInvocation.feeBump - ) - // Not using the root returnValue parameter because it may not be available depending on the rpcHandler. - return this.extractContractIdFromWrapClassicAssetResponse(output) - } catch (error) { - throw STPError.failedToWrapAsset(error as StellarPlusError) - } - } - - protected extractContractIdFromWrapClassicAssetResponse( - response: SorobanRpcNamespace.Api.GetSuccessfulTransactionResponse - ): string { - return Address.fromScAddress( - response.resultMetaXdr.v3().sorobanMeta()?.returnValue().address() as xdr.ScAddress - ).toString() - } - - // This functions can be invoked with two different sets of arguments. The first set is when the keys are provided directly. - /** - * @args {RestoreFootprintArgs} args - The arguments for the invocation. - * @param {EnvelopeHeader} args.header - The header for the transaction. - * @param {AccountHandler[]} args.signers - The signers for the transaction. - * @param {FeeBumpHeader=} args.feeBump - The fee bump header for the transaction. This is optional. - * - * Option 1: Provide the keys directly. - * @param {xdr.LedgerKey[]} args.keys - The keys to restore. - * Option 2: Provide the restore preamble. - * @param { RestoreFootprintWithRestorePreamble} args.restorePreamble - The restore preamble. - * @param {string} args.restorePreamble.minResourceFee - The minimum resource fee. - * @param {SorobanDataBuilder} args.restorePreamble.transactionData - The transaction data. - * - * @returns {Promise} - * - * @description - Execute a transaction to restore a given footprint. - */ - public async restoreFootprint(args: RestoreFootprintArgs): Promise { - const { header, signers, feeBump } = args - - const sorobanData = isRestoreFootprintWithLedgerKeys(args) - ? new SorobanDataBuilder().setReadWrite(args.keys).build() - : args.restorePreamble.transactionData.build() - - const txInvocation = { - signers, - header, - feeBump, - } - - // const options: OperationOptions.ExtendFootprintTTL = { - // extendTo, - // } - - const options: OperationOptions.RestoreFootprint = {} - // const extendTTLOperation = [Operation.extendFootprintTtl(options)] - const restoreFootprintOperation = [Operation.restoreFootprint(options)] - - const { builtTx, updatedTxInvocation } = await this.buildCustomTransaction(restoreFootprintOperation, txInvocation) - - const builtTxWithFootprint = TransactionBuilder.cloneFrom(builtTx).setSorobanData(sorobanData).build() - - const simulatedTransaction = await this.simulateTransaction(builtTxWithFootprint) - const assembledTransaction = await this.assembleTransaction(builtTxWithFootprint, simulatedTransaction) - - try { - const output = await this.processSorobanTransaction( - assembledTransaction, - updatedTxInvocation.signers, - updatedTxInvocation.feeBump - ) - - return Promise.resolve() - } catch (error) { - throw STPError.failedToRestoreFootprintWithError(error as StellarPlusError, assembledTransaction) - } - } - - protected getRpcHandler(): RpcHandler { - return this.rpcHandler - } -} diff --git a/src/stellar-plus/core/soroban-transaction-processor/types.ts b/src/stellar-plus/core/soroban-transaction-processor/types.ts deleted file mode 100644 index b0d18f3..0000000 --- a/src/stellar-plus/core/soroban-transaction-processor/types.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { SorobanDataBuilder, Asset as StellarAsset, xdr } from '@stellar/stellar-sdk' - -import { AccountHandler } from 'stellar-plus/account/account-handler/types' -import { EnvelopeHeader, FeeBumpHeader, TransactionInvocation } from 'stellar-plus/core/types' - -export type SorobanInvokeArgs = SorobanSimulateArgs & { - signers: AccountHandler[] - feeBump?: FeeBumpHeader -} - -export type SorobanSimulateArgs = { - method: string - methodArgs: T - header: EnvelopeHeader -} - -export type SorobanUploadArgs = TransactionInvocation & { - wasm: Buffer -} - -export type SorobanDeployArgs = TransactionInvocation & { - wasmHash: string -} - -export type WrapClassicAssetArgs = TransactionInvocation & { - asset: StellarAsset -} - -export type ExtendFootprintTTLArgs = TransactionInvocation & { - extendTo: number - footprint: xdr.LedgerFootprint -} - -export type RestoreFootprintArgs = TransactionInvocation & - (RestoreFootprintWithLedgerKeys | RestoreFootprintWithRestorePreamble) - -export type RestoreFootprintWithLedgerKeys = { - keys: xdr.LedgerKey[] -} - -export type RestoreFootprintWithRestorePreamble = { - restorePreamble: { - minResourceFee: string - transactionData: SorobanDataBuilder - } -} - -export function isRestoreFootprintWithLedgerKeys( - args: RestoreFootprintArgs -): args is RestoreFootprintWithLedgerKeys & TransactionInvocation { - return 'keys' in args -} - -export function isRestoreFootprintWithRestorePreamble( - args: RestoreFootprintArgs -): args is RestoreFootprintWithRestorePreamble & TransactionInvocation { - return 'restorePreamble' in args -} diff --git a/src/stellar-plus/core/transaction-submitter/classic/channel-accounts-submitter/errors.ts b/src/stellar-plus/core/transaction-submitter/classic/channel-accounts-submitter/errors.ts deleted file mode 100644 index 585d12c..0000000 --- a/src/stellar-plus/core/transaction-submitter/classic/channel-accounts-submitter/errors.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { FeeBumpTransaction, Transaction } from '@stellar/stellar-sdk' -import { HorizonApi } from '@stellar/stellar-sdk/lib/horizon' - -import { StellarPlusError } from 'stellar-plus/error' -import { diagnoseSubmitError, extractDataFromSubmitTransactionError } from 'stellar-plus/error/helpers/horizon' - -export enum ChannelAccountsTransactionSubmitterErrorCodes { - // CHATS0 General - CHATS001 = 'CHATS001', - CHATS002 = 'CHATS002', -} - -const failedToReleaseChannelNotFound = (publicKey: string): StellarPlusError => { - return new StellarPlusError({ - code: ChannelAccountsTransactionSubmitterErrorCodes.CHATS001, - message: 'Failed to release channel!', - source: 'ChannelAccountsTransactionSubmitter', - details: `Failed to release channel! The channel account ${publicKey} was not found in the list of locked channels!`, - }) -} - -const failedToSubmitTransaction = (error: Error, tx: Transaction | FeeBumpTransaction): StellarPlusError => { - return new StellarPlusError({ - code: ChannelAccountsTransactionSubmitterErrorCodes.CHATS002, - message: 'Failed to submit transaction!', - source: 'ChannelAccountsTransactionSubmitter', - details: `Failed to submit transaction! A problem occurred while submitting the transaction to the network for processing! Check the meta property for more details.`, - ...diagnoseSubmitError(error, tx), - }) -} - -const transactionSubmittedFailed = (response: HorizonApi.SubmitTransactionResponse): StellarPlusError => { - return new StellarPlusError({ - code: ChannelAccountsTransactionSubmitterErrorCodes.CHATS002, - message: 'Failed to submit transaction!', - source: 'ChannelAccountsTransactionSubmitter', - details: `Failed to submit transaction! A problem occurred while submitting the transaction to the network for processing! Check the meta property for more details.`, - meta: { - horizonSubmitTransactionData: extractDataFromSubmitTransactionError(response), - data: { response }, - }, - }) -} - -export const CHATSError = { - failedToReleaseChannelNotFound, - failedToSubmitTransaction, - transactionSubmittedFailed, -} diff --git a/src/stellar-plus/core/transaction-submitter/classic/channel-accounts-submitter/index.ts b/src/stellar-plus/core/transaction-submitter/classic/channel-accounts-submitter/index.ts deleted file mode 100644 index 676e2cb..0000000 --- a/src/stellar-plus/core/transaction-submitter/classic/channel-accounts-submitter/index.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { FeeBumpTransaction, Horizon as HorizonNamespace, Transaction, TransactionBuilder } from '@stellar/stellar-sdk' - -import { DefaultAccountHandler } from 'stellar-plus/account/account-handler/default/types' -import { TransactionSubmitter as TransactionSubmitter } from 'stellar-plus/core/transaction-submitter/classic/types' -import { FeeBumpHeader, TransactionInvocation } from 'stellar-plus/core/types' -import { HorizonHandlerClient } from 'stellar-plus/horizon/index' -import { HorizonHandler } from 'stellar-plus/horizon/types' -import { Network } from 'stellar-plus/types' - -import { CHATSError } from './errors' - -export class ChannelAccountsTransactionSubmitter implements TransactionSubmitter { - private feeBump?: FeeBumpHeader - private freeChannels: DefaultAccountHandler[] - private lockedChannels: DefaultAccountHandler[] - private network: Network - private horizonHandler: HorizonHandler - - /** - * - * @param {Network} network - The network to use. - * @param {FeeBumpHeader=} feeBump - The fee bump header to use for wrapping transactions. If not provided during the invocations, this default fee bump header will be used. - * - * @description - The channel accounts transaction submitter is used for submitting transactions using a pool of channel accounts. - * - * @see https://developers.stellar.org/docs/encyclopedia/channel-accounts for more information. - * @see ChannelAccountsHandler for a helper class for managing channel accounts. - */ - constructor(network: Network, feeBump?: FeeBumpHeader) { - this.network = network - this.feeBump = feeBump - this.horizonHandler = new HorizonHandlerClient(network) - this.freeChannels = [] - this.lockedChannels = [] - } - - /** - * - * @param {DefaultAccountHandler[]} channels - The channel accounts to register. - * - * @description - Registers the provided channel accounts to the pool. - * - * @see ChannelAccountsHandler for a helper class for managing channel accounts. - */ - public registerChannels(channels: DefaultAccountHandler[]): void { - this.freeChannels = [...this.freeChannels, ...channels] - } - - private async allocateChannel(): Promise { - if (this.freeChannels.length === 0) { - return await this.noChannelPipeline() - } else { - const channel = this.freeChannels.pop() as DefaultAccountHandler - this.lockedChannels.push(channel) - - return channel - } - } - - private releaseChannel(channelPublicKey: string): void { - const channelIndex = this.lockedChannels.findIndex((channel) => channel.getPublicKey() === channelPublicKey) - if (channelIndex === -1) { - throw CHATSError.failedToReleaseChannelNotFound(channelPublicKey) - } - - const channel = this.lockedChannels[channelIndex] - this.lockedChannels.splice(channelIndex, 1) - this.freeChannels.push(channel) - } - - /** - * - * @param {TransactionInvocation} txInvocation - The transaction invocation settings to use when building the transaction envelope. - * - * @description - Creates a transaction envelope using the provided transaction invocation settings. This step will allocate a channel account to use for the transaction. - * - * @returns {{ envelope: TransactionBuilder, updatedTxInvocation: TransactionInvocation }} The transaction envelope and the updated transaction invocation. - * - * @see https://developers.stellar.org/docs/encyclopedia/channel-accounts for more information. - */ - public async createEnvelope(txInvocation: TransactionInvocation): Promise<{ - envelope: TransactionBuilder - updatedTxInvocation: TransactionInvocation - }> { - const { header } = txInvocation - if (this.feeBump && !txInvocation.feeBump) { - txInvocation.feeBump = this.feeBump - } - - // console.log("Waiting for Channel!"); - const channel = await this.allocateChannel() - - const sourceAccount = await this.horizonHandler.loadAccount(channel.getPublicKey() as string) - - const envelope = new TransactionBuilder(sourceAccount, { - fee: header.fee, - networkPassphrase: this.network.networkPassphrase, - }) - - const updatedSigners = [...txInvocation.signers, channel] - const updatedTxInvocation = { ...txInvocation, signers: updatedSigners } - return { envelope, updatedTxInvocation } - } - - //submit(envelope: Transaction, signers: AccountHandler[], feeBump?: FeeBumpHeader): Promise - - /** - * - * @param {Transaction} envelope - The transaction to submit. - * - * @description - Submits the provided transaction to the network. This step will release the channel account used for the transaction. - * - * @returns {Promise} The response from the Horizon server. - */ - public async submit( - envelope: Transaction | FeeBumpTransaction - ): Promise { - const innerEnvelope = (envelope as FeeBumpTransaction).innerTransaction - const allocatedChannel = innerEnvelope.source - - // stellar-base vs stellar-sdk conversion - const envelopeXdr = envelope.toXDR() - const classicEnvelope = TransactionBuilder.fromXDR(envelopeXdr, this.network.networkPassphrase) as Transaction - - try { - const response = await this.horizonHandler.server.submitTransaction(classicEnvelope) - - this.releaseChannel(allocatedChannel) - return response as HorizonNamespace.HorizonApi.SubmitTransactionResponse - } catch (error) { - this.releaseChannel(allocatedChannel) - throw CHATSError.failedToSubmitTransaction(error as Error, envelope) - } - } - - /** - * - * @param { HorizonNamespace.SubmitTransactionResponse } response - The response from the Horizon server. - * - * @returns { HorizonNamespace.SubmitTransactionResponse } The response from the Horizon server. - * - * @description - Post processes the response from the Horizon server. This step will throw an error if the transaction failed. - * This function can be overriden to implement a custom post processing logic. - */ - public postProcessTransaction( - response: HorizonNamespace.HorizonApi.SubmitTransactionResponse - ): HorizonNamespace.HorizonApi.SubmitTransactionResponse { - if (!response.successful) { - // const restulObject = xdrNamespace.TransactionResult.fromXDR(response.result_xdr, 'base64') - // const resultMetaObject = xdrNamespace.TransactionResultMeta.fromXDR(response.result_meta_xdr, 'base64') - throw CHATSError.transactionSubmittedFailed(response) - } - - return response - } - - /** - * - * @description - Waits for a channel to be available and then allocates it. This step will wait for 1 second before trying again. - * This function can be overriden to implement a custom waiting logic. - * - * @returns {Promise} The allocated channel account. - */ - private noChannelPipeline(): Promise { - return new Promise((resolve) => { - setTimeout(() => { - resolve(this.allocateChannel()) - }, 1000) - }) - } - - /** - * - * @description - Returns the list of channels registered to the pool. - * - * @returns {DefaultAccountHandler[]} The list of channels registered to the pool. - */ - public getChannels(): DefaultAccountHandler[] { - return [...this.freeChannels, ...this.lockedChannels] - } -} diff --git a/src/stellar-plus/core/transaction-submitter/classic/default/errors.ts b/src/stellar-plus/core/transaction-submitter/classic/default/errors.ts deleted file mode 100644 index 8f32b96..0000000 --- a/src/stellar-plus/core/transaction-submitter/classic/default/errors.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { FeeBumpTransaction, Transaction } from '@stellar/stellar-sdk' -import { HorizonApi } from '@stellar/stellar-sdk/lib/horizon' -import { AxiosError } from 'axios' - -import { StellarPlusError } from 'stellar-plus/error' -import { diagnoseSubmitError, extractDataFromSubmitTransactionError } from 'stellar-plus/error/helpers/horizon' - -export enum DefaultTransactionSubmitterErrorCodes { - // DTS0 General - DTS001 = 'DTS001', - DTS002 = 'DTS002', -} - -const failedToSubmitTransaction = ( - error: Error | AxiosError, - tx: Transaction | FeeBumpTransaction -): StellarPlusError => { - return new StellarPlusError({ - code: DefaultTransactionSubmitterErrorCodes.DTS001, - message: 'Failed to submit transaction!', - source: 'DefaultTransactionSubmitter', - details: `Failed to submit transaction! A problem occurred while submitting the transaction to the network for processing! Check the meta property for more details.`, - ...diagnoseSubmitError(error, tx), - }) -} - -const transactionSubmittedFailed = (response: HorizonApi.SubmitTransactionResponse): StellarPlusError => { - return new StellarPlusError({ - code: DefaultTransactionSubmitterErrorCodes.DTS002, - message: 'Transaction Failed!', - source: 'DefaultTransactionSubmitter', - details: `Transaction submitted failed! A problem occurred while processing the transaction after submission! Check the meta property for more details.`, - meta: { - horizonSubmitTransactionData: extractDataFromSubmitTransactionError(response), - data: { response }, - }, - }) -} - -export const DTSError = { - failedToSubmitTransaction, - transactionSubmittedFailed, -} diff --git a/src/stellar-plus/core/transaction-submitter/classic/default/index.ts b/src/stellar-plus/core/transaction-submitter/classic/default/index.ts deleted file mode 100644 index f641e65..0000000 --- a/src/stellar-plus/core/transaction-submitter/classic/default/index.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { Horizon as HorizonNamespace, Transaction, TransactionBuilder } from '@stellar/stellar-sdk' - -import { TransactionSubmitter as TransactionSubmitter } from 'stellar-plus/core/transaction-submitter/classic/types' -import { FeeBumpHeader, TransactionInvocation } from 'stellar-plus/core/types' -import { HorizonHandlerClient } from 'stellar-plus/horizon/index' -import { HorizonHandler } from 'stellar-plus/horizon/types' -import { Network } from 'stellar-plus/types' - -import { DTSError } from './errors' - -export class DefaultTransactionSubmitter implements TransactionSubmitter { - private feeBump?: FeeBumpHeader - private network: Network - private horizonHandler: HorizonHandler - - /** - * - * @param {Network} network - The network to use. - * @param {FeeBumpHeader=} feeBump - The fee bump header to use for wrapping transactions. If not provided during the invocations, this default fee bump header will be used. - * - * @description - The default transaction submitter is used for submitting transactions using a single account. - * - */ - constructor(network: Network, feeBump?: FeeBumpHeader) { - this.network = network - this.horizonHandler = new HorizonHandlerClient(network) - this.feeBump = feeBump - } - - /** - * - * @param {TransactionInvocation} txInvocation - The transaction invocation to create the envelope for. - * - * @description - Creates the transaction envelope for the given transaction invocation. - * - * @returns {{envelope: TransactionBuilder, updatedTxInvocation: TransactionInvocation}} The transaction envelope and the updated transaction invocation. - */ - public async createEnvelope(txInvocation: TransactionInvocation): Promise<{ - envelope: TransactionBuilder - updatedTxInvocation: TransactionInvocation - }> { - const { header } = txInvocation - if (this.feeBump && !txInvocation.feeBump) { - txInvocation.feeBump = this.feeBump - } - - const sourceAccount = await this.horizonHandler.loadAccount(header.source) - - const envelope = new TransactionBuilder(sourceAccount, { - fee: header.fee, - networkPassphrase: this.network.networkPassphrase, - }) - - return { envelope, updatedTxInvocation: txInvocation } - } - - /** - * - * @param {Transaction} envelope - The transaction envelope to submit. - * - * @description - Submits the given transaction envelope. - * - * @returns {Horizon.SubmitTransactionResponse} The transaction submission response. - */ - public async submit(envelope: Transaction): Promise { - try { - // stellar-base vs stellar-sdk conversion - // TODO: Review post lib update to stellar-sdk v11 - - const envelopeXdr = envelope.toXDR() - const classicEnvelope = TransactionBuilder.fromXDR(envelopeXdr, this.network.networkPassphrase) as Transaction - return (await this.horizonHandler.server.submitTransaction( - classicEnvelope - )) as HorizonNamespace.HorizonApi.SubmitTransactionResponse - } catch (error) { - throw DTSError.failedToSubmitTransaction(error as Error, envelope) - } - } - - /** - * - * @param {Horizon.SubmitTransactionResponse} response - The response from the Horizon server. - * - * @returns {Horizon.SubmitTransactionResponse} The response from the Horizon server. - * - * @description - Post processes the transaction response from the Horizon server. - * This method can be overridden to provide custom post processing. - */ - public postProcessTransaction( - response: HorizonNamespace.HorizonApi.SubmitTransactionResponse - ): HorizonNamespace.HorizonApi.SubmitTransactionResponse { - if (!response.successful) { - throw DTSError.transactionSubmittedFailed(response) - } - - return response - } -} diff --git a/src/stellar-plus/core/transaction-submitter/classic/types.ts b/src/stellar-plus/core/transaction-submitter/classic/types.ts deleted file mode 100644 index 3b4bcf8..0000000 --- a/src/stellar-plus/core/transaction-submitter/classic/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { - FeeBumpTransaction, - Horizon as HorizonNamespace, - // SorobanRpc as SorobanRpcNamespace, - Transaction, - TransactionBuilder, -} from '@stellar/stellar-sdk' - -import { TransactionInvocation } from 'stellar-plus/core/types' - -export type TransactionSubmitter = { - createEnvelope(txInvocation: TransactionInvocation): Promise<{ - envelope: TransactionBuilder - updatedTxInvocation: TransactionInvocation - }> - submit(envelope: Transaction | FeeBumpTransaction): Promise - postProcessTransaction( - response: HorizonNamespace.HorizonApi.SubmitTransactionResponse - ): HorizonNamespace.HorizonApi.SubmitTransactionResponse -} diff --git a/src/stellar-plus/core/types.d.ts b/src/stellar-plus/core/types.ts similarity index 56% rename from src/stellar-plus/core/types.d.ts rename to src/stellar-plus/core/types.ts index 49ae5b6..dd13ef0 100644 --- a/src/stellar-plus/core/types.d.ts +++ b/src/stellar-plus/core/types.ts @@ -1,10 +1,12 @@ import { AccountHandler } from 'stellar-plus/account/account-handler/types' +import { SupportedInnerPlugins as SorobanTransactionExecutionPlugin } from 'stellar-plus/core/pipelines/soroban-transaction/types' export type TransactionInvocation = { signers: AccountHandler[] header: EnvelopeHeader feeBump?: FeeBumpHeader sponsor?: AccountHandler + executionPlugins?: SorobanTransactionExecutionPlugin[] } export type SorobanSimulationInvocation = { @@ -21,3 +23,14 @@ export type FeeBumpHeader = { signers: AccountHandler[] header: EnvelopeHeader } + +export type SignatureRequirement = { + publicKey: string + thresholdLevel: SignatureThreshold +} + +export enum SignatureThreshold { + low = 1, + medium = 2, + high = 3, +} diff --git a/src/stellar-plus/error/helpers/conveyor-belt.ts b/src/stellar-plus/error/helpers/conveyor-belt.ts new file mode 100644 index 0000000..0a77427 --- /dev/null +++ b/src/stellar-plus/error/helpers/conveyor-belt.ts @@ -0,0 +1,14 @@ +export type ConveyorBeltErrorMeta = { + item: Input + meta: Meta +} + +export const extractConveyorBeltErrorMeta = ( + item: Input, + meta: Meta +): ConveyorBeltErrorMeta => { + return { + item, + meta, + } +} diff --git a/src/stellar-plus/error/helpers/result-meta-xdr.test.ts b/src/stellar-plus/error/helpers/result-meta-xdr.test.ts index 7330f35..1114d23 100644 --- a/src/stellar-plus/error/helpers/result-meta-xdr.test.ts +++ b/src/stellar-plus/error/helpers/result-meta-xdr.test.ts @@ -1,12 +1,13 @@ -import Stellar from '@stellar/stellar-sdk'; -import { extractSorobanResultXdrOpErrorCode } from './result-meta-xdr'; +import Stellar from '@stellar/stellar-sdk' -jest.mock('@stellar/stellar-sdk'); +import { extractSorobanResultXdrOpErrorCode } from './result-meta-xdr' + +jest.mock('@stellar/stellar-sdk') describe('extractSorobanResultXdrOpErrorCode', () => { afterEach(() => { - jest.resetAllMocks(); - }); + jest.resetAllMocks() + }) beforeEach(() => { Stellar.xdr.TransactionResult = { @@ -17,46 +18,50 @@ describe('extractSorobanResultXdrOpErrorCode', () => { it('should extract op error code from XDR string', () => { const mockResultXdrObject = { result: () => ({ - results: () => [{ - tr: () => ({ - value: () => ({ - switch: () => ({ - name: 'opErrorCode', + results: () => [ + { + tr: () => ({ + value: () => ({ + switch: () => ({ + name: 'opErrorCode', + }), }), }), - }), - }], + }, + ], }), - }; + } - Stellar.xdr.TransactionResult.fromXDR.mockReturnValueOnce(mockResultXdrObject); + Stellar.xdr.TransactionResult.fromXDR.mockReturnValueOnce(mockResultXdrObject) - const result = extractSorobanResultXdrOpErrorCode('base64EncodedXDR'); + const result = extractSorobanResultXdrOpErrorCode('base64EncodedXDR') - expect(result).toBe('opErrorCode'); - expect(Stellar.xdr.TransactionResult.fromXDR).toHaveBeenCalledWith('base64EncodedXDR', 'base64'); - }); + expect(result).toBe('opErrorCode') + expect(Stellar.xdr.TransactionResult.fromXDR).toHaveBeenCalledWith('base64EncodedXDR', 'base64') + }) it('should handle XDR object with result method', () => { const mockResultXdrObject = { result: jest.fn(() => ({ - results: jest.fn(() => [{ - tr: jest.fn(() => ({ - value: jest.fn(() => ({ - switch: jest.fn(() => ({ - name: 'opErrorCode', + results: jest.fn(() => [ + { + tr: jest.fn(() => ({ + value: jest.fn(() => ({ + switch: jest.fn(() => ({ + name: 'opErrorCode', + })), })), })), - })), - }]), + }, + ]), })), - }; + } - const result = extractSorobanResultXdrOpErrorCode(mockResultXdrObject); + const result = extractSorobanResultXdrOpErrorCode(mockResultXdrObject) - expect(result).toBe('opErrorCode'); - expect(mockResultXdrObject.result).toHaveBeenCalled(); - }); + expect(result).toBe('opErrorCode') + expect(mockResultXdrObject.result).toHaveBeenCalled() + }) it('should handle XDR object with result.switch method', () => { const mockResultXdrObject = { @@ -65,34 +70,34 @@ describe('extractSorobanResultXdrOpErrorCode', () => { name: 'opErrorCode', })), })), - }; + } - const result = extractSorobanResultXdrOpErrorCode(mockResultXdrObject); + const result = extractSorobanResultXdrOpErrorCode(mockResultXdrObject) - expect(result).toBe('opErrorCode'); - expect(mockResultXdrObject.result).toHaveBeenCalled(); - }); + expect(result).toBe('opErrorCode') + expect(mockResultXdrObject.result).toHaveBeenCalled() + }) it('should handle XDR object with missing results or switch methods', () => { - const result = extractSorobanResultXdrOpErrorCode({}); + const result = extractSorobanResultXdrOpErrorCode({}) - expect(result).toBe('not_found'); - }); + expect(result).toBe('not_found') + }) it('should handle decoding errors', () => { const mockResultXdrObject = { result: () => { - throw new Error('Decoding error'); + throw new Error('Decoding error') }, - }; + } - const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => { }); + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {}) - const result = extractSorobanResultXdrOpErrorCode(mockResultXdrObject); + const result = extractSorobanResultXdrOpErrorCode(mockResultXdrObject) - expect(result).toBe('fail_in_decode_xdr'); - expect(consoleSpy).toHaveBeenCalledWith('Fail in decode xdr: %s, Error: %s', mockResultXdrObject, expect.any(Error)); + expect(result).toBe('fail_in_decode_xdr') + expect(consoleSpy).toHaveBeenCalledWith('Fail in decode xdr: %s, Error: %s', mockResultXdrObject, expect.any(Error)) - consoleSpy.mockRestore(); - }); -}); + consoleSpy.mockRestore() + }) +}) diff --git a/src/stellar-plus/error/index.ts b/src/stellar-plus/error/index.ts index cc56940..8a43acc 100644 --- a/src/stellar-plus/error/index.ts +++ b/src/stellar-plus/error/index.ts @@ -40,4 +40,12 @@ export class StellarPlusError extends Error { meta: { ...meta, error: args?.error }, }) } + + static fromUnkownError(error: unknown): StellarPlusError { + if (error instanceof StellarPlusError) { + return error + } + + return StellarPlusError.unexpectedError({ error: error as Error }) + } } diff --git a/src/stellar-plus/error/types.ts b/src/stellar-plus/error/types.ts index 77963a4..6cc29e1 100644 --- a/src/stellar-plus/error/types.ts +++ b/src/stellar-plus/error/types.ts @@ -3,11 +3,11 @@ import { FreighterAccountHandlerErrorCodes } from 'stellar-plus/account/account- import { FriendbotErrorCodes } from 'stellar-plus/account/helpers/friendbot/errors' import { ClassicAssetHandlerErrorCodes } from 'stellar-plus/asset/classic/errors' import { ChannelAccountsErrorCodes } from 'stellar-plus/channel-accounts/errors' -import { ClassicTransactionProcessorErrorCodes } from 'stellar-plus/core/classic-transaction-processor/errors' import { ContractEngineErrorCodes } from 'stellar-plus/core/contract-engine/errors' -import { SorobanTransactionProcessorErrorCodes } from 'stellar-plus/core/soroban-transaction-processor/errors' -import { ChannelAccountsTransactionSubmitterErrorCodes } from 'stellar-plus/core/transaction-submitter/classic/channel-accounts-submitter/errors' -import { DefaultTransactionSubmitterErrorCodes } from 'stellar-plus/core/transaction-submitter/classic/default/errors' +import { ErrorCodesPipelineBuildTransaction } from 'stellar-plus/core/pipelines/build-transaction/errors' +import { ErrorCodesPipelineClassicSignRequirements } from 'stellar-plus/core/pipelines/classic-sign-requirements/errors' +import { ErrorCodesPipelineSimulateTransaction } from 'stellar-plus/core/pipelines/simulate-transaction/errors' +import { ErrorCodesPipelineSorobanGetTransaction } from 'stellar-plus/core/pipelines/soroban-get-transaction/errors' import { ValidationCloudRpcHandlerErrorCodes } from 'stellar-plus/rpc/validation-cloud-handler/errors' import { AxiosErrorInfo } from './helpers/axios' @@ -39,10 +39,10 @@ export type ErrorCodes = | DefaultAccountHandlerErrorCodes | FreighterAccountHandlerErrorCodes | ValidationCloudRpcHandlerErrorCodes - | DefaultTransactionSubmitterErrorCodes - | ClassicTransactionProcessorErrorCodes - | SorobanTransactionProcessorErrorCodes - | ChannelAccountsTransactionSubmitterErrorCodes + | ErrorCodesPipelineBuildTransaction + | ErrorCodesPipelineSimulateTransaction + | ErrorCodesPipelineSorobanGetTransaction + | ErrorCodesPipelineClassicSignRequirements export enum GeneralErrorCodes { ER000 = 'ER000', @@ -61,4 +61,5 @@ export type Meta = { sorobanGetTransactionData?: GetTransactionErrorInfo sorobanSendTransactionData?: SendTransactionErrorInfo horizonSubmitTransactionData?: SubmitTransactionMetaInfo + conveyorBeltErrorMeta?: unknown } diff --git a/src/stellar-plus/horizon/index.ts b/src/stellar-plus/horizon/index.ts index 2c4e3fa..28c5047 100644 --- a/src/stellar-plus/horizon/index.ts +++ b/src/stellar-plus/horizon/index.ts @@ -2,22 +2,22 @@ import { Horizon } from '@stellar/stellar-sdk' import { StellarPlusError } from 'stellar-plus/error' import { HorizonHandler } from 'stellar-plus/horizon/types' -import { Network } from 'stellar-plus/types' +import { NetworkConfig } from 'stellar-plus/types' export class HorizonHandlerClient implements HorizonHandler { - private network: Network + private networkConfig: NetworkConfig public server: Horizon.Server /** * - * @param {Network} network - The network to use. + * @param {NetworkConfig} networkConfig - The network to use. * * @description - The horizon handler is used for interacting with the Horizon server. * */ - constructor(network: Network) { - this.network = network - this.server = new Horizon.Server(this.network.horizonUrl) + constructor(networkConfig: NetworkConfig) { + this.networkConfig = networkConfig + this.server = new Horizon.Server(this.networkConfig.horizonUrl) } /** diff --git a/src/stellar-plus/index.ts b/src/stellar-plus/index.ts index 8bf7346..287e928 100644 --- a/src/stellar-plus/index.ts +++ b/src/stellar-plus/index.ts @@ -1,6 +1,8 @@ import { ChannelAccounts as ChannelAccountsHandler } from 'stellar-plus/channel-accounts/index' import { CertificateOfDepositClient } from 'stellar-plus/soroban/contracts/certificate-of-deposit' -import { Profiler as SorobanProfiler } from 'stellar-plus/utils/profiler/soroban' + +import { pipelineUtils } from './utils/pipeline' +import { plugins } from './utils/pipeline/plugins' export * as Account from 'stellar-plus/account/index' export * as Asset from 'stellar-plus/asset/index' @@ -8,17 +10,16 @@ export * as Constants from 'stellar-plus/constants' export { HorizonHandlerClient as HorizonHandler } from 'stellar-plus/horizon/index' export { SorobanHandlerClient as SorobanHandler } from 'stellar-plus/soroban/index' -export { ContractEngine } from 'stellar-plus/core/contract-engine' - export { Core } from 'stellar-plus/core/index' -export const Contracts = { +export const ContractClients = { CertificateOfDeposit: CertificateOfDepositClient, } export const Utils = { ChannelAccountsHandler, - SorobanProfiler, + Pipeline: pipelineUtils, + Plugins: plugins, } export * as RPC from 'stellar-plus/rpc/index' diff --git a/src/stellar-plus/rpc/default-handler/index.ts b/src/stellar-plus/rpc/default-handler/index.ts index c4fd6fe..54e9c2e 100644 --- a/src/stellar-plus/rpc/default-handler/index.ts +++ b/src/stellar-plus/rpc/default-handler/index.ts @@ -1,23 +1,24 @@ import { FeeBumpTransaction, SorobanRpc, Transaction, xdr } from '@stellar/stellar-sdk' import { RpcHandler } from 'stellar-plus/rpc/types' -import { Network } from 'stellar-plus/types' +import { NetworkConfig } from 'stellar-plus/types' export class DefaultRpcHandler implements RpcHandler { + readonly type = 'RpcHandler' private server: SorobanRpc.Server - private network: Network + private networkConfig: NetworkConfig /** * - * @param {Network} network - The network to use. + * @param {NetworkConfig} networkConfig - The network to use. * * @description - The default rpc handler is used for interacting with the Soroban server. * It uses the URL provided by the network to connect to the Soroban server and carry out the RPC functions. * */ - constructor(network: Network) { - this.network = network - this.server = new SorobanRpc.Server(this.network.rpcUrl) + constructor(networkConfig: NetworkConfig) { + this.networkConfig = networkConfig + this.server = new SorobanRpc.Server(this.networkConfig.rpcUrl) } /** @@ -41,7 +42,7 @@ export class DefaultRpcHandler implements RpcHandler { * * @description - Simulates the transaction on the Soroban server. */ - async simulateTransaction(tx: Transaction): Promise { + async simulateTransaction(tx: Transaction | FeeBumpTransaction): Promise { const response = await this.server.simulateTransaction(tx) return response } diff --git a/src/stellar-plus/rpc/types.ts b/src/stellar-plus/rpc/types.ts index 27cae00..31dc567 100644 --- a/src/stellar-plus/rpc/types.ts +++ b/src/stellar-plus/rpc/types.ts @@ -1,6 +1,7 @@ import { FeeBumpTransaction, SorobanRpc, Transaction, xdr } from '@stellar/stellar-sdk' export type RpcHandler = { + readonly type: 'RpcHandler' getTransaction(txHash: string): Promise getLatestLedger(): Promise getHealth(): Promise diff --git a/src/stellar-plus/rpc/validation-cloud-handler/index.ts b/src/stellar-plus/rpc/validation-cloud-handler/index.ts index 53c761a..9f15d9b 100644 --- a/src/stellar-plus/rpc/validation-cloud-handler/index.ts +++ b/src/stellar-plus/rpc/validation-cloud-handler/index.ts @@ -12,33 +12,35 @@ import { SendTransactionAPIResponse, SimulateTransactionAPIResponse, } from 'stellar-plus/rpc/validation-cloud-handler/types' -import { Network } from 'stellar-plus/types' +import { NetworkConfig } from 'stellar-plus/types' import { VCRPCError } from './errors' export class ValidationCloudRpcHandler implements RpcHandler { + readonly type = 'RpcHandler' + private apiKey: string - private network: Network + private networkConfig: NetworkConfig private baseUrl: string private id: string /** * - * @param {Network} network - The network to use. + * @param {NetworkConfig} networkConfig - The network to use. * @param {string} apiKey - The API key to authenticate with Validation Cloud's API. * * @description - This rpc handler integrates directly with Validation Cloud's API. And uses their RPC infrastructure to carry out the RPC functions. * */ - constructor(network: Network, apiKey: string) { + constructor(networkConfig: NetworkConfig, apiKey: string) { if (!apiKey) { throw VCRPCError.invalidApiKey() } - this.network = network + this.networkConfig = networkConfig this.apiKey = apiKey this.baseUrl = - this.network.name === 'testnet' + this.networkConfig.name === 'testnet' ? 'https://testnet.stellar.validationcloud.io/v1/' : 'https://testnet.stellar.validationcloud.io/v1/' // no support to mainnet yet diff --git a/src/stellar-plus/soroban/contracts/certificate-of-deposit/index.ts b/src/stellar-plus/soroban/contracts/certificate-of-deposit/index.ts index f6a6830..99bce9c 100644 --- a/src/stellar-plus/soroban/contracts/certificate-of-deposit/index.ts +++ b/src/stellar-plus/soroban/contracts/certificate-of-deposit/index.ts @@ -1,9 +1,9 @@ import { Address, ContractSpec } from '@stellar/stellar-sdk' import { ContractEngine } from 'stellar-plus/core/contract-engine' -import { i128, u32, u64 } from 'stellar-plus/types' - -import { Methods, spec } from './constants' +import { ContractEngineConstructorArgs } from 'stellar-plus/core/contract-engine/types' +import { TransactionInvocation } from 'stellar-plus/core/types' +import { Methods, spec } from 'stellar-plus/soroban/contracts/certificate-of-deposit/constants' import { CertificateOfDepositContract, CertificateOfDepositContractConstructorArgs, @@ -14,7 +14,8 @@ import { GetTimeLeftArgs, Initialize, WithdrawArgs, -} from './types' +} from 'stellar-plus/soroban/contracts/certificate-of-deposit/types' +import { i128, u32, u64 } from 'stellar-plus/types' export class CertificateOfDepositClient extends ContractEngine implements CertificateOfDepositContract { private methods: typeof Methods @@ -22,17 +23,24 @@ export class CertificateOfDepositClient extends ContractEngine implements Certif /** * * @param {string} contractId - The contract ID of the deployed Certificate of Deposit to use. - * @param {Network} network - The network to use. + * @param {NetworkConfig} networkConfig - The network to use. * @param {RpcHandler} rpcHandler - The RPC handler to use. * * @description - The certificate of deposit client is used for interacting with the certificate of deposit contract. * */ constructor(args: CertificateOfDepositContractConstructorArgs) { + const contractSpec = args.contractParameters.spec || (spec as ContractSpec) + const contractParameters = { + ...args.contractParameters, + spec: contractSpec, + } + super({ ...args, - spec: spec as ContractSpec, - }) + contractParameters, + } as ContractEngineConstructorArgs) + this.methods = Methods } @@ -56,9 +64,10 @@ export class CertificateOfDepositClient extends ContractEngine implements Certif await this.invokeContract({ method: this.methods.deposit, methodArgs: { amount, address }, - signers: args.signers, - header: args.header, - feeBump: args.feeBump, + // signers: args.signers, + // header: args.header, + // feeBump: args.feeBump, + ...(args as TransactionInvocation), }) } @@ -204,9 +213,10 @@ export class CertificateOfDepositClient extends ContractEngine implements Certif penalty_rate: penaltyRate as u64, allowance_period: args.allowancePeriod as u32, }, - signers: args.signers, - header: args.header, - feeBump: args.feeBump, + // signers: args.signers, + // header: args.header, + // feeBump: args.feeBump, + ...(args as TransactionInvocation), }) } diff --git a/src/stellar-plus/soroban/contracts/certificate-of-deposit/types.ts b/src/stellar-plus/soroban/contracts/certificate-of-deposit/types.ts index 1d94c93..67ebcf8 100644 --- a/src/stellar-plus/soroban/contracts/certificate-of-deposit/types.ts +++ b/src/stellar-plus/soroban/contracts/certificate-of-deposit/types.ts @@ -2,8 +2,7 @@ import { ContractSpec } from '@stellar/stellar-sdk' import { Options } from 'stellar-plus/core/contract-engine/types' import { TransactionInvocation } from 'stellar-plus/core/types' -import { RpcHandler } from 'stellar-plus/rpc/types' -import { Network, i128, u64 } from 'stellar-plus/types' +import { NetworkConfig, i128, u64 } from 'stellar-plus/types' export enum methods { get_position = 'get_position', @@ -20,12 +19,13 @@ export type CertificateOfDepositContract = { } export type CertificateOfDepositContractConstructorArgs = { - network: Network - spec?: ContractSpec //optional when compared to ContractEngine - contractId?: string - rpcHandler?: RpcHandler - wasm?: Buffer - wasmHash?: string + networkConfig: NetworkConfig + contractParameters: { + spec?: ContractSpec + contractId?: string + wasm?: Buffer + wasmHash?: string + } options?: Options } diff --git a/src/stellar-plus/soroban/index.ts b/src/stellar-plus/soroban/index.ts index 63dd55a..0193bdc 100644 --- a/src/stellar-plus/soroban/index.ts +++ b/src/stellar-plus/soroban/index.ts @@ -1,20 +1,20 @@ import { SorobanRpc } from '@stellar/stellar-sdk' import { SorobanHandler } from 'stellar-plus/soroban/types' -import { Network } from 'stellar-plus/types' +import { NetworkConfig } from 'stellar-plus/types' export class SorobanHandlerClient implements SorobanHandler { - private network: Network + private networkConfig: NetworkConfig public server: SorobanRpc.Server /** * - * @param {Network} network - The network to use. + * @param {NetworkConfig} networkConfig - The network to use. * * @description - The soroban handler is used for interacting with the Soroban server. * */ - constructor(network: Network) { - this.network = network - this.server = new SorobanRpc.Server(this.network.rpcUrl) + constructor(networkConfig: NetworkConfig) { + this.networkConfig = networkConfig + this.server = new SorobanRpc.Server(this.networkConfig.rpcUrl) } } diff --git a/src/stellar-plus/test/account.test.ts b/src/stellar-plus/test/account.test.ts deleted file mode 100644 index 2535b90..0000000 --- a/src/stellar-plus/test/account.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -import Freighter from '@stellar/freighter-api' -import Stellar from '@stellar/stellar-sdk' -import axios from 'axios' - -import { FreighterAccountHandlerClient } from 'stellar-plus/account/account-handler/freighter' - -import { MockAccountResponse } from './mocks/account-response-mock' -import { MockSubmitTransaction } from './mocks/transaction-mock' -import { Constants } from '..' -import { Base, DefaultAccountHandler, FreighterAccountHandler } from '../account' - -jest.mock('@stellar/stellar-sdk') -jest.mock('@stellar/freighter-api', () => { - return { - isConnected: jest.fn().mockResolvedValue(true), - isAllowed: jest.fn().mockResolvedValue(true), - setAllowed: jest.fn().mockResolvedValue(true), - getNetworkDetails: jest.fn().mockResolvedValue({ - networkPassphrase: 'Test SDF Network ; September 2015', - }), - getPublicKey: jest.fn().mockResolvedValue('GBDMM7FQBVQPZFQPXVS3ZKK4UMELIWPBLG2BZQSWERD2KZR44WI6PTBQ'), - } -}) - -describe('Test account handler', () => { - beforeEach(() => { - const userKey = 'GBDMM7FQBVQPZFQPXVS3ZKK4UMELIWPBLG2BZQSWERD2KZR44WI6PTBQ' - const userSecret = 'SCA6IAHZA53NVVOYVUQQI3YCNVUUBNJ4WMNQHFLKI4AKPXFGCU5NCXOV' - const issuerKey = 'GD3MJLWE54WOGKAT4SOSMDCPJA6ZTHZ4TW73XFIOCVIHFIDYWDUKAYZT' - mockKeypair(userKey, userSecret) - mockServer(userKey, issuerKey) - }) - - afterEach(() => { - jest.restoreAllMocks() - }) - - function mockKeypair(publicKey: string, secret: string): void { - const mockedKeypair = { - publicKey: jest.fn().mockReturnValue(publicKey), - secret: jest.fn().mockReturnValue(secret), - } - Stellar.Keypair.fromSecret = jest.fn().mockReturnValue(mockedKeypair) - Stellar.Keypair.random = jest.fn().mockReturnValue(mockedKeypair) - } - - function mockServer(userKey: string, issuerKey: string): void { - const mockAccountResponse = new MockAccountResponse(userKey, issuerKey) - const mockLoadAccount = jest.fn().mockReturnValue(mockAccountResponse) - const mockSubmitTransaction = jest.fn().mockResolvedValue(MockSubmitTransaction) - const mockServer = jest.fn().mockImplementation(() => ({ - loadAccount: mockLoadAccount, - submitTransaction: mockSubmitTransaction, - })) - Stellar.Server = mockServer - Stellar.Horizon.Server = mockServer - } - - test('create and initialize default account with random secret key', async function () { - const network = Constants.testnet - const account = new DefaultAccountHandler({ network }) - - axios.get = jest.fn().mockResolvedValue({ data: 'Success' }) - await account.friendbot?.initialize() - - expect(account.getPublicKey()).toHaveLength(56) - expect(axios.get).toHaveBeenCalledWith(`${Constants.testnet.friendbotUrl}?addr=${account.getPublicKey()}`) - }) - - test('create and initialize base account', async function () { - const publicKey = 'CCZUQBT62C3E7NRKQKMVKMS6SY5UNLGJINOLRGXMOU35WXC6RRBSMZGM' - const account = new Base({ publicKey }) - - expect(account.getPublicKey()).toBe(publicKey) - }) - - test('create and sing base account handler', async function () { - const userSecret = 'SCA6IAHZA53NVVOYVUQQI3YCNVUUBNJ4WMNQHFLKI4AKPXFGCU5NCXOV' - const account = new DefaultAccountHandler({ network: Constants.testnet, secretKey: userSecret }) - const mockTransaction: any = { - sign: jest.fn().mockReturnValue('sign'), - toXDR: jest.fn().mockReturnValue('toXDR'), - } - const result = account.sign(mockTransaction) - - expect(result).toBe('toXDR') - expect(mockTransaction.sign).toHaveBeenCalled() - }) - - test('create and connect freighter account', async function () { - const network = Constants.testnet - const account = new FreighterAccountHandlerClient({ network }) - const loadPublicKey = jest.spyOn(account, 'loadPublicKey') - const isFreighterInstalled = jest.spyOn(account, 'isFreighterInstalled') - const isFreighterConnected = jest.spyOn(account, 'isFreighterConnected') - - await account.connect() - const publicKey = account.getPublicKey() - - expect(loadPublicKey).toHaveBeenCalledTimes(1) - expect(isFreighterInstalled).toHaveBeenCalledTimes(1) - expect(isFreighterConnected).toHaveBeenCalledTimes(1) - expect(account.getPublicKey()).toBe(publicKey) - }) - - test('create, connect and disconnect freighter account', async function () { - const network = Constants.testnet - const account = new FreighterAccountHandler({ network }) - const loadPublicKey = jest.spyOn(account, 'loadPublicKey') - const isFreighterInstalled = jest.spyOn(account, 'isFreighterInstalled') - const isFreighterConnected = jest.spyOn(account, 'isFreighterConnected') - - await account.connect() - account.disconnect() - - expect(loadPublicKey).toHaveBeenCalledTimes(1) - expect(isFreighterInstalled).toHaveBeenCalledTimes(1) - expect(isFreighterConnected).toHaveBeenCalledTimes(1) - expect(account.getPublicKey()).toBe('') - }) - - test('create and freighter not installed', async function () { - const network = Constants.testnet - const account = new FreighterAccountHandler({ network }) - const loadPublicKey = jest.spyOn(account, 'loadPublicKey') - const isFreighterInstalled = jest.spyOn(account, 'isFreighterInstalled').mockResolvedValue(false) - const isFreighterConnected = jest.spyOn(account, 'isFreighterConnected') - - await account.connect() - const connected = await account.isFreighterConnected() - - expect(loadPublicKey).toHaveBeenCalledTimes(1) - expect(isFreighterInstalled).toHaveBeenCalledTimes(2) - expect(isFreighterConnected).toHaveBeenCalledTimes(2) - expect(account.getPublicKey()).toBe('') - expect(connected).toBe(false) - }) - - test('Network is not correct', async function () { - const network = Constants.testnet - const account = new FreighterAccountHandler({ network }) - const isFreighterInstalled = jest.spyOn(account, 'isFreighterInstalled') - const isApplicationAuthorized = jest.spyOn(account, 'isApplicationAuthorized') - - jest - .spyOn(Freighter, 'getNetworkDetails') - .mockResolvedValue({ networkPassphrase: 'Error network', network: 'network', networkUrl: 'url' }) - const isConnected = await account.isFreighterConnected() - - expect(isConnected).toBeFalsy() - expect(isFreighterInstalled).toHaveBeenCalledTimes(1) - expect(isApplicationAuthorized).toHaveBeenCalledTimes(1) - }) - - test('Application is not allowed', async function () { - const account = new FreighterAccountHandler({ network: Constants.testnet }) - jest.spyOn(Freighter, 'isAllowed').mockResolvedValue(false) - - const isConnected = await account.isApplicationAuthorized() - - expect(isConnected).toBeFalsy() - }) - - test('Enforce connection', async function () { - const network = Constants.testnet - const account = new FreighterAccountHandler({ network }) - const isFreighterInstalled = jest.spyOn(account, 'isFreighterInstalled') - const isApplicationAuthorized = jest.spyOn(account, 'isApplicationAuthorized').mockResolvedValueOnce(false) - - const enforceConnection = true - const freighterCallback = jest.fn() - const isConnected = await account.isFreighterConnected(enforceConnection, freighterCallback) - - expect(isConnected).toBeFalsy() - expect(isFreighterInstalled).toHaveBeenCalledTimes(2) - expect(isApplicationAuthorized).toHaveBeenCalledTimes(1) - }) -}) diff --git a/src/stellar-plus/test/asset-classic.test.ts b/src/stellar-plus/test/asset-classic.test.ts deleted file mode 100644 index 13bfdf3..0000000 --- a/src/stellar-plus/test/asset-classic.test.ts +++ /dev/null @@ -1,225 +0,0 @@ -import Stellar from '@stellar/stellar-sdk' - -import { MockAccountResponse } from './mocks/account-response-mock' -import { MockSubmitTransaction, mockTransactionInvocation, mockTransactionSubmitter } from './mocks/transaction-mock' -import { Constants } from '..' -import { DefaultAccountHandler } from '../account' -import { ClassicAssetHandler } from '../asset' -import { AssetTypes } from '../asset/types' - -jest.mock('@stellar/stellar-sdk') - -describe('Test classic asset handler', () => { - beforeEach(() => { - initMockStellar() - }) - - function mockKeypair(publicKey: any, secret: any) { - const mockKeypair = { - publicKey: jest.fn().mockReturnValue(publicKey), - secret: jest.fn().mockReturnValue(secret), - } - Stellar.Keypair.fromSecret = jest.fn().mockReturnValue(mockKeypair) - } - - function mockServer(userKey: string, issuerKey: string) { - const mockAccountResponse = new MockAccountResponse(userKey, issuerKey) - const mockLoadAccount = jest.fn().mockReturnValue(mockAccountResponse) - const mockSubmitTransaction = jest.fn().mockResolvedValue(MockSubmitTransaction) - const mockServer = jest.fn().mockImplementation(() => ({ - loadAccount: mockLoadAccount, - submitTransaction: mockSubmitTransaction, - })) - Stellar.Server = mockServer - Stellar.Horizon.Server = mockServer - } - - function mockAsset(issuerKey: string) { - Stellar.Asset = jest.fn().mockImplementation(() => ({ - getIssuer: jest.fn().mockReturnValue(issuerKey), - })) - } - - function initMockStellar() { - const userKey = 'GBDMM7FQBVQPZFQPXVS3ZKK4UMELIWPBLG2BZQSWERD2KZR44WI6PTBQ' - const userSecret = 'SCA6IAHZA53NVVOYVUQQI3YCNVUUBNJ4WMNQHFLKI4AKPXFGCU5NCXOV' - const issuerKey = 'GD3MJLWE54WOGKAT4SOSMDCPJA6ZTHZ4TW73XFIOCVIHFIDYWDUKAYZT' - mockKeypair(userKey, userSecret) - mockServer(userKey, issuerKey) - mockAsset(issuerKey) - } - - afterEach(() => { - jest.restoreAllMocks() - }) - - test('should return the balance of classic asset', async () => { - const issuerKey = 'GD3MJLWE54WOGKAT4SOSMDCPJA6ZTHZ4TW73XFIOCVIHFIDYWDUKAYZT' - const issuerSecret = 'SANYKKTYB25CYEVLFOYNPO767X5222DKJP7D55HEVSTIGU2D4SCCNOI4' - - const mockIssuerAccount = new DefaultAccountHandler({ - network: Constants.testnet, - secretKey: issuerSecret, - }) - - const classicAssetHandler = new ClassicAssetHandler({ - code: 'CAKE', - issuerPublicKey: issuerKey, - network: Constants.testnet, - issuerAccount: mockIssuerAccount, - }) - - const assetBalance = await classicAssetHandler.balance(issuerKey) - expect(assetBalance).toEqual(3000000) - }) - - test('add Trustline and mint classic asset', async () => { - const userSecret = 'SCA6IAHZA53NVVOYVUQQI3YCNVUUBNJ4WMNQHFLKI4AKPXFGCU5NCXOV' - const issuerKey = 'GD3MJLWE54WOGKAT4SOSMDCPJA6ZTHZ4TW73XFIOCVIHFIDYWDUKAYZT' - const issuerSecret = 'SANYKKTYB25CYEVLFOYNPO767X5222DKJP7D55HEVSTIGU2D4SCCNOI4' - - const user = new DefaultAccountHandler({ - secretKey: userSecret, - network: Constants.testnet, - }) - - mockKeypair(issuerKey, issuerSecret) - const assetIssuer = new DefaultAccountHandler({ - secretKey: issuerSecret, - network: Constants.testnet, - }) - - const txInvocationConfig = { - header: { - source: user.getPublicKey(), - fee: '1000', - timeout: 30, - }, - signers: [], - } - - const cakeToken = new ClassicAssetHandler({ - code: 'CAKE', - issuerPublicKey: issuerKey, - network: Constants.testnet, - issuerAccount: assetIssuer, - transactionSubmitter: mockTransactionSubmitter(), - }) - - const processTransaction = jest.spyOn(cakeToken, 'processTransaction').mockResolvedValue(true) - - const mockTransactionInvocation = { - ...txInvocationConfig, - signers: [user], - } - - await cakeToken.addTrustlineAndMint({ to: user.getPublicKey(), amount: 100, ...mockTransactionInvocation }) - - const userBalance = await cakeToken.balance(user.getPublicKey()) - expect(processTransaction).toHaveBeenCalled() - expect(userBalance).toEqual(3000000) - }) - - test('should do the transfer of classic asset', async () => { - const issuerKey = 'GD3MJLWE54WOGKAT4SOSMDCPJA6ZTHZ4TW73XFIOCVIHFIDYWDUKAYZT' - const issuerSecret = 'SANYKKTYB25CYEVLFOYNPO767X5222DKJP7D55HEVSTIGU2D4SCCNOI4' - const userOneSecret = 'SCA6IAHZA53NVVOYVUQQI3YCNVUUBNJ4WMNQHFLKI4AKPXFGCU5NCXOV' - const userTwoKey = 'GDIIZURIQTZYEEE6URMWG7WWQJDLATOBERNS3QFEERLIPJJQEXBSKGGI' - const userTwoSecret = 'SDD4DJRVD7WC72UKTURX6FBOCRJIE2FBG3UC2ZLBLZOSP47QA5YIKU3Z' - - const userOne = new DefaultAccountHandler({ - secretKey: userOneSecret, - network: Constants.testnet, - }) - - mockKeypair(userTwoKey, userTwoSecret) - const userTwo = new DefaultAccountHandler({ - secretKey: userTwoSecret, - network: Constants.testnet, - }) - - mockKeypair(issuerKey, issuerSecret) - const assetIssuer = new DefaultAccountHandler({ - secretKey: issuerSecret, - network: Constants.testnet, - }) - - const classicAssetHandler = new ClassicAssetHandler({ - code: 'CAKE', - issuerPublicKey: issuerKey, - network: Constants.testnet, - issuerAccount: assetIssuer, - transactionSubmitter: mockTransactionSubmitter(issuerKey), - }) - const txInvocation = mockTransactionInvocation(userOne.getPublicKey()) - - const processTransaction = jest.spyOn(classicAssetHandler, 'processTransaction').mockResolvedValue(true) - await classicAssetHandler.transfer({ - from: userOne.getPublicKey(), to: userTwo.getPublicKey(), amount: 100000, ...txInvocation - }) - expect(processTransaction).toHaveBeenCalled() - }) - - test('should mint classic asset', async () => { - const userSecret = 'SCA6IAHZA53NVVOYVUQQI3YCNVUUBNJ4WMNQHFLKI4AKPXFGCU5NCXOV' - const issuerKey = 'GD3MJLWE54WOGKAT4SOSMDCPJA6ZTHZ4TW73XFIOCVIHFIDYWDUKAYZT' - const issuerSecret = 'SANYKKTYB25CYEVLFOYNPO767X5222DKJP7D55HEVSTIGU2D4SCCNOI4' - - const user = new DefaultAccountHandler({ - secretKey: userSecret, - network: Constants.testnet, - }) - - mockKeypair(issuerKey, issuerSecret) - - const assetIssuer = new DefaultAccountHandler({ - secretKey: issuerSecret, - network: Constants.testnet, - }) - - const txInvocationConfig = { - header: { - source: user.getPublicKey(), - fee: '1000', - timeout: 30, - }, - signers: [], - } - - const cakeToken = new ClassicAssetHandler({ - code: 'CAKE', - issuerPublicKey: issuerKey, - network: Constants.testnet, - issuerAccount: assetIssuer, - transactionSubmitter: mockTransactionSubmitter(), - }) - - const processTransaction = jest.spyOn(cakeToken, 'processTransaction').mockResolvedValue(true) - await cakeToken.mint({ to: user.getPublicKey(), amount: 100, ...txInvocationConfig }) - const userBalance = await cakeToken.balance(user.getPublicKey()) - - expect(userBalance).toEqual(3000000) - expect(processTransaction).toHaveBeenCalled() - }) - - test('should have the correct type, symbol and name for the asset', async () => { - const mockIssuerAccount = new DefaultAccountHandler({ - network: Constants.testnet, - secretKey: 'SC7QP27MA524VRVSBOWQ3TKWAWR27WADFMKPIT4IFSXSKAYCTCBNCECZ', - }) - - const classicAssetHandler = new ClassicAssetHandler({ - code: 'ABC', - issuerPublicKey: 'GBDMM7FQBVQPZFQPXVS3ZKK4UMELIWPBLG2BZQSWERD2KZR44WI6PTBQ', - network: Constants.testnet, - issuerAccount: mockIssuerAccount, - }) - - const symbol = await classicAssetHandler.symbol() - const name = await classicAssetHandler.name() - - expect(classicAssetHandler.type).toEqual(AssetTypes.credit_alphanum4) - expect(symbol).toEqual('ABC') - expect(name).toEqual('ABC') - }) -}) diff --git a/src/stellar-plus/test/classic-transaction-processor.test.ts b/src/stellar-plus/test/classic-transaction-processor.test.ts deleted file mode 100644 index b972eb9..0000000 --- a/src/stellar-plus/test/classic-transaction-processor.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import Stellar from '@stellar/stellar-sdk' - -import { MockAccountResponse } from './mocks/account-response-mock' -import { MockSubmitTransaction, mockTransactionInvocation, mockTransactionSubmitter } from './mocks/transaction-mock' -import { Constants } from '..' -import { TransactionProcessor } from '../core/classic-transaction-processor' - -jest.mock('@stellar/stellar-sdk') - -describe('Test classic transaction processor', () => { - beforeEach(() => { - initMockStellar() - }) - - function mockKeypair(publicKey: any, secret: any) { - const mockKeypair = { - publicKey: jest.fn().mockReturnValue(publicKey), - secret: jest.fn().mockReturnValue(secret), - } - Stellar.Keypair.fromSecret = jest.fn().mockReturnValue(mockKeypair) - } - - function mockServer(userKey: string, issuerKey: string) { - const mockAccountResponse = new MockAccountResponse(userKey, issuerKey) - const mockLoadAccount = jest.fn().mockReturnValue(mockAccountResponse) - const mockSubmitTransaction = jest.fn().mockResolvedValue(MockSubmitTransaction) - const mockServer = jest.fn().mockImplementation(() => ({ - loadAccount: mockLoadAccount, - submitTransaction: mockSubmitTransaction, - server: { - submitTransaction: mockSubmitTransaction, - }, - })) - Stellar.Server = mockServer - Stellar.Horizon.Server = mockServer - } - - function mockAsset(issuerKey: string) { - Stellar.Asset = jest.fn().mockImplementation(() => ({ - getIssuer: jest.fn().mockReturnValue(issuerKey), - })) - } - - function initMockStellar() { - const userKey = 'GBDMM7FQBVQPZFQPXVS3ZKK4UMELIWPBLG2BZQSWERD2KZR44WI6PTBQ' - const userSecret = 'SCA6IAHZA53NVVOYVUQQI3YCNVUUBNJ4WMNQHFLKI4AKPXFGCU5NCXOV' - const issuerKey = 'GD3MJLWE54WOGKAT4SOSMDCPJA6ZTHZ4TW73XFIOCVIHFIDYWDUKAYZT' - mockKeypair(userKey, userSecret) - mockServer(userKey, issuerKey) - mockAsset(issuerKey) - } - - afterEach(() => { - jest.restoreAllMocks() - }) - - test('Build custom transaction', async () => { - const network = Constants.testnet - const transactionProcessor = new TransactionProcessor({ network: network, transactionSubmitter: mockTransactionSubmitter() }) - - const transaction = await transactionProcessor.buildCustomTransaction('operations', 'txInvocation') - expect(String(transaction)).toBe(String({ builtTx: true, updatedTxInvocation: mockTransactionInvocation() })) - }) - - test('Process transaction', async () => { - const network = Constants.testnet - const transactionProcessor = new TransactionProcessor({ network: network, transactionSubmitter: mockTransactionSubmitter() }) - const processTransaction = jest - .spyOn(transactionProcessor as any, 'signEnvelope') - .mockResolvedValue('signedEnvelope') - const transaction = await transactionProcessor.processTransaction('Transaction', 'signers') - const transactionExpected = MockSubmitTransaction - expect(transaction).toStrictEqual(transactionExpected) - expect(processTransaction).toHaveBeenCalledTimes(1) - }) -}) diff --git a/src/stellar-plus/test/contracts.test.ts b/src/stellar-plus/test/contracts.test.ts deleted file mode 100644 index 7ef0441..0000000 --- a/src/stellar-plus/test/contracts.test.ts +++ /dev/null @@ -1,258 +0,0 @@ -import Stellar from '@stellar/stellar-sdk' - -import { MockAccountResponse } from './mocks/account-response-mock' -import { MockSubmitTransaction } from './mocks/transaction-mock' -import { Constants, Contracts, RPC } from '..' -import { DefaultAccountHandler } from '../account' - -jest.mock('@stellar/stellar-sdk') - -describe('Test contracts handler', () => { - beforeEach(() => { - initMockStellar() - }) - - function mockKeypair(publicKey: any, secret: any) { - const mockKeypair = { - publicKey: jest.fn().mockReturnValue(publicKey), - secret: jest.fn().mockReturnValue(secret), - } - Stellar.Keypair.fromSecret = jest.fn().mockReturnValue(mockKeypair) - } - - function mockServer(userKey: string, issuerKey: string) { - const mockAccountResponse = new MockAccountResponse(userKey, issuerKey) - const mockLoadAccount = jest.fn().mockReturnValue(mockAccountResponse) - const mockSubmitTransaction = jest.fn().mockResolvedValue(MockSubmitTransaction) - const mockServer = jest.fn().mockImplementation(() => ({ - loadAccount: mockLoadAccount, - submitTransaction: mockSubmitTransaction, - server: { - submitTransaction: mockSubmitTransaction, - }, - getTransaction: jest.fn().mockResolvedValue('Success'), - prepareTransaction: jest.fn().mockResolvedValue('Success'), - simulateTransaction: jest.fn().mockResolvedValue('Success'), - })) - Stellar.Server = mockServer - Stellar.Horizon.Server = mockServer - Stellar.SorobanRpc.Server = mockServer - } - - function mockAsset(issuerKey: string) { - Stellar.Asset = jest.fn().mockImplementation(() => ({ - getIssuer: jest.fn().mockReturnValue(issuerKey), - })) - } - - function initMockStellar() { - const userKey = 'GBDMM7FQBVQPZFQPXVS3ZKK4UMELIWPBLG2BZQSWERD2KZR44WI6PTBQ' - const userSecret = 'SCA6IAHZA53NVVOYVUQQI3YCNVUUBNJ4WMNQHFLKI4AKPXFGCU5NCXOV' - const issuerKey = 'GD3MJLWE54WOGKAT4SOSMDCPJA6ZTHZ4TW73XFIOCVIHFIDYWDUKAYZT' - mockKeypair(userKey, userSecret) - mockServer(userKey, issuerKey) - mockAsset(issuerKey) - Stellar.Address = jest.fn().mockReturnValue('address') - } - - afterEach(() => { - jest.restoreAllMocks() - }) - - test('Get transaction of default RPC', async () => { - const network = Constants.testnet - const rpcHandler = new RPC.DefaultRpcHandler(network) - - const transaction = await rpcHandler.getTransaction('HASH') - expect(transaction).toBe('Success') - }) - - test('Prepare transaction of default RPC handler', async () => { - const network = Constants.testnet - const rpcHandler = new RPC.DefaultRpcHandler(network) - const transaction = await rpcHandler.prepareTransaction('Transaction') - expect(transaction).toBe('Success') - }) - - test('Simulate transaction of default RPC handler', async () => { - const network = Constants.testnet - const rpcHandler = new RPC.DefaultRpcHandler(network) - const transaction = await rpcHandler.simulateTransaction('Transaction') - expect(transaction).toBe('Success') - }) - - test('should return the position of soroban contract', async () => { - const contractId = 'CCZUQBT62C3E7NRKQKMVKMS6SY5UNLGJINOLRGXMOU35WXC6RRBSMZGM' - const userSecret = 'SCA6IAHZA53NVVOYVUQQI3YCNVUUBNJ4WMNQHFLKI4AKPXFGCU5NCXOV' - const network = Constants.testnet - - const rpcHandler = new RPC.DefaultRpcHandler(network) - const codContract = new Contracts.CertificateOfDeposit({ - network: network, - contractId: contractId, - rpcHandler: rpcHandler, - }) - const user = new DefaultAccountHandler({ - secretKey: userSecret, - network: Constants.testnet, - }) - - const userAInvocationHeader = { - header: { - source: user.getPublicKey(), - fee: '500000', - timeout: 30, - }, - signers: [user], - } - - jest.spyOn(codContract as any, 'invokeContract').mockResolvedValue('Success') - await codContract.deposit({ - address: user.getPublicKey(), - amount: BigInt(1000000), - ...userAInvocationHeader, - }) - - jest.spyOn(codContract as any, 'readFromContract').mockResolvedValue('1000000') - const position = await codContract.getPosition({ - address: user.getPublicKey(), - ...userAInvocationHeader, - }) - - const mockAddressObject = jest.fn().mockReturnValue('address') - const mockAddress = new mockAddressObject() - expect(position).toBe(1000000) - expect((codContract as any).invokeContract).toHaveBeenCalledWith({ - method: 'deposit', - methodArgs: { amount: BigInt(1000000), address: mockAddress }, - signers: [user], - header: userAInvocationHeader.header, - feeBump: undefined, - }) - }) - - test('withdraw of soroban contract', async () => { - const contractId = 'CCZUQBT62C3E7NRKQKMVKMS6SY5UNLGJINOLRGXMOU35WXC6RRBSMZGM' - const userSecret = 'SCA6IAHZA53NVVOYVUQQI3YCNVUUBNJ4WMNQHFLKI4AKPXFGCU5NCXOV' - const network = Constants.testnet - - const rpcHandler = new RPC.DefaultRpcHandler(network) - const codContract = new Contracts.CertificateOfDeposit({ - network: network, - contractId: contractId, - rpcHandler: rpcHandler, - }) - const user = new DefaultAccountHandler({ - secretKey: userSecret, - network: Constants.testnet, - }) - - const userInvocationHeader = { - header: { - source: user.getPublicKey(), - fee: '500000', - timeout: 30, - }, - signers: [user], - } - - jest.spyOn(codContract as any, 'invokeContract').mockResolvedValue('Success') - await codContract.withdraw({ - address: user.getPublicKey(), - acceptPrematureWithdraw: true, - ...userInvocationHeader, - }) - - const mockAddressObject = jest.fn().mockReturnValue('address') - const mockAddress = new mockAddressObject() - expect((codContract as any).invokeContract).toHaveBeenCalledWith({ - method: 'withdraw', - methodArgs: { address: mockAddress, accept_premature_withdraw: true }, - signers: [user], - header: userInvocationHeader.header, - feeBump: undefined, - }) - }) - - test('Get estimated yield of soroban contract', async () => { - const contractId = 'CCZUQBT62C3E7NRKQKMVKMS6SY5UNLGJINOLRGXMOU35WXC6RRBSMZGM' - const userSecret = 'SCA6IAHZA53NVVOYVUQQI3YCNVUUBNJ4WMNQHFLKI4AKPXFGCU5NCXOV' - const network = Constants.testnet - - const rpcHandler = new RPC.DefaultRpcHandler(network) - const codContract = new Contracts.CertificateOfDeposit({ - network: network, - contractId: contractId, - rpcHandler: rpcHandler, - }) - const user = new DefaultAccountHandler({ - secretKey: userSecret, - network: Constants.testnet, - }) - - const userInvocationHeader = { - header: { - source: user.getPublicKey(), - fee: '500000', - timeout: 30, - }, - signers: [user], - } - - jest.spyOn(codContract as any, 'readFromContract').mockResolvedValue('1000') - const estimatedYield = await codContract.getEstimatedYield({ - address: user.getPublicKey(), - ...userInvocationHeader, - }) - - const mockAddressObject = jest.fn().mockReturnValue('address') - const mockAddress = new mockAddressObject() - expect((codContract as any).readFromContract).toHaveBeenCalledWith({ - method: 'get_estimated_yield', - methodArgs: { address: mockAddress }, - header: userInvocationHeader.header, - }) - expect(estimatedYield).toBe(1000) - }) - - test('Get time left of soroban contract', async () => { - const contractId = 'CCZUQBT62C3E7NRKQKMVKMS6SY5UNLGJINOLRGXMOU35WXC6RRBSMZGM' - const userSecret = 'SCA6IAHZA53NVVOYVUQQI3YCNVUUBNJ4WMNQHFLKI4AKPXFGCU5NCXOV' - const network = Constants.testnet - - const rpcHandler = new RPC.DefaultRpcHandler(network) - const codContract = new Contracts.CertificateOfDeposit({ - network: network, - contractId: contractId, - rpcHandler: rpcHandler, - }) - const user = new DefaultAccountHandler({ - secretKey: userSecret, - network: Constants.testnet, - }) - - const userInvocationHeader = { - header: { - source: user.getPublicKey(), - fee: '500000', - timeout: 30, - }, - signers: [user], - } - - jest.spyOn(codContract as any, 'readFromContract').mockResolvedValue('10011234') - const getTimeLeft = await codContract.getTimeLeft({ - address: user.getPublicKey(), - ...userInvocationHeader, - }) - - const mockAddressObject = jest.fn().mockReturnValue('address') - const mockAddress = new mockAddressObject() - expect((codContract as any).readFromContract).toHaveBeenCalledWith({ - method: 'get_time_left', - methodArgs: { address: mockAddress }, - header: userInvocationHeader.header, - }) - expect(getTimeLeft).toBe(10011234) - }) -}) diff --git a/src/stellar-plus/test/horizon.test.ts b/src/stellar-plus/test/horizon.test.ts deleted file mode 100644 index ecdb93c..0000000 --- a/src/stellar-plus/test/horizon.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import Stellar from '@stellar/stellar-sdk' - -import { MockAccountResponse } from './mocks/account-response-mock' -import { MockSubmitTransaction } from './mocks/transaction-mock' -import { Constants } from '..' -import { HorizonHandlerClient } from '../horizon' - -jest.mock('@stellar/stellar-sdk') - -describe('Horizon handler client test', () => { - beforeEach(() => { - const userKey = 'GBDMM7FQBVQPZFQPXVS3ZKK4UMELIWPBLG2BZQSWERD2KZR44WI6PTBQ' - const issuerKey = 'GD3MJLWE54WOGKAT4SOSMDCPJA6ZTHZ4TW73XFIOCVIHFIDYWDUKAYZT' - const mockAccountResponse = new MockAccountResponse(userKey, issuerKey) - const mockLoadAccount = jest.fn().mockReturnValue(mockAccountResponse) - const mockSubmitTransaction = jest.fn().mockResolvedValue(MockSubmitTransaction) - const mockServer = jest.fn().mockImplementation(() => ({ - loadAccount: mockLoadAccount, - submitTransaction: mockSubmitTransaction, - server: { - submitTransaction: mockSubmitTransaction, - }, - getTransaction: jest.fn().mockResolvedValue('Success'), - prepareTransaction: jest.fn().mockResolvedValue('Success'), - simulateTransaction: jest.fn().mockResolvedValue('Success'), - })) - Stellar.Horizon.Server = mockServer - }) - - afterEach(() => { - jest.restoreAllMocks() - }) - - test('should load an account successfully', async () => { - const accountId = 'GBDMM7FQBVQPZFQPXVS3ZKK4UMELIWPBLG2BZQSWERD2KZR44WI6PTBQ' - const issuerKey = 'GD3MJLWE54WOGKAT4SOSMDCPJA6ZTHZ4TW73XFIOCVIHFIDYWDUKAYZT' - const mockAccountResponse = new MockAccountResponse(accountId, issuerKey) - const horizonHandlerClient = new HorizonHandlerClient(Constants.testnet) - - const account = await horizonHandlerClient.loadAccount(accountId) - - expect(account).toEqual(mockAccountResponse) - expect(horizonHandlerClient.server.loadAccount).toHaveBeenCalledWith(accountId) - }) - - test('should throw an error when loading an account fails', async () => { - const errorMessage = 'Failed to load account' - const accountId = 'GBDMM7FQBVQPZFQPXVS3ZKK4UMELIWPBLG2BZQSWERD2KZR44WI6PTBQ' - const mockServer = jest.fn().mockImplementation(() => ({ - loadAccount: jest.fn().mockRejectedValue(new Error(errorMessage)), - })) - Stellar.Horizon.Server = mockServer - const horizonHandlerClient = new HorizonHandlerClient(Constants.testnet) - - expect(horizonHandlerClient.loadAccount(accountId)).rejects.toThrow('Failed to load account from Horizon server.') - }) -}) diff --git a/src/stellar-plus/test/mocks/accounts.ts b/src/stellar-plus/test/mocks/accounts.ts index 772b171..d158ccb 100644 --- a/src/stellar-plus/test/mocks/accounts.ts +++ b/src/stellar-plus/test/mocks/accounts.ts @@ -21,4 +21,7 @@ export const mockedStellarAccount: Account = { }, } -export const mockedDefaultAccountHandler = new DefaultAccountHandler({ network: NETWORK, secretKey: ACCOUNT_A_SK }) +export const mockedDefaultAccountHandler = new DefaultAccountHandler({ + networkConfig: NETWORK, + secretKey: ACCOUNT_A_SK, +}) diff --git a/src/stellar-plus/test/mocks/transaction-mock.ts b/src/stellar-plus/test/mocks/transaction-mock.ts index 7936871..c8089de 100644 --- a/src/stellar-plus/test/mocks/transaction-mock.ts +++ b/src/stellar-plus/test/mocks/transaction-mock.ts @@ -1,5 +1,6 @@ +import { FeeBumpHeader, TransactionInvocation, TransactionXdr } from 'stellar-plus/types' + import { AccountHandler } from '../../account/account-handler/types' -import { TransactionSubmitter } from '../../core/transaction-submitter/classic/types' import { EnvelopeHeader } from '../../core/types' export const MockSubmitTransaction = { @@ -25,23 +26,23 @@ export function mockHeader(sourceKey = mockAccount): EnvelopeHeader { export function mockAccountHandler(accountKey = mockAccount): AccountHandler { return { - sign(tx: any) { + sign(_tx: any): TransactionXdr { return 'success' }, - getPublicKey() { + getPublicKey(): string { return accountKey }, } } -export function mockFeeBumpHeader(signerKey = mockAccount) { +export function mockFeeBumpHeader(signerKey = mockAccount): FeeBumpHeader { return { signers: [mockAccountHandler(signerKey)], header: mockHeader(signerKey), } } -export function mockTransactionInvocation(signerKey = mockAccount) { +export function mockTransactionInvocation(signerKey = mockAccount): TransactionInvocation { return { signers: [mockAccountHandler(signerKey)], header: mockHeader(signerKey), @@ -49,23 +50,6 @@ export function mockTransactionInvocation(signerKey = mockAccount) { } } -export function mockTransactionSubmitter(signerKey?: string): TransactionSubmitter { - return { - async createEnvelope(txInvocation: any): Promise<{ - envelope: any - updatedTxInvocation: any - }> { - return { envelope: mockTransactionBuilder, updatedTxInvocation: mockTransactionInvocation(signerKey) } - }, - postProcessTransaction(response?: any): any { - return MockSubmitTransaction - }, - async submit(envelope: any): Promise { - return Promise - }, - } -} - export const mockTransactionBuilder = { build: jest.fn().mockReturnValue('success'), addOperation: jest.fn().mockReturnValue({ @@ -81,3 +65,5 @@ export const mockTransactionBuilder = { }), toXDR: jest.fn().mockReturnValue('success'), } + +// export const mockUnsignedClassicTransaction = new TransactionBuilder(new Account(mockAccount, '123'), { fee: '500' }) diff --git a/src/stellar-plus/types.ts b/src/stellar-plus/types.ts index c7e471a..0b477ef 100644 --- a/src/stellar-plus/types.ts +++ b/src/stellar-plus/types.ts @@ -1,5 +1,8 @@ -import { Address as StellarAddress } from '@stellar/stellar-sdk' -import { Transaction as _Transaction, FeeBumpTransaction as _FeeBumpTransaction } from '@stellar/stellar-sdk' +import { + Address as StellarAddress, + FeeBumpTransaction as _FeeBumpTransaction, + Transaction as _Transaction, +} from '@stellar/stellar-sdk' import { EnvelopeHeader as _EnvelopeHeader, @@ -16,7 +19,7 @@ export type FeeBumpTransaction = _FeeBumpTransaction // // Networks // -export type Network = { +export type NetworkConfig = { name: NetworksList networkPassphrase: string rpcUrl: string diff --git a/src/stellar-plus/utils/pipeline/conveyor-belts/index.ts b/src/stellar-plus/utils/pipeline/conveyor-belts/index.ts new file mode 100644 index 0000000..73e6ea6 --- /dev/null +++ b/src/stellar-plus/utils/pipeline/conveyor-belts/index.ts @@ -0,0 +1,95 @@ +import { v4 as uuidv4 } from 'uuid' + +import { StellarPlusError } from 'stellar-plus/error' + +import { BeltMetadata, BeltPluginType, ConveyorBeltType, GenericPlugin } from './types' + +export class ConveyorBelt implements ConveyorBeltType { + readonly type: BeltType + readonly id: string + + protected plugins: BeltPluginType[] + + constructor(args: { type: BeltType; plugins: BeltPluginType[] }) { + this.type = args.type + this.id = uuidv4() as string + this.plugins = args.plugins + } + + public async execute(item: Input, existingItemId?: string): Promise { + const itemId = existingItemId || (uuidv4() as string) + + const preProcessedItem = await this.preProcess(item, itemId) + let processedItem: Output + try { + processedItem = (await this.process(preProcessedItem, itemId)) as Output + } catch (e) { + const error = StellarPlusError.fromUnkownError(e) + const processedError = await this.processError(error, itemId) + throw processedError + } + const postProcessedItem = await this.postProcess(processedItem, itemId) + + return postProcessedItem as Output + } + + private async preProcess(item: Input, itemId: string): Promise { + let preProcessedItem = item as Input + + for (const plugin of this.plugins) { + if (plugin.preProcess) { + preProcessedItem = (await plugin.preProcess(preProcessedItem, this.getMeta(itemId))) as Input + } + } + + return preProcessedItem + } + + private async postProcess(item: Output, itemId: string): Promise { + let postProcessedItem = item + + for (const plugin of this.plugins) { + if (plugin.postProcess) { + postProcessedItem = (await plugin.postProcess(postProcessedItem, this.getMeta(itemId))) as Output + } + } + + return postProcessedItem + } + + private async processError(error: StellarPlusError, itemId: string): Promise { + let processedError = error + + for (const plugin of this.plugins) { + if (plugin.processError) { + processedError = (await plugin.processError(processedError, this.getMeta(itemId))) as StellarPlusError + } + } + + return processedError + } + + protected async process(_item: Input, _itemId: string): Promise { + throw new Error('process function not implemented') + } + + protected getMeta(itemId: string): BeltMetadata { + return { + itemId, + beltId: this.id, + beltType: this.type as string, + } + } + + // private async errorProcess(error: StellarPlusError, itemId: string, beltId: string): Promise { + // let processedError = error + + // for (const plugin of this.plugins) { + // if (plugin.errorProcess) { + // processedError = (await plugin.errorProcess(processedError, itemId, beltId)) as StellarPlusError + // } + // } + + // return processedError + // } +} diff --git a/src/stellar-plus/utils/pipeline/conveyor-belts/types.ts b/src/stellar-plus/utils/pipeline/conveyor-belts/types.ts new file mode 100644 index 0000000..a585835 --- /dev/null +++ b/src/stellar-plus/utils/pipeline/conveyor-belts/types.ts @@ -0,0 +1,28 @@ +import { StellarPlusError } from 'stellar-plus/error' + +export type ConveyorBeltType = { + type: BeltType + id: string + execute: (item: Input) => Promise +} + +export type BeltPluginType = { + readonly name: string + readonly type: BeltType + + preProcess?: (item: Input, meta: BeltMetadata) => Promise + postProcess?: (item: Output, meta: BeltMetadata) => Promise + processError?: (error: StellarPlusError, meta: BeltMetadata) => Promise +} + +export type BeltMetadata = { + itemId: string + beltId: string + beltType: string +} + +export enum GenericPlugin { + id = 'GenericPlugin', +} + +export type BeltProcessFunction = (item: Input) => Promise diff --git a/src/stellar-plus/utils/pipeline/index.ts b/src/stellar-plus/utils/pipeline/index.ts new file mode 100644 index 0000000..7e8a5ac --- /dev/null +++ b/src/stellar-plus/utils/pipeline/index.ts @@ -0,0 +1,7 @@ +import { ConveyorBelt } from './conveyor-belts' +import { MultiBeltPipeline } from './multi-belt-pipeline' + +export const pipelineUtils = { + ConveyorBelt: ConveyorBelt, + MultiBeltPipeline: MultiBeltPipeline, +} diff --git a/src/stellar-plus/utils/pipeline/multi-belt-pipeline/index.ts b/src/stellar-plus/utils/pipeline/multi-belt-pipeline/index.ts new file mode 100644 index 0000000..cbab896 --- /dev/null +++ b/src/stellar-plus/utils/pipeline/multi-belt-pipeline/index.ts @@ -0,0 +1,54 @@ +import { ConveyorBelt } from 'stellar-plus/utils/pipeline/conveyor-belts' +import { GenericPlugin } from 'stellar-plus/utils/pipeline/conveyor-belts/types' +import { + BeltMainPluginType, + BeltPlugin, + MultiBeltPipelineOptions, +} from 'stellar-plus/utils/pipeline/multi-belt-pipeline/types' + +import { filterPluginsByTypes } from '../plugins/helpers' + +export class MultiBeltPipeline< + Input, + Output, + BeltType extends string, + SupportedInnerPlugins extends { type: string }, +> extends ConveyorBelt { + protected innerPlugins: SupportedInnerPlugins[] + + constructor(options: MultiBeltPipelineOptions) { + const mainPlugins = filterPluginsByTypes< + BeltPlugin, + BeltType | GenericPlugin + >(options.plugins || [], [options.beltType as BeltType, GenericPlugin.id]) as + | BeltMainPluginType[] + | undefined + + super({ + type: options.beltType, + plugins: mainPlugins || [], + }) + + const innerPlugins = filterPluginsByTypes( + options.plugins || [], + [options.beltType as BeltType], + true + ) as SupportedInnerPlugins[] + + this.innerPlugins = innerPlugins || [] + } + + protected getInnerPluginsByType( + executionPlugins: SupportedInnerPlugins[], + pipelineType: PipelineType, + excludeGeneric?: boolean + ): SupportedInnerPlugins[] { + const filterTypes = excludeGeneric + ? [pipelineType as PipelineType] + : [pipelineType as PipelineType, GenericPlugin.id] + return filterPluginsByTypes( + [...(this.innerPlugins as SupportedInnerPlugins[]), ...executionPlugins], + filterTypes as PluginType[] + ) + } +} diff --git a/src/stellar-plus/utils/pipeline/multi-belt-pipeline/types.ts b/src/stellar-plus/utils/pipeline/multi-belt-pipeline/types.ts new file mode 100644 index 0000000..972e667 --- /dev/null +++ b/src/stellar-plus/utils/pipeline/multi-belt-pipeline/types.ts @@ -0,0 +1,19 @@ +import { BeltPluginType, GenericPlugin } from '../conveyor-belts/types' + +export type MultiBeltPipelineOptions = { + plugins?: BeltPlugin[] + beltType: BeltType +} + +export type BeltMainPluginType = BeltPluginType< + Input, + Output, + BeltType | GenericPlugin +> + +export type BeltPlugin = BeltMainPluginType< + Input, + Output, + BeltType +> & + SupportedInnerPlugins diff --git a/src/stellar-plus/utils/pipeline/plugins/generic/debug/index.ts b/src/stellar-plus/utils/pipeline/plugins/generic/debug/index.ts new file mode 100644 index 0000000..90e4c81 --- /dev/null +++ b/src/stellar-plus/utils/pipeline/plugins/generic/debug/index.ts @@ -0,0 +1,40 @@ +import { StellarPlusError } from 'stellar-plus/error' +import { BeltMetadata, BeltPluginType, GenericPlugin } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +export class DebugPlugin implements BeltPluginType { + readonly name: string = 'DebugPlugin' + readonly type: GenericPlugin + + private debugLevel: 'all' | 'info' | 'warn' | 'error' + + constructor(debugLevel: 'info' | 'error' | 'all' = 'error') { + this.type = GenericPlugin.id + this.debugLevel = debugLevel + } + + public async preProcess(item: Input, meta: BeltMetadata): Promise { + this.log('info', `>> Start ${meta.beltType}`) + this.log('all', ` Belt:${meta.beltId} \n Item:${meta.itemId}`) + + return item + } + + public async postProcess(item: Output, meta: BeltMetadata): Promise { + this.log('info', `<< Finish ${meta.beltType}`) + this.log('all', ` Belt:${meta.beltId} \n Item:${meta.itemId}`) + + return item + } + + public async processError(error: StellarPlusError, _meta: BeltMetadata): Promise { + this.log('error', `Error: ${error}`) + + return error + } + + private log = (level: 'all' | 'info' | 'warn' | 'error', message: string): void => { + if (this.debugLevel === 'all' || this.debugLevel === level) { + console.log(message) + } + } +} diff --git a/src/stellar-plus/utils/pipeline/plugins/generic/debug/types.ts b/src/stellar-plus/utils/pipeline/plugins/generic/debug/types.ts new file mode 100644 index 0000000..105be7d --- /dev/null +++ b/src/stellar-plus/utils/pipeline/plugins/generic/debug/types.ts @@ -0,0 +1,8 @@ +export type DebugData = { + elapsedTime: string + meta: any +} & TransactionResources + +export type TransactionResources = { + logs: string[] +} diff --git a/src/stellar-plus/utils/pipeline/plugins/generic/index.ts b/src/stellar-plus/utils/pipeline/plugins/generic/index.ts new file mode 100644 index 0000000..8a8470b --- /dev/null +++ b/src/stellar-plus/utils/pipeline/plugins/generic/index.ts @@ -0,0 +1,7 @@ +import { DebugPlugin } from './debug' +import { InjectPreprocessParameterPlugin } from './inject-preprocess-parameter' + +export const genericPlugins = { + debug: DebugPlugin, + injectPreprocessParameter: InjectPreprocessParameterPlugin, +} diff --git a/src/stellar-plus/utils/pipeline/plugins/generic/inject-preprocess-parameter/index.ts b/src/stellar-plus/utils/pipeline/plugins/generic/inject-preprocess-parameter/index.ts new file mode 100644 index 0000000..45b161e --- /dev/null +++ b/src/stellar-plus/utils/pipeline/plugins/generic/inject-preprocess-parameter/index.ts @@ -0,0 +1,38 @@ +import { StellarPlusError } from 'stellar-plus/error' +import { BeltMetadata, BeltPluginType } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +export class InjectPreprocessParameterPlugin + implements BeltPluginType +{ + readonly name: string = 'InjectPreprocessParameterPlugin' + readonly type: Type + + private parameter: ParameterType + private step: 'preProcess' | 'postProcess' | 'processError' + + constructor(parameter: ParameterType, type: Type, step: 'preProcess' | 'postProcess' | 'processError') { + this.type = type + this.parameter = parameter + this.step = step + } + + public async preProcess(item: Input, _meta: BeltMetadata): Promise { + return this.step === 'preProcess' ? this.inject(item) : item + } + + public async postProcess(item: Output, _meta: BeltMetadata): Promise { + return this.step === 'postProcess' ? this.inject(item) : item + } + + public async processError(error: StellarPlusError, _meta: BeltMetadata): Promise { + return this.step === 'processError' ? this.inject(error) : error + } + + private inject(item: ItemType): ItemType { + const updatedItem = { + ...item, + ...this.parameter, + } + return updatedItem + } +} diff --git a/src/stellar-plus/utils/pipeline/plugins/helpers.ts b/src/stellar-plus/utils/pipeline/plugins/helpers.ts new file mode 100644 index 0000000..3e5e764 --- /dev/null +++ b/src/stellar-plus/utils/pipeline/plugins/helpers.ts @@ -0,0 +1,26 @@ +import { GenericPlugin } from '../conveyor-belts/types' + +export const filterPluginsByType = ( + plugins: PluginType[], + typeFilter: BeltType +): PluginType[] => { + return plugins.filter((plugin) => plugin.type === typeFilter) +} + +export const filterPluginsByTypes = ( + plugins: PluginType[], + typesFilter: BeltType[], + invertResult: boolean = false //When true, reuturns only plugins that do not match the typesFilter +): PluginType[] => { + if (invertResult) { + return plugins.filter((plugin) => !typesFilter.includes(plugin.type as BeltType)) + } + return plugins.filter((plugin) => typesFilter.includes(plugin.type as BeltType)) +} + +export const filterPluginsByName = ( + plugins: PluginType[], + nameFilter: string +): PluginType[] => { + return plugins.filter((plugin) => plugin.name === nameFilter) +} diff --git a/src/stellar-plus/utils/pipeline/plugins/index.ts b/src/stellar-plus/utils/pipeline/plugins/index.ts new file mode 100644 index 0000000..878981c --- /dev/null +++ b/src/stellar-plus/utils/pipeline/plugins/index.ts @@ -0,0 +1,17 @@ +import { genericPlugins } from './generic' +import { filterPluginsByName, filterPluginsByType, filterPluginsByTypes } from './helpers' +import { simulateTransactionPlugins } from './simulate-transaction' +import { sorobanGetTransactionPlugins } from './soroban-get-transaction' +import { sorobanTransactionPlugins } from './soroban-transaction' +import { submitTransactionPlugins } from './submit-transaction' + +export const plugins = { + generic: genericPlugins, + simulateTransaction: simulateTransactionPlugins, + sorobanGetTransaction: sorobanGetTransactionPlugins, + sorobanTransaction: sorobanTransactionPlugins, + submitTransaction: submitTransactionPlugins, + filterPluginsByType: filterPluginsByType, + filterPluginsByTypes: filterPluginsByTypes, + filterPluginsByName: filterPluginsByName, +} diff --git a/src/stellar-plus/utils/pipeline/plugins/simulate-transaction/auto-restore/index.ts b/src/stellar-plus/utils/pipeline/plugins/simulate-transaction/auto-restore/index.ts new file mode 100644 index 0000000..3564967 --- /dev/null +++ b/src/stellar-plus/utils/pipeline/plugins/simulate-transaction/auto-restore/index.ts @@ -0,0 +1,163 @@ +import { + Account, + Operation, + SorobanDataBuilder, + SorobanRpc, + Transaction, + TransactionBuilder, +} from '@stellar/stellar-sdk' + +import { + BuildTransactionPipelineInput, + BuildTransactionPipelineOutput, + BuildTransactionPipelineType, +} from 'stellar-plus/core/pipelines/build-transaction/types' +import { + SimulateTransactionPipelineInput, + SimulateTransactionPipelineOutput, + SimulateTransactionPipelineType, +} from 'stellar-plus/core/pipelines/simulate-transaction/types' +import { SorobanTransactionPipeline } from 'stellar-plus/core/pipelines/soroban-transaction' +import { + SorobanTransactionPipelineInput, + SorobanTransactionPipelineOptions, + SorobanTransactionPipelineOutput, + SorobanTransactionPipelinePlugin, +} from 'stellar-plus/core/pipelines/soroban-transaction/types' +import { FeeBumpHeader } from 'stellar-plus/core/types' +import { DefaultRpcHandler } from 'stellar-plus/rpc' +import { RpcHandler } from 'stellar-plus/rpc/types' +import { NetworkConfig, TransactionInvocation } from 'stellar-plus/types' +import { BeltMetadata, BeltPluginType } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +import { InjectPreprocessParameterPlugin } from '../../generic/inject-preprocess-parameter' +import { FeeBumpWrapperPlugin } from '../../submit-transaction/fee-bump' + +export class AutoRestorePlugin + implements + BeltPluginType< + SimulateTransactionPipelineInput, + SimulateTransactionPipelineOutput, + SimulateTransactionPipelineType + > +{ + readonly type = SimulateTransactionPipelineType.id + readonly name: string = 'AutoRestorePlugin' + + private restoreTxInvocation: TransactionInvocation + private networkConfig: NetworkConfig + private rpcHandler: RpcHandler + private sorobanTransactionPipelinePlugins: SorobanTransactionPipelinePlugin[] = [] + + constructor(restoreTxInvocation: TransactionInvocation, networkConfig: NetworkConfig, customRpcHandler?: RpcHandler) { + this.restoreTxInvocation = restoreTxInvocation + this.networkConfig = networkConfig + this.rpcHandler = customRpcHandler ? customRpcHandler : new DefaultRpcHandler(this.networkConfig) + + if (restoreTxInvocation.feeBump) { + this.sorobanTransactionPipelinePlugins.push( + new FeeBumpWrapperPlugin(restoreTxInvocation.feeBump as FeeBumpHeader) + ) + } + } + + public async postProcess( + item: SimulateTransactionPipelineOutput, + _meta: BeltMetadata + ): Promise { + const { response, assembledTransaction }: SimulateTransactionPipelineOutput = item + + if (SorobanRpc.Api.isSimulationRestore(response)) { + await this.restore((response as SorobanRpc.Api.SimulateTransactionRestoreResponse).restorePreamble) + if (assembledTransaction.source === this.restoreTxInvocation.header.source) { + const updatedTransaction = this.bumpSequence( + assembledTransaction, + response as SorobanRpc.Api.SimulateTransactionRestoreResponse + ) + + return { + ...item, + assembledTransaction: updatedTransaction, + } as SimulateTransactionPipelineOutput + } + } + + return item + } + + private async restore(restorePreamble: { + minResourceFee: string + transactionData: SorobanDataBuilder + }): Promise { + const operation = Operation.restoreFootprint({}) + + const sorobanTransactionData = restorePreamble.transactionData.build() + const injectionParameter = { sorobanData: sorobanTransactionData } + + const executionPluginToInjectSorobanData = new InjectPreprocessParameterPlugin< + BuildTransactionPipelineInput, + BuildTransactionPipelineOutput, + BuildTransactionPipelineType, + typeof injectionParameter + >(injectionParameter, BuildTransactionPipelineType.id, 'preProcess') + + const sorobanTransactionPipeline = new SorobanTransactionPipeline(this.networkConfig, { + rpcHandler: this.rpcHandler, + plugins: this.sorobanTransactionPipelinePlugins, + } as SorobanTransactionPipelineOptions) + + return await sorobanTransactionPipeline.execute({ + txInvocation: this.restoreTxInvocation, + + operations: [operation], + networkConfig: this.networkConfig, + options: { + executionPlugins: [executionPluginToInjectSorobanData], + }, + } as SorobanTransactionPipelineInput) + } + + private bumpSequence( + transaction: Transaction, + simulationResponse: SorobanRpc.Api.SimulateTransactionRestoreResponse + ): Transaction { + let updatedTimeBounds: { minTime: string; maxTime: string } | undefined + + if (transaction.timeBounds) { + const additionalTimeout = this.restoreTxInvocation.header.timeout ? this.restoreTxInvocation.header.timeout : 30 + + updatedTimeBounds = { + minTime: transaction.timeBounds?.minTime, + maxTime: (Number(transaction.timeBounds?.maxTime) + additionalTimeout).toString(), + } + } + + // The sequence number extracted from the existing envelope is already bumped by 1 + // so building the new envelope will automatically bump it once more. + const sourceAccount = new Account(transaction.source, transaction.sequence) + + const updatedTransaction = new TransactionBuilder(sourceAccount, { + fee: transaction.fee, + memo: transaction.memo, + networkPassphrase: transaction.networkPassphrase, + timebounds: updatedTimeBounds, + ledgerbounds: transaction.ledgerBounds, + minAccountSequence: transaction.minAccountSequence, + minAccountSequenceAge: transaction.minAccountSequenceAge, + minAccountSequenceLedgerGap: transaction.minAccountSequenceLedgerGap, + extraSigners: transaction.extraSigners, + sorobanData: simulationResponse.transactionData.build(), + }) + + transaction + .toEnvelope() + .v1() + .tx() + .operations() + .forEach((op) => { + updatedTransaction.addOperation(op) + }) + + return updatedTransaction.build() + } +} diff --git a/src/stellar-plus/utils/pipeline/plugins/simulate-transaction/extract-invocation-output/index.ts b/src/stellar-plus/utils/pipeline/plugins/simulate-transaction/extract-invocation-output/index.ts new file mode 100644 index 0000000..e8f23eb --- /dev/null +++ b/src/stellar-plus/utils/pipeline/plugins/simulate-transaction/extract-invocation-output/index.ts @@ -0,0 +1,53 @@ +import { ContractSpec } from '@stellar/stellar-sdk' + +import { + SimulateTransactionPipelineInput, + SimulateTransactionPipelineOutput, + SimulateTransactionPipelineType, + SimulatedInvocationOutput, +} from 'stellar-plus/core/pipelines/simulate-transaction/types' +import { BeltMetadata, BeltPluginType } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +export class ExtractInvocationOutputFromSimulationPlugin + implements + BeltPluginType< + SimulateTransactionPipelineInput, + SimulateTransactionPipelineOutput, + SimulateTransactionPipelineType + > +{ + readonly type = SimulateTransactionPipelineType.id + readonly name: string = 'ExtractContractIdPlugin' + private spec: ContractSpec + private method: string + + constructor(spec: ContractSpec, method: string) { + this.spec = spec + this.method = method + } + + public async postProcess( + item: SimulateTransactionPipelineOutput, + _meta: BeltMetadata + ): Promise { + const { response, output } = item + + if (!response.result) { + // throw CEError.simulationMissingResult(simulated) + throw new Error('simulationMissingResult') + } + + const value = this.spec.funcResToNative(this.method, response.result.retval) as unknown + + const pluginOutput = { value } as SimulatedInvocationOutput + + const updatedItem = { + ...item, + output: { + ...output, + ...pluginOutput, + }, + } + return updatedItem + } +} diff --git a/src/stellar-plus/utils/pipeline/plugins/simulate-transaction/extract-transaction-resources/index.ts b/src/stellar-plus/utils/pipeline/plugins/simulate-transaction/extract-transaction-resources/index.ts new file mode 100644 index 0000000..2e10afa --- /dev/null +++ b/src/stellar-plus/utils/pipeline/plugins/simulate-transaction/extract-transaction-resources/index.ts @@ -0,0 +1,73 @@ +import { SorobanRpc, xdr } from '@stellar/stellar-sdk' + +import { TransactionResources } from 'stellar-plus/core/contract-engine/types' +import { + SimulateTransactionPipelineInput, + SimulateTransactionPipelineOutput, + SimulateTransactionPipelineType, +} from 'stellar-plus/core/pipelines/simulate-transaction/types' +import { BeltMetadata, BeltPluginType } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +export class ExtractTransactionResourcesPlugin + implements + BeltPluginType< + SimulateTransactionPipelineInput, + SimulateTransactionPipelineOutput, + SimulateTransactionPipelineType + > +{ + readonly type = SimulateTransactionPipelineType.id + readonly name: string = 'ExtractTransactionResourcesPlugin' + private callback?: (args: TransactionResources, itemId: string) => void + + constructor(callback?: (args: TransactionResources, itemId: string) => void) { + this.callback = callback + } + + public async postProcess( + item: SimulateTransactionPipelineOutput, + meta: BeltMetadata + ): Promise { + const simulatedTransaction = item.response as SorobanRpc.Api.SimulateTransactionSuccessResponse + + const calculateEventSize = (event: xdr.DiagnosticEvent): number => { + if (event.event()?.type().name === 'diagnostic') { + return 0 + } + return event.toXDR().length + } + + const sorobanTransactionData = simulatedTransaction.transactionData.build() + const events = simulatedTransaction.events?.map((event) => calculateEventSize(event)) + const returnValueSize = simulatedTransaction.result?.retval.toXDR().length + const transactionDataSize = sorobanTransactionData.toXDR().length + const eventsSize = events?.reduce((accumulator, currentValue) => accumulator + currentValue, 0) + + const resources: TransactionResources = { + cpuInstructions: Number(sorobanTransactionData?.resources().instructions()), + ram: Number(simulatedTransaction.cost?.memBytes), + minResourceFee: Number(simulatedTransaction.minResourceFee), + ledgerReadBytes: sorobanTransactionData?.resources().readBytes(), + ledgerWriteBytes: sorobanTransactionData?.resources().writeBytes(), + ledgerEntryReads: sorobanTransactionData?.resources().footprint().readOnly().length, + ledgerEntryWrites: sorobanTransactionData?.resources().footprint().readWrite().length, + eventSize: eventsSize, + returnValueSize: returnValueSize, + transactionSize: transactionDataSize, + } + + const updatedItem = { + ...item, + output: { + ...item.output, + resources, + }, + } + + if (this.callback) { + this.callback(resources, meta.itemId) + } + + return updatedItem + } +} diff --git a/src/stellar-plus/utils/pipeline/plugins/simulate-transaction/index.ts b/src/stellar-plus/utils/pipeline/plugins/simulate-transaction/index.ts new file mode 100644 index 0000000..7b59151 --- /dev/null +++ b/src/stellar-plus/utils/pipeline/plugins/simulate-transaction/index.ts @@ -0,0 +1,9 @@ +import { AutoRestorePlugin } from './auto-restore' +import { ExtractTransactionResourcesPlugin } from './extract-transaction-resources' +import { ExtractInvocationOutputPlugin } from '../soroban-get-transaction/extract-invocation-output' + +export const simulateTransactionPlugins = { + autoRestore: AutoRestorePlugin, + extractInvocationOutput: ExtractInvocationOutputPlugin, + extractTransactionResources: ExtractTransactionResourcesPlugin, +} diff --git a/src/stellar-plus/utils/pipeline/plugins/soroban-get-transaction/extract-contract-id/index.ts b/src/stellar-plus/utils/pipeline/plugins/soroban-get-transaction/extract-contract-id/index.ts new file mode 100644 index 0000000..2e71215 --- /dev/null +++ b/src/stellar-plus/utils/pipeline/plugins/soroban-get-transaction/extract-contract-id/index.ts @@ -0,0 +1,44 @@ +import { Address, xdr } from '@stellar/stellar-sdk' + +import { + ContractIdOutput, + SorobanGetTransactionPipelineInput, + SorobanGetTransactionPipelineOutput, + SorobanGetTransactionPipelineType, +} from 'stellar-plus/core/pipelines/soroban-get-transaction/types' +import { BeltMetadata, BeltPluginType } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +export class ExtractContractIdPlugin + implements + BeltPluginType< + SorobanGetTransactionPipelineInput, + SorobanGetTransactionPipelineOutput, + SorobanGetTransactionPipelineType + > +{ + readonly type = SorobanGetTransactionPipelineType.id + readonly name: string = 'ExtractContractIdPlugin' + + public async postProcess( + item: SorobanGetTransactionPipelineOutput, + _meta: BeltMetadata + ): Promise { + const { response, output } = item + + const contractId = Address.fromScAddress( + response.resultMetaXdr.v3().sorobanMeta()?.returnValue().address() as xdr.ScAddress + ).toString() + + const pluginOutput: ContractIdOutput = { + contractId, + } + + return { + ...item, + output: { + ...output, + ...pluginOutput, + }, + } + } +} diff --git a/src/stellar-plus/utils/pipeline/plugins/soroban-get-transaction/extract-fee-charged/index.ts b/src/stellar-plus/utils/pipeline/plugins/soroban-get-transaction/extract-fee-charged/index.ts new file mode 100644 index 0000000..dbd689d --- /dev/null +++ b/src/stellar-plus/utils/pipeline/plugins/soroban-get-transaction/extract-fee-charged/index.ts @@ -0,0 +1,50 @@ +import { + FeeChargedOutput, + SorobanGetTransactionPipelineInput, + SorobanGetTransactionPipelineOutput, + SorobanGetTransactionPipelineType, +} from 'stellar-plus/core/pipelines/soroban-get-transaction/types' +import { BeltMetadata, BeltPluginType } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +export class ExtractFeeChargedPlugin + implements + BeltPluginType< + SorobanGetTransactionPipelineInput, + SorobanGetTransactionPipelineOutput, + SorobanGetTransactionPipelineType + > +{ + readonly type = SorobanGetTransactionPipelineType.id + readonly name: string = 'ExtractFeeChargedPlugin' + + private callback?: (args: FeeChargedOutput, itemId: string) => void + + constructor(callback?: (args: FeeChargedOutput, itemId: string) => void) { + this.callback = callback + } + + public async postProcess( + item: SorobanGetTransactionPipelineOutput, + _meta: BeltMetadata + ): Promise { + const { response, output } = item + + const feeCharged = response.resultXdr.feeCharged().toString() + + const pluginOutput: FeeChargedOutput = { + feeCharged, + } + + if (this.callback) { + this.callback(pluginOutput, _meta.itemId) + } + + return { + ...item, + output: { + ...output, + ...pluginOutput, + }, + } + } +} diff --git a/src/stellar-plus/utils/pipeline/plugins/soroban-get-transaction/extract-invocation-output/index.ts b/src/stellar-plus/utils/pipeline/plugins/soroban-get-transaction/extract-invocation-output/index.ts new file mode 100644 index 0000000..35f051a --- /dev/null +++ b/src/stellar-plus/utils/pipeline/plugins/soroban-get-transaction/extract-invocation-output/index.ts @@ -0,0 +1,52 @@ +import { ContractSpec } from '@stellar/stellar-sdk' + +import { + ContractInvocationOutput, + SorobanGetTransactionPipelineInput, + SorobanGetTransactionPipelineOutput, + SorobanGetTransactionPipelineType, +} from 'stellar-plus/core/pipelines/soroban-get-transaction/types' +import { BeltMetadata, BeltPluginType } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +export class ExtractInvocationOutputPlugin + implements + BeltPluginType< + SorobanGetTransactionPipelineInput, + SorobanGetTransactionPipelineOutput, + SorobanGetTransactionPipelineType + > +{ + readonly type = SorobanGetTransactionPipelineType.id + readonly name: string = 'ExtractInvocationOutputPlugin' + private spec: ContractSpec + private method: string + + constructor(spec: ContractSpec, method: string) { + this.spec = spec + this.method = method + } + + public async postProcess( + item: SorobanGetTransactionPipelineOutput, + _meta: BeltMetadata + ): Promise { + const { response, output }: SorobanGetTransactionPipelineOutput = item as SorobanGetTransactionPipelineOutput + + const value = this.spec.funcResToNative( + this.method, + response.resultMetaXdr.v3().sorobanMeta()?.returnValue().toXDR('base64') as string + ) as unknown + + const pluginOutput: ContractInvocationOutput = { + value: String(value as OutputType), + } + + return { + ...item, + output: { + ...output, + ...pluginOutput, + }, + } + } +} diff --git a/src/stellar-plus/utils/pipeline/plugins/soroban-get-transaction/extract-wasm-hash/index.ts b/src/stellar-plus/utils/pipeline/plugins/soroban-get-transaction/extract-wasm-hash/index.ts new file mode 100644 index 0000000..88d0848 --- /dev/null +++ b/src/stellar-plus/utils/pipeline/plugins/soroban-get-transaction/extract-wasm-hash/index.ts @@ -0,0 +1,42 @@ +import { + ContractWasmHashOutput, + SorobanGetTransactionPipelineInput, + SorobanGetTransactionPipelineOutput, + SorobanGetTransactionPipelineType, +} from 'stellar-plus/core/pipelines/soroban-get-transaction/types' +import { BeltMetadata, BeltPluginType } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +export class ExtractWasmHashPlugin + implements + BeltPluginType< + SorobanGetTransactionPipelineInput, + SorobanGetTransactionPipelineOutput, + SorobanGetTransactionPipelineType + > +{ + readonly type = SorobanGetTransactionPipelineType.id + readonly name: string = 'ExtractWasmHashPlugin' + + public async postProcess( + item: SorobanGetTransactionPipelineOutput, + _meta: BeltMetadata + ): Promise { + const { response, output } = item + + const wasmHash = (response.resultMetaXdr.v3().sorobanMeta()?.returnValue().value() as Buffer).toString( + 'hex' + ) as string + + const pluginOutput: ContractWasmHashOutput = { + wasmHash, + } + + return { + ...item, + output: { + ...output, + ...pluginOutput, + }, + } + } +} diff --git a/src/stellar-plus/utils/pipeline/plugins/soroban-get-transaction/index.ts b/src/stellar-plus/utils/pipeline/plugins/soroban-get-transaction/index.ts new file mode 100644 index 0000000..4827d8d --- /dev/null +++ b/src/stellar-plus/utils/pipeline/plugins/soroban-get-transaction/index.ts @@ -0,0 +1,11 @@ +import { ExtractContractIdPlugin } from './extract-contract-id' +import { ExtractFeeChargedPlugin } from './extract-fee-charged' +import { ExtractInvocationOutputPlugin } from './extract-invocation-output' +import { ExtractWasmHashPlugin } from './extract-wasm-hash' + +export const sorobanGetTransactionPlugins = { + extractContractId: ExtractContractIdPlugin, + extractFeeCharged: ExtractFeeChargedPlugin, + extractInvocationOutput: ExtractInvocationOutputPlugin, + extractWasmHash: ExtractWasmHashPlugin, +} diff --git a/src/stellar-plus/utils/pipeline/plugins/soroban-transaction/channel-accounts/index.ts b/src/stellar-plus/utils/pipeline/plugins/soroban-transaction/channel-accounts/index.ts new file mode 100644 index 0000000..30fddde --- /dev/null +++ b/src/stellar-plus/utils/pipeline/plugins/soroban-transaction/channel-accounts/index.ts @@ -0,0 +1,168 @@ +import { AccountHandler } from 'stellar-plus/account' +import { + ClassicTransactionPipelineInput, + ClassicTransactionPipelineOutput, + ClassicTransactionPipelineType, +} from 'stellar-plus/core/pipelines/classic-transaction/types' +import { + SorobanTransactionPipelineInput, + SorobanTransactionPipelineOutput, + SorobanTransactionPipelineType, +} from 'stellar-plus/core/pipelines/soroban-transaction/types' +import { StellarPlusError } from 'stellar-plus/error' +import { FeeBumpHeader } from 'stellar-plus/types' +import { BeltMetadata, BeltPluginType } from 'stellar-plus/utils/pipeline/conveyor-belts/types' +import { + ChannelAccountsPluginConstructorArgs, + InputType, +} from 'stellar-plus/utils/pipeline/plugins/soroban-transaction/channel-accounts/types' + +class ChannelAccountsPlugin implements BeltPluginType { + readonly type + readonly name = 'ChannelAccountsPlugin' + private freeChannels: AccountHandler[] + + private lockedChannels: { channel: AccountHandler; id: string }[] // Channels are allocated to items, and released when the item is processed. + private feeBump?: FeeBumpHeader + + constructor(typeId: Type, channels?: AccountHandler[]) { + this.type = typeId + // this.feeBump = feeBump + // this.horizonHandler = new HorizonHandlerClient(network) + this.freeChannels = [] + this.lockedChannels = [] + if (channels) { + this.registerChannels(channels) + } + } + + //=========================================== + // belt process modifiers + //=========================================== + + public async preProcess(item: Input, meta: BeltMetadata): Promise { + const allocatedChannel = await this.allocateChannel(meta.itemId) + const updatedItem = this.injectChannel(item, allocatedChannel) + + return updatedItem + } + + public async postProcess(item: Output, meta: BeltMetadata): Promise { + this.releaseChannel(meta.itemId) + return item + } + + public async processError(error: StellarPlusError, meta: BeltMetadata): Promise { + this.releaseChannel(meta.itemId) + return error + } + + //=========================================== + // public methods + //=========================================== + + /** + * + * @description - Returns the list of channels registered to the pool. + * + * @returns {AccountHandler[]} The list of channels registered to the pool. + */ + public getChannels(): AccountHandler[] { + const lockedChannels = this.lockedChannels.map((c) => c.channel) + + return [...this.freeChannels, ...lockedChannels] + } + + /** + * + * @param {AccountHandler[]} channels - The channel accounts to register. + * + * @description - Registers the provided channel accounts to the pool. + * + * @see ChannelAccountsHandler for a helper class for managing channel accounts. + */ + public registerChannels(channels: AccountHandler[]): void { + this.freeChannels = [...this.freeChannels, ...channels] + } + + //=========================================== + // Internal methods + //=========================================== + + private async allocateChannel(id: string): Promise { + if (this.freeChannels.length === 0) { + return await this.noChannelPipeline(id) + } else { + const channel = this.freeChannels.pop() as AccountHandler + + this.lockedChannels.push({ + id, + channel, + }) + + return channel + } + } + + private releaseChannel(id: string): void { + const channel = this.lockedChannels.find((c) => c.id === id)?.channel + if (!channel) { + throw new Error(`locked channel not found for item ${id}`) + } + + this.lockedChannels = this.lockedChannels.filter((c) => c.id !== id) + this.freeChannels.push(channel) + } + + /** + * + * @description - Waits for a channel to be available and then allocates it. This step will wait for 1 second before trying again. + * This function can be overriden to implement a custom waiting logic. + * + * @returns {Promise} The allocated channel account. + */ + private noChannelPipeline(id: string): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve(this.allocateChannel(id)) + }, 1000) + }) + } + + private injectChannel(item: Input, channel: AccountHandler): Input { + const { header } = item.txInvocation + + const updatedTxInvocation = { + ...item.txInvocation, + ...{ header: { ...header, source: channel.getPublicKey() } }, + signers: [...item.txInvocation.signers, channel], + feeBump: this.feeBump && !item.txInvocation.feeBump ? (this.feeBump as FeeBumpHeader) : item.txInvocation.feeBump, + } + + const updatedItem = { + ...item, + ...{ txInvocation: updatedTxInvocation }, + } + return updatedItem + } +} + +export class ClassicChannelAccountsPlugin extends ChannelAccountsPlugin< + ClassicTransactionPipelineInput, + ClassicTransactionPipelineOutput, + ClassicTransactionPipelineType +> { + constructor(args: ChannelAccountsPluginConstructorArgs) { + super(ClassicTransactionPipelineType.id, args.channels) + } +} + +export class SorobanChannelAccountsPlugin extends ChannelAccountsPlugin< + SorobanTransactionPipelineInput, + SorobanTransactionPipelineOutput, + SorobanTransactionPipelineType +> { + constructor(args: ChannelAccountsPluginConstructorArgs) { + super(SorobanTransactionPipelineType.id, args.channels) + } +} diff --git a/src/stellar-plus/utils/pipeline/plugins/soroban-transaction/channel-accounts/types.ts b/src/stellar-plus/utils/pipeline/plugins/soroban-transaction/channel-accounts/types.ts new file mode 100644 index 0000000..e4a52ad --- /dev/null +++ b/src/stellar-plus/utils/pipeline/plugins/soroban-transaction/channel-accounts/types.ts @@ -0,0 +1,12 @@ +import { AccountHandler } from 'stellar-plus/account' +import { FeeBumpHeader } from 'stellar-plus/core/types' +import { TransactionInvocation } from 'stellar-plus/types' + +export type InputType = { + txInvocation: TransactionInvocation +} + +export type ChannelAccountsPluginConstructorArgs = { + channels?: AccountHandler[] + feeBump?: FeeBumpHeader +} diff --git a/src/stellar-plus/utils/pipeline/plugins/soroban-transaction/index.ts b/src/stellar-plus/utils/pipeline/plugins/soroban-transaction/index.ts new file mode 100644 index 0000000..4081053 --- /dev/null +++ b/src/stellar-plus/utils/pipeline/plugins/soroban-transaction/index.ts @@ -0,0 +1,7 @@ +import { SorobanChannelAccountsPlugin } from './channel-accounts' +import { ProfilerPlugin } from './profiler' + +export const sorobanTransactionPlugins = { + channelAccounts: SorobanChannelAccountsPlugin, + profiler: ProfilerPlugin, +} diff --git a/src/stellar-plus/utils/pipeline/plugins/soroban-transaction/profiler/index.ts b/src/stellar-plus/utils/pipeline/plugins/soroban-transaction/profiler/index.ts new file mode 100644 index 0000000..5564ec8 --- /dev/null +++ b/src/stellar-plus/utils/pipeline/plugins/soroban-transaction/profiler/index.ts @@ -0,0 +1,178 @@ +import { xdr } from '@stellar/stellar-sdk' + +import { TransactionResources } from 'stellar-plus/core/contract-engine/types' +import { FeeChargedOutput } from 'stellar-plus/core/pipelines/soroban-get-transaction/types' +import { + SorobanTransactionPipelineInput, + SorobanTransactionPipelineOutput, + SorobanTransactionPipelineType, +} from 'stellar-plus/core/pipelines/soroban-transaction/types' +import { StellarPlusError } from 'stellar-plus/error' +import { BeltMetadata, BeltPluginType } from 'stellar-plus/utils/pipeline/conveyor-belts/types' +import { ExtractTransactionResourcesPlugin } from 'stellar-plus/utils/pipeline/plugins/simulate-transaction/extract-transaction-resources' +import { ExtractFeeChargedPlugin } from 'stellar-plus/utils/pipeline/plugins/soroban-get-transaction/extract-fee-charged' +import { InnerPlugins, LogEntry } from 'stellar-plus/utils/pipeline/plugins/soroban-transaction/profiler/types' +import { ProfilingHandler } from 'stellar-plus/utils/profiler/profiling-handler/index' + +export class ProfilerPlugin + implements + BeltPluginType +{ + readonly type = SorobanTransactionPipelineType.id + readonly name = 'ProfilerPlugin' + + private timers: { [key: string]: { start: number; end: number } } = {} + private logs: { [key: string]: LogEntry } = {} + public data: ProfilingHandler + public plugins: InnerPlugins[] = [] + + private costHandler: ( + methodName: string, + costs: TransactionResources, + elapsedTime: number, + feeCharged: number + ) => void + + constructor() { + this.data = new ProfilingHandler() + + this.costHandler = this.data.resourceHandler as ( + methodName: string, + costs: TransactionResources, + elapsedTime: number, + feeCharged: number + ) => void + + this.plugins.push(new ExtractTransactionResourcesPlugin(this.extractResources)) + this.plugins.push(new ExtractFeeChargedPlugin(this.extractFeeCharged)) + } + + public async preProcess( + item: SorobanTransactionPipelineInput, + meta: BeltMetadata + ): Promise { + this.startTimer(meta.itemId) + this.createLogEntry(item, meta) + + return this.injectPlugins(item) + } + + public async postProcess( + item: SorobanTransactionPipelineOutput, + meta: BeltMetadata + ): Promise { + this.stopTimer(meta.itemId) + this.setStatus(meta.itemId, 'success') + this.log(meta) + + return item + } + + public async processError(error: StellarPlusError, meta: BeltMetadata): Promise { + this.stopTimer(meta.itemId) + this.setStatus(meta.itemId, 'error') + this.log(meta) + + return error + } + + private startTimer(id: string): void { + this.timers[id] = { + start: Date.now(), + end: 0, + } + } + + private stopTimer(id: string): void { + this.verifyLogEntry(id) + this.timers[id] = { + start: this.timers[id].start, + end: Date.now(), + } + } + + private setStatus(id: string, status: 'running' | 'success' | 'error'): void { + this.verifyLogEntry(id) + this.logs[id].status = status + } + + //todo: move error to StellarPlusError + private verifyLogEntry(logId: string): void { + if (!this.logs[logId]) { + throw new Error(`log entry not found for item ${logId}`) + } + } + + private createLogEntry(item: SorobanTransactionPipelineInput, meta: BeltMetadata): void { + const methodName = this.getMethodNameFromOperation(item.operations[0]) + + const logEntry: LogEntry = { + methodName, + status: 'running', + feeCharged: 0, + elapsedTime: '', + resources: {} as TransactionResources, + } + + this.logs[meta.itemId] = logEntry + } + + private log(meta: BeltMetadata): void { + const logId = meta.itemId + + this.costHandler( + this.logs[logId].methodName, + this.logs[logId].resources, + this.timers[logId].end - this.timers[logId].start, + this.logs[logId].feeCharged + ) + } + + private extractResources = (resources: TransactionResources, itemId: string): void => { + this.verifyLogEntry(itemId) + + this.logs[itemId].resources = { ...resources } + } + + private extractFeeCharged = (output: FeeChargedOutput, itemId: string): void => { + this.verifyLogEntry(itemId) + + this.logs[itemId].feeCharged = Number(output.feeCharged) + } + + private injectPlugins = (item: SorobanTransactionPipelineInput): SorobanTransactionPipelineInput => { + const updatedItem = { + ...item, + options: item.options ? { ...item.options } : {}, + } + updatedItem.options.executionPlugins = updatedItem.options.executionPlugins + ? [...updatedItem.options.executionPlugins, ...this.plugins] + : [...this.plugins] + + return updatedItem + } + + private getMethodNameFromOperation = (operation: xdr.Operation): string => { + if (operation.body().switch() === xdr.OperationType.invokeHostFunction()) { + const hostFunction = operation.body().invokeHostFunctionOp() + + if (hostFunction.hostFunction().switch() === xdr.HostFunctionType.hostFunctionTypeInvokeContract()) { + const invokeContractHostFunction = hostFunction.hostFunction().invokeContract() + + return invokeContractHostFunction.functionName() as string + } + + if (hostFunction.hostFunction().switch() === xdr.HostFunctionType.hostFunctionTypeUploadContractWasm()) { + return 'uploadContractWasm' + } + + if (hostFunction.hostFunction().switch() === xdr.HostFunctionType.hostFunctionTypeCreateContract()) { + return 'createContract' + } + + return hostFunction.hostFunction().switch().name + } + + return operation.body().switch().name + } +} diff --git a/src/stellar-plus/utils/pipeline/plugins/soroban-transaction/profiler/types.ts b/src/stellar-plus/utils/pipeline/plugins/soroban-transaction/profiler/types.ts new file mode 100644 index 0000000..2afc7ba --- /dev/null +++ b/src/stellar-plus/utils/pipeline/plugins/soroban-transaction/profiler/types.ts @@ -0,0 +1,14 @@ +import { TransactionResources } from 'stellar-plus/core/contract-engine/types' +import { SimulateTransactionPipelinePlugin } from 'stellar-plus/core/pipelines/simulate-transaction/types' +import { SorobanGetTransactionPipelinePlugin } from 'stellar-plus/core/pipelines/soroban-get-transaction/types' + +export type LogEntry = { + methodName: string + status: 'success' | 'error' | 'running' + resources: TransactionResources + feeCharged: number + elapsedTime?: string +} + +// to be merged with all accepted types +export type InnerPlugins = SimulateTransactionPipelinePlugin | SorobanGetTransactionPipelinePlugin diff --git a/src/stellar-plus/utils/pipeline/plugins/submit-transaction/fee-bump/index.ts b/src/stellar-plus/utils/pipeline/plugins/submit-transaction/fee-bump/index.ts new file mode 100644 index 0000000..7d7df6b --- /dev/null +++ b/src/stellar-plus/utils/pipeline/plugins/submit-transaction/fee-bump/index.ts @@ -0,0 +1,65 @@ +import { FeeBumpTransaction, Transaction } from '@stellar/stellar-sdk' + +import { ClassicSignRequirementsPipeline } from 'stellar-plus/core/pipelines/classic-sign-requirements' +import { FeeBumpPipeline } from 'stellar-plus/core/pipelines/fee-bump' +import { SignTransactionPipeline } from 'stellar-plus/core/pipelines/sign-transaction' +import { + SubmitTransactionPipelineInput, + SubmitTransactionPipelineOutput, + SubmitTransactionPipelineType, +} from 'stellar-plus/core/pipelines/submit-transaction/types' +import { FeeBumpHeader } from 'stellar-plus/types' +import { BeltMetadata, BeltPluginType } from 'stellar-plus/utils/pipeline/conveyor-belts/types' + +export class FeeBumpWrapperPlugin + implements + BeltPluginType +{ + readonly type = SubmitTransactionPipelineType.id + readonly name: string = 'FeeBumpWrapperPlugin' + + private feeBumpHeader: FeeBumpHeader + + constructor(feeBumpHeader: FeeBumpHeader) { + this.feeBumpHeader = feeBumpHeader + } + + public async preProcess( + item: SubmitTransactionPipelineInput, + meta: BeltMetadata + ): Promise { + const { itemId } = meta + const { transaction }: SubmitTransactionPipelineInput = item + + if ((transaction as FeeBumpTransaction).innerTransaction) { + throw new Error('Transaction is already a FeeBump, FeeBumpWrapperPlugin should not be used') + } + + const feeBumpPipeline = new FeeBumpPipeline() + const feeBumpEnvelope = await feeBumpPipeline.execute( + { + innerTransaction: transaction as Transaction, + feeBumpHeader: this.feeBumpHeader, + }, + itemId + ) + + const classicSignRequirementsPipeline = new ClassicSignRequirementsPipeline() + const classicSignRequirements = await classicSignRequirementsPipeline.execute(feeBumpEnvelope, itemId) + const signTransactionPipeline = new SignTransactionPipeline() + const signedTransaction = await signTransactionPipeline.execute( + { + transaction: feeBumpEnvelope, + signatureRequirements: classicSignRequirements, + signers: this.feeBumpHeader.signers, + }, + itemId + ) + + const updatedItem: SubmitTransactionPipelineInput = { + ...item, + transaction: signedTransaction, + } + return updatedItem + } +} diff --git a/src/stellar-plus/utils/pipeline/plugins/submit-transaction/index.ts b/src/stellar-plus/utils/pipeline/plugins/submit-transaction/index.ts new file mode 100644 index 0000000..c406372 --- /dev/null +++ b/src/stellar-plus/utils/pipeline/plugins/submit-transaction/index.ts @@ -0,0 +1,5 @@ +import { FeeBumpWrapperPlugin } from './fee-bump' + +export const submitTransactionPlugins = { + feeBump: FeeBumpWrapperPlugin, +} diff --git a/src/stellar-plus/utils/profiler/soroban/index.ts b/src/stellar-plus/utils/profiler/profiling-handler/index.ts similarity index 96% rename from src/stellar-plus/utils/profiler/soroban/index.ts rename to src/stellar-plus/utils/profiler/profiling-handler/index.ts index cca46ff..db98918 100644 --- a/src/stellar-plus/utils/profiler/soroban/index.ts +++ b/src/stellar-plus/utils/profiler/profiling-handler/index.ts @@ -1,4 +1,4 @@ -import { Options, TransactionResources } from 'stellar-plus/core/contract-engine/types' +import { TransactionResources } from 'stellar-plus/core/contract-engine/types' import { AggregateType, AggregationMethod, @@ -7,12 +7,12 @@ import { GetLogOptions, LogEntry, ResourcesList, -} from 'stellar-plus/utils/profiler/soroban/types' +} from 'stellar-plus/utils/profiler/profiling-handler/types' -export class Profiler { +export class ProfilingHandler { private log: LogEntry[] = [] - private costHandler = ( + public resourceHandler = ( methodName: string, costs: TransactionResources, elapsedTime: number, @@ -27,12 +27,12 @@ export class Profiler { this.log.push(entry) } - public getOptionsArgs = (): Options => { - return { - debug: true, - costHandler: this.costHandler, - } - } + // public getOptionsArgs = (): Options => { + // return { + + // costHandler: this.costHandler, + // } + // } public getLog = (options?: GetLogOptions): LogEntry[] | string => { const filteredLog = options?.filter ? this.filterLog(this.log, options.filter) : this.log diff --git a/src/stellar-plus/utils/profiler/soroban/soroban-profiler.test.ts b/src/stellar-plus/utils/profiler/profiling-handler/soroban-profiler.test.ts similarity index 92% rename from src/stellar-plus/utils/profiler/soroban/soroban-profiler.test.ts rename to src/stellar-plus/utils/profiler/profiling-handler/soroban-profiler.test.ts index 48ab0df..806e0cf 100644 --- a/src/stellar-plus/utils/profiler/soroban/soroban-profiler.test.ts +++ b/src/stellar-plus/utils/profiler/profiling-handler/soroban-profiler.test.ts @@ -1,6 +1,6 @@ import { TransactionResources } from 'stellar-plus/core/contract-engine/types' -import { Profiler } from '.' +import { ProfilingHandler } from '.' const mockLogs = [ { @@ -95,32 +95,19 @@ const populateLogEntries = ( } describe('Profiler', () => { - let profiler: Profiler + let profiler: ProfilingHandler beforeEach(() => { - profiler = new Profiler() + profiler = new ProfilingHandler() - const args = profiler.getOptionsArgs() - populateLogEntries(args.costHandler!) + const logFunction = profiler.resourceHandler + populateLogEntries(logFunction) }) it('should add a log entry when costHandler is invoked', () => { expect(profiler.getLog()).toHaveLength(4) }) - it('should return correct options arguments', () => { - const options = profiler.getOptionsArgs() - expect(options).toEqual({ - debug: true, - - costHandler: expect.any(Function) as ( - methodName: string, - costs: TransactionResources, - elapsedTime: number - ) => void, - }) - }) - it('should return the entire log without options', () => { const log = profiler.getLog() expect(log).toEqual(mockLogs) diff --git a/src/stellar-plus/utils/profiler/soroban/types.ts b/src/stellar-plus/utils/profiler/profiling-handler/types.ts similarity index 100% rename from src/stellar-plus/utils/profiler/soroban/types.ts rename to src/stellar-plus/utils/profiler/profiling-handler/types.ts