Skip to content

Commit b537252

Browse files
Merge main into release
2 parents 6b3efc4 + 3012742 commit b537252

File tree

58 files changed

+2234
-748
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+2234
-748
lines changed

docs/capabilities/algorand-client.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ The `AlgorandClient` has a number of manager class instances that help you quick
5454

5555
### Creating transactions
5656

57-
You can compose a transaction via `algorand.createTransaction...`, which gives you an instance of the [`AlgorandClientTransactionCreator`](../code/classes/types_algorand_client_transaction_creator.AlgorandClientTransactionCreator.md) class. Intellisense will guide you on the different options.
57+
You can compose a transaction via `algorand.createTransaction.`, which gives you an instance of the [`AlgorandClientTransactionCreator`](../code/classes/types_algorand_client_transaction_creator.AlgorandClientTransactionCreator.md) class. Intellisense will guide you on the different options.
5858

5959
The signature for the calls to send a single transaction usually look like:
6060

@@ -147,8 +147,10 @@ There are two common base interfaces that get reused:
147147
- [`SendParams`](../code/interfaces/types_transaction.SendParams.md)
148148
- `maxRoundsToWaitForConfirmation?: number` - The number of rounds to wait for confirmation. By default until the latest lastValid has past.
149149
- `suppressLog?: boolean` - Whether to suppress log messages from transaction send, default: do not suppress.
150+
- `populateAppCallResources?: boolean` - Whether to use simulate to automatically populate app call resources in the txn objects. Defaults to `Config.populateAppCallResources`.
151+
- `coverAppCallInnerTransactionFees?: boolean` - Whether to use simulate to automatically calculate required app call inner transaction fees and cover them in the parent app call transaction fee
150152

151-
Then on top of that the base type gets extended for the specific type of transaction you are issuing. These are all defined as part of [`TransactionComposer`](./transaction-composer.md).
153+
Then on top of that the base type gets extended for the specific type of transaction you are issuing. These are all defined as part of [`TransactionComposer`](./transaction-composer.md) and we recommend reading these docs, especially when leveraging either `populateAppCallResources` or `coverAppCallInnerTransactionFees`.
152154

153155
### Transaction configuration
154156

docs/capabilities/app-client.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,8 @@ The ARC4 ABI specification supports ABI method calls as arguments to other ABI m
258258

259259
To illustrate this, let's consider an example of two ABI methods with the following signatures:
260260

261-
- `myMethod(pay, appl): void`
262-
- `myOtherMethod(pay): void`
261+
- `myMethod(pay,appl)void`
262+
- `myOtherMethod(pay)void`
263263

264264
These signatures are compatible, so `myOtherMethod` can be passed as an ABI method call argument to `myMethod`, which would look like:
265265

docs/capabilities/testing.md

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,37 @@ import { algorandFixture } from '@algorandfoundation/algokit-utils/testing'
2424

2525
### Using with Jest
2626

