App client
App client¶
Application client that works with ARC-0032 application spec defined smart contracts (e.g. via Beaker).
App client is a higher-order use case capability provided by AlgoKit Utils that builds on top of the core capabilities, particularly App deployment and App management. It allows you to access a high productivity application client that works with ARC-0032 application spec defined smart contracts, which you can use to create, update, delete, deploy and call a smart contract and access state data for it.
To see some usage examples check out the automated tests.
Design¶
The design for the app client is based on a wrapper for parsing an ARC-0032 application spec and wrapping the App deployment functionality and corresponding design. It's also heavily inspired by beaker-ts, which this library aims to eventually replace.
Creating an application client¶
To create an application you can either use algokit.getAppClient(appDetails, algod)
or import { ApplicationClient } from '@algorandfoundation/algokit-utils/types/app-client'
and new ApplicationClient(appDetails, algod)
The appDetails
parameter is of type AppSpecAppDetails
, which contains some core properties and then one of two key mechanisms to specify the app to target.
- Core parameters - Core parameters that can always be applied
app: AppSpec | string
- Either the parsed ARC-0032AppSpec
, or a raw JSONstring
which will get parsed as anAppSpec
sender?: SendTransactionFrom
- Optional sender to send/sign all transactions with (if left out then individual methods must have a sender passed to them)params?: SuggestedParams
- Optional sending parameters if you want to avoid an extra call to algod- App target - How to resolve an existing app (if one exists), which can either be:
ResolveAppById
- When you want to resolve an existing app by app ID, which consists of the following parameters:id: number
- The app ID, which should be set as0
if you have yet to deploy the contractname? string
- The optional name to mark the contract with if you are deploying it, otherwisecontract.name
is used from the app spec
ResolveAppByCreatorAndName
- When you want to resolve an existing app by name for a given creator account, which consists of the following parameters:creatorAddress: string
- The address of the creator account of the app for which to search for the deployed app undername?: string
- An overridden name to identify the contract with, otherwisecontract.name
is used from the app spec- And either:
indexer: Indexer
- An indexer instance so the existing app deployments can be queriedexistingDeployments: AppLookup
- The result of an existing indexer lookup to generate an app lookup, which avoids extra indexer calls from being made
Creating, updating, deploying and deleting the app¶
Once you have an application client you can perform the following actions related to creating and managing the lifecycle of an app:
compile(compilationParams?)
- Allows you to compile the application (approval and clear program)), including deploy-time parameter replacements and deploy-time immutability and permanence control; it returns the compiled AVM code and source mapsdeploy(deploymentParams?)
- Allows you to perform an idempotent (safely retryable) deployment of the smart contract app per the design ofdeployApp
create(createParams?)
- Allows you to perform a creation of the smart contract appupdate(updateParams?)
- Allows you to perform an update of the (existing) smart contract appdelete(deleteParams?)
- Allows you to delete the (existing) smart contract app
The input payload for create
and update
are the same and are a union of AppClientCallParams
and AppClientCompilationParams
. The input payload for delete
is AppClientCallParams
. The input payload for deploy
is AppClientDeployParams
.
The return payload for these methods directly matches the equivalent underlying App management / App deployment methods (since these methods are wrappers):
Calling the app¶
To make a call to a smart contract you can use the following methods (which determine the on complete action that the call will use):
call(call?)
- A normal (noop
on completion action) calloptIn(call?)
- An opt-in callcloseOut(call?)
- A close-out callclearState(call?)
- A clear state call (note: calls the clear program)callOfType(call, callType)
- Make a call with a specified call type
These calls will only work if the Application Client knows the ID of the app, which will occur if:
- The app ID is passed into the constructor;
- You have passed
creatorAccount
and the smart contract name to the constructor and the contract already exists; or - You have called
create
ordeploy
using that Application Client.
The input payload for all of these calls is the same as delete
; AppClientCallParams
.
The return payload for all of these is the same as callApp
.
Getting a reference to the app¶
To get reference information for the app from outside the Application Client you can call getAppReference()
. If you passed the creatorAddress
and app name to the constructor then this method will return the full AppMetadata
per getCreatorAppsByName
. If you just passed in the app ID or used create
rather than deploy
then you will just receive an AppReference
(which is also a sub-type of the AppMetadata
):
appId: number
- The id of the appappAddress: string
- The Algorand address of the account associated with the app
Funding the app account¶
Often there is a need to fund an app account to cover minimum balance requirements for boxes and other scenarios. There is a helper method that will do this for you fundAppAccount(fundParams)
.
The input parameters can either be:
- An
AlgoAmount
value (note: requiressender
to be passed into the constructor) - A
FundAppAccountParams
, which has the following properties: amount: AlgoAmount
- The amount to fundsender?: SendTransactionFrom
- The sender/signer to use; if unspecified then the sender that was passed into the constructor of the Application Client is usednote?: TransactionNote
- The transaction note to use when issuing the transactionsendParams?: SendTransactionParams
- The transaction sending configuration
This call will only work if the Application Client knows the ID of the app, which will occur if:
- The app ID is passed into the constructor;
- You have passed
creatorAccount
and the smart contract name to the constructor and the contract already exists; or - You have called
create
ordeploy
using that Application Client.
If you are passing the funding payment in as an ABI argument so it can be validated by the ABI method then you'll want to issue the skipSending
configuration. That might look something like this as an example:
const result = await appClient.call({
method: 'bootstrap',
methodArgs: {
args: [
appClient.fundAppAccount({
amount: algokit.microAlgos(200_000),
sendParams: { skipSending: true },
}),
],
boxes: ['Box1'],
},
})
Reading state¶
There are various methods defined that let you read state from the smart contract app:
getGlobalState()
- Gets the current global state using `algokit.getAppGlobalStategetLocalState(account: string | SendTransactionFrom)
- Gets the current local state for the given account address using `algokit.getAppLocalState.getBoxNames()
- Gets the current box names usingalgokit.getAppBoxNames
getBoxValue(name)
- Gets the current value of the given box using `algokit.getAppBoxValuegetBoxValueFromABIType(name)
- Gets the current value of the given box from an ABI type using `algokit.getAppBoxValueFromABITypegetBoxValues(filter)
- Gets the current values of the boxes using `algokit.getAppBoxValuesgetBoxValuesFromABIType(type, filter)
- Gets the current values of the boxes from an ABI type using `algokit.getAppBoxValuesFromABIType
These calls will only work if the Application Client knows the ID of the app, which will occur if:
- The app ID is passed into the constructor;
- You have passed
creatorAccount
and the smart contract name to the constructor and the contract already exists; or - You have called
create
ordeploy
using that Application Client.
Handling logic errors and diagnosing errors¶
Often when calling a smart contract during development you will get logic errors that cause an exception to throw. This may be because of a failing assertion, a lack of fees, exhaustion of opcode budget, or any number of other reasons.
When this occurs, you will generally get an error that looks something like: TransactionPool.Remember: transaction {TRANSACTION_ID}: logic eval error: {ERROR_MESSAGE}. Details: pc={PROGRAM_COUNTER_VALUE}, opcodes={LIST_OF_OP_CODES}
.
The information in that error message can be parsed and when combined with the source map from compilation you can expose debugging information that makes it much easier to understand what's happening.
The Application Client automatically provides this functionality for all smart contract calls. It also exposes a function that can be used for any custom calls you manually construct and need to add into your own try/catch exposeLogicError(e: Error, isClear?: boolean)
.
When an error is thrown then the resulting error that is re-thrown will be a LogicError
object, which has the following fields:
message: string
- The formatted error message{ERROR_MESSAGE}. at:{TEAL_LINE}. {ERROR_DESCRIPTION}
stack: string
- A stack trace of the TEAL code showing where the error was with the 5 lines either side of itled: LogicErrorDetails
- The parsed logic error details from the error message, with the following properties:txId: string
- The transaction ID that triggered the errorpc: number
- The program countermsg: string
- The raw error messagedesc: string
- The full error descriptiontraces: Record<string, unknown>[]
- Any traces that were included in the errorprogram: string[]
- The TEAL program split by lineteal_line: number
- The line number in the TEAL program that triggered the error
Note: This information will only show if the Application Client has a source map. This will occur if:
- You have called
create
,update
ordeploy
- You have called
importSourceMaps(sourceMaps)
and provided the source maps (which you can get by callingexportSourceMaps()
after callingcreate
,update
ordeploy
and it returns a serialisable value)
If you want to go a step further and automatically issue a dry run transaction when there is an error when an ABI method is called you can turn on debug mode:
algokit.Config.configure({ debug: true })
Note
The "dry run" feature has been deprecated and is now replaced by the "simulation" feature. Please refer to the Simulation Documentation for more details.
If you do that then the exception will have the traces
property within the underlying exception will have key information from the simulation within it and this will get populated into the led.traces
property of the thrown error.
AppClientCallParams
¶
All methods that call the smart contract apart from deploy
make use of this type. It consists of the following core properties, all of which are optional:
sender?: SendTransactionFrom
- The sender/signer to use; if unspecified then the sender that was passed into the constructor of the Application Client is usednote?: TransactionNote
- The transaction note to use when issuing the transactionsendParams?: SendTransactionParams
- The transaction sending configuration
In addition to these parameters, it may specify call arguments parameters (AppClientCallArgs
).
AppClientCallArgs
¶
Whenever an app call is specified, including within deploy
this type specifies the arguments. There are two forms you can use:
- Raw call - Directly specifies the values that will be populated onto an
algosdk.Transaction
- ABI call - Specifies a ARC-0004 ABI call along with relevant values (like boxes) that will be directly populated onto an
algosdk.Transaction
. Consists of the ABI app call args type with themethod
parameter replaced with a string (since the Application Client only needs it as a string): method: string
- The name of the method (e.g.hello
) or the ABI signature of the method (e.g.hello(string)string
) for when you have multiple methods with the same name and need to differentiate between themmethodArgs?: ABIAppCallArg[]
- An array of arguments to pass into the ABI methodboxes: (BoxReference | BoxIdentifier | algosdk.BoxReference)[]
- Any boxes to load to the boxes arraylease: string | Uint8Array
: A lease to assign to the transaction to enforce a mutually exclusive transaction (useful to prevent double-posting and other scenarios)
If you want to get call args for manually populating into an algosdk.Transaction
you can use the getCallArgs
method on Application Client.
Also, if you want to manually construct an ABI call (e.g. to use with AtomicTransactionComposer directly) then you can use getABIMethod
and/or getABIMethodParams
AppClientCompilationParams
¶
When calling create
or update
there are extra parameter that need to be passed to facilitate the compilation of the code in addition to the other parameters in AppClientCallParams
:
deployTimeParams?: TealTemplateParams
- Any deploy-time parameters to replace in the TEAL codeupdatable?: boolean
- Whether or not the contract should have deploy-time immutability control set, undefined = ignoredeletable?: boolean
- Whether or not the contract should have deploy-time permanence control set, undefined = ignore
AppClientDeployParams
¶
When calling deploy
the AppClientDeployParams
type defines the input parameters, all of which are optional. This type closely models the semantics of deployApp
.
version?: string
- The version of the contract, uses "1.0" by defaultsender?: SendTransactionFrom
- The sender/signer to use; if unspecified then the sender that was passed into the constructor of the Application Client is usedsendParams?: SendTransactionParams
- The transaction sending configurationdeployTimeParams?: TealTemplateParams
- Any deploy-time parameters to replace in the TEAL codeallowUpdate?: boolean
- Whether or not to allow updates in the contract using the deploy-time updatability control if present in your contract; if this is not specified then it will automatically be determined based on the AppSpec definition (if there is an update method)allowDelete?: boolean
- Whether or not to allow deletes in the contract using the deploy-time deletability control if present in your contract; if this is not specified then it will automatically be determined based on the AppSpec definition (if there is a delete method)onSchemaBreak?: 'replace' | 'fail' | OnSchemaBreak
determines what should happen if a breaking change to the schema is detected (e.g. if you need more global or local state that was previously requested when the contract was originally created)onUpdate?: 'update' | 'replace' | 'fail' | OnUpdate
determines what should happen if an update to the smart contract is detected (e.g. the TEAL code has changed since last deployment)createArgs?: AppClientCallArgs
- The args to use if a create is neededupdateArgs?: AppClientCallArgs
- The args to use if an update is neededdeleteArgs?: AppClientCallArgs
- The args to use if a delete is needed