Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 40 additions & 17 deletions src/types/algorand-client-transaction-sender.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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) =>
Expand All @@ -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`
Expand All @@ -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
Comment on lines -53 to -54
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're able to completely remove the dependency on the asset and app manager

this._algod = algod
}

/**
Expand Down Expand Up @@ -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<CompiledTeal> {
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!))),
}
}

Expand All @@ -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 }
}
Expand Down Expand Up @@ -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.`)
}
Expand All @@ -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) =>
Expand Down
212 changes: 45 additions & 167 deletions src/types/algorand-client.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
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'
Comment on lines +5 to +8
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove remove the dependency on all of the managers from the AlgorandClient

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<AlgoSdkClients> & {
clientManager?: Partial<InterfaceOf<ClientManager>>
accountManager?: Partial<InterfaceOf<AccountManager>>
appManager?: Partial<InterfaceOf<AppManager>>
assetManager?: Partial<InterfaceOf<AssetManager>>
appDeployer?: Partial<InterfaceOf<AppDeployer>>
Comment on lines +13 to +17
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partial interfaces of the managers are passed in to AlgorandClient ctor

}

/**
* 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<InterfaceOf<ClientManager>>
private _accountManager: Partial<InterfaceOf<AccountManager>>
private _appManager: Partial<InterfaceOf<AppManager>>
private _appDeployer: Partial<InterfaceOf<AppDeployer>>
private _assetManager: Partial<InterfaceOf<AssetManager>>
private _transactionSender: AlgorandClientTransactionSender
private _transactionCreator: AlgorandClientTransactionCreator

Expand All @@ -30,21 +35,30 @@ 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
* error transformers from the set.
*/
private _errorTransformers: Set<ErrorTransformer> = 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 ?? {}
}

/**
Expand All @@ -61,57 +75,6 @@ export class AlgorandClient {
return this
}

/**
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The static methods would live in a new "Builder" class. This would be the main difference as far as users are concerned. AlgorandClient.localnet() would become something along the lines of new AlgorandClientBuilder().localnet().

* 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
Expand Down Expand Up @@ -155,7 +118,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 {
Expand All @@ -170,7 +133,7 @@ export class AlgorandClient {
* const clientManager = AlgorandClient.mainNet().client;
*/
public get client() {
return this._clientManager
return this._clientManager ?? { algod: this._algod }
}

/**
Expand Down Expand Up @@ -232,12 +195,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],
})
}
Expand Down Expand Up @@ -269,93 +236,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)
}
}
Loading
Loading