From e018b1bb46564c63760a19875789328886b55677 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Tue, 23 Sep 2025 08:54:12 -0400 Subject: [PATCH 1/4] spike: tree shakeable algorand client --- .../algorand-client-transaction-sender.ts | 57 +++-- src/types/algorand-client.ts | 238 ++++++------------ src/types/composer.ts | 21 +- 3 files changed, 120 insertions(+), 196 deletions(-) diff --git a/src/types/algorand-client-transaction-sender.ts b/src/types/algorand-client-transaction-sender.ts index 8f163bf1..0516c378 100644 --- a/src/types/algorand-client-transaction-sender.ts +++ b/src/types/algorand-client-transaction-sender.ts @@ -1,10 +1,8 @@ -import algosdk, { Address } from 'algosdk' +import algosdk, { Address, Algodv2, ProgramSourceMap } from 'algosdk' import { Buffer } from 'buffer' import { Config } from '../config' import { asJson, defaultJsonValueReplacer } from '../util' -import { SendAppCreateTransactionResult, SendAppTransactionResult, SendAppUpdateTransactionResult } from './app' -import { AppManager } from './app-manager' -import { AssetManager } from './asset-manager' +import { ABIReturn, CompiledTeal, SendAppCreateTransactionResult, SendAppTransactionResult, SendAppUpdateTransactionResult } from './app' import { AppCallMethodCall, AppCallParams, @@ -20,6 +18,7 @@ import { } from './composer' import { SendParams, SendSingleTransactionResult } from './transaction' import Transaction = algosdk.Transaction +import { getABIReturnValue } from '../transaction' const getMethodCallForLog = ({ method, args }: { method: algosdk.ABIMethod; args?: unknown[] }) => { return `${method.name}(${(args ?? []).map((a) => @@ -32,11 +31,27 @@ const getMethodCallForLog = ({ method, args }: { method: algosdk.ABIMethod; args )})` } +function getABIReturn( + confirmation: algosdk.modelsv2.PendingTransactionResponse | undefined, + method: algosdk.ABIMethod | undefined, +): ABIReturn | undefined { + if (!method || !confirmation || method.returns.type === 'void') { + return undefined + } + + // The parseMethodResponse method mutates the second parameter :( + const resultDummy: algosdk.ABIResult = { + txID: '', + method, + rawReturnValue: new Uint8Array(), + } + return getABIReturnValue(algosdk.AtomicTransactionComposer.parseMethodResponse(method, resultDummy, confirmation), method.returns.type) +} + /** Orchestrates sending transactions for `AlgorandClient`. */ export class AlgorandClientTransactionSender { private _newGroup: () => TransactionComposer - private _assetManager: AssetManager - private _appManager: AppManager + private _algod: algosdk.Algodv2 /** * Creates a new `AlgorandClientSender` @@ -48,10 +63,9 @@ export class AlgorandClientTransactionSender { * const transactionSender = new AlgorandClientTransactionSender(() => new TransactionComposer(), assetManager, appManager) * ``` */ - constructor(newGroup: () => TransactionComposer, assetManager: AssetManager, appManager: AppManager) { + constructor(newGroup: () => TransactionComposer, algod: Algodv2) { this._newGroup = newGroup - this._assetManager = assetManager - this._appManager = appManager + this._algod = algod } /** @@ -120,7 +134,19 @@ export class AlgorandClientTransactionSender { return async (params) => { const result = await this._send(c, log)(params) - return { ...result, return: AppManager.getABIReturn(result.confirmation, 'method' in params ? params.method : undefined) } + return { ...result, return: getABIReturn(result.confirmation, 'method' in params ? params.method : undefined) } + } + } + + private async compileTeal(teal: string): Promise { + const resp = await this._algod.compile(teal).do() + const bytes = new Uint8Array(Buffer.from(resp.result, 'base64')) + return { + compiledHash: resp.hash, + teal, + compiled: resp.result, + compiledBase64ToBytes: bytes, + sourceMap: new ProgramSourceMap(JSON.parse(algosdk.encodeJSON(resp.sourcemap!))), } } @@ -134,10 +160,8 @@ export class AlgorandClientTransactionSender { return async (params) => { const result = await this._sendAppCall(c, log)(params) - const compiledApproval = - typeof params.approvalProgram === 'string' ? this._appManager.getCompilationResult(params.approvalProgram) : undefined - const compiledClear = - typeof params.clearStateProgram === 'string' ? this._appManager.getCompilationResult(params.clearStateProgram) : undefined + const compiledApproval = typeof params.approvalProgram === 'string' ? await this.compileTeal(params.approvalProgram) : undefined + const compiledClear = typeof params.clearStateProgram === 'string' ? await this.compileTeal(params.clearStateProgram) : undefined return { ...result, compiledApproval, compiledClear } } @@ -524,8 +548,7 @@ export class AlgorandClientTransactionSender { if (params.ensureZeroBalance) { let balance = 0n try { - const accountAssetInfo = await this._assetManager.getAccountInformation(params.sender, params.assetId) - balance = accountAssetInfo.balance + balance = (await this._algod.accountAssetInformation(params.sender, params.assetId).do()).assetHolding?.amount ?? 0n } catch { throw new Error(`Account ${params.sender} is not opted-in to Asset ${params.assetId}; can't opt-out.`) } @@ -534,7 +557,7 @@ export class AlgorandClientTransactionSender { } } - params.creator = params.creator ?? (await this._assetManager.getById(params.assetId)).creator + params.creator = (await this._algod.getAssetByID(params.assetId).do()).params.creator return await this._send((c) => c.addAssetOptOut, { preLog: (params, transaction) => diff --git a/src/types/algorand-client.ts b/src/types/algorand-client.ts index 24bd16f2..af0a8381 100644 --- a/src/types/algorand-client.ts +++ b/src/types/algorand-client.ts @@ -1,26 +1,57 @@ -import algosdk, { Address } from 'algosdk' -import { MultisigAccount, SigningAccount, TransactionSignerAccount } from './account' -import { AccountManager } from './account-manager' +import algosdk, { Address, Algodv2 } from 'algosdk' +import type { AccountManager } from './account-manager' import { AlgorandClientTransactionCreator } from './algorand-client-transaction-creator' import { AlgorandClientTransactionSender } from './algorand-client-transaction-sender' -import { AppDeployer } from './app-deployer' -import { AppManager } from './app-manager' -import { AssetManager } from './asset-manager' -import { AlgoSdkClients, ClientManager } from './client-manager' +import type { AppDeployer } from './app-deployer' +import type { AppManager } from './app-manager' +import type { AssetManager } from './asset-manager' +import type { AlgoSdkClients, ClientManager } from './client-manager' import { ErrorTransformer, TransactionComposer } from './composer' -import { AlgoConfig } from './network-client' -import Account = algosdk.Account -import LogicSigAccount = algosdk.LogicSigAccount +import { InterfaceOf } from './instance-of' + +type AlgorandClientConfig = Partial & { + clientManager?: ClientManager + accountManager?: AccountManager + appManager?: AppManager + assetManager?: AssetManager + appDeployer?: AppDeployer +} + +class ErrorEverywhere { + constructor() { + throw new Error('All methods throw an error, including the constructor.') + } + static _throw() { + throw new Error('This method always throws an error.') + } +} + +// Proxy to handle any method call +const ErrorEverywhereProxy = new Proxy(ErrorEverywhere, { + construct() { + throw new Error('Cannot instantiate: all methods throw an error.') + }, + get(target, prop) { + if (typeof prop === 'string') { + return () => { + throw new Error(`Method "${prop}" always throws an error.`) + } + } + + // @ts-expect-error any + return target[prop] + }, +}) /** * A client that brokers easy access to Algorand functionality. */ export class AlgorandClient { - private _clientManager: ClientManager - private _accountManager: AccountManager - private _appManager: AppManager - private _appDeployer: AppDeployer - private _assetManager: AssetManager + private _clientManager: Partial> + private _accountManager: Partial> + private _appManager: Partial> + private _appDeployer: Partial> + private _assetManager: Partial> private _transactionSender: AlgorandClientTransactionSender private _transactionCreator: AlgorandClientTransactionCreator @@ -30,6 +61,8 @@ export class AlgorandClient { private _defaultValidityWindow: bigint | undefined = undefined + private _algod: Algodv2 + /** * A set of error transformers to use when an error is caught in simulate or execute * `registerErrorTransformer` and `unregisterErrorTransformer` can be used to add and remove @@ -37,14 +70,21 @@ export class AlgorandClient { */ private _errorTransformers: Set = new Set() - private constructor(config: AlgoConfig | AlgoSdkClients) { - this._clientManager = new ClientManager(config, this) - this._accountManager = new AccountManager(this._clientManager) - this._appManager = new AppManager(this._clientManager.algod) - this._assetManager = new AssetManager(this._clientManager.algod, () => this.newGroup()) - this._transactionSender = new AlgorandClientTransactionSender(() => this.newGroup(), this._assetManager, this._appManager) + private constructor(config: AlgorandClientConfig) { + const algod = config.algod ?? config.clientManager?.algod + + if (algod === undefined) { + throw new Error('An algod client must be provided in the config or clientManager') + } + + this._algod = algod + this._clientManager = config.clientManager ?? {} + this._accountManager = config.accountManager ?? {} + this._appManager = config.appManager ?? {} + this._assetManager = config.assetManager ?? {} + this._transactionSender = new AlgorandClientTransactionSender(() => this.newGroup(), this._algod) this._transactionCreator = new AlgorandClientTransactionCreator(() => this.newGroup()) - this._appDeployer = new AppDeployer(this._appManager, this._transactionSender, this._clientManager.indexerIfPresent) + this._appDeployer = config.appDeployer ?? (ErrorEverywhereProxy as unknown as AppDeployer) } /** @@ -61,57 +101,6 @@ export class AlgorandClient { return this } - /** - * Sets the default signer to use if no other signer is specified. - * @param signer The signer to use, either a `TransactionSigner` or a `TransactionSignerAccount` - * @returns The `AlgorandClient` so method calls can be chained - * @example - * ```typescript - * const signer = new SigningAccount(account, account.addr) - * const algorand = AlgorandClient.mainNet().setDefaultSigner(signer) - * ``` - */ - public setDefaultSigner(signer: algosdk.TransactionSigner | TransactionSignerAccount): AlgorandClient { - this._accountManager.setDefaultSigner(signer) - return this - } - - /** - * Tracks the given account (object that encapsulates an address and a signer) for later signing. - * @param account The account to register, which can be a `TransactionSignerAccount` or - * a `algosdk.Account`, `algosdk.LogicSigAccount`, `SigningAccount` or `MultisigAccount` - * @example - * ```typescript - * const accountManager = AlgorandClient.mainNet() - * .setSignerFromAccount(algosdk.generateAccount()) - * .setSignerFromAccount(new algosdk.LogicSigAccount(program, args)) - * .setSignerFromAccount(new SigningAccount(account, sender)) - * .setSignerFromAccount(new MultisigAccount({version: 1, threshold: 1, addrs: ["ADDRESS1...", "ADDRESS2..."]}, [account1, account2])) - * .setSignerFromAccount({addr: "SENDERADDRESS", signer: transactionSigner}) - * ``` - * @returns The `AlgorandClient` so method calls can be chained - */ - public setSignerFromAccount(account: TransactionSignerAccount | Account | LogicSigAccount | SigningAccount | MultisigAccount) { - this._accountManager.setSignerFromAccount(account) - return this - } - - /** - * Tracks the given signer against the given sender for later signing. - * @param sender The sender address to use this signer for - * @param signer The signer to sign transactions with for the given sender - * @returns The `AlgorandClient` so method calls can be chained - * @example - * ```typescript - * const signer = new SigningAccount(account, account.addr) - * const algorand = AlgorandClient.mainNet().setSigner(signer.addr, signer.signer) - * ``` - */ - public setSigner(sender: string | Address, signer: algosdk.TransactionSigner) { - this._accountManager.setSigner(sender, signer) - return this - } - /** * Sets a cache value to use for suggested transaction params. * @param suggestedParams The suggested params to use @@ -155,7 +144,7 @@ export class AlgorandClient { } } - this._cachedSuggestedParams = await this._clientManager.algod.getTransactionParams().do() + this._cachedSuggestedParams = await this._algod.getTransactionParams().do() this._cachedSuggestedParamsExpiry = new Date(new Date().getTime() + this._cachedSuggestedParamsTimeout) return { @@ -170,7 +159,7 @@ export class AlgorandClient { * const clientManager = AlgorandClient.mainNet().client; */ public get client() { - return this._clientManager + return this._clientManager ?? { algod: this._algod } } /** @@ -232,12 +221,16 @@ export class AlgorandClient { * const result = await composer.addTransaction(payment).send() */ public newGroup() { + const errorGetSigner = (addr: string | Address) => { + throw new Error(`No signer available for address ${addr}`) + } + const getSigner = this.account.getSigner ?? errorGetSigner + return new TransactionComposer({ - algod: this.client.algod, - getSigner: (addr: string | Address) => this.account.getSigner(addr), + algod: this._algod, + getSigner, getSuggestedParams: () => this.getSuggestedParams(), defaultValidityWindow: this._defaultValidityWindow, - appManager: this._appManager, errorTransformers: [...this._errorTransformers], }) } @@ -269,93 +262,4 @@ export class AlgorandClient { public get createTransaction() { return this._transactionCreator } - - // Static methods to create an `AlgorandClient` - - /** - * Creates an `AlgorandClient` pointing at default LocalNet ports and API token. - * @returns An instance of the `AlgorandClient`. - * @example - * const algorand = AlgorandClient.defaultLocalNet(); - */ - public static defaultLocalNet() { - return new AlgorandClient({ - algodConfig: ClientManager.getDefaultLocalNetConfig('algod'), - indexerConfig: ClientManager.getDefaultLocalNetConfig('indexer'), - kmdConfig: ClientManager.getDefaultLocalNetConfig('kmd'), - }) - } - - /** - * Creates an `AlgorandClient` pointing at TestNet using AlgoNode. - * @returns An instance of the `AlgorandClient`. - * @example - * const algorand = AlgorandClient.testNet(); - */ - public static testNet() { - return new AlgorandClient({ - algodConfig: ClientManager.getAlgoNodeConfig('testnet', 'algod'), - indexerConfig: ClientManager.getAlgoNodeConfig('testnet', 'indexer'), - kmdConfig: undefined, - }) - } - - /** - * Creates an `AlgorandClient` pointing at MainNet using AlgoNode. - * @returns An instance of the `AlgorandClient`. - * @example - * const algorand = AlgorandClient.mainNet(); - */ - public static mainNet() { - return new AlgorandClient({ - algodConfig: ClientManager.getAlgoNodeConfig('mainnet', 'algod'), - indexerConfig: ClientManager.getAlgoNodeConfig('mainnet', 'indexer'), - kmdConfig: undefined, - }) - } - - /** - * Creates an `AlgorandClient` pointing to the given client(s). - * @param clients The clients to use. - * @returns An instance of the `AlgorandClient`. - * @example - * const algorand = AlgorandClient.fromClients({ algod, indexer, kmd }); - */ - public static fromClients(clients: AlgoSdkClients) { - return new AlgorandClient(clients) - } - - /** - * Creates an `AlgorandClient` loading the configuration from environment variables. - * - * Retrieve configurations from environment variables when defined or get default LocalNet configuration if they aren't defined. - * - * Expects to be called from a Node.js environment. - * - * If `process.env.ALGOD_SERVER` is defined it will use that along with optional `process.env.ALGOD_PORT` and `process.env.ALGOD_TOKEN`. - * - * If `process.env.INDEXER_SERVER` is defined it will use that along with optional `process.env.INDEXER_PORT` and `process.env.INDEXER_TOKEN`. - * - * If either aren't defined it will use the default LocalNet config. - * - * It will return a KMD configuration that uses `process.env.KMD_PORT` (or port 4002) if `process.env.ALGOD_SERVER` is defined, - * otherwise it will use the default LocalNet config unless it detects testnet or mainnet. - * @returns An instance of the `AlgorandClient`. - * @example - * const client = AlgorandClient.fromEnvironment(); - */ - public static fromEnvironment() { - return new AlgorandClient(ClientManager.getConfigFromEnvironmentOrLocalNet()) - } - - /** - * Creates an `AlgorandClient` from the given config. - * @param config The config to use. - * @returns An instance of the `AlgorandClient`. - * @example - * const client = AlgorandClient.fromConfig({ algodConfig, indexerConfig, kmdConfig }); - */ - public static fromConfig(config: AlgoConfig) { - return new AlgorandClient(config) - } } diff --git a/src/types/composer.ts b/src/types/composer.ts index 0a71fa86..5113736b 100644 --- a/src/types/composer.ts +++ b/src/types/composer.ts @@ -507,11 +507,6 @@ export type TransactionComposerParams = { * then will be 10 rounds (or 1000 rounds if issuing transactions to LocalNet). */ defaultValidityWindow?: bigint - /** An existing `AppManager` to use to manage app compilation and cache compilation results. - * - * If not specified then an ephemeral one will be created. - */ - appManager?: AppManager /** * An array of error transformers to use when an error is caught in simulate or execute * callbacks can later be registered with `registerErrorTransformer` @@ -574,8 +569,6 @@ export class TransactionComposer { /** Whether the validity window was explicitly set on construction */ private defaultValidityWindowIsExplicit = false - private appManager: AppManager - private errorTransformers: ErrorTransformer[] private async transformError(originalError: unknown): Promise { @@ -600,6 +593,11 @@ export class TransactionComposer { return transformedError } + private async compileTeal(teal: string): Promise { + const compileResponse = await this.algod.compile(teal).do() + return new Uint8Array(Buffer.from(compileResponse.result, 'base64')) + } + /** * Create a `TransactionComposer`. * @param params The configuration for this composer @@ -612,7 +610,6 @@ export class TransactionComposer { this.getSigner = params.getSigner this.defaultValidityWindow = params.defaultValidityWindow ?? this.defaultValidityWindow this.defaultValidityWindowIsExplicit = params.defaultValidityWindow !== undefined - this.appManager = params.appManager ?? new AppManager(params.algod) this.errorTransformers = params.errorTransformers ?? [] } @@ -1600,13 +1597,13 @@ export class TransactionComposer { const approvalProgram = 'approvalProgram' in params ? typeof params.approvalProgram === 'string' - ? (await this.appManager.compileTeal(params.approvalProgram)).compiledBase64ToBytes + ? await this.compileTeal(params.approvalProgram) : params.approvalProgram : undefined const clearStateProgram = 'clearStateProgram' in params ? typeof params.clearStateProgram === 'string' - ? (await this.appManager.compileTeal(params.clearStateProgram)).compiledBase64ToBytes + ? await this.compileTeal(params.clearStateProgram) : params.clearStateProgram : undefined @@ -1757,13 +1754,13 @@ export class TransactionComposer { const approvalProgram = 'approvalProgram' in params ? typeof params.approvalProgram === 'string' - ? (await this.appManager.compileTeal(params.approvalProgram)).compiledBase64ToBytes + ? await this.compileTeal(params.approvalProgram) : params.approvalProgram : undefined const clearStateProgram = 'clearStateProgram' in params ? typeof params.clearStateProgram === 'string' - ? (await this.appManager.compileTeal(params.clearStateProgram)).compiledBase64ToBytes + ? await this.compileTeal(params.clearStateProgram) : params.clearStateProgram : undefined From faf59f5cd6b34f4a833b6f52620b32d8dba1eee3 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Tue, 23 Sep 2025 08:57:44 -0400 Subject: [PATCH 2/4] spike: use InterfaceOf in config --- src/types/algorand-client.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/types/algorand-client.ts b/src/types/algorand-client.ts index af0a8381..83394c36 100644 --- a/src/types/algorand-client.ts +++ b/src/types/algorand-client.ts @@ -10,11 +10,11 @@ import { ErrorTransformer, TransactionComposer } from './composer' import { InterfaceOf } from './instance-of' type AlgorandClientConfig = Partial & { - clientManager?: ClientManager - accountManager?: AccountManager - appManager?: AppManager - assetManager?: AssetManager - appDeployer?: AppDeployer + clientManager?: InterfaceOf + accountManager?: InterfaceOf + appManager?: InterfaceOf + assetManager?: InterfaceOf + appDeployer?: InterfaceOf } class ErrorEverywhere { From 4f9cefeaa462b8b85aac351e068f455e1428edb1 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Tue, 23 Sep 2025 09:00:11 -0400 Subject: [PATCH 3/4] spike: use partial in config --- src/types/algorand-client.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/types/algorand-client.ts b/src/types/algorand-client.ts index 83394c36..a4c03aff 100644 --- a/src/types/algorand-client.ts +++ b/src/types/algorand-client.ts @@ -10,11 +10,11 @@ import { ErrorTransformer, TransactionComposer } from './composer' import { InterfaceOf } from './instance-of' type AlgorandClientConfig = Partial & { - clientManager?: InterfaceOf - accountManager?: InterfaceOf - appManager?: InterfaceOf - assetManager?: InterfaceOf - appDeployer?: InterfaceOf + clientManager?: Partial> + accountManager?: Partial> + appManager?: Partial> + assetManager?: Partial> + appDeployer?: Partial> } class ErrorEverywhere { From f6bb0c202c8bfc97d7e664b0ff639900ef02ea2f Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Tue, 23 Sep 2025 09:01:36 -0400 Subject: [PATCH 4/4] spike: rm unused proxy --- src/types/algorand-client.ts | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/src/types/algorand-client.ts b/src/types/algorand-client.ts index a4c03aff..6e6c7e52 100644 --- a/src/types/algorand-client.ts +++ b/src/types/algorand-client.ts @@ -17,32 +17,6 @@ type AlgorandClientConfig = Partial & { appDeployer?: Partial> } -class ErrorEverywhere { - constructor() { - throw new Error('All methods throw an error, including the constructor.') - } - static _throw() { - throw new Error('This method always throws an error.') - } -} - -// Proxy to handle any method call -const ErrorEverywhereProxy = new Proxy(ErrorEverywhere, { - construct() { - throw new Error('Cannot instantiate: all methods throw an error.') - }, - get(target, prop) { - if (typeof prop === 'string') { - return () => { - throw new Error(`Method "${prop}" always throws an error.`) - } - } - - // @ts-expect-error any - return target[prop] - }, -}) - /** * A client that brokers easy access to Algorand functionality. */ @@ -84,7 +58,7 @@ export class AlgorandClient { this._assetManager = config.assetManager ?? {} this._transactionSender = new AlgorandClientTransactionSender(() => this.newGroup(), this._algod) this._transactionCreator = new AlgorandClientTransactionCreator(() => this.newGroup()) - this._appDeployer = config.appDeployer ?? (ErrorEverywhereProxy as unknown as AppDeployer) + this._appDeployer = config.appDeployer ?? {} } /**