27-
To integrate with [Jest](https://jestjs.io/) you need to pass the `fixture.beforeEach` method into Jest's `beforeEach` method and then within each test you can access `fixture.context` to access per-test isolated fixture values.
27+
To integrate with [Jest](https://jestjs.io/) you need to pass the `fixture.newScope` method into Jest's `beforeEach` method (for per test isolation) or `beforeAll` method (for test suite isolation) and then within each test you can access `fixture.context` to access the isolated fixture values.
28+
29+
#### Per-test isolation
2830

2931
```typescript
3032
import { describe, test, beforeEach } from '@jest/globals'
3133
import { algorandFixture } from './testing'
3234

3335
describe('MY MODULE', () => {
3436
const fixture = algorandFixture()
35-
beforeEach(fixture.beforeEach, 10_000)
37+
beforeEach(fixture.newScope, 10_000) // Add a 10s timeout to cater for occasionally slow LocalNet calls
38+
39+
test('MY TEST', async () => {
40+
const { algorand, testAccount /* ... */ } = fixture.context
41+
42+
// Test stuff!
43+
})
44+
})
45+
```
46+
47+
Occasionally there may be a delay when first running the fixture setup so we add a 10s timeout to avoid intermittent test failures (`10_000`).
48+
49+
#### Test suite isolation
50+
51+
```typescript
52+
import { describe, test, beforeAll } from '@jest/globals'
53+
import { algorandFixture } from './testing'
54+
55+
describe('MY MODULE', () => {
56+
const fixture = algorandFixture()
57+
beforeAll(fixture.newScope, 10_000) // Add a 10s timeout to cater for occasionally slow LocalNet calls
3658

3759
test('MY TEST', async () => {
3860
const { algorand, testAccount /* ... */ } = fixture.context
@@ -46,15 +68,37 @@ Occasionally there may be a delay when first running the fixture setup so we add
4668

4769
### Using with vitest
4870

49-
To integrate with [vitest](https://vitest.dev/) you need to pass the `fixture.beforeEach` method into vitest's `beforeEach` method and then within each test you can access `fixture.context` to access per-test isolated fixture values.
71+
To integrate with [vitest](https://vitest.dev/) you need to pass the `fixture.beforeEach` method into vitest's `beforeEach` method (for per test isolation) or `beforeAll` method (for test suite isolation) and then within each test you can access `fixture.context` to access the isolated fixture values.
72+
73+
#### Per-test isolation
5074

5175
```typescript
5276
import { describe, test, beforeEach } from 'vitest'
5377
import { algorandFixture } from './testing'
5478

5579
describe('MY MODULE', () => {
5680
const fixture = algorandFixture()
57-
beforeEach(fixture.beforeEach, 10_000)
81+
beforeEach(fixture.newScope, 10_000) // Add a 10s timeout to cater for occasionally slow LocalNet calls
82+
83+
test('MY TEST', async () => {
84+
const { algorand, testAccount /* ... */ } = fixture.context
85+
86+
// Test stuff!
87+
})
88+
})
89+
```
90+
91+
Occasionally there may be a delay when first running the fixture setup so we add a 10s timeout to avoid intermittent test failures (`10_000`).
92+
93+
#### Test suite isolation
94+
95+
```typescript
96+
import { describe, test, beforeAll } from 'vitest'
97+
import { algorandFixture } from './testing'
98+
99+
describe('MY MODULE', () => {
100+
const fixture = algorandFixture()
101+
beforeAll(fixture.newScope, 10_000) // Add a 10s timeout to cater for occasionally slow LocalNet calls
58102

59103
test('MY TEST', async () => {
60104
const { algorand, testAccount /* ... */ } = fixture.context
@@ -64,6 +108,8 @@ describe('MY MODULE', () => {
64108
})
65109
```
66110

111+
Occasionally there may be a delay when first running the fixture setup so we add a 10s timeout to avoid intermittent test failures (`10_000`).
112+
67113
### Fixture configuration
68114

69115
When calling `algorandFixture()` you can optionally pass in some fixture configuration, with any of these properties (all optional):
@@ -72,6 +118,7 @@ When calling `algorandFixture()` you can optionally pass in some fixture configu
72118
- `indexer?: Indexer` - An optional indexer client, if not specified then it will create one against environment variables defined network (if present) or default LocalNet
73119
- `kmd?: Kmd` - An optional kmd client, if not specified then it will create one against environment variables defined network (if present) or default LocalNet
74120
- `testAccountFunding?: AlgoAmount` - The [amount](./amount.md) of funds to allocate to the default testing account, if not specified then it will get `10` ALGO
121+
- `accountGetter?: (algod: Algodv2, kmd?: Kmd) => Promise<Account>` - Optional override for how to get an account; this allows you to retrieve test accounts from a known or cached list of accounts.
75122

76123
### Using the fixture context
77124

@@ -84,7 +131,7 @@ The `fixture.context` property is of type [`AlgorandTestAutomationContext`](../c
84131
- `transactionLogger: TransactionLogger` - Transaction logger that will log transaction IDs for all transactions issued by `algod`
85132
- `testAccount: Account` - Funded test account that is ephemerally created for each test
86133
- `generateAccount: (params: GetTestAccountParams) => Promise<Account>` - Generate and fund an additional ephemerally created account
87-
- `waitForIndexer: () => Promise<void>` - Wait for the indexer to catch up with all transactions logged by transactionLogger
134+
- `waitForIndexer()` - Waits for indexer to catch up with the latest transaction that has been captured by the `transactionLogger` in the Algorand fixture
88135
- `waitForIndexerTransaction: (transactionId: string) => Promise<TransactionLookupResult>` - Wait for the indexer to catch up with the given transaction ID
89136

90137
## Log capture fixture
@@ -170,7 +217,7 @@ This means it's easy to create tests that are flaky and have intermittent test f
170217
The testing capability provides mechanisms for waiting for indexer to catch up, namely:
171218

172219
- `algotesting.runWhenIndexerCaughtUp(run: () => Promise<T>)` - Executes the given action every 200ms up to 20 times until there is no longer an error with a `status` property with `404` and then returns the result of the action; this will work for any call that calls indexer APIs expecting to return a single record
173-
- `algorandFixture.waitForIndexer()` - Waits for indexer to catch up with all transactions that have been captured by the `transactionLogger` in the Algorand fixture
220+
- `algorandFixture.waitForIndexer()` - Waits for indexer to catch up with the latest transaction that has been captured by the `transactionLogger` in the Algorand fixture
174221
- `algorandFixture.waitForIndexerTransaction(transactionId)` - Waits for indexer to catch up with the single transaction of the given ID
175222

176223
## Logging transactions

docs/capabilities/transaction-composer.md

Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ To get an instance of `TransactionComposer` you can either get it from an [app c
88

99
```typescript
1010
const composerFromAlgorand = algorand.newGroup()
11-
const composerFromAppClient = appClient.newGroup()
11+
const composerFromAppClient = appClient.algorand.newGroup()
1212
const composerFromConstructor = new TransactionComposer({
1313
algod,
1414
/* Return the algosdk.TransactionSigner for this address*/
@@ -33,17 +33,152 @@ The [methods to construct a transaction](../code/classes/types_composer.default.
3333
For example:
3434

3535
```typescript
36+
const myMethod = algosdk.ABIMethod.fromSignature('my_method()void')
3637
const result = algorand
3738
.newGroup()
3839
.addPayment({ sender: 'SENDER', receiver: 'RECEIVER', amount: (100).microAlgo() })
3940
.addAppCallMethodCall({
4041
sender: 'SENDER',
4142
appId: 123n,
42-
method: abiMethod,
43+
method: myMethod,
4344
args: [1, 2, 3],
4445
})
4546
```
4647

48+
## Sending a transaction
49+
50+
Once you have constructed all the required transactions, they can be sent by calling `send()` on the `TransactionComposer`.
51+
Additionally `send()` takes a number of parameters which allow you to opt-in to some additional behaviours as part of sending the transaction or transaction group, mostly significantly `populateAppCallResources` and `coverAppCallInnerTransactionFees`.
52+
53+
### Populating App Call Resource
54+
55+
`populateAppCallResources` automatically updates the relevant app call transactions in the group to include the account, app, asset and box resources required for the transactions to execute successfully. It leverages the simulate endpoint to discover the accessed resources, which have not been explicitly specified. This setting only applies when you have constucted at least one app call transaction. You can read more about [resources and the reference arrays](https://developer.algorand.org/docs/get-details/dapps/smart-contracts/apps/?from_query=resources#reference-arrays) in the docs.
56+
57+
For example:
58+
59+
```typescript
60+
const myMethod = algosdk.ABIMethod.fromSignature('my_method()void')
61+
const result = algorand
62+
.newGroup()
63+
.addAppCallMethodCall({
64+
sender: 'SENDER',
65+
appId: 123n,
66+
method: myMethod,
67+
args: [1, 2, 3],
68+
})
69+
.send({
70+
populateAppCallResources: true,
71+
})
72+
```
73+
74+
If `my_method` in the above example accesses any resources, they will be automatically discovered and added before sending the transaction to the network.
75+
76+
### Covering App Call Inner Transaction Fees
77+
78+
`coverAppCallInnerTransactionFees` automatically calculate the required fee for a parent app call transaction that sends inner transactions. It leverages the simulate endpoint to discover the inner transactions sent and calculates a fee delta to resolve the optimal fee. This feature also takes care of accounting for any surplus transaction fee at the various levels, so as to effectively minimise the fees needed to successfully handle complex scenarios. This setting only applies when you have constucted at least one app call transaction.
79+
80+
For example:
81+
82+
```typescript
83+
const myMethod = algosdk.ABIMethod.fromSignature('my_method()void')
84+
const result = algorand
85+
.newGroup()
86+
.addAppCallMethodCall({
87+
sender: 'SENDER',
88+
appId: 123n,
89+
method: myMethod,
90+
args: [1, 2, 3],
91+
maxFee: microAlgo(5000), // NOTE: a maxFee value is required when enabling coverAppCallInnerTransactionFees
92+
})
93+
.send({
94+
coverAppCallInnerTransactionFees: true,
95+
})
96+
```
97+
98+
Assuming the app account is not covering any of the inner transaction fees, if `my_method` in the above example sends 2 inner transactions, then the fee calculated for the parent transaction will be 3000 µALGO when the transaction is sent to the network.
99+
100+
The above example also has a `maxFee` of 5000 µALGO specified. An exception will be thrown if the transaction fee execeeds that value, which allows you to set fee limits. The `maxFee` field is required when enabling `coverAppCallInnerTransactionFees`.
101+
102+
Because `maxFee` is required and an `algosdk.Transaction` does not hold any max fee information, you cannot use the generic `addTransaction()` method on the composer with `coverAppCallInnerTransactionFees` enabled. Instead use the below, which provides a better overall experience:
103+
104+
```typescript
105+
const myMethod = algosdk.ABIMethod.fromSignature('my_method()void')
106+
107+
// Does not work
108+
const result = algorand
109+
.newGroup()
110+
.addTransaction((await localnet.algorand.createTransaction.appCallMethodCall({
111+
sender: 'SENDER',
112+
appId: 123n,
113+
method: myMethod,
114+
args: [1, 2, 3],
115+
maxFee: microAlgo(5000), // This is only used to create the algosdk.Transaction object and isn't made available to the composer.
116+
})).transactions[0]),
117+
.send({
118+
coverAppCallInnerTransactionFees: true,
119+
})
120+
121+
// Works as expected
122+
const result = algorand
123+
.newGroup()
124+
.addAppCallMethodCall({
125+
sender: 'SENDER',
126+
appId: 123n,
127+
method: myMethod,
128+
args: [1, 2, 3],
129+
maxFee: microAlgo(5000),
130+
})
131+
.send({
132+
coverAppCallInnerTransactionFees: true,
133+
})
134+
```
135+
136+
A more complex valid scenario which leverages an app client to send an ABI method call with ABI method call transactions argument is below:
137+
138+
```typescript
139+
const appFactory = algorand.client.getAppFactory({
140+
appSpec: 'APP_SPEC',
141+
defaultSender: sender.addr,
142+
})
143+
144+
const { appClient: appClient1 } = await appFactory.send.bare.create()
145+
const { appClient: appClient2 } = await appFactory.send.bare.create()
146+
147+
const paymentArg = algorand.createTransaction.payment({
148+
sender: sender.addr,
149+
receiver: receiver.addr,
150+
amount: microAlgo(1),
151+
})
152+
153+
// Note the use of .params. here, this ensure that maxFee is still available to the composer
154+
const appCallArg = await appClient2.params.call({
155+
method: 'my_other_method',
156+
args: [],
157+
maxFee: microAlgo(2000),
158+
})
159+
160+
const result = await appClient1.algorand
161+
.newGroup()
162+
.addAppCallMethodCall(
163+
await appClient1.params.call({
164+
method: 'my_method',
165+
args: [paymentArg, appCallArg],
166+
maxFee: microAlgo(5000),
167+
}),
168+
)
169+
.send({
170+
coverAppCallInnerTransactionFees: true,
171+
})
172+
```
173+
174+
This feature should efficiently calculate the minimum fee needed to execute an app call transaction with inners, however we always recommend testing your specific scenario behaves as expected before releasing.
175+
176+
### Covering App Call Op Budget
177+
178+
The high level Algorand contract authoring languages all have support for ensuring appropriate app op budget is available via `ensure_budget` in Algorand Python, `ensureBudget` in Algorand TypeScript and `increaseOpcodeBudget` in TEALScript. This is great, as it allows contract authors to ensure appropriate budget is available by automatically sending op-up inner transactions to increase the budget available. These op-up inner transactions require the fees to be covered by an account, which is generally the responsibility of the application consumer.
179+
180+
Application consumers may not be immediately aware of the number of op-up inner transactions sent, so it can be difficult for them to determine the exact fees required to successfully execute an application call. Fortunately the `coverAppCallInnerTransactionFees` setting above can be leveraged to automatically cover the fees for any op-up inner transaction that an application sends. Additionally if a contract author decides to cover the fee for an op-up inner transaction, then the application consumer will not be charged a fee for that transaction.
181+
47182
## Simulating a transaction
48183

49184
Transactions can be simulated using the simulate endpoint in algod, which enables evaluating the transaction on the network without it actually being commited to a block.

docs/capabilities/transaction.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@ There are various variations of the `ConfirmedTransactionResult` that are expose
2020

2121
## Further reading
2222

23-
To understand how to create, simulate and send transactions consult the [`AlgorandClient`](./algorand-client.md) and [`AlgorandClient`](./algokit-composer.md) documentation.
23+
To understand how to create, simulate and send transactions consult the [`AlgorandClient`](./algorand-client.md) and [`TransactionComposer`](./transaction-composer.md) documentation.

docs/capabilities/typed-app-clients.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ To generate a typed client from an app spec file you can use [AlgoKit CLI](https
2121
> algokit generate client application.json --output /absolute/path/to/client.ts
2222
```
2323

24-
Note: If you are using a version of AlgoKit Utils >= 7.0.0 in your project, you will need to generate using >= 4.0.0. See [AlgoKit CLI generator version pinning](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/generate.md#version-pinning) for more information on how to lock to a specific version.
24+
Note: AlgoKit Utils >= 7.0.0 is compatible with the older 3.0.0 generated typed clients, however if you want to utilise the new features or leverage ARC-56 support, you will need to generate using >= 4.0.0. See [AlgoKit CLI generator version pinning](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/generate.md#version-pinning) for more information on how to lock to a specific version.
2525

2626
## Getting a typed client instance
2727

@@ -193,7 +193,7 @@ const factory = algorand.client.getTypedAppFactory(HelloWorldAppFactory, {
193193
// Create the app and get a typed app client for the created app (note: this creates a new instance of the app every time,
194194
// you can use .deploy() to deploy idempotently if the app wasn't previously
195195
// deployed or needs to be updated if that's allowed)
196-
const { appClient } = await factory.create()
196+
const { appClient } = await factory.send.create()
197197

198198
// Make a call to an ABI method and print the result
199199
const response = await appClient.hello({ name: 'world' })

docs/code/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
- [types/dispenser-client.spec](modules/types_dispenser_client_spec.md)
4444
- [types/expand](modules/types_expand.md)
4545
- [types/indexer](modules/types_indexer.md)
46+
- [types/interface-of](modules/types_interface_of.md)
4647
- [types/kmd-account-manager](modules/types_kmd_account_manager.md)
4748
- [types/lifecycle-events](modules/types_lifecycle_events.md)
4849
- [types/logging](modules/types_logging.md)

0 commit comments

Comments
 (0)