From c4a7d200977ff88e25111671856bf3c6ce833fa8 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Sun, 17 Mar 2024 01:18:42 +0800 Subject: [PATCH 1/2] Tweaks to AlgorandClient: * Rename AlgoKitClient to AlgorandClient and added static construction methods * Added fluent with* methods to AlgorandClient to allow for fluent configuration * Extracted client related stuff to ClientManager and account stuff to AccountManager and added extra functionality to both * Add transaction and confirmation to the result of sending a single transaction for better ergonomics * Added ability to specify suggested params and create a copy of suggested params when providing it * Moved classes to types directory * Added getAccountInformation to make tests terser (this method should have been added ages ago) * Incorporated client into testing fixture * Changed all possible bigint | numbers' to number (where it's impractical for them to hit 2^53) * Incorporated TransactionSignerAccount with TransactionSigner to allow for terser code * Rename ID to Id for consistency with rest of algokit utils --- src/account/account.ts | 41 ++- src/client.ts | 128 -------- src/index.ts | 4 +- src/testing/fixtures/algorand-fixture.ts | 24 +- src/types/account-manager.ts | 239 +++++++++++++++ src/types/account.ts | 5 + .../algorand-client.spec.ts} | 124 ++++---- src/types/algorand-client.ts | 274 ++++++++++++++++++ src/types/client-manager.ts | 146 ++++++++++ src/{ => types}/composer.ts | 62 ++-- src/types/network-client.ts | 4 +- src/types/testing.ts | 11 +- 12 files changed, 826 insertions(+), 236 deletions(-) delete mode 100644 src/client.ts create mode 100644 src/types/account-manager.ts rename src/{client.spec.ts => types/algorand-client.spec.ts} (58%) create mode 100644 src/types/algorand-client.ts create mode 100644 src/types/client-manager.ts rename src/{ => types}/composer.ts (94%) diff --git a/src/account/account.ts b/src/account/account.ts index db7224ce..6c381005 100644 --- a/src/account/account.ts +++ b/src/account/account.ts @@ -3,11 +3,12 @@ import { Config } from '../config' import { getOrCreateKmdWalletAccount } from '../localnet/get-or-create-kmd-wallet-account' import { isLocalNet } from '../localnet/is-localnet' import { getSenderAddress } from '../transaction/transaction' -import { MultisigAccount, SigningAccount, TransactionSignerAccount } from '../types/account' +import { AccountInformation, MultisigAccount, SigningAccount, TransactionSignerAccount } from '../types/account' import { AlgoAmount } from '../types/amount' import { SendTransactionFrom } from '../types/transaction' import { getAccountConfigFromEnvironment } from './get-account-config-from-environment' import { mnemonicAccount } from './mnemonic-account' +import AccountInformationModel = algosdk.modelsv2.Account import Account = algosdk.Account import Algodv2 = algosdk.Algodv2 import Kmd = algosdk.Kmd @@ -127,3 +128,41 @@ export function getAccountAddressAsUint8Array(account: SendTransactionFrom | str export function getAccountAddressAsString(addressEncodedInB64: string): string { return algosdk.encodeAddress(Buffer.from(addressEncodedInB64, 'base64')) } + +/** + * Returns the given sender account's current status, balance and spendable amounts. + * + * @example + * ```typescript + * const address = "XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA"; + * const accountInfo = await account.getInformation(address); + * ``` + * + * [Response data schema details](https://developer.algorand.org/docs/rest-apis/algod/#get-v2accountsaddress) + * @param sender The address of the sender/account to look up + * @returns The account information + */ +export async function getAccountInformation(sender: string | SendTransactionFrom, algod: Algodv2): Promise { + const account = AccountInformationModel.from_obj_for_encoding( + await algod.accountInformation(typeof sender === 'string' ? sender : getSenderAddress(sender)).do(), + ) + + return { + ...account, + // None of these can practically overflow 2^53 + amount: Number(account.amount), + amountWithoutPendingRewards: Number(account.amountWithoutPendingRewards), + minBalance: Number(account.minBalance), + pendingRewards: Number(account.pendingRewards), + rewards: Number(account.rewards), + round: Number(account.round), + totalAppsOptedIn: Number(account.totalAppsOptedIn), + totalAssetsOptedIn: Number(account.totalAssetsOptedIn), + totalCreatedApps: Number(account.totalCreatedApps), + totalCreatedAssets: Number(account.totalCreatedAssets), + appsTotalExtraPages: account.appsTotalExtraPages ? Number(account.appsTotalExtraPages) : undefined, + rewardBase: account.rewardBase ? Number(account.rewardBase) : undefined, + totalBoxBytes: account.totalBoxBytes ? Number(account.totalBoxBytes) : undefined, + totalBoxes: account.totalBoxes ? Number(account.totalBoxes) : undefined, + } +} diff --git a/src/client.ts b/src/client.ts deleted file mode 100644 index da87e9f3..00000000 --- a/src/client.ts +++ /dev/null @@ -1,128 +0,0 @@ -import algosdk from 'algosdk' -import AlgokitComposer, { - AppCallParams, - AssetConfigParams, - AssetCreateParams, - AssetDestroyParams, - AssetFreezeParams, - AssetOptInParams, - AssetTransferParams, - KeyRegParams, - MethodCallParams, - PayTxnParams, -} from './composer' - -export default class AlgokitClient { - /** The algosdk algod client */ - algod: algosdk.Algodv2 - - /** A map of address to transaction signer functions */ - signers: { [address: string]: algosdk.TransactionSigner } = {} - - /** The amount of time a suggested params response will be cached for */ - cachedSuggestedParamsTimeout: number = 3000 // three seconds - - /** The last suggested params response */ - cachedSuggestedParams?: { params: algosdk.SuggestedParams; time: number } - - /** The default signer to use if no signer is provided or found in `signers` */ - defaultSigner?: algosdk.TransactionSigner - - constructor({ algodClient, defaultSigner }: { algodClient: algosdk.Algodv2; defaultSigner?: algosdk.TransactionSigner }) { - this.algod = algodClient - this.defaultSigner = defaultSigner - } - - /** Get suggested params (either cached or from algod) */ - async getSuggestedParams() { - if (this.cachedSuggestedParams && Date.now() - this.cachedSuggestedParams.time < this.cachedSuggestedParamsTimeout) { - return this.cachedSuggestedParams.params - } - - const params = await this.algod.getTransactionParams().do() - this.cachedSuggestedParams = { params, time: Date.now() } - - return params - } - - /** Start a new `AlgokitComposer` transaction group */ - newGroup() { - return new AlgokitComposer( - this.algod, - (addr: string) => this.signers[addr] || this.defaultSigner, - () => this.getSuggestedParams(), - ) - } - - /** - * Methods for sending a transaction - */ - send = { - payment: (params: PayTxnParams) => { - return this.newGroup().addPayment(params).execute() - }, - assetCreate: (params: AssetCreateParams) => { - return this.newGroup().addAssetCreate(params).execute() - }, - assetConfig: (params: AssetConfigParams) => { - return this.newGroup().addAssetConfig(params).execute() - }, - assetFreeze: (params: AssetFreezeParams) => { - return this.newGroup().addAssetFreeze(params).execute() - }, - assetDestroy: (params: AssetDestroyParams) => { - return this.newGroup().addAssetDestroy(params).execute() - }, - assetTransfer: (params: AssetTransferParams) => { - return this.newGroup().addAssetTransfer(params).execute() - }, - appCall: (params: AppCallParams) => { - return this.newGroup().addAppCall(params).execute() - }, - keyReg: (params: KeyRegParams) => { - return this.newGroup().addKeyReg(params).execute() - }, - methodCall: (params: MethodCallParams) => { - return this.newGroup().addMethodCall(params).execute() - }, - assetOptIn: (params: AssetOptInParams) => { - return this.newGroup().addAssetOptIn(params).execute() - }, - } - - /** - * Methods for building transactions - */ - transactions = { - payment: async (params: PayTxnParams) => { - return (await this.newGroup().addPayment(params).buildGroup()).map((ts) => ts.txn)[0] - }, - assetCreate: async (params: AssetCreateParams) => { - return (await this.newGroup().addAssetCreate(params).buildGroup()).map((ts) => ts.txn)[0] - }, - assetConfig: async (params: AssetConfigParams) => { - return (await this.newGroup().addAssetConfig(params).buildGroup()).map((ts) => ts.txn)[0] - }, - assetFreeze: async (params: AssetFreezeParams) => { - return (await this.newGroup().addAssetFreeze(params).buildGroup()).map((ts) => ts.txn)[0] - }, - assetDestroy: async (params: AssetDestroyParams) => { - return (await this.newGroup().addAssetDestroy(params).buildGroup()).map((ts) => ts.txn)[0] - }, - assetTransfer: async (params: AssetTransferParams) => { - return (await this.newGroup().addAssetTransfer(params).buildGroup()).map((ts) => ts.txn)[0] - }, - appCall: async (params: AppCallParams) => { - return (await this.newGroup().addAppCall(params).buildGroup()).map((ts) => ts.txn)[0] - }, - keyReg: async (params: KeyRegParams) => { - return (await this.newGroup().addKeyReg(params).buildGroup()).map((ts) => ts.txn)[0] - }, - methodCall: async (params: MethodCallParams) => { - return (await this.newGroup().addMethodCall(params).buildGroup()).map((ts) => ts.txn) - }, - assetOptIn: async (params: AssetOptInParams) => { - return (await this.newGroup().addAssetOptIn(params).buildGroup()).map((ts) => ts.txn)[0] - }, - } -} diff --git a/src/index.ts b/src/index.ts index 919ec638..a23c7cc1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,8 +4,6 @@ export * from './app' export * from './app-client' export * from './app-deploy' export * from './asset' -export * from './client' -export * from './composer' export * from './config' export * from './debugging' export * from './dispenser-client' @@ -14,3 +12,5 @@ export * from './localnet' export * from './network-client' export * from './transaction' export * from './transfer' +export * from './types/algorand-client' +export * from './types/composer' diff --git a/src/testing/fixtures/algorand-fixture.ts b/src/testing/fixtures/algorand-fixture.ts index e10a862e..a1bf5a3f 100644 --- a/src/testing/fixtures/algorand-fixture.ts +++ b/src/testing/fixtures/algorand-fixture.ts @@ -7,6 +7,7 @@ import { getConfigFromEnvOrDefaults, lookupTransactionById, } from '../../' +import AlgorandClient from '../../types/algorand-client' import { AlgoConfig } from '../../types/network-client' import { AlgorandFixture, AlgorandFixtureConfig, AlgorandTestAutomationContext, GetTestAccountParams } from '../../types/testing' import { getTestAccount } from '../account' @@ -92,21 +93,29 @@ export function algorandFixture(fixtureConfig?: AlgorandFixtureConfig, config?: const indexer = fixtureConfig?.indexer ?? getAlgoIndexerClient(config.indexerConfig) const kmd = fixtureConfig?.kmd ?? getAlgoKmdClient(config.kmdConfig) let context: AlgorandTestAutomationContext + let algorandClient: AlgorandClient const beforeEach = async () => { Config.configure({ debug: true }) const transactionLogger = new TransactionLogger() const transactionLoggerAlgod = transactionLogger.capture(algod) + const acc = await getTestAccount( + { initialFunds: fixtureConfig?.testAccountFunding ?? algos(10), suppressLog: true }, + transactionLoggerAlgod, + kmd, + ) + algorandClient = AlgorandClient.fromClients({ algod: transactionLoggerAlgod, indexer, kmd }).withAccount(acc) + const testAccount = { ...acc, signer: algorandClient.account.getSigner(acc.addr) } context = { algod: transactionLoggerAlgod, indexer: indexer, kmd: kmd, - testAccount: await getTestAccount( - { initialFunds: fixtureConfig?.testAccountFunding ?? algos(10), suppressLog: true }, - transactionLoggerAlgod, - kmd, - ), - generateAccount: (params: GetTestAccountParams) => getTestAccount(params, transactionLoggerAlgod, kmd), + testAccount, + generateAccount: async (params: GetTestAccountParams) => { + const account = await getTestAccount(params, transactionLoggerAlgod, kmd) + algorandClient.withAccount(account) + return { ...account, signer: algorandClient.account.getSigner(account.addr) } + }, transactionLogger: transactionLogger, waitForIndexer: () => transactionLogger.waitForIndexer(indexer), waitForIndexerTransaction: (transactionId: string) => runWhenIndexerCaughtUp(() => lookupTransactionById(transactionId, indexer)), @@ -117,6 +126,9 @@ export function algorandFixture(fixtureConfig?: AlgorandFixtureConfig, config?: get context() { return context }, + get algorand() { + return algorandClient + }, beforeEach, } } diff --git a/src/types/account-manager.ts b/src/types/account-manager.ts new file mode 100644 index 00000000..3c8c339e --- /dev/null +++ b/src/types/account-manager.ts @@ -0,0 +1,239 @@ +import algosdk from 'algosdk' +import * as algokit from '..' +import { AccountInformation, SigningAccount, TransactionSignerAccount } from './account' +import { AlgoAmount } from './amount' +import { ClientManager } from './client-manager' +import { SendTransactionFrom } from './transaction' + +/** Creates and keeps track of signing accounts against sending addresses. */ +export class AccountManager { + private _clientManager: ClientManager + private _accounts: { [address: string]: TransactionSignerAccount } = {} + private _defaultSigner?: algosdk.TransactionSigner + + /** + * Create a new account creator + * @param algod The algod client to use + * @param kmd The optional kmd client to use + */ + constructor(clientManager: ClientManager) { + this._clientManager = clientManager + } + + /** + * 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 `AccountManager` so method calls can be chained + */ + public withDefaultSigner(signer: algosdk.TransactionSigner | TransactionSignerAccount): AccountManager { + this._defaultSigner = 'signer' in signer ? signer.signer : signer + return this + } + + /** + * Records the given account against the address of the account for later retrieval and returns a `TransactionSignerAccount`. + * @param account The account to use. + * @returns A `TransactionSignerAccount` for the given account. + */ + private signerAccount(account: T): TransactionSignerAccount & { account: T } { + const acc = { + addr: algokit.getSenderAddress(account), + signer: algokit.getSenderTransactionSigner(account), + } + this._accounts[acc.addr] = acc + return { ...acc, account } + } + + /** + * Tracks the given account for later signing. + * @param account The account to register + * @returns The AccountCreator instance for method chaining + */ + public withAccount(account: TransactionSignerAccount | SendTransactionFrom) { + const acc = + 'signer' in account && 'addr' in account + ? account + : { signer: algokit.getSenderTransactionSigner(account), addr: algokit.getSenderAddress(account) } + this._accounts[acc.addr] = acc + return this + } + + /** + * Tracks the given account 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 AccountCreator instance for method chaining + */ + public withSigner(sender: string, signer: algosdk.TransactionSigner) { + this._accounts[sender] = { addr: sender, signer } + return this + } + + /** + * Returns the `TransactionSigner` for the given sender address. + * + * If no signer has been registered for that address then the default signer is used if registered. + * + * @param sender The sender address + * @returns The `TransactionSigner` or throws an error if not found + */ + public getSigner(sender: string): algosdk.TransactionSigner { + const signer = this._accounts[sender]?.signer ?? this._defaultSigner + if (!signer) throw new Error(`No signer found for address ${sender}`) + return signer + } + + /** + * Returns the `TransactionSignerAccount` for the given sender address. + * @param sender The sender address + * @returns The `TransactionSignerAccount` or throws an error if not found + */ + public getAccount(sender: string): TransactionSignerAccount { + const account = this._accounts[sender] + if (!account) throw new Error(`No signer found for address ${sender}`) + return account + } + + /** + * Returns the given sender account's current status, balance and spendable amounts. + * + * @example + * ```typescript + * const address = "XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA"; + * const accountInfo = await account.getInformation(address); + * ``` + * + * [Response data schema details](https://developer.algorand.org/docs/rest-apis/algod/#get-v2accountsaddress) + * @param sender The address of the sender/account to look up + * @returns The account information + */ + public async getInformation(sender: string | TransactionSignerAccount): Promise { + return algokit.getAccountInformation(sender, this._clientManager.algod) + } + + /** + * Tracks and returns an Algorand account with secret key loaded (i.e. that can sign transactions) by taking the mnemonic secret. + * + * @example + * ```typescript + * const account = await account.fromMnemonic("mnemonic secret ...") + * const rekeyedAccount = await account.fromMnemonic("mnemonic secret ...", "SENDERADDRESS...") + * ``` + * @param mnemonicSecret The mnemonic secret representing the private key of an account; **Note: Be careful how the mnemonic is handled**, + * never commit it into source control and ideally load it from the environment (ideally via a secret storage service) rather than the file system. + * @param sender The optional sender address to use this signer for (aka a rekeyed account) + * @returns The account + */ + public fromMnemonic = (mnemonicSecret: string, sender?: string) => { + const account = algokit.mnemonicAccount(mnemonicSecret) + return this.signerAccount(sender ? algokit.rekeyedAccount(account, sender) : account) + } + + /** + * Tracks and returns an Algorand account with private key loaded by convention from environment variables based on the given name identifier. + * + * Note: This function expects to run in a Node.js environment. + * + * ## Convention: + * * **Non-LocalNet:** will load process.env['\{NAME\}_MNEMONIC'] as a mnemonic secret; **Note: Be careful how the mnemonic is handled**, + * never commit it into source control and ideally load it via a secret storage service rather than the file system. + * If process.env['\{NAME\}_SENDER'] is defined then it will use that for the sender address (i.e. to support rekeyed accounts) + * * **LocalNet:** will load the account from a KMD wallet called \{NAME\} and if that wallet doesn't exist it will create it and fund the account for you + * + * This allows you to write code that will work seamlessly in production and local development (LocalNet) without manual config locally (including when you reset the LocalNet). + * + * @example Default + * + * If you have a mnemonic secret loaded into `process.env.MY_ACCOUNT_MNEMONIC` then you can call the following to get that private key loaded into an account object: + * ```typescript + * const account = await account.fromEnvironment('MY_ACCOUNT', algod) + * ``` + * + * If that code runs against LocalNet then a wallet called `MY_ACCOUNT` will automatically be created with an account that is automatically funded with 1000 (default) ALGOs from the default LocalNet dispenser. + * If not running against LocalNet then it will use proces.env.MY_ACCOUNT_MNEMONIC as the private key and (if present) process.env.MY_ACCOUNT_SENDER as the sender address. + * + * @param name The name identifier of the account + * @param fundWith The optional amount to fund the account with when it gets created (when targeting LocalNet), if not specified then 1000 Algos will be funded from the dispenser account + * @returns The account + */ + public fromEnvironment = async (name: string, fundWith?: AlgoAmount) => + this.signerAccount(await algokit.mnemonicAccountFromEnvironment({ name, fundWith }, this._clientManager.algod, this._clientManager.kmd)) + + /** + * Tracks and returns an Algorand account with private key loaded from the given KMD wallet (identified by name). + * + * @param name: The name of the wallet to retrieve an account from + * @param predicate: An optional filter to use to find the account (otherwise it will return a random account from the wallet) + * @param sender The optional sender address to use this signer for (aka a rekeyed account) + * @example Get default funded account in a LocalNet + * + * ```typescript + * const defaultDispenserAccount = await account.fromKmd('unencrypted-default-wallet', + * a => a.status !== 'Offline' && a.amount > 1_000_000_000 + * ) + * ``` + * @returns The account + */ + public fromKmd = async ( + name: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + predicate?: (account: Record) => boolean, + sender?: string, + ) => { + const account = await algokit.getKmdWalletAccount({ name, predicate }, this._clientManager.algod, this._clientManager.kmd) + if (!account) throw new Error(`Unable to find KMD account ${name}${predicate ? ' with predicate' : ''}`) + return this.signerAccount(sender ? algokit.rekeyedAccount(account, sender) : account) + } + + /** + * Tracks and returns an account that supports partial or full multisig signing. + * + * @example + * ```typescript + * const account = await account.multisig({version: 1, threshold: 1, addrs: ["ADDRESS1...", "ADDRESS2..."]}, + * await account.fromEnvironment('ACCOUNT1')) + * ``` + * @param multisigParams The parameters that define the multisig account + * @param signingAccounts The signers that are currently present + * @returns A multisig account wrapper + */ + public multisig = (multisigParams: algosdk.MultisigMetadata, signingAccounts: (algosdk.Account | SigningAccount)[]) => { + return this.signerAccount(algokit.multisigAccount(multisigParams, signingAccounts)) + } + + /** + * Tracks and returns a new, random Algorand account with secret key loaded. + * + * @example + * ```typescript + * const account = await account.random() + * ``` + * @returns The account + */ + public random = () => this.signerAccount(algokit.randomAccount()) + + /** + * Returns an account (with private key loaded) that can act as a dispenser. + * + * @example + * ```typescript + * const account = await account.dispenser() + * ``` + * If running on LocalNet then it will return the default dispenser account automatically, + * otherwise it will load the account mnemonic stored in process.env.DISPENSER_MNEMONIC. + * @returns The account + */ + public dispenser = async () => this.signerAccount(await algokit.getDispenserAccount(this._clientManager.algod, this._clientManager.kmd)) + + /** + * Returns an Algorand account with private key loaded for the default LocalNet dispenser account (that can be used to fund other accounts). + * + * @example + * ```typescript + * const account = await account.localNetDispenser() + * ``` + * @returns The account + */ + public localNetDispenser = async () => + this.signerAccount(await algokit.getLocalNetDispenserAccount(this._clientManager.algod, this._clientManager.kmd)) +} diff --git a/src/types/account.ts b/src/types/account.ts index ee102cf4..a0dd0629 100644 --- a/src/types/account.ts +++ b/src/types/account.ts @@ -1,4 +1,5 @@ import algosdk from 'algosdk' +import AccountInformationModel = algosdk.modelsv2.Account import Account = algosdk.Account import MultisigMetadata = algosdk.MultisigMetadata import Transaction = algosdk.Transaction @@ -126,3 +127,7 @@ export interface AccountConfig { /** @deprecated Renamed to senderAddress in 2.3.1 */ senderMnemonic?: string } + +type NumberConverter = { [key in keyof T]: ToNumberIfExtends } +type ToNumberIfExtends = K extends E ? number : K +export type AccountInformation = Omit, 'get_obj_for_encoding'> diff --git a/src/client.spec.ts b/src/types/algorand-client.spec.ts similarity index 58% rename from src/client.spec.ts rename to src/types/algorand-client.spec.ts index a5c3b477..d85a8ca9 100644 --- a/src/client.spec.ts +++ b/src/types/algorand-client.spec.ts @@ -1,75 +1,67 @@ /* eslint-disable no-console */ -import algosdk from 'algosdk' -import { TestContractClient } from '../tests/example-contracts/client/TestContractClient' -import AlgokitClient from './client' -import * as algokit from './index' -import { algorandFixture } from './testing' - -describe('client', () => { - let client: AlgokitClient - let alice: algosdk.Account - let bob: algosdk.Account +import { TestContractClient } from '../../tests/example-contracts/client/TestContractClient' +import * as algokit from '../index' +import { algorandFixture } from '../testing' +import { TransactionSignerAccount } from './account' +import AlgorandClient from './algorand-client' + +describe('AlgorandClient', () => { + let algorand: AlgorandClient + let alice: TransactionSignerAccount + let bob: TransactionSignerAccount let appClient: TestContractClient - let appID: bigint + let appId: number const fixture = algorandFixture() beforeAll(async () => { - const algod = algokit.getAlgoClient(algokit.getDefaultLocalNetConfig('algod')) - client = new AlgokitClient({ algodClient: algod }) await fixture.beforeEach() alice = fixture.context.testAccount bob = await fixture.context.generateAccount({ initialFunds: algokit.microAlgos(100_000) }) - client.signers[alice.addr] = algosdk.makeBasicAccountTransactionSigner(alice) - client.signers[bob.addr] = algosdk.makeBasicAccountTransactionSigner(bob) - - appClient = new TestContractClient( - { - id: 0, - resolveBy: 'id', - sender: { addr: alice.addr, signer: client.signers[alice.addr] }, - }, - algod, - ) - await appClient.create.createApplication({}) + algorand = fixture.algorand + appClient = algorand.client.getTypedAppClientById(TestContractClient, { + id: 0, + sender: alice, + }) - appID = BigInt((await appClient.appClient.getAppReference()).appId) + const app = await appClient.create.createApplication({}) + appId = Number(app.appId) }) test('sendPayment', async () => { - const alicePreBalance = (await client.algod.accountInformation(alice.addr).do()).amount - const bobPreBalance = (await client.algod.accountInformation(bob.addr).do()).amount - await client.send.payment({ sender: alice.addr, to: bob.addr, amount: algokit.microAlgos(1) }) - const alicePostBalance = (await client.algod.accountInformation(alice.addr).do()).amount - const bobPostBalance = (await client.algod.accountInformation(bob.addr).do()).amount + const alicePreBalance = (await algorand.account.getInformation(alice)).amount + const bobPreBalance = (await algorand.account.getInformation(bob)).amount + await algorand.send.payment({ sender: alice.addr, to: bob.addr, amount: algokit.microAlgos(1) }) + const alicePostBalance = (await algorand.account.getInformation(alice)).amount + const bobPostBalance = (await algorand.account.getInformation(bob)).amount expect(alicePostBalance).toBe(alicePreBalance - 1001) expect(bobPostBalance).toBe(bobPreBalance + 1) }) test('sendAssetCreate', async () => { - const createResult = await client.send.assetCreate({ sender: alice.addr, total: 100n }) + const createResult = await algorand.send.assetCreate({ sender: alice.addr, total: 100n }) - const assetIndex = Number(createResult.confirmations![0].assetIndex) + const assetIndex = Number(createResult.confirmation.assetIndex) expect(assetIndex).toBeGreaterThan(0) }) test('addAtc from generated client', async () => { - const alicePreBalance = (await client.algod.accountInformation(alice.addr).do()).amount - const bobPreBalance = (await client.algod.accountInformation(bob.addr).do()).amount + const alicePreBalance = (await algorand.account.getInformation(alice)).amount + const bobPreBalance = (await algorand.account.getInformation(bob)).amount const doMathAtc = await appClient.compose().doMath({ a: 1, b: 2, operation: 'sum' }).atc() - const result = await client + const result = await algorand .newGroup() .addPayment({ sender: alice.addr, to: bob.addr, amount: algokit.microAlgos(1) }) .addAtc(doMathAtc) .execute() - const alicePostBalance = (await client.algod.accountInformation(alice.addr).do()).amount - const bobPostBalance = (await client.algod.accountInformation(bob.addr).do()).amount + const alicePostBalance = (await algorand.account.getInformation(alice)).amount + const bobPostBalance = (await algorand.account.getInformation(bob)).amount expect(alicePostBalance).toBe(alicePreBalance - 2001) expect(bobPostBalance).toBe(bobPreBalance + 1) @@ -78,22 +70,22 @@ describe('client', () => { }) test('addMethodCall', async () => { - const alicePreBalance = (await client.algod.accountInformation(alice.addr).do()).amount - const bobPreBalance = (await client.algod.accountInformation(bob.addr).do()).amount + const alicePreBalance = (await algorand.account.getInformation(alice)).amount + const bobPreBalance = (await algorand.account.getInformation(bob)).amount - const methodRes = await client + const methodRes = await algorand .newGroup() .addPayment({ sender: alice.addr, to: bob.addr, amount: algokit.microAlgos(1), note: new Uint8Array([1]) }) .addMethodCall({ sender: alice.addr, - appID: appID, + appId: appId, method: appClient.appClient.getABIMethod('doMath')!, args: [1, 2, 'sum'], }) .execute() - const alicePostBalance = (await client.algod.accountInformation(alice.addr).do()).amount - const bobPostBalance = (await client.algod.accountInformation(bob.addr).do()).amount + const alicePostBalance = (await algorand.account.getInformation(alice)).amount + const bobPostBalance = (await algorand.account.getInformation(bob)).amount expect(alicePostBalance).toBe(alicePreBalance - 2001) expect(bobPostBalance).toBe(bobPreBalance + 1) @@ -104,12 +96,12 @@ describe('client', () => { test('method with txn arg', async () => { const txnArgParams = { sender: alice.addr, - appID: appID, + appId: appId, method: appClient.appClient.getABIMethod('txnArg')!, args: [{ type: 'pay' as const, sender: alice.addr, to: alice.addr, amount: algokit.microAlgos(0) }], } - const txnRes = await client + const txnRes = await algorand .newGroup() .addPayment({ sender: alice.addr, to: alice.addr, amount: algokit.microAlgos(0), note: new Uint8Array([1]) }) .addMethodCall(txnArgParams) @@ -122,50 +114,50 @@ describe('client', () => { const helloWorldParams = { type: 'methodCall' as const, sender: alice.addr, - appID: appID, + appId: appId, method: appClient.appClient.getABIMethod('helloWorld')!, } - const methodArgRes = await client + const methodArgRes = await algorand .newGroup() .addMethodCall({ sender: alice.addr, - appID, + appId: appId, method: appClient.appClient.getABIMethod('methodArg')!, args: [helloWorldParams], }) .execute() expect(methodArgRes.returns?.[0].returnValue?.valueOf()).toBe('Hello, World!') - expect(methodArgRes.returns?.[1].returnValue?.valueOf()).toBe(BigInt(appID)) + expect(methodArgRes.returns?.[1].returnValue?.valueOf()).toBe(BigInt(appId)) }) test('method with method call arg that has a txn arg', async () => { const txnArgParams = { sender: alice.addr, - appID: appID, + appId: appId, method: appClient.appClient.getABIMethod('txnArg')!, args: [{ type: 'pay' as const, sender: alice.addr, to: alice.addr, amount: algokit.microAlgos(0) }], } - const nestedTxnArgRes = await client + const nestedTxnArgRes = await algorand .newGroup() .addMethodCall({ sender: alice.addr, - appID, + appId: appId, method: appClient.appClient.getABIMethod('nestedTxnArg')!, args: [{ type: 'methodCall', ...txnArgParams }], }) .execute() expect(nestedTxnArgRes.returns?.[0].returnValue?.valueOf()).toBe(alice.addr) - expect(nestedTxnArgRes.returns?.[1].returnValue?.valueOf()).toBe(BigInt(appID)) + expect(nestedTxnArgRes.returns?.[1].returnValue?.valueOf()).toBe(BigInt(appId)) }) test('method with two method call args that each have a txn arg', async () => { const txnArgParams = { sender: alice.addr, - appID: appID, + appId: appId, method: appClient.appClient.getABIMethod('txnArg')!, args: [{ type: 'pay' as const, sender: alice.addr, to: alice.addr, amount: algokit.microAlgos(0) }], } @@ -173,17 +165,17 @@ describe('client', () => { const secondTxnArgParams = { type: 'methodCall' as const, sender: alice.addr, - appID, + appId: appId, method: appClient.appClient.getABIMethod('txnArg')!, args: [{ type: 'pay' as const, sender: alice.addr, to: alice.addr, amount: algokit.microAlgos(1) }], note: new Uint8Array([1]), } - const doubleNestedTxnArgRes = await client + const doubleNestedTxnArgRes = await algorand .newGroup() .addMethodCall({ sender: alice.addr, - appID, + appId: appId, method: appClient.appClient.getABIMethod('doubleNestedTxnArg')!, args: [{ type: 'methodCall', ...txnArgParams }, secondTxnArgParams], }) @@ -191,18 +183,18 @@ describe('client', () => { expect(doubleNestedTxnArgRes.returns?.[0].returnValue?.valueOf()).toBe(alice.addr) expect(doubleNestedTxnArgRes.returns?.[1].returnValue?.valueOf()).toBe(alice.addr) - expect(doubleNestedTxnArgRes.returns?.[2].returnValue?.valueOf()).toBe(BigInt(appID)) + expect(doubleNestedTxnArgRes.returns?.[2].returnValue?.valueOf()).toBe(BigInt(appId)) }) test('assetOptIn', async () => { - const { algod, testAccount } = fixture.context - const assetID = BigInt((await client.send.assetCreate({ sender: alice.addr, total: 1n })).confirmations![0].assetIndex!) + const { algod } = fixture.context + const assetId = Number((await algorand.send.assetCreate({ sender: alice.addr, total: 1 })).confirmation.assetIndex!) - await client.send.assetOptIn({ - sender: testAccount.addr, - assetID, - signer: algosdk.makeBasicAccountTransactionSigner(testAccount), + await algorand.send.assetOptIn({ + sender: alice.addr, + assetId: assetId, + signer: alice, }) - expect(await algod.accountAssetInformation(testAccount.addr, Number(assetID)).do()).toBeDefined() + expect(await algod.accountAssetInformation(alice.addr, Number(assetId)).do()).toBeDefined() }) }) diff --git a/src/types/algorand-client.ts b/src/types/algorand-client.ts new file mode 100644 index 00000000..cd302a0f --- /dev/null +++ b/src/types/algorand-client.ts @@ -0,0 +1,274 @@ +import algosdk from 'algosdk' +import { getAlgoNodeConfig, getConfigFromEnvOrDefaults, getDefaultLocalNetConfig } from '..' +import { TransactionSignerAccount } from './account' +import { AccountManager } from './account-manager' +import { AlgoSdkClients, ClientManager } from './client-manager' +import AlgokitComposer, { + AppCallParams, + AssetConfigParams, + AssetCreateParams, + AssetDestroyParams, + AssetFreezeParams, + AssetOptInParams, + AssetTransferParams, + KeyRegParams, + MethodCallParams, + PayTxnParams, +} from './composer' +import { AlgoConfig } from './network-client' +import { SendAtomicTransactionComposerResults, SendTransactionFrom } from './transaction' + +async function unwrapSingleSendResult(results: Promise) { + const result = await results + + return { + // Last item covers when a group is created by an app call with ABI transaction parameters + transaction: result.transactions[result.transactions.length - 1], + confirmation: result.confirmations![result.confirmations!.length - 1], + txId: result.txIds[0], + ...result, + } +} + +/** A client that brokers easy access to Algorand functionality. */ +export default class AlgorandClient { + private _clientManager: ClientManager + private _accountManager: AccountManager + + private _cachedSuggestedParams?: algosdk.SuggestedParams + private _cachedSuggestedParamsExpiry?: Date + private _cachedSuggestedParamsTimeout: number = 3_000 // three seconds + + private constructor(config: AlgoConfig | AlgoSdkClients) { + this._clientManager = new ClientManager(config) + this._accountManager = new AccountManager(this._clientManager) + } + + /** + * 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 + */ + public withDefaultSigner(signer: algosdk.TransactionSigner | TransactionSignerAccount): AlgorandClient { + this._accountManager.withDefaultSigner(signer) + return this + } + + /** + * Tracks the given account for later signing. + * @param account The account to register + * @returns The `AlgorandClient` so method calls can be chained + */ + public withAccount(account: TransactionSignerAccount | SendTransactionFrom) { + this._accountManager.withAccount(account) + return this + } + + /** + * Tracks the given account 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 + */ + public withSigner(sender: string, signer: algosdk.TransactionSigner) { + this._accountManager.withSigner(sender, signer) + return this + } + + /** + * Sets a cache value to use for suggested params. + * @param suggestedParams The suggested params to use + * @param until A date until which to cache, or if not specified then the timeout is used + * @returns The `AlgorandClient` so method calls can be chained + */ + public withSuggestedParams(suggestedParams: algosdk.SuggestedParams, until?: Date) { + this._cachedSuggestedParams = suggestedParams + this._cachedSuggestedParamsExpiry = until ?? new Date(+new Date() + this._cachedSuggestedParamsTimeout) + return this + } + + /** + * Sets the timeout for caching suggested params. + * @param timeout The timeout in milliseconds + * @returns The `AlgorandClient` so method calls can be chained + */ + public withSuggestedParamsTimeout(timeout: number) { + this._cachedSuggestedParamsTimeout = timeout + return this + } + + /** Get suggested params for a transaction (either cached or from algod if the cache is stale or empty) */ + async getSuggestedParams(): Promise { + if (this._cachedSuggestedParams && (!this._cachedSuggestedParamsExpiry || this._cachedSuggestedParamsExpiry > new Date())) { + return { + ...this._cachedSuggestedParams, + } + } + + this._cachedSuggestedParams = await this._clientManager.algod.getTransactionParams().do() + this._cachedSuggestedParamsExpiry = new Date(new Date().getTime() + this._cachedSuggestedParamsTimeout) + + return { + ...this._cachedSuggestedParams, + } + } + + /** Get clients, including algosdk clients and app clients. */ + public get client() { + return this._clientManager + } + + /** Get or create accounts that can sign transactions. */ + public get account() { + return this._accountManager + } + + /** Start a new `AlgokitComposer` transaction group */ + newGroup() { + return new AlgokitComposer( + this.client.algod, + (addr: string) => this.account.getSigner(addr), + () => this.getSuggestedParams(), + ) + } + + /** + * Methods for sending a transaction + */ + send = { + payment: (params: PayTxnParams) => { + return unwrapSingleSendResult(this.newGroup().addPayment(params).execute()) + }, + assetCreate: (params: AssetCreateParams) => { + return unwrapSingleSendResult(this.newGroup().addAssetCreate(params).execute()) + }, + assetConfig: (params: AssetConfigParams) => { + return unwrapSingleSendResult(this.newGroup().addAssetConfig(params).execute()) + }, + assetFreeze: (params: AssetFreezeParams) => { + return unwrapSingleSendResult(this.newGroup().addAssetFreeze(params).execute()) + }, + assetDestroy: (params: AssetDestroyParams) => { + return unwrapSingleSendResult(this.newGroup().addAssetDestroy(params).execute()) + }, + assetTransfer: (params: AssetTransferParams) => { + return unwrapSingleSendResult(this.newGroup().addAssetTransfer(params).execute()) + }, + appCall: (params: AppCallParams) => { + return unwrapSingleSendResult(this.newGroup().addAppCall(params).execute()) + }, + keyReg: (params: KeyRegParams) => { + return unwrapSingleSendResult(this.newGroup().addKeyReg(params).execute()) + }, + methodCall: (params: MethodCallParams) => { + return unwrapSingleSendResult(this.newGroup().addMethodCall(params).execute()) + }, + assetOptIn: (params: AssetOptInParams) => { + return unwrapSingleSendResult(this.newGroup().addAssetOptIn(params).execute()) + }, + } + + /** + * Methods for building transactions + */ + transactions = { + payment: async (params: PayTxnParams) => { + return (await this.newGroup().addPayment(params).buildGroup()).map((ts) => ts.txn)[0] + }, + assetCreate: async (params: AssetCreateParams) => { + return (await this.newGroup().addAssetCreate(params).buildGroup()).map((ts) => ts.txn)[0] + }, + assetConfig: async (params: AssetConfigParams) => { + return (await this.newGroup().addAssetConfig(params).buildGroup()).map((ts) => ts.txn)[0] + }, + assetFreeze: async (params: AssetFreezeParams) => { + return (await this.newGroup().addAssetFreeze(params).buildGroup()).map((ts) => ts.txn)[0] + }, + assetDestroy: async (params: AssetDestroyParams) => { + return (await this.newGroup().addAssetDestroy(params).buildGroup()).map((ts) => ts.txn)[0] + }, + assetTransfer: async (params: AssetTransferParams) => { + return (await this.newGroup().addAssetTransfer(params).buildGroup()).map((ts) => ts.txn)[0] + }, + appCall: async (params: AppCallParams) => { + return (await this.newGroup().addAppCall(params).buildGroup()).map((ts) => ts.txn)[0] + }, + keyReg: async (params: KeyRegParams) => { + return (await this.newGroup().addKeyReg(params).buildGroup()).map((ts) => ts.txn)[0] + }, + methodCall: async (params: MethodCallParams) => { + return (await this.newGroup().addMethodCall(params).buildGroup()).map((ts) => ts.txn) + }, + assetOptIn: async (params: AssetOptInParams) => { + return (await this.newGroup().addAssetOptIn(params).buildGroup()).map((ts) => ts.txn)[0] + }, + } + + // Static methods to create an `AlgorandClient` + + /** + * Returns an `AlgorandClient` pointing at default LocalNet ports and API token. + * @returns The `AlgorandClient` + */ + public static defaultLocalNet() { + return new AlgorandClient({ + algodConfig: getDefaultLocalNetConfig('algod'), + indexerConfig: getDefaultLocalNetConfig('indexer'), + kmdConfig: getDefaultLocalNetConfig('kmd'), + }) + } + + /** + * Returns an `AlgorandClient` pointing at TestNet using AlgoNode. + * @returns The `AlgorandClient` + */ + public static testNet() { + return new AlgorandClient({ + algodConfig: getAlgoNodeConfig('testnet', 'algod'), + indexerConfig: getAlgoNodeConfig('testnet', 'indexer'), + kmdConfig: undefined, + }) + } + + /** + * Returns an `AlgorandClient` pointing at MainNet using AlgoNode. + * @returns The `AlgorandClient` + */ + public static mainNet() { + return new AlgorandClient({ + algodConfig: getAlgoNodeConfig('mainnet', 'algod'), + indexerConfig: getAlgoNodeConfig('mainnet', 'indexer'), + kmdConfig: undefined, + }) + } + + /** + * Returns an `AlgorandClient` pointing to the given client(s). + * @param clients The clients to use + * @returns The `AlgorandClient` + */ + public static fromClients(clients: AlgoSdkClients) { + return new AlgorandClient(clients) + } + + /** + * Returns an `AlgorandClient` loading the configuration from environment variables. + * + * Retrieve configurations from environment variables when defined or get defaults. + * + * Expects to be called from a Node.js environment. + * @returns The `AlgorandClient` + */ + public static fromEnvironment() { + return new AlgorandClient(getConfigFromEnvOrDefaults()) + } + + /** + * Returns an `AlgorandClient` from the given config. + * @param config The config to use + * @returns The `AlgorandClient` + */ + public static fromConfig(config: AlgoConfig) { + return new AlgorandClient(config) + } +} diff --git a/src/types/client-manager.ts b/src/types/client-manager.ts new file mode 100644 index 00000000..125dc14a --- /dev/null +++ b/src/types/client-manager.ts @@ -0,0 +1,146 @@ +import algosdk from 'algosdk' +import { getAlgoClient, getAlgoIndexerClient, getAlgoKmdClient, getTestNetDispenserApiClient } from '..' +import { AppLookup } from './app' +import { + AppDetails, + AppDetailsBase, + AppSpecAppDetailsBase, + ApplicationClient, + ResolveAppByCreatorAndNameBase, + ResolveAppByIdBase, +} from './app-client' +import { TestNetDispenserApiClientParams } from './dispenser-client' +import { AlgoConfig } from './network-client' + +/** Clients from algosdk that interact with the official Algorand APIs */ +export interface AlgoSdkClients { + /** Algod client, see https://developer.algorand.org/docs/rest-apis/algod/ */ + algod: algosdk.Algodv2 + /** Optional indexer client, see https://developer.algorand.org/docs/rest-apis/indexer/ */ + indexer?: algosdk.Indexer + /** Optional KMD client, see https://developer.algorand.org/docs/rest-apis/kmd/ */ + kmd?: algosdk.Kmd +} + +/** Exposes access to various API clients. */ +export class ClientManager { + private _algod: algosdk.Algodv2 + private _indexer?: algosdk.Indexer + private _kmd?: algosdk.Kmd + + /** + * algosdk clients or config for interacting with the official Algorand APIs. + * @param clientsOrConfig The clients or config to use + */ + constructor(clientsOrConfig: AlgoConfig | AlgoSdkClients) { + const _clients = + 'algod' in clientsOrConfig + ? clientsOrConfig + : { + algod: getAlgoClient(clientsOrConfig.algodConfig), + indexer: clientsOrConfig.indexerConfig ? getAlgoIndexerClient(clientsOrConfig.indexerConfig) : undefined, + kmd: clientsOrConfig.kmdConfig ? getAlgoKmdClient(clientsOrConfig.kmdConfig) : undefined, + } + this._algod = _clients.algod + this._indexer = _clients.indexer + this._kmd = _clients.kmd + } + + /** Returns an algosdk Algod API client. */ + public get algod(): algosdk.Algodv2 { + return this._algod + } + + /** Returns an algosdk Indexer API client or throws an error if it's not been provided. */ + public get indexer(): algosdk.Indexer { + if (!this._indexer) throw new Error('Attempt to use Indexer client in AlgoKit instance with no Indexer configured') + return this._indexer + } + + /** Returns an algosdk KMD API client or throws an error if it's not been provided. */ + public get kmd(): algosdk.Kmd { + if (!this._kmd) throw new Error('Attempt to use Kmd client in AlgoKit instance with no Kmd configured') + return this._kmd + } + + /** + * Returns a TestNet Dispenser API client. + * Refer to [docs](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md) on guidance to obtain an access token. + * + * @param params An object containing parameters for the TestNetDispenserApiClient class. + * Or null if you want the client to load the access token from the environment variable `ALGOKIT_DISPENSER_ACCESS_TOKEN`. + * @example + * const client = algokit.getTestNetDispenserApiClient( + * { + * authToken: 'your_auth_token', + * requestTimeout: 15, + * } + * ) + * + * @returns An instance of the TestNetDispenserApiClient class. + */ + public getTestNetDispenser(params: TestNetDispenserApiClientParams | null = null) { + return getTestNetDispenserApiClient(params) + } + + /** + * Returns a new `ApplicationClient` client, resolving the app by creator address and name. + * @param details The details to resolve the app by creator address and name + * @param cachedAppLookup A cached app lookup that matches a name to on-chain details; either this is needed or indexer is required to be passed in to this manager on construction. + * @returns The `ApplicationClient` + */ + public getAppClientByCreatorAndName(details: AppClientByCreatorAndNameDetails, cachedAppLookup?: AppLookup) { + return new ApplicationClient( + { ...details, resolveBy: 'creatorAndName', findExistingUsing: cachedAppLookup ?? this.indexer }, + this._algod, + ) + } + + /** + * Returns a new `ApplicationClient` client, resolving the app by app ID. + * @param details The details to resolve the app by ID + * @returns The `ApplicationClient` + */ + public getAppClientById(details: AppClientByIdDetails) { + return new ApplicationClient({ ...details, resolveBy: 'id' }, this._algod) + } + + /** + * Returns a new typed client, resolving the app by creator address and name. + * @param typedClient The typed client type to use + * @param details The details to resolve the app by creator address and name + * @param cachedAppLookup A cached app lookup that matches a name to on-chain details; either this is needed or indexer is required to be passed in to this manager on construction. + * @returns The typed client instance + */ + public getTypedAppClientByCreatorAndName( + typedClient: TypedAppClient, + details: TypedAppClientByCreatorAndNameDetails, + cachedAppLookup?: AppLookup, + ) { + return new typedClient({ ...details, resolveBy: 'creatorAndName', findExistingUsing: cachedAppLookup ?? this.indexer }, this._algod) + } + + /** + * Returns a new typed client, resolving the app by app ID. + * @param typedClient The typed client type to use + * @param details The details to resolve the app by ID + * @returns The typed client instance + */ + public getTypedAppClientById(typedClient: TypedAppClient, details: TypedAppClientByIdDetails) { + return new typedClient({ ...details, resolveBy: 'id' }, this._algod) + } +} + +export interface TypedAppClient { + new (details: AppDetails, algod: algosdk.Algodv2): TClient +} + +export type AppClientByCreatorAndNameDetails = AppSpecAppDetailsBase & + AppDetailsBase & + Omit + +export type TypedAppClientByCreatorAndNameDetails = AppDetailsBase & Omit + +export type AppClientByIdDetails = AppSpecAppDetailsBase & AppDetailsBase & ResolveAppByIdBase + +export type TypedAppClientByIdDetails = AppDetailsBase & ResolveAppByIdBase diff --git a/src/composer.ts b/src/types/composer.ts similarity index 94% rename from src/composer.ts rename to src/types/composer.ts index ec2605a1..52a62026 100644 --- a/src/composer.ts +++ b/src/types/composer.ts @@ -1,12 +1,13 @@ import algosdk from 'algosdk' -import { sendAtomicTransactionComposer } from './transaction/transaction' -import { AlgoAmount } from './types/amount' +import { sendAtomicTransactionComposer } from '../transaction/transaction' +import { TransactionSignerAccount } from './account' +import { AlgoAmount } from './amount' export type CommonTxnParams = { /** The address sending the transaction */ sender: string /** The function used to sign transactions */ - signer?: algosdk.TransactionSigner + signer?: algosdk.TransactionSigner | TransactionSignerAccount /** Change the signing key of the sender to the given address */ rekeyTo?: string /** Note to attach to the transaction*/ @@ -26,9 +27,9 @@ export type CommonTxnParams = { * If left undefined, the value from algod will be used. * Only set this when you intentionally want this to be some time in the future */ - firstValidRound?: bigint + firstValidRound?: number /** The last round this transaction is valid. It is recommended to use validityWindow instead */ - lastValidRound?: bigint + lastValidRound?: number } export type PayTxnParams = CommonTxnParams & { @@ -40,7 +41,7 @@ export type PayTxnParams = CommonTxnParams & { export type AssetCreateParams = CommonTxnParams & { /** The total amount of the smallest divisible unit to create */ - total: bigint + total: number | bigint /** The amount of decimal places the asset should have */ decimals?: number /** Whether the asset is frozen by default in the creator address */ @@ -65,7 +66,7 @@ export type AssetCreateParams = CommonTxnParams & { export type AssetConfigParams = CommonTxnParams & { /** ID of the asset */ - assetID: bigint + assetId: number /** The address that can change the manager, reserve, clawback, and freeze addresses */ manager?: string /** The address that holds the uncirculated supply */ @@ -78,7 +79,7 @@ export type AssetConfigParams = CommonTxnParams & { export type AssetFreezeParams = CommonTxnParams & { /** The ID of the asset */ - assetID: bigint + assetId: number /** The account to freeze or unfreeze */ account: string /** Whether the assets in the account should be frozen */ @@ -87,24 +88,24 @@ export type AssetFreezeParams = CommonTxnParams & { export type AssetDestroyParams = CommonTxnParams & { /** ID of the asset */ - assetID: bigint + assetId: number } export type KeyRegParams = CommonTxnParams & { voteKey?: Uint8Array selectionKey?: Uint8Array - voteFirst: bigint - voteLast: bigint - voteKeyDilution: bigint + voteFirst: number + voteLast: number + voteKeyDilution: number nonParticipation: boolean stateProofKey?: Uint8Array } export type AssetTransferParams = CommonTxnParams & { /** ID of the asset */ - assetID: bigint + assetId: number /** Amount of the asset to transfer (smallest divisible unit) */ - amount: bigint + amount: number | bigint /** The account to send the asset to */ to: string /** The account to take the asset from */ @@ -115,14 +116,14 @@ export type AssetTransferParams = CommonTxnParams & { export type AssetOptInParams = CommonTxnParams & { /** ID of the asset */ - assetID: bigint + assetId: number } export type AppCallParams = CommonTxnParams & { /** The [OnComplete](https://developer.algorand.org/docs/get-details/dapps/avm/teal/specification/#oncomplete) */ onComplete?: algosdk.OnApplicationComplete /** ID of the application */ - appID?: bigint + appId?: number /** The program to execute for all OnCompletes other than ClearState */ approvalProgram?: Uint8Array /** The program to execute for ClearState OnComplete */ @@ -143,9 +144,9 @@ export type AppCallParams = CommonTxnParams & { /** Account references */ accountReferences?: string[] /** App references */ - appReferences?: bigint[] + appReferences?: number[] /** Asset references */ - assetReferences?: bigint[] + assetReferences?: number[] /** Number of extra pages required for the programs */ extraPages?: number /** Box references */ @@ -155,7 +156,7 @@ export type AppCallParams = CommonTxnParams & { export type MethodCallParams = CommonTxnParams & Omit & { /** ID of the application */ - appID: bigint + appId: number /** The ABI method to call */ method: algosdk.ABIMethod /** Arguments to the ABI method */ @@ -378,7 +379,10 @@ export default class AlgokitComposer { throw Error(`Unsupported method arg transaction type: ${arg.type}`) } - methodArgs.push({ txn, signer: params.signer || this.getSigner(params.sender) }) + methodArgs.push({ + txn, + signer: params.signer ? ('signer' in params.signer ? params.signer.signer : params.signer) : this.getSigner(params.sender), + }) return } @@ -389,11 +393,11 @@ export default class AlgokitComposer { methodAtc.addMethodCall({ ...params, - appID: Number(params.appID || 0), + appID: Number(params.appId || 0), note: params.note ? new Uint8Array(Buffer.from(params.note)) : undefined, lease: params.lease ? new Uint8Array(Buffer.from(params.lease)) : undefined, suggestedParams, - signer: params.signer ?? this.getSigner(params.sender), + signer: params.signer ? ('signer' in params.signer ? params.signer.signer : params.signer) : this.getSigner(params.sender), methodArgs: methodArgs, }) @@ -445,7 +449,7 @@ export default class AlgokitComposer { const onComplete = params.onComplete || algosdk.OnApplicationComplete.NoOpOC - if (!params.appID) { + if (!params.appId) { if (params.approvalProgram === undefined || params.clearProgram === undefined) { throw new Error('approvalProgram and clearProgram are required for application creation') } @@ -458,7 +462,7 @@ export default class AlgokitComposer { }) } - txn = algosdk.makeApplicationCallTxnFromObject({ ...sdkParams, onComplete, appIndex: Number(params.appID || 0) }) + txn = algosdk.makeApplicationCallTxnFromObject({ ...sdkParams, onComplete, appIndex: Number(params.appId || 0) }) return this.commonTxnBuildStep(params, txn, suggestedParams) } @@ -466,7 +470,7 @@ export default class AlgokitComposer { private buildAssetConfig(params: AssetConfigParams, suggestedParams: algosdk.SuggestedParams) { const txn = algosdk.makeAssetConfigTxnWithSuggestedParamsFromObject({ from: params.sender, - assetIndex: Number(params.assetID), + assetIndex: Number(params.assetId), suggestedParams, manager: params.manager, reserve: params.reserve, @@ -481,7 +485,7 @@ export default class AlgokitComposer { private buildAssetDestroy(params: AssetDestroyParams, suggestedParams: algosdk.SuggestedParams) { const txn = algosdk.makeAssetDestroyTxnWithSuggestedParamsFromObject({ from: params.sender, - assetIndex: Number(params.assetID), + assetIndex: Number(params.assetId), suggestedParams, }) @@ -491,7 +495,7 @@ export default class AlgokitComposer { private buildAssetFreeze(params: AssetFreezeParams, suggestedParams: algosdk.SuggestedParams) { const txn = algosdk.makeAssetFreezeTxnWithSuggestedParamsFromObject({ from: params.sender, - assetIndex: Number(params.assetID), + assetIndex: Number(params.assetId), freezeTarget: params.account, freezeState: params.frozen, suggestedParams, @@ -504,7 +508,7 @@ export default class AlgokitComposer { const txn = algosdk.makeAssetTransferTxnWithSuggestedParamsFromObject({ from: params.sender, to: params.to, - assetIndex: Number(params.assetID), + assetIndex: Number(params.assetId), amount: params.amount, suggestedParams, closeRemainderTo: params.closeAssetTo, @@ -563,7 +567,7 @@ export default class AlgokitComposer { return this.buildMethodCall(txn, suggestedParams) } - const signer = txn.signer ?? this.getSigner(txn.sender) + const signer = txn.signer ? ('signer' in txn.signer ? txn.signer.signer : txn.signer) : this.getSigner(txn.sender) switch (txn.type) { case 'pay': { diff --git a/src/types/network-client.ts b/src/types/network-client.ts index 42580d13..89a72714 100644 --- a/src/types/network-client.ts +++ b/src/types/network-client.ts @@ -15,7 +15,7 @@ export interface AlgoConfig { /** Algo client configuration */ algodConfig: AlgoClientConfig /** Indexer client configuration */ - indexerConfig: AlgoClientConfig + indexerConfig?: AlgoClientConfig /** Kmd configuration */ - kmdConfig: AlgoClientConfig + kmdConfig?: AlgoClientConfig } diff --git a/src/types/testing.ts b/src/types/testing.ts index 3442ed4b..66d23f6f 100644 --- a/src/types/testing.ts +++ b/src/types/testing.ts @@ -3,6 +3,8 @@ import { TransactionLogger } from '../testing' import { TestLogger } from '../testing/test-logger' import { AlgoAmount } from '../types/amount' import { SendTransactionFrom } from '../types/transaction' +import { TransactionSignerAccount } from './account' +import AlgorandClient from './algorand-client' import { TransactionLookupResult } from './indexer' import Account = algosdk.Account import Algodv2 = algosdk.Algodv2 @@ -23,9 +25,9 @@ export interface AlgorandTestAutomationContext { /** Transaction logger that will log transaction IDs for all transactions issued by `algod` */ transactionLogger: TransactionLogger /** Default, funded test account that is ephemerally created */ - testAccount: Account + testAccount: Account & TransactionSignerAccount /** Generate and fund an additional ephemerally created account */ - generateAccount: (params: GetTestAccountParams) => Promise + generateAccount: (params: GetTestAccountParams) => Promise /** Wait for the indexer to catch up with all transactions logged by `transactionLogger` */ waitForIndexer: () => Promise /** Wait for the indexer to catch up with the given transaction ID */ @@ -68,6 +70,11 @@ export interface AlgorandFixture { */ get context(): AlgorandTestAutomationContext + /** + * Retrieve an `AlgorandClient` loaded with the current context, including testAccount and any generated accounts loaded as signers. + */ + get algorand(): AlgorandClient + /** * Testing framework agnostic handler method to run before each test to prepare the `context` for that test. */ From 389345443ece942514a13ce124c6a33330351c4c Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Fri, 22 Mar 2024 10:42:37 -0400 Subject: [PATCH 2/2] use bigint where applicable --- src/types/algorand-client.spec.ts | 6 +++--- src/types/composer.ts | 32 +++++++++++++++---------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/types/algorand-client.spec.ts b/src/types/algorand-client.spec.ts index d85a8ca9..5a6273f4 100644 --- a/src/types/algorand-client.spec.ts +++ b/src/types/algorand-client.spec.ts @@ -10,7 +10,7 @@ describe('AlgorandClient', () => { let alice: TransactionSignerAccount let bob: TransactionSignerAccount let appClient: TestContractClient - let appId: number + let appId: bigint const fixture = algorandFixture() @@ -27,7 +27,7 @@ describe('AlgorandClient', () => { }) const app = await appClient.create.createApplication({}) - appId = Number(app.appId) + appId = BigInt(app.appId) }) test('sendPayment', async () => { @@ -188,7 +188,7 @@ describe('AlgorandClient', () => { test('assetOptIn', async () => { const { algod } = fixture.context - const assetId = Number((await algorand.send.assetCreate({ sender: alice.addr, total: 1 })).confirmation.assetIndex!) + const assetId = BigInt((await algorand.send.assetCreate({ sender: alice.addr, total: 1n })).confirmation.assetIndex!) await algorand.send.assetOptIn({ sender: alice.addr, diff --git a/src/types/composer.ts b/src/types/composer.ts index 52a62026..d441b395 100644 --- a/src/types/composer.ts +++ b/src/types/composer.ts @@ -27,9 +27,9 @@ export type CommonTxnParams = { * If left undefined, the value from algod will be used. * Only set this when you intentionally want this to be some time in the future */ - firstValidRound?: number + firstValidRound?: bigint /** The last round this transaction is valid. It is recommended to use validityWindow instead */ - lastValidRound?: number + lastValidRound?: bigint } export type PayTxnParams = CommonTxnParams & { @@ -41,7 +41,7 @@ export type PayTxnParams = CommonTxnParams & { export type AssetCreateParams = CommonTxnParams & { /** The total amount of the smallest divisible unit to create */ - total: number | bigint + total: bigint /** The amount of decimal places the asset should have */ decimals?: number /** Whether the asset is frozen by default in the creator address */ @@ -66,7 +66,7 @@ export type AssetCreateParams = CommonTxnParams & { export type AssetConfigParams = CommonTxnParams & { /** ID of the asset */ - assetId: number + assetId: bigint /** The address that can change the manager, reserve, clawback, and freeze addresses */ manager?: string /** The address that holds the uncirculated supply */ @@ -79,7 +79,7 @@ export type AssetConfigParams = CommonTxnParams & { export type AssetFreezeParams = CommonTxnParams & { /** The ID of the asset */ - assetId: number + assetId: bigint /** The account to freeze or unfreeze */ account: string /** Whether the assets in the account should be frozen */ @@ -88,24 +88,24 @@ export type AssetFreezeParams = CommonTxnParams & { export type AssetDestroyParams = CommonTxnParams & { /** ID of the asset */ - assetId: number + assetId: bigint } export type KeyRegParams = CommonTxnParams & { voteKey?: Uint8Array selectionKey?: Uint8Array - voteFirst: number - voteLast: number - voteKeyDilution: number + voteFirst: bigint + voteLast: bigint + voteKeyDilution: bigint nonParticipation: boolean stateProofKey?: Uint8Array } export type AssetTransferParams = CommonTxnParams & { /** ID of the asset */ - assetId: number + assetId: bigint /** Amount of the asset to transfer (smallest divisible unit) */ - amount: number | bigint + amount: bigint /** The account to send the asset to */ to: string /** The account to take the asset from */ @@ -116,14 +116,14 @@ export type AssetTransferParams = CommonTxnParams & { export type AssetOptInParams = CommonTxnParams & { /** ID of the asset */ - assetId: number + assetId: bigint } export type AppCallParams = CommonTxnParams & { /** The [OnComplete](https://developer.algorand.org/docs/get-details/dapps/avm/teal/specification/#oncomplete) */ onComplete?: algosdk.OnApplicationComplete /** ID of the application */ - appId?: number + appId?: bigint /** The program to execute for all OnCompletes other than ClearState */ approvalProgram?: Uint8Array /** The program to execute for ClearState OnComplete */ @@ -144,9 +144,9 @@ export type AppCallParams = CommonTxnParams & { /** Account references */ accountReferences?: string[] /** App references */ - appReferences?: number[] + appReferences?: bigint[] /** Asset references */ - assetReferences?: number[] + assetReferences?: bigint[] /** Number of extra pages required for the programs */ extraPages?: number /** Box references */ @@ -156,7 +156,7 @@ export type AppCallParams = CommonTxnParams & { export type MethodCallParams = CommonTxnParams & Omit & { /** ID of the application */ - appId: number + appId: bigint /** The ABI method to call */ method: algosdk.ABIMethod /** Arguments to the ABI method */