From 0557f10045999c9bf34b9e187add2ba1eeac9507 Mon Sep 17 00:00:00 2001 From: Hoang Dinh Date: Thu, 24 Apr 2025 12:26:59 +1000 Subject: [PATCH 01/18] wip - build payment with algokit-core --- package-lock.json | 6 +++++ package.json | 1 + src/types/composer.ts | 58 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index ac46e59c..afb7b15f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@algorandfoundation/tealscript": "^0.106.3", + "algokit_transact": "file:../algokit-core/algokit_transact-0.1.0.tgz", "buffer": "^6.0.3" }, "devDependencies": { @@ -3074,6 +3075,11 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/algokit_transact": { + "version": "0.1.0", + "resolved": "file:../algokit-core/algokit_transact-0.1.0.tgz", + "integrity": "sha512-Il4DEzeuKcBAJWGl6j6i4RhCDk0+9A49XCxX/EdfK007JO+qWZ4QPK9fgWsjr6wNhPZE+7+k9oVcmep7ADhZGg==" + }, "node_modules/algorand-msgpack": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/algorand-msgpack/-/algorand-msgpack-1.1.0.tgz", diff --git a/package.json b/package.json index 26c74024..d5bf83c8 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ }, "dependencies": { "@algorandfoundation/tealscript": "^0.106.3", + "algokit_transact": "file:../algokit-core/algokit_transact-0.1.0.tgz", "buffer": "^6.0.3" }, "peerDependencies": { diff --git a/src/types/composer.ts b/src/types/composer.ts index ce2e0dd4..dad62e0c 100644 --- a/src/types/composer.ts +++ b/src/types/composer.ts @@ -1,3 +1,4 @@ +import { addressFromString, Transaction as AlgokitCoreTransaction, encodeTransactionRaw } from 'algokit_transact' import algosdk, { Address } from 'algosdk' import { Config } from '../config' import { encodeLease, getABIReturnValue, sendAtomicTransactionComposer } from '../transaction/transaction' @@ -1621,8 +1622,10 @@ export class TransactionComposer { }) } + // TODO: make sure that this is the only place a payment txn is built private buildPayment(params: PaymentParams, suggestedParams: algosdk.SuggestedParams) { - return this.commonTxnBuildStep(algosdk.makePaymentTxnWithSuggestedParamsFromObject, params, { + return this.commonTxnBuildStep(buildPaymentWithAlgokitCore, params, { + // return this.commonTxnBuildStep(algosdk.makePaymentTxnWithSuggestedParamsFromObject, params, { sender: params.sender, receiver: params.receiver, amount: params.amount.microAlgo, @@ -2096,3 +2099,56 @@ export class TransactionComposer { return encoder.encode(arc2Payload) } } + +function getAlgokitCoreAddress(address: string | Address) { + return addressFromString(typeof address === 'string' ? address : address.toString()) +} + +function buildPaymentWithAlgokitCore({ + sender, + receiver, + amount, + closeRemainderTo, + rekeyTo, + note, + lease, + suggestedParams, +}: algosdk.PaymentTransactionParams & algosdk.CommonTransactionParams) { + const txnModel: AlgokitCoreTransaction = { + header: { + sender: getAlgokitCoreAddress(sender), + transactionType: 'Payment', + fee: BigInt(suggestedParams.fee), + firstValid: BigInt(suggestedParams.firstValid), + lastValid: BigInt(suggestedParams.lastValid), + genesisHash: suggestedParams.genesisHash, + genesisId: suggestedParams.genesisID, + rekeyTo: rekeyTo ? getAlgokitCoreAddress(rekeyTo) : undefined, + note: note, + lease: lease, + }, + payFields: { + amount: BigInt(amount), + receiver: getAlgokitCoreAddress(receiver), + closeRemainderTo: closeRemainderTo ? getAlgokitCoreAddress(closeRemainderTo) : undefined, + }, + } + + // TODO: do we need to move this logic to Rust core? + + let fee = BigInt(suggestedParams.fee) + if (!suggestedParams.flatFee) { + const minFee = BigInt(suggestedParams.minFee) + const numAddlBytesAfterSigning = 75 + const estimateTxnSize = encodeTransactionRaw(txnModel).length + numAddlBytesAfterSigning + + fee *= BigInt(estimateTxnSize) + // If suggested fee too small and will be rejected, set to min tx fee + if (fee < minFee) { + fee = minFee + } + } + txnModel.header.fee = fee + + return algosdk.decodeUnsignedTransaction(encodeTransactionRaw(txnModel)) +} From 7b10db2391c8b9eab7e3a2d8aabcfb5584dbddf0 Mon Sep 17 00:00:00 2001 From: Hoang Dinh Date: Thu, 24 Apr 2025 23:12:43 +1000 Subject: [PATCH 02/18] wip --- src/types/algorand-client-transaction-sender.ts | 1 + src/types/composer.ts | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/algorand-client-transaction-sender.ts b/src/types/algorand-client-transaction-sender.ts index 8f163bf1..0ce1b644 100644 --- a/src/types/algorand-client-transaction-sender.ts +++ b/src/types/algorand-client-transaction-sender.ts @@ -201,6 +201,7 @@ export class AlgorandClientTransactionSender { * ``` * @returns The result of the payment transaction and the transaction that was sent */ + // TODO: PD - convert this to use algokit core completely payment = this._send((c) => c.addPayment, { preLog: (params, transaction) => `Sending ${params.amount.microAlgo} µALGO from ${params.sender} to ${params.receiver} via transaction ${transaction.txID()}`, diff --git a/src/types/composer.ts b/src/types/composer.ts index dad62e0c..a9f93792 100644 --- a/src/types/composer.ts +++ b/src/types/composer.ts @@ -1625,7 +1625,6 @@ export class TransactionComposer { // TODO: make sure that this is the only place a payment txn is built private buildPayment(params: PaymentParams, suggestedParams: algosdk.SuggestedParams) { return this.commonTxnBuildStep(buildPaymentWithAlgokitCore, params, { - // return this.commonTxnBuildStep(algosdk.makePaymentTxnWithSuggestedParamsFromObject, params, { sender: params.sender, receiver: params.receiver, amount: params.amount.microAlgo, From ca9ba1f273fa5b3a0f30f8665bfe1049ab8c49af Mon Sep 17 00:00:00 2001 From: Hoang Dinh Date: Mon, 28 Apr 2025 14:40:58 +1000 Subject: [PATCH 03/18] wip - convert send.payment to use core not done yet, still waiting for the generated http client --- src/app-deploy.ts | 2 +- src/transaction/legacy-bridge.ts | 2 +- .../algorand-client-transaction-sender.ts | 45 ++++++++++++++++--- src/types/algorand-client.ts | 7 ++- src/types/composer.ts | 6 +-- src/types/transaction.ts | 2 +- 6 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/app-deploy.ts b/src/app-deploy.ts index 36d3e8a6..076ac8ba 100644 --- a/src/app-deploy.ts +++ b/src/app-deploy.ts @@ -77,7 +77,7 @@ export async function deployApp( }) const deployer = new AppDeployer( appManager, - new AlgorandClientTransactionSender(newGroup, new AssetManager(algod, newGroup), appManager), + new AlgorandClientTransactionSender(newGroup, new AssetManager(algod, newGroup), appManager, algod), indexer, ) diff --git a/src/transaction/legacy-bridge.ts b/src/transaction/legacy-bridge.ts index 87970336..afb92856 100644 --- a/src/transaction/legacy-bridge.ts +++ b/src/transaction/legacy-bridge.ts @@ -50,7 +50,7 @@ export async function legacySendTransactionBridge (suggestedParams ? { ...suggestedParams } : await algod.getTransactionParams().do()), appManager, }) - const transactionSender = new AlgorandClientTransactionSender(newGroup, new AssetManager(algod, newGroup), appManager) + const transactionSender = new AlgorandClientTransactionSender(newGroup, new AssetManager(algod, newGroup), appManager, algod) const transactionCreator = new AlgorandClientTransactionCreator(newGroup) if (sendParams.fee) { diff --git a/src/types/algorand-client-transaction-sender.ts b/src/types/algorand-client-transaction-sender.ts index 0ce1b644..ed8ff02c 100644 --- a/src/types/algorand-client-transaction-sender.ts +++ b/src/types/algorand-client-transaction-sender.ts @@ -1,7 +1,9 @@ -import algosdk, { Address } from 'algosdk' +import algosdk, { Address, Algodv2 } from 'algosdk' import { Buffer } from 'buffer' import { Config } from '../config' +import { waitForConfirmation } from '../transaction' import { asJson, defaultJsonValueReplacer } from '../util' +import { AlgoAmount } from './amount' import { SendAppCreateTransactionResult, SendAppTransactionResult, SendAppUpdateTransactionResult } from './app' import { AppManager } from './app-manager' import { AssetManager } from './asset-manager' @@ -16,6 +18,7 @@ import { AppUpdateParams, AssetCreateParams, AssetOptOutParams, + CommonTransactionParams, TransactionComposer, } from './composer' import { SendParams, SendSingleTransactionResult } from './transaction' @@ -37,6 +40,7 @@ export class AlgorandClientTransactionSender { private _newGroup: () => TransactionComposer private _assetManager: AssetManager private _appManager: AppManager + private _algod: Algodv2 /** * Creates a new `AlgorandClientSender` @@ -48,10 +52,11 @@ export class AlgorandClientTransactionSender { * const transactionSender = new AlgorandClientTransactionSender(() => new TransactionComposer(), assetManager, appManager) * ``` */ - constructor(newGroup: () => TransactionComposer, assetManager: AssetManager, appManager: AppManager) { + constructor(newGroup: () => TransactionComposer, assetManager: AssetManager, appManager: AppManager, algod: Algodv2) { this._newGroup = newGroup this._assetManager = assetManager this._appManager = appManager + this._algod = algod } /** @@ -201,11 +206,39 @@ export class AlgorandClientTransactionSender { * ``` * @returns The result of the payment transaction and the transaction that was sent */ - // TODO: PD - convert this to use algokit core completely - payment = this._send((c) => c.addPayment, { - preLog: (params, transaction) => + payment = async ( + params: CommonTransactionParams & { + receiver: string | Address + amount: AlgoAmount + closeRemainderTo?: string | Address + } & SendParams, + ): Promise => { + const composer = this._newGroup() + composer.addPayment(params) + const { atc, transactions } = await composer.build() + + const transaction = transactions[0].txn + + Config.getLogger(params?.suppressLog).debug( `Sending ${params.amount.microAlgo} µALGO from ${params.sender} to ${params.receiver} via transaction ${transaction.txID()}`, - }) + ) + + atc.buildGroup() + const signedTxns = await atc.gatherSignatures() + + await this._algod.sendRawTransaction(signedTxns).do() + const confirmation = await waitForConfirmation(transaction.txID(), params.maxRoundsToWaitForConfirmation ?? 5, this._algod) + + return { + txIds: [transaction.txID()], + returns: undefined, + confirmation: confirmation, + transaction: transaction, + confirmations: [confirmation], + transactions: [transaction], + } satisfies SendSingleTransactionResult + } + /** * Create a new Algorand Standard Asset. * diff --git a/src/types/algorand-client.ts b/src/types/algorand-client.ts index 2e723c59..bd11d7dd 100644 --- a/src/types/algorand-client.ts +++ b/src/types/algorand-client.ts @@ -35,7 +35,12 @@ export class AlgorandClient { this._accountManager = new AccountManager(this._clientManager) this._appManager = new AppManager(this._clientManager.algod) this._assetManager = new AssetManager(this._clientManager.algod, () => this.newGroup()) - this._transactionSender = new AlgorandClientTransactionSender(() => this.newGroup(), this._assetManager, this._appManager) + this._transactionSender = new AlgorandClientTransactionSender( + () => this.newGroup(), + this._assetManager, + this._appManager, + this._clientManager.algod, + ) this._transactionCreator = new AlgorandClientTransactionCreator(() => this.newGroup()) this._appDeployer = new AppDeployer(this._appManager, this._transactionSender, this._clientManager.indexerIfPresent) } diff --git a/src/types/composer.ts b/src/types/composer.ts index a9f93792..dfa5f092 100644 --- a/src/types/composer.ts +++ b/src/types/composer.ts @@ -1624,7 +1624,7 @@ export class TransactionComposer { // TODO: make sure that this is the only place a payment txn is built private buildPayment(params: PaymentParams, suggestedParams: algosdk.SuggestedParams) { - return this.commonTxnBuildStep(buildPaymentWithAlgokitCore, params, { + return this.commonTxnBuildStep(buildPaymentWithAlgoKitCore, params, { sender: params.sender, receiver: params.receiver, amount: params.amount.microAlgo, @@ -2103,7 +2103,7 @@ function getAlgokitCoreAddress(address: string | Address) { return addressFromString(typeof address === 'string' ? address : address.toString()) } -function buildPaymentWithAlgokitCore({ +function buildPaymentWithAlgoKitCore({ sender, receiver, amount, @@ -2133,8 +2133,6 @@ function buildPaymentWithAlgokitCore({ }, } - // TODO: do we need to move this logic to Rust core? - let fee = BigInt(suggestedParams.fee) if (!suggestedParams.flatFee) { const minFee = BigInt(suggestedParams.minFee) diff --git a/src/types/transaction.ts b/src/types/transaction.ts index 012ffa7f..dbb763e7 100644 --- a/src/types/transaction.ts +++ b/src/types/transaction.ts @@ -47,7 +47,7 @@ export interface SendTransactionParams { } /** Result from sending a single transaction. */ -export type SendSingleTransactionResult = Expand +export type SendSingleTransactionResult = Expand & ConfirmedTransactionResult> /** The result of sending a transaction */ export interface SendTransactionResult { From f69718866f9d6f1f7e5d0089c355b91091aafd11 Mon Sep 17 00:00:00 2001 From: Hoang Dinh Date: Mon, 28 Apr 2025 20:51:54 +1000 Subject: [PATCH 04/18] wip - clean up --- src/types/algokit-core-bridge.ts | 54 +++++++++++++++++++ .../algorand-client-transaction-sender.ts | 2 + src/types/composer.ts | 54 +------------------ 3 files changed, 57 insertions(+), 53 deletions(-) create mode 100644 src/types/algokit-core-bridge.ts diff --git a/src/types/algokit-core-bridge.ts b/src/types/algokit-core-bridge.ts new file mode 100644 index 00000000..29c72f5a --- /dev/null +++ b/src/types/algokit-core-bridge.ts @@ -0,0 +1,54 @@ +import { addressFromString, Transaction as AlgokitCoreTransaction, encodeTransactionRaw } from 'algokit_transact' +import algosdk, { Address } from 'algosdk' + +function getAlgokitCoreAddress(address: string | Address) { + return addressFromString(typeof address === 'string' ? address : address.toString()) +} + +// Experimental feature to build algosdk payment transactions with algokit-core +export function buildPayment({ + sender, + receiver, + amount, + closeRemainderTo, + rekeyTo, + note, + lease, + suggestedParams, +}: algosdk.PaymentTransactionParams & algosdk.CommonTransactionParams) { + const txnModel: AlgokitCoreTransaction = { + header: { + sender: getAlgokitCoreAddress(sender), + transactionType: 'Payment', + fee: BigInt(suggestedParams.fee), + firstValid: BigInt(suggestedParams.firstValid), + lastValid: BigInt(suggestedParams.lastValid), + genesisHash: suggestedParams.genesisHash, + genesisId: suggestedParams.genesisID, + rekeyTo: rekeyTo ? getAlgokitCoreAddress(rekeyTo) : undefined, + note: note, + lease: lease, + }, + payFields: { + amount: BigInt(amount), + receiver: getAlgokitCoreAddress(receiver), + closeRemainderTo: closeRemainderTo ? getAlgokitCoreAddress(closeRemainderTo) : undefined, + }, + } + + let fee = BigInt(suggestedParams.fee) + if (!suggestedParams.flatFee) { + const minFee = BigInt(suggestedParams.minFee) + const numAddlBytesAfterSigning = 75 + const estimateTxnSize = encodeTransactionRaw(txnModel).length + numAddlBytesAfterSigning + + fee *= BigInt(estimateTxnSize) + // If suggested fee too small and will be rejected, set to min tx fee + if (fee < minFee) { + fee = minFee + } + } + txnModel.header.fee = fee + + return algosdk.decodeUnsignedTransaction(encodeTransactionRaw(txnModel)) +} diff --git a/src/types/algorand-client-transaction-sender.ts b/src/types/algorand-client-transaction-sender.ts index ed8ff02c..9dc142c3 100644 --- a/src/types/algorand-client-transaction-sender.ts +++ b/src/types/algorand-client-transaction-sender.ts @@ -167,6 +167,7 @@ export class AlgorandClientTransactionSender { } /** + * Experimental feature: * Send a payment transaction to transfer Algo between accounts. * @param params The parameters for the payment transaction * @example Basic example @@ -226,6 +227,7 @@ export class AlgorandClientTransactionSender { atc.buildGroup() const signedTxns = await atc.gatherSignatures() + // TODO: replace this with the generated http client await this._algod.sendRawTransaction(signedTxns).do() const confirmation = await waitForConfirmation(transaction.txID(), params.maxRoundsToWaitForConfirmation ?? 5, this._algod) diff --git a/src/types/composer.ts b/src/types/composer.ts index dfa5f092..bd686bd7 100644 --- a/src/types/composer.ts +++ b/src/types/composer.ts @@ -1,9 +1,9 @@ -import { addressFromString, Transaction as AlgokitCoreTransaction, encodeTransactionRaw } from 'algokit_transact' import algosdk, { Address } from 'algosdk' import { Config } from '../config' import { encodeLease, getABIReturnValue, sendAtomicTransactionComposer } from '../transaction/transaction' import { asJson, calculateExtraProgramPages } from '../util' import { TransactionSignerAccount } from './account' +import { buildPayment as buildPaymentWithAlgoKitCore } from './algokit-core-bridge' import { AlgoAmount } from './amount' import { AppManager, BoxIdentifier, BoxReference } from './app-manager' import { Expand } from './expand' @@ -1622,7 +1622,6 @@ export class TransactionComposer { }) } - // TODO: make sure that this is the only place a payment txn is built private buildPayment(params: PaymentParams, suggestedParams: algosdk.SuggestedParams) { return this.commonTxnBuildStep(buildPaymentWithAlgoKitCore, params, { sender: params.sender, @@ -2098,54 +2097,3 @@ export class TransactionComposer { return encoder.encode(arc2Payload) } } - -function getAlgokitCoreAddress(address: string | Address) { - return addressFromString(typeof address === 'string' ? address : address.toString()) -} - -function buildPaymentWithAlgoKitCore({ - sender, - receiver, - amount, - closeRemainderTo, - rekeyTo, - note, - lease, - suggestedParams, -}: algosdk.PaymentTransactionParams & algosdk.CommonTransactionParams) { - const txnModel: AlgokitCoreTransaction = { - header: { - sender: getAlgokitCoreAddress(sender), - transactionType: 'Payment', - fee: BigInt(suggestedParams.fee), - firstValid: BigInt(suggestedParams.firstValid), - lastValid: BigInt(suggestedParams.lastValid), - genesisHash: suggestedParams.genesisHash, - genesisId: suggestedParams.genesisID, - rekeyTo: rekeyTo ? getAlgokitCoreAddress(rekeyTo) : undefined, - note: note, - lease: lease, - }, - payFields: { - amount: BigInt(amount), - receiver: getAlgokitCoreAddress(receiver), - closeRemainderTo: closeRemainderTo ? getAlgokitCoreAddress(closeRemainderTo) : undefined, - }, - } - - let fee = BigInt(suggestedParams.fee) - if (!suggestedParams.flatFee) { - const minFee = BigInt(suggestedParams.minFee) - const numAddlBytesAfterSigning = 75 - const estimateTxnSize = encodeTransactionRaw(txnModel).length + numAddlBytesAfterSigning - - fee *= BigInt(estimateTxnSize) - // If suggested fee too small and will be rejected, set to min tx fee - if (fee < minFee) { - fee = minFee - } - } - txnModel.header.fee = fee - - return algosdk.decodeUnsignedTransaction(encodeTransactionRaw(txnModel)) -} From 0fbe3933bb0a6fb297f64653cb196dd5c92d43b1 Mon Sep 17 00:00:00 2001 From: Hoang Dinh Date: Tue, 29 Apr 2025 09:25:06 +1000 Subject: [PATCH 05/18] wip - send raw txns with api_client --- package-lock.json | 20 ++++++++++++++++ package.json | 3 ++- src/types/algokit-core-bridge.ts | 24 +++++++++++++++++++ .../algorand-client-transaction-sender.ts | 3 ++- 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 98eb910a..8cea6b55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "license": "MIT", "dependencies": { + "@algorand/algod-client": "file:../algokit-core/api/api_clients/typescript/algorand-algod-client-0.0.1.tgz", "@algorandfoundation/tealscript": "^0.106.3", "algokit_transact": "file:../algokit-core/algokit_transact-0.1.0.tgz", "buffer": "^6.0.3" @@ -66,6 +67,15 @@ "node": ">=0.10.0" } }, + "node_modules/@algorand/algod-client": { + "version": "0.0.1", + "resolved": "file:../algokit-core/api/api_clients/typescript/algorand-algod-client-0.0.1.tgz", + "integrity": "sha512-+cdvDs1JvpPHH5ELO4GRn7skIhSmJRmTyzpoDrRMYfdlrcMd6hKVijoCF/9+cLlz30aJ+9MzQhQhMldVkP4lLg==", + "dependencies": { + "es6-promise": "^4.2.4", + "whatwg-fetch": "^3.0.0" + } + }, "node_modules/@algorandfoundation/tealscript": { "version": "0.106.3", "resolved": "https://registry.npmjs.org/@algorandfoundation/tealscript/-/tealscript-0.106.3.tgz", @@ -4582,6 +4592,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, "node_modules/esbuild": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", @@ -12474,6 +12489,11 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", diff --git a/package.json b/package.json index b59ddc1e..52328803 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "esbuild": "0.25.0" }, "dependencies": { + "@algorand/algod-client": "file:../algokit-core/api/api_clients/typescript/algorand-algod-client-0.0.1.tgz", "@algorandfoundation/tealscript": "^0.106.3", "algokit_transact": "file:../algokit-core/algokit_transact-0.1.0.tgz", "buffer": "^6.0.3" @@ -172,4 +173,4 @@ "@semantic-release/github" ] } -} \ No newline at end of file +} diff --git a/src/types/algokit-core-bridge.ts b/src/types/algokit-core-bridge.ts index 29c72f5a..a00f1b4d 100644 --- a/src/types/algokit-core-bridge.ts +++ b/src/types/algokit-core-bridge.ts @@ -1,3 +1,4 @@ +import * as algodApi from '@algorand/algod-client' import { addressFromString, Transaction as AlgokitCoreTransaction, encodeTransactionRaw } from 'algokit_transact' import algosdk, { Address } from 'algosdk' @@ -52,3 +53,26 @@ export function buildPayment({ return algosdk.decodeUnsignedTransaction(encodeTransactionRaw(txnModel)) } + +export function sendRawTransaction(signedTxn: Uint8Array) { + // Covers all auth methods included in your OpenAPI yaml definition + const authConfig: algodApi.AuthMethodsConfiguration = { + api_key: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + } + + // Create configuration parameter object + const serverConfig = new algodApi.ServerConfiguration('http://localhost:4001', {}) + const configurationParameters = { + httpApi: new algodApi.IsomorphicFetchHttpLibrary(), // Can also be ignored - default is usually fine + baseServer: serverConfig, // First server is default + authMethods: authConfig, // No auth is default + promiseMiddleware: [], + } + + // Convert to actual configuration + const config = algodApi.createConfiguration(configurationParameters) + const api = new algodApi.AlgodApi(config) + + const httpFile = new File([signedTxn], '', { type: 'application/x-binary' }) + return api.rawTransaction(httpFile) +} diff --git a/src/types/algorand-client-transaction-sender.ts b/src/types/algorand-client-transaction-sender.ts index 9dc142c3..b1a3f7b2 100644 --- a/src/types/algorand-client-transaction-sender.ts +++ b/src/types/algorand-client-transaction-sender.ts @@ -3,6 +3,7 @@ import { Buffer } from 'buffer' import { Config } from '../config' import { waitForConfirmation } from '../transaction' import { asJson, defaultJsonValueReplacer } from '../util' +import { sendRawTransaction } from './algokit-core-bridge' import { AlgoAmount } from './amount' import { SendAppCreateTransactionResult, SendAppTransactionResult, SendAppUpdateTransactionResult } from './app' import { AppManager } from './app-manager' @@ -228,7 +229,7 @@ export class AlgorandClientTransactionSender { const signedTxns = await atc.gatherSignatures() // TODO: replace this with the generated http client - await this._algod.sendRawTransaction(signedTxns).do() + await sendRawTransaction(signedTxns[0]) const confirmation = await waitForConfirmation(transaction.txID(), params.maxRoundsToWaitForConfirmation ?? 5, this._algod) return { From 9461b494bb8a1639e227dd0850aea0aa89545fbb Mon Sep 17 00:00:00 2001 From: Hoang Dinh Date: Wed, 30 Apr 2025 09:40:47 +1000 Subject: [PATCH 06/18] wip - only use algokit core if no algod specified --- .../algorand-client-transaction-sender.ts | 19 ++++++++++- src/types/client-manager.ts | 33 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/types/algorand-client-transaction-sender.ts b/src/types/algorand-client-transaction-sender.ts index b1a3f7b2..9f3538f8 100644 --- a/src/types/algorand-client-transaction-sender.ts +++ b/src/types/algorand-client-transaction-sender.ts @@ -1,3 +1,4 @@ +import { AlgodApi } from '@algorand/algod-client' import algosdk, { Address, Algodv2 } from 'algosdk' import { Buffer } from 'buffer' import { Config } from '../config' @@ -42,22 +43,32 @@ export class AlgorandClientTransactionSender { private _assetManager: AssetManager private _appManager: AppManager private _algod: Algodv2 + private _algoKitCoreAlgod?: AlgodApi /** * Creates a new `AlgorandClientSender` * @param newGroup A lambda that starts a new `TransactionComposer` transaction group * @param assetManager An `AssetManager` instance * @param appManager An `AppManager` instance + * @param algod An `Algodv2` instance + * @param algoKitCoreAlgod An optional `AlgodApi` instance for AlgoKit core * @example * ```typescript * const transactionSender = new AlgorandClientTransactionSender(() => new TransactionComposer(), assetManager, appManager) * ``` */ - constructor(newGroup: () => TransactionComposer, assetManager: AssetManager, appManager: AppManager, algod: Algodv2) { + constructor( + newGroup: () => TransactionComposer, + assetManager: AssetManager, + appManager: AppManager, + algod: Algodv2, + algoKitCoreAlgod?: AlgodApi, + ) { this._newGroup = newGroup this._assetManager = assetManager this._appManager = appManager this._algod = algod + this._algoKitCoreAlgod = algoKitCoreAlgod } /** @@ -215,6 +226,12 @@ export class AlgorandClientTransactionSender { closeRemainderTo?: string | Address } & SendParams, ): Promise => { + if (!this._algoKitCoreAlgod) { + return this._send((c) => c.addPayment, { + preLog: (params, transaction) => + `Sending ${params.amount.microAlgo} µALGO from ${params.sender} to ${params.receiver} via transaction ${transaction.txID()}`, + })(params) + } const composer = this._newGroup() composer.addPayment(params) const { atc, transactions } = await composer.build() diff --git a/src/types/client-manager.ts b/src/types/client-manager.ts index cd11ae84..926d0574 100644 --- a/src/types/client-manager.ts +++ b/src/types/client-manager.ts @@ -1,3 +1,4 @@ +import * as algodApi from '@algorand/algod-client' import algosdk, { SuggestedParams } from 'algosdk' import { AlgoHttpClientWithRetry } from './algo-http-client-with-retry' import { type AlgorandClient } from './algorand-client' @@ -47,6 +48,7 @@ export type ClientTypedAppFactoryParams = Expand Date: Wed, 30 Apr 2025 14:32:30 +1000 Subject: [PATCH 07/18] wip --- src/types/algokit-core-bridge.ts | 22 ++++++++++++++++++- .../algorand-client-transaction-sender.ts | 5 ++--- src/types/algorand-client.ts | 1 + src/types/client-manager.ts | 16 +++++++++----- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/types/algokit-core-bridge.ts b/src/types/algokit-core-bridge.ts index a00f1b4d..24314584 100644 --- a/src/types/algokit-core-bridge.ts +++ b/src/types/algokit-core-bridge.ts @@ -1,6 +1,7 @@ import * as algodApi from '@algorand/algod-client' +import { RequestContext, SecurityAuthentication } from '@algorand/algod-client' import { addressFromString, Transaction as AlgokitCoreTransaction, encodeTransactionRaw } from 'algokit_transact' -import algosdk, { Address } from 'algosdk' +import algosdk, { Address, TokenHeader } from 'algosdk' function getAlgokitCoreAddress(address: string | Address) { return addressFromString(typeof address === 'string' ? address : address.toString()) @@ -76,3 +77,22 @@ export function sendRawTransaction(signedTxn: Uint8Array) { const httpFile = new File([signedTxn], '', { type: 'application/x-binary' }) return api.rawTransaction(httpFile) } + +export class TokenHeaderAuthenticationMethod implements SecurityAuthentication { + private _header: string + private _key: string + + public constructor(tokenHeader: TokenHeader) { + const [header, key] = Object.entries(tokenHeader)[0] + this._header = header + this._key = key + } + + public getName(): string { + return 'custom_header' + } + + public applySecurityAuthentication(context: RequestContext) { + context.setHeaderParam(this._header, this._key) + } +} diff --git a/src/types/algorand-client-transaction-sender.ts b/src/types/algorand-client-transaction-sender.ts index 9f3538f8..1e4e7262 100644 --- a/src/types/algorand-client-transaction-sender.ts +++ b/src/types/algorand-client-transaction-sender.ts @@ -4,7 +4,6 @@ import { Buffer } from 'buffer' import { Config } from '../config' import { waitForConfirmation } from '../transaction' import { asJson, defaultJsonValueReplacer } from '../util' -import { sendRawTransaction } from './algokit-core-bridge' import { AlgoAmount } from './amount' import { SendAppCreateTransactionResult, SendAppTransactionResult, SendAppUpdateTransactionResult } from './app' import { AppManager } from './app-manager' @@ -245,8 +244,8 @@ export class AlgorandClientTransactionSender { atc.buildGroup() const signedTxns = await atc.gatherSignatures() - // TODO: replace this with the generated http client - await sendRawTransaction(signedTxns[0]) + const httpFile = new File(signedTxns, '', { type: 'application/x-binary' }) + await this._algoKitCoreAlgod.rawTransaction(httpFile) const confirmation = await waitForConfirmation(transaction.txID(), params.maxRoundsToWaitForConfirmation ?? 5, this._algod) return { diff --git a/src/types/algorand-client.ts b/src/types/algorand-client.ts index bd11d7dd..367c4e97 100644 --- a/src/types/algorand-client.ts +++ b/src/types/algorand-client.ts @@ -40,6 +40,7 @@ export class AlgorandClient { this._assetManager, this._appManager, this._clientManager.algod, + this._clientManager.algoKitCoreAlgod, ) this._transactionCreator = new AlgorandClientTransactionCreator(() => this.newGroup()) this._appDeployer = new AppDeployer(this._appManager, this._transactionSender, this._clientManager.indexerIfPresent) diff --git a/src/types/client-manager.ts b/src/types/client-manager.ts index 926d0574..20267268 100644 --- a/src/types/client-manager.ts +++ b/src/types/client-manager.ts @@ -1,6 +1,7 @@ import * as algodApi from '@algorand/algod-client' import algosdk, { SuggestedParams } from 'algosdk' import { AlgoHttpClientWithRetry } from './algo-http-client-with-retry' +import { TokenHeaderAuthenticationMethod } from './algokit-core-bridge' import { type AlgorandClient } from './algorand-client' import { AppClient, AppClientParams, ResolveAppClientByCreatorAndName } from './app-client' import { AppFactory, AppFactoryParams } from './app-factory' @@ -606,19 +607,24 @@ export class ClientManager { public static getAlgoKitCoreAlgodClient(algoClientConfig: AlgoClientConfig): algodApi.AlgodApi { const { token, server, port } = algoClientConfig - const tokenHeader = typeof token === 'string' ? { 'X-Algo-API-Token': token } : (token ?? {}) + const authMethodConfig = + token === undefined + ? undefined + : typeof token === 'string' + ? new TokenHeaderAuthenticationMethod({ 'X-Algo-API-Token': token }) + : new TokenHeaderAuthenticationMethod(token) // Covers all auth methods included in your OpenAPI yaml definition const authConfig: algodApi.AuthMethodsConfiguration = { - api_key: token, + default: authMethodConfig, } // Create configuration parameter object const serverConfig = new algodApi.ServerConfiguration(`${server}:${port}`, {}) const configurationParameters = { - httpApi: new algodApi.IsomorphicFetchHttpLibrary(), // Can also be ignored - default is usually fine - baseServer: serverConfig, // First server is default - authMethods: authConfig, // No auth is default + httpApi: new algodApi.IsomorphicFetchHttpLibrary(), + baseServer: serverConfig, + authMethods: authConfig, promiseMiddleware: [], } From 599e4de9b1460b7779f5da36b855ed610a47376f Mon Sep 17 00:00:00 2001 From: Hoang Dinh Date: Wed, 30 Apr 2025 15:04:08 +1000 Subject: [PATCH 08/18] test done --- src/transaction/transaction.spec.ts | 33 +++++++++++++++++-- .../algorand-client-transaction-sender.ts | 3 +- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/transaction/transaction.spec.ts b/src/transaction/transaction.spec.ts index f17eaf6f..4cd62349 100644 --- a/src/transaction/transaction.spec.ts +++ b/src/transaction/transaction.spec.ts @@ -1,16 +1,18 @@ import algosdk, { ABIMethod, ABIType, Account, Address } from 'algosdk' import invariant from 'tiny-invariant' -import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'vitest' +import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest' import { APP_SPEC as nestedContractAppSpec } from '../../tests/example-contracts/client/TestContractClient' import innerFeeContract from '../../tests/example-contracts/inner-fee/application.json' import externalARC32 from '../../tests/example-contracts/resource-packer/artifacts/ExternalApp.arc32.json' import v8ARC32 from '../../tests/example-contracts/resource-packer/artifacts/ResourcePackerv8.arc32.json' import v9ARC32 from '../../tests/example-contracts/resource-packer/artifacts/ResourcePackerv9.arc32.json' -import { algo, microAlgo } from '../amount' +import { algo, algos, microAlgo } from '../amount' import { Config } from '../config' -import { algorandFixture } from '../testing' +import { algorandFixture, getTestAccount } from '../testing' +import { AlgorandClient } from '../types/algorand-client' import { AlgoAmount } from '../types/amount' import { AppClient } from '../types/app-client' +import { ClientManager } from '../types/client-manager' import { PaymentParams, TransactionComposer } from '../types/composer' import { Arc2TransactionNote } from '../types/transaction' import { getABIReturnValue, waitForConfirmation } from './transaction' @@ -1157,3 +1159,28 @@ describe('abi return', () => { ]) }) }) + +describe('When create algorand client with config from environment', () => { + test('payment transactions are sent by algokit core algod client', async () => { + const algorandClient = AlgorandClient.fromConfig(ClientManager.getConfigFromEnvironmentOrLocalNet()) + const algodSpy = vi.spyOn(algorandClient.client.algod, 'sendRawTransaction') + const algoKitCoreAlgodSpy = vi.spyOn(algorandClient.client.algoKitCoreAlgod!, 'rawTransaction') + + const testAccount = await getTestAccount({ initialFunds: algos(10), suppressLog: true }, algorandClient) + algorandClient.setSignerFromAccount(testAccount) + + const testPayTransaction = { + sender: testAccount, + receiver: testAccount, + amount: (1).microAlgo(), + } as PaymentParams + + const fee = (1).algo() + const { confirmation } = await algorandClient.send.payment({ ...testPayTransaction, staticFee: fee }) + + expect(algodSpy).not.toHaveBeenCalled() + expect(algorandClient.client.algoKitCoreAlgod).toBeDefined() + expect(algoKitCoreAlgodSpy).toBeCalledTimes(2) + expect(confirmation.txn.txn.fee).toBe(fee.microAlgo) + }) +}) diff --git a/src/types/algorand-client-transaction-sender.ts b/src/types/algorand-client-transaction-sender.ts index 1e4e7262..5c3c35c3 100644 --- a/src/types/algorand-client-transaction-sender.ts +++ b/src/types/algorand-client-transaction-sender.ts @@ -231,6 +231,7 @@ export class AlgorandClientTransactionSender { `Sending ${params.amount.microAlgo} µALGO from ${params.sender} to ${params.receiver} via transaction ${transaction.txID()}`, })(params) } + const composer = this._newGroup() composer.addPayment(params) const { atc, transactions } = await composer.build() @@ -244,7 +245,7 @@ export class AlgorandClientTransactionSender { atc.buildGroup() const signedTxns = await atc.gatherSignatures() - const httpFile = new File(signedTxns, '', { type: 'application/x-binary' }) + const httpFile = new File(signedTxns, '') await this._algoKitCoreAlgod.rawTransaction(httpFile) const confirmation = await waitForConfirmation(transaction.txID(), params.maxRoundsToWaitForConfirmation ?? 5, this._algod) From 9b0a125661ba6da10eb02c3942bd517bdd910f84 Mon Sep 17 00:00:00 2001 From: Hoang Dinh Date: Wed, 30 Apr 2025 15:18:44 +1000 Subject: [PATCH 09/18] clean up --- src/types/algokit-core-bridge.ts | 24 ------------------- .../algorand-client-transaction-sender.ts | 2 +- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/src/types/algokit-core-bridge.ts b/src/types/algokit-core-bridge.ts index 24314584..1da5fd81 100644 --- a/src/types/algokit-core-bridge.ts +++ b/src/types/algokit-core-bridge.ts @@ -1,4 +1,3 @@ -import * as algodApi from '@algorand/algod-client' import { RequestContext, SecurityAuthentication } from '@algorand/algod-client' import { addressFromString, Transaction as AlgokitCoreTransaction, encodeTransactionRaw } from 'algokit_transact' import algosdk, { Address, TokenHeader } from 'algosdk' @@ -55,29 +54,6 @@ export function buildPayment({ return algosdk.decodeUnsignedTransaction(encodeTransactionRaw(txnModel)) } -export function sendRawTransaction(signedTxn: Uint8Array) { - // Covers all auth methods included in your OpenAPI yaml definition - const authConfig: algodApi.AuthMethodsConfiguration = { - api_key: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - } - - // Create configuration parameter object - const serverConfig = new algodApi.ServerConfiguration('http://localhost:4001', {}) - const configurationParameters = { - httpApi: new algodApi.IsomorphicFetchHttpLibrary(), // Can also be ignored - default is usually fine - baseServer: serverConfig, // First server is default - authMethods: authConfig, // No auth is default - promiseMiddleware: [], - } - - // Convert to actual configuration - const config = algodApi.createConfiguration(configurationParameters) - const api = new algodApi.AlgodApi(config) - - const httpFile = new File([signedTxn], '', { type: 'application/x-binary' }) - return api.rawTransaction(httpFile) -} - export class TokenHeaderAuthenticationMethod implements SecurityAuthentication { private _header: string private _key: string diff --git a/src/types/algorand-client-transaction-sender.ts b/src/types/algorand-client-transaction-sender.ts index 5c3c35c3..56db5074 100644 --- a/src/types/algorand-client-transaction-sender.ts +++ b/src/types/algorand-client-transaction-sender.ts @@ -239,7 +239,7 @@ export class AlgorandClientTransactionSender { const transaction = transactions[0].txn Config.getLogger(params?.suppressLog).debug( - `Sending ${params.amount.microAlgo} µALGO from ${params.sender} to ${params.receiver} via transaction ${transaction.txID()}`, + `AlgoKit core: sending ${params.amount.microAlgo} µALGO from ${params.sender} to ${params.receiver} via transaction ${transaction.txID()}`, ) atc.buildGroup() From 1bb70743966311d76f1039e0e5cb909a5fcf423a Mon Sep 17 00:00:00 2001 From: Hoang Dinh Date: Wed, 30 Apr 2025 20:07:31 +1000 Subject: [PATCH 10/18] wip - wait for confirmation with algokit core --- src/app-deploy.ts | 2 +- src/types/algokit-core-bridge.ts | 62 ++++++++++++++++++- .../algorand-client-transaction-sender.ts | 20 +++--- src/types/algorand-client.ts | 1 - 4 files changed, 69 insertions(+), 16 deletions(-) diff --git a/src/app-deploy.ts b/src/app-deploy.ts index 076ac8ba..36d3e8a6 100644 --- a/src/app-deploy.ts +++ b/src/app-deploy.ts @@ -77,7 +77,7 @@ export async function deployApp( }) const deployer = new AppDeployer( appManager, - new AlgorandClientTransactionSender(newGroup, new AssetManager(algod, newGroup), appManager, algod), + new AlgorandClientTransactionSender(newGroup, new AssetManager(algod, newGroup), appManager), indexer, ) diff --git a/src/types/algokit-core-bridge.ts b/src/types/algokit-core-bridge.ts index 1da5fd81..a02085d2 100644 --- a/src/types/algokit-core-bridge.ts +++ b/src/types/algokit-core-bridge.ts @@ -1,6 +1,7 @@ -import { RequestContext, SecurityAuthentication } from '@algorand/algod-client' +import { AlgodApi, PendingTransactionResponse, RequestContext, SecurityAuthentication } from '@algorand/algod-client' import { addressFromString, Transaction as AlgokitCoreTransaction, encodeTransactionRaw } from 'algokit_transact' import algosdk, { Address, TokenHeader } from 'algosdk' +import { toNumber } from '../util' function getAlgokitCoreAddress(address: string | Address) { return addressFromString(typeof address === 'string' ? address : address.toString()) @@ -72,3 +73,62 @@ export class TokenHeaderAuthenticationMethod implements SecurityAuthentication { context.setHeaderParam(this._header, this._key) } } + +/** + * Wait until the transaction is confirmed or rejected, or until `timeout` + * number of rounds have passed. + * + * @param algod An AlgoKit core algod client + * @param transactionId The transaction ID to wait for + * @param maxRoundsToWait Maximum number of rounds to wait + * + * @return Pending transaction information + * @throws Throws an error if the transaction is not confirmed or rejected in the next `timeout` rounds + */ +export const waitForConfirmation = async function ( + transactionId: string, + maxRoundsToWait: number | bigint, + algod: AlgodApi, +): Promise { + if (maxRoundsToWait < 0) { + throw new Error(`Invalid timeout, received ${maxRoundsToWait}, expected > 0`) + } + + // Get current round + const status = await algod.getStatus() + if (status === undefined) { + throw new Error('Unable to get node status') + } + + // Loop for up to `timeout` rounds looking for a confirmed transaction + const startRound = BigInt(status.lastRound) + 1n + let currentRound = startRound + while (currentRound < startRound + BigInt(maxRoundsToWait)) { + try { + const pendingInfo = await algod.pendingTransactionInformation(transactionId, 'msgpack') + + if (pendingInfo !== undefined) { + const confirmedRound = pendingInfo.confirmedRound + if (confirmedRound && confirmedRound > 0) { + return pendingInfo + } else { + const poolError = pendingInfo.poolError + if (poolError != null && poolError.length > 0) { + // If there was a pool error, then the transaction has been rejected! + throw new Error(`Transaction ${transactionId} was rejected; pool error: ${poolError}`) + } + } + } + } catch (e: unknown) { + if ((e as Error).name === 'URLTokenBaseHTTPError') { + currentRound++ + continue + } + } + + await algod.waitForBlock(toNumber(currentRound)) + currentRound++ + } + + throw new Error(`Transaction ${transactionId} not confirmed after ${maxRoundsToWait} rounds`) +} diff --git a/src/types/algorand-client-transaction-sender.ts b/src/types/algorand-client-transaction-sender.ts index 56db5074..e5a7b39f 100644 --- a/src/types/algorand-client-transaction-sender.ts +++ b/src/types/algorand-client-transaction-sender.ts @@ -1,9 +1,9 @@ import { AlgodApi } from '@algorand/algod-client' -import algosdk, { Address, Algodv2 } from 'algosdk' +import algosdk, { Address } from 'algosdk' import { Buffer } from 'buffer' import { Config } from '../config' -import { waitForConfirmation } from '../transaction' import { asJson, defaultJsonValueReplacer } from '../util' +import { waitForConfirmation } from './algokit-core-bridge' import { AlgoAmount } from './amount' import { SendAppCreateTransactionResult, SendAppTransactionResult, SendAppUpdateTransactionResult } from './app' import { AppManager } from './app-manager' @@ -41,7 +41,6 @@ export class AlgorandClientTransactionSender { private _newGroup: () => TransactionComposer private _assetManager: AssetManager private _appManager: AppManager - private _algod: Algodv2 private _algoKitCoreAlgod?: AlgodApi /** @@ -49,24 +48,16 @@ export class AlgorandClientTransactionSender { * @param newGroup A lambda that starts a new `TransactionComposer` transaction group * @param assetManager An `AssetManager` instance * @param appManager An `AppManager` instance - * @param algod An `Algodv2` instance * @param algoKitCoreAlgod An optional `AlgodApi` instance for AlgoKit core * @example * ```typescript * const transactionSender = new AlgorandClientTransactionSender(() => new TransactionComposer(), assetManager, appManager) * ``` */ - constructor( - newGroup: () => TransactionComposer, - assetManager: AssetManager, - appManager: AppManager, - algod: Algodv2, - algoKitCoreAlgod?: AlgodApi, - ) { + constructor(newGroup: () => TransactionComposer, assetManager: AssetManager, appManager: AppManager, algoKitCoreAlgod?: AlgodApi) { this._newGroup = newGroup this._assetManager = assetManager this._appManager = appManager - this._algod = algod this._algoKitCoreAlgod = algoKitCoreAlgod } @@ -247,7 +238,10 @@ export class AlgorandClientTransactionSender { const httpFile = new File(signedTxns, '') await this._algoKitCoreAlgod.rawTransaction(httpFile) - const confirmation = await waitForConfirmation(transaction.txID(), params.maxRoundsToWaitForConfirmation ?? 5, this._algod) + // TODO: check the support for msgpack in pendingTransactionInformation + // TODO: conversation about decoding generic types in Rust (whenever we have to do it with algosdk) + const confirmation1 = await waitForConfirmation(transaction.txID(), params.maxRoundsToWaitForConfirmation ?? 5, this._algoKitCoreAlgod) + const confirmation = confirmation1 as algosdk.modelsv2.PendingTransactionResponse return { txIds: [transaction.txID()], diff --git a/src/types/algorand-client.ts b/src/types/algorand-client.ts index 367c4e97..aa131f12 100644 --- a/src/types/algorand-client.ts +++ b/src/types/algorand-client.ts @@ -39,7 +39,6 @@ export class AlgorandClient { () => this.newGroup(), this._assetManager, this._appManager, - this._clientManager.algod, this._clientManager.algoKitCoreAlgod, ) this._transactionCreator = new AlgorandClientTransactionCreator(() => this.newGroup()) From b40a3d2278f5d293817bb7aa05c2b4eec83ba574 Mon Sep 17 00:00:00 2001 From: Hoang Dinh Date: Wed, 30 Apr 2025 22:20:57 +1000 Subject: [PATCH 11/18] wip - inject into http client with retry --- src/transaction/transaction.spec.ts | 7 +- src/types/algo-http-client-with-retry.ts | 65 ++++++++++++++++++- .../algorand-client-transaction-sender.ts | 57 ++++++++-------- src/types/client-manager.ts | 1 - 4 files changed, 95 insertions(+), 35 deletions(-) diff --git a/src/transaction/transaction.spec.ts b/src/transaction/transaction.spec.ts index 4cd62349..c4ef04ab 100644 --- a/src/transaction/transaction.spec.ts +++ b/src/transaction/transaction.spec.ts @@ -1160,6 +1160,7 @@ describe('abi return', () => { }) }) +// TODO: PD - fix this test, how??? describe('When create algorand client with config from environment', () => { test('payment transactions are sent by algokit core algod client', async () => { const algorandClient = AlgorandClient.fromConfig(ClientManager.getConfigFromEnvironmentOrLocalNet()) @@ -1178,9 +1179,9 @@ describe('When create algorand client with config from environment', () => { const fee = (1).algo() const { confirmation } = await algorandClient.send.payment({ ...testPayTransaction, staticFee: fee }) - expect(algodSpy).not.toHaveBeenCalled() - expect(algorandClient.client.algoKitCoreAlgod).toBeDefined() - expect(algoKitCoreAlgodSpy).toBeCalledTimes(2) + // expect(algodSpy).not.toHaveBeenCalled() + // expect(algorandClient.client.algoKitCoreAlgod).toBeDefined() + // expect(algoKitCoreAlgodSpy).toBeCalledTimes(2) expect(confirmation.txn.txn.fee).toBe(fee.microAlgo) }) }) diff --git a/src/types/algo-http-client-with-retry.ts b/src/types/algo-http-client-with-retry.ts index 214e194d..28abdca3 100644 --- a/src/types/algo-http-client-with-retry.ts +++ b/src/types/algo-http-client-with-retry.ts @@ -1,6 +1,8 @@ -import { IntDecoding, parseJSON, stringifyJSON } from 'algosdk' +import * as algodApi from '@algorand/algod-client' +import { decodeSignedTransaction, IntDecoding, parseJSON, SignedTransaction, stringifyJSON, TokenHeader, TransactionType } from 'algosdk' import { BaseHTTPClientResponse, Query, URLTokenBaseHTTPClient } from 'algosdk/client' import { Config } from '../config' +import { TokenHeaderAuthenticationMethod } from './algokit-core-bridge' /** A HTTP Client that wraps the Algorand SDK HTTP Client with retries */ export class AlgoHttpClientWithRetry extends URLTokenBaseHTTPClient { @@ -94,6 +96,45 @@ export class AlgoHttpClientWithRetry extends URLTokenBaseHTTPClient { query?: Query, requestHeaders: Record = {}, ): Promise { + if (relativePath.startsWith('/v2/transactions')) { + let signedTxn: SignedTransaction | undefined = undefined + try { + // Try to decode the data into a single transaction + // This will fail when sending a transaction group, in that case, we will ignore the error + signedTxn = decodeSignedTransaction(data) + } catch { + // Ignore errors here + } + if (signedTxn && signedTxn.txn.type === TransactionType.pay) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const baseUrl = (this as any).baseURL as URL + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const tokenHeader = (this as any).tokenHeader as TokenHeader + + const algoKitCoreAlgod = getAlgoKitCoreAlgodClient(baseUrl.toString(), tokenHeader) + return await this.callWithRetry(async () => { + try { + const response = await algoKitCoreAlgod.rawTransaction(new File([data], '')) + const json = JSON.stringify(response) + const encoder = new TextEncoder() + return { + status: 200, + headers: {}, // TODO: PD - do we need the headers? + body: encoder.encode(json), + } + } catch (e) { + if (e instanceof algodApi.ApiException) { + return { + body: e.body, + status: e.code, + headers: e.headers, + } + } + throw e + } + }) + } + } return await this.callWithRetry(() => super.post(relativePath, data, query, requestHeaders)) } @@ -106,3 +147,25 @@ export class AlgoHttpClientWithRetry extends URLTokenBaseHTTPClient { return await this.callWithRetry(() => super.delete(relativePath, data, query, requestHeaders)) } } + +function getAlgoKitCoreAlgodClient(baseUrl: string, tokenHeader: TokenHeader): algodApi.AlgodApi { + const authMethodConfig = new TokenHeaderAuthenticationMethod(tokenHeader) + // Covers all auth methods included in your OpenAPI yaml definition + const authConfig: algodApi.AuthMethodsConfiguration = { + default: authMethodConfig, + } + + // Create configuration parameter object + const fixedBaseUrl = baseUrl.replace(/\/+$/, '') + const serverConfig = new algodApi.ServerConfiguration(fixedBaseUrl, {}) + const configurationParameters = { + httpApi: new algodApi.IsomorphicFetchHttpLibrary(), + baseServer: serverConfig, + authMethods: authConfig, + promiseMiddleware: [], + } + + // Convert to actual configuration + const config = algodApi.createConfiguration(configurationParameters) + return new algodApi.AlgodApi(config) +} diff --git a/src/types/algorand-client-transaction-sender.ts b/src/types/algorand-client-transaction-sender.ts index e5a7b39f..081f1e3e 100644 --- a/src/types/algorand-client-transaction-sender.ts +++ b/src/types/algorand-client-transaction-sender.ts @@ -3,7 +3,6 @@ import algosdk, { Address } from 'algosdk' import { Buffer } from 'buffer' import { Config } from '../config' import { asJson, defaultJsonValueReplacer } from '../util' -import { waitForConfirmation } from './algokit-core-bridge' import { AlgoAmount } from './amount' import { SendAppCreateTransactionResult, SendAppTransactionResult, SendAppUpdateTransactionResult } from './app' import { AppManager } from './app-manager' @@ -216,41 +215,39 @@ export class AlgorandClientTransactionSender { closeRemainderTo?: string | Address } & SendParams, ): Promise => { - if (!this._algoKitCoreAlgod) { - return this._send((c) => c.addPayment, { - preLog: (params, transaction) => - `Sending ${params.amount.microAlgo} µALGO from ${params.sender} to ${params.receiver} via transaction ${transaction.txID()}`, - })(params) - } + return this._send((c) => c.addPayment, { + preLog: (params, transaction) => + `Sending ${params.amount.microAlgo} µALGO from ${params.sender} to ${params.receiver} via transaction ${transaction.txID()}`, + })(params) - const composer = this._newGroup() - composer.addPayment(params) - const { atc, transactions } = await composer.build() + // const composer = this._newGroup() + // composer.addPayment(params) + // const { atc, transactions } = await composer.build() - const transaction = transactions[0].txn + // const transaction = transactions[0].txn - Config.getLogger(params?.suppressLog).debug( - `AlgoKit core: sending ${params.amount.microAlgo} µALGO from ${params.sender} to ${params.receiver} via transaction ${transaction.txID()}`, - ) + // Config.getLogger(params?.suppressLog).debug( + // `AlgoKit core: sending ${params.amount.microAlgo} µALGO from ${params.sender} to ${params.receiver} via transaction ${transaction.txID()}`, + // ) - atc.buildGroup() - const signedTxns = await atc.gatherSignatures() + // atc.buildGroup() + // const signedTxns = await atc.gatherSignatures() - const httpFile = new File(signedTxns, '') - await this._algoKitCoreAlgod.rawTransaction(httpFile) - // TODO: check the support for msgpack in pendingTransactionInformation - // TODO: conversation about decoding generic types in Rust (whenever we have to do it with algosdk) - const confirmation1 = await waitForConfirmation(transaction.txID(), params.maxRoundsToWaitForConfirmation ?? 5, this._algoKitCoreAlgod) - const confirmation = confirmation1 as algosdk.modelsv2.PendingTransactionResponse + // const httpFile = new File(signedTxns, '') + // await this._algoKitCoreAlgod.rawTransaction(httpFile) + // // TODO: check the support for msgpack in pendingTransactionInformation + // // TODO: conversation about decoding generic types in Rust (whenever we have to do it with algosdk) + // const confirmation = await waitForConfirmation(transaction.txID(), params.maxRoundsToWaitForConfirmation ?? 5, this._algoKitCoreAlgod) + // // const confirmation = confirmation1 as algosdk.modelsv2.PendingTransactionResponse - return { - txIds: [transaction.txID()], - returns: undefined, - confirmation: confirmation, - transaction: transaction, - confirmations: [confirmation], - transactions: [transaction], - } satisfies SendSingleTransactionResult + // return { + // txIds: [transaction.txID()], + // returns: undefined, + // confirmation: confirmation, + // transaction: transaction, + // confirmations: [confirmation], + // transactions: [transaction], + // } satisfies SendSingleTransactionResult } /** diff --git a/src/types/client-manager.ts b/src/types/client-manager.ts index 20267268..98067da2 100644 --- a/src/types/client-manager.ts +++ b/src/types/client-manager.ts @@ -603,7 +603,6 @@ export class ClientManager { return new algosdk.Algodv2(httpClientWithRetry, server) } - // TODO: write test for this public static getAlgoKitCoreAlgodClient(algoClientConfig: AlgoClientConfig): algodApi.AlgodApi { const { token, server, port } = algoClientConfig From 626629391d5212bb36b7e6f2dd1005e58d062d0d Mon Sep 17 00:00:00 2001 From: Hoang Dinh Date: Thu, 1 May 2025 11:49:07 +1000 Subject: [PATCH 12/18] fix error --- src/types/algo-http-client-with-retry.ts | 26 ++++++++---------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/types/algo-http-client-with-retry.ts b/src/types/algo-http-client-with-retry.ts index 28abdca3..7c1e96f7 100644 --- a/src/types/algo-http-client-with-retry.ts +++ b/src/types/algo-http-client-with-retry.ts @@ -106,6 +106,8 @@ export class AlgoHttpClientWithRetry extends URLTokenBaseHTTPClient { // Ignore errors here } if (signedTxn && signedTxn.txn.type === TransactionType.pay) { + const encoder = new TextEncoder() + // eslint-disable-next-line @typescript-eslint/no-explicit-any const baseUrl = (this as any).baseURL as URL // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -113,24 +115,12 @@ export class AlgoHttpClientWithRetry extends URLTokenBaseHTTPClient { const algoKitCoreAlgod = getAlgoKitCoreAlgodClient(baseUrl.toString(), tokenHeader) return await this.callWithRetry(async () => { - try { - const response = await algoKitCoreAlgod.rawTransaction(new File([data], '')) - const json = JSON.stringify(response) - const encoder = new TextEncoder() - return { - status: 200, - headers: {}, // TODO: PD - do we need the headers? - body: encoder.encode(json), - } - } catch (e) { - if (e instanceof algodApi.ApiException) { - return { - body: e.body, - status: e.code, - headers: e.headers, - } - } - throw e + const response = await algoKitCoreAlgod.rawTransaction(new File([data], '')) + const json = JSON.stringify(response) + return { + status: 200, + headers: {}, // TODO: PD - do we need the headers? + body: encoder.encode(json), } }) } From 6623617d755b51c351ab4d2484a1dc2df9166a8a Mon Sep 17 00:00:00 2001 From: Hoang Dinh Date: Thu, 1 May 2025 14:21:17 +1000 Subject: [PATCH 13/18] inject to AlgoHttpClientWithRetry --- src/types/algo-http-client-with-retry.ts | 39 +++++++++++++++++++----- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/src/types/algo-http-client-with-retry.ts b/src/types/algo-http-client-with-retry.ts index 7c1e96f7..3a0689dc 100644 --- a/src/types/algo-http-client-with-retry.ts +++ b/src/types/algo-http-client-with-retry.ts @@ -57,6 +57,30 @@ export class AlgoHttpClientWithRetry extends URLTokenBaseHTTPClient { } async get(relativePath: string, query?: Query, requestHeaders: Record = {}): Promise { + if (relativePath.startsWith('/v2/transactions/pending/')) { + const possibleTxnId = relativePath.replace('/v2/transactions/pending/', '').replace(/\/+$/, '') + // TODO: test for possibleTxnId + if (possibleTxnId) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const baseUrl = (this as any).baseURL as URL + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const tokenHeader = (this as any).tokenHeader as TokenHeader + const algoKitCoreAlgod = getAlgoKitCoreAlgodClient(baseUrl.toString(), tokenHeader) + + return await this.callWithRetry(async () => { + const httpInfo = await algoKitCoreAlgod.pendingTransactionInformationWithHttpInfo(possibleTxnId, 'msgpack') + const binary = await httpInfo.body.binary() + const arrayBuffer = await binary.arrayBuffer() + const uint8Array = new Uint8Array(arrayBuffer) + return { + status: httpInfo.httpStatusCode, + headers: httpInfo.headers, + body: uint8Array, + } + }) + } + } + const response = await this.callWithRetry(() => super.get(relativePath, query, requestHeaders)) if ( relativePath.startsWith('/v2/accounts/') && @@ -90,6 +114,7 @@ export class AlgoHttpClientWithRetry extends URLTokenBaseHTTPClient { return response } + // TODO: verify error handling async post( relativePath: string, data: Uint8Array, @@ -106,8 +131,6 @@ export class AlgoHttpClientWithRetry extends URLTokenBaseHTTPClient { // Ignore errors here } if (signedTxn && signedTxn.txn.type === TransactionType.pay) { - const encoder = new TextEncoder() - // eslint-disable-next-line @typescript-eslint/no-explicit-any const baseUrl = (this as any).baseURL as URL // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -115,12 +138,14 @@ export class AlgoHttpClientWithRetry extends URLTokenBaseHTTPClient { const algoKitCoreAlgod = getAlgoKitCoreAlgodClient(baseUrl.toString(), tokenHeader) return await this.callWithRetry(async () => { - const response = await algoKitCoreAlgod.rawTransaction(new File([data], '')) - const json = JSON.stringify(response) + const httpInfo = await algoKitCoreAlgod.rawTransactionWithHttpInfo(new File([data], '')) + const binary = await httpInfo.body.binary() + const arrayBuffer = await binary.arrayBuffer() + const uint8Array = new Uint8Array(arrayBuffer) return { - status: 200, - headers: {}, // TODO: PD - do we need the headers? - body: encoder.encode(json), + status: httpInfo.httpStatusCode, + headers: httpInfo.headers, + body: uint8Array, } }) } From 151dc8667a57f50bc469e8b55a4291a49ab48452 Mon Sep 17 00:00:00 2001 From: Hoang Dinh Date: Fri, 2 May 2025 11:08:17 +1000 Subject: [PATCH 14/18] switch to use the new http client --- src/types/algo-http-client-with-retry.ts | 61 +++++++++++++++++++++--- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/src/types/algo-http-client-with-retry.ts b/src/types/algo-http-client-with-retry.ts index 3a0689dc..cd1e17e9 100644 --- a/src/types/algo-http-client-with-retry.ts +++ b/src/types/algo-http-client-with-retry.ts @@ -1,5 +1,14 @@ import * as algodApi from '@algorand/algod-client' -import { decodeSignedTransaction, IntDecoding, parseJSON, SignedTransaction, stringifyJSON, TokenHeader, TransactionType } from 'algosdk' +import { + BaseHTTPClientError, + decodeSignedTransaction, + IntDecoding, + parseJSON, + SignedTransaction, + stringifyJSON, + TokenHeader, + TransactionType, +} from 'algosdk' import { BaseHTTPClientResponse, Query, URLTokenBaseHTTPClient } from 'algosdk/client' import { Config } from '../config' import { TokenHeaderAuthenticationMethod } from './algokit-core-bridge' @@ -35,6 +44,7 @@ export class AlgoHttpClientWithRetry extends URLTokenBaseHTTPClient { if (numTries >= AlgoHttpClientWithRetry.MAX_TRIES) { throw err } + // Only retry for one of the hardcoded conditions if ( !( @@ -68,7 +78,7 @@ export class AlgoHttpClientWithRetry extends URLTokenBaseHTTPClient { const algoKitCoreAlgod = getAlgoKitCoreAlgodClient(baseUrl.toString(), tokenHeader) return await this.callWithRetry(async () => { - const httpInfo = await algoKitCoreAlgod.pendingTransactionInformationWithHttpInfo(possibleTxnId, 'msgpack') + const httpInfo = await algoKitCoreAlgod.pendingTransactionInformationResponse(possibleTxnId, 'msgpack') const binary = await httpInfo.body.binary() const arrayBuffer = await binary.arrayBuffer() const uint8Array = new Uint8Array(arrayBuffer) @@ -138,13 +148,41 @@ export class AlgoHttpClientWithRetry extends URLTokenBaseHTTPClient { const algoKitCoreAlgod = getAlgoKitCoreAlgodClient(baseUrl.toString(), tokenHeader) return await this.callWithRetry(async () => { - const httpInfo = await algoKitCoreAlgod.rawTransactionWithHttpInfo(new File([data], '')) - const binary = await httpInfo.body.binary() + const responseContext = await algoKitCoreAlgod.rawTransactionResponse(new File([data], '')) + + const binary = await responseContext.body.binary() const arrayBuffer = await binary.arrayBuffer() const uint8Array = new Uint8Array(arrayBuffer) + + if (responseContext.httpStatusCode !== 200) { + let bodyErrorMessage: string | undefined + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const decoded: Record = JSON.parse(new TextDecoder().decode(uint8Array)) + if (decoded.message) { + bodyErrorMessage = decoded.message + } + } catch { + // ignore any error that happened while we are parsing the error response + } + + let message = `Network request error. Received status ${responseContext.httpStatusCode} (${responseContext.httpStatusText})` + if (bodyErrorMessage) { + message += `: ${bodyErrorMessage}` + } + + throw new URLTokenBaseHTTPError(message, { + body: uint8Array, + status: responseContext.httpStatusCode, + headers: responseContext.headers, + }) + } + return { - status: httpInfo.httpStatusCode, - headers: httpInfo.headers, + status: responseContext.httpStatusCode, + statusText: responseContext.httpStatusText, + headers: responseContext.headers, body: uint8Array, } }) @@ -184,3 +222,14 @@ function getAlgoKitCoreAlgodClient(baseUrl: string, tokenHeader: TokenHeader): a const config = algodApi.createConfiguration(configurationParameters) return new algodApi.AlgodApi(config) } + +class URLTokenBaseHTTPError extends Error implements BaseHTTPClientError { + constructor( + message: string, + public response: BaseHTTPClientResponse, + ) { + super(message) + this.name = 'URLTokenBaseHTTPError' + this.response = response + } +} From 33d3ac0fa663235e606c0874df7ec540fcef4b12 Mon Sep 17 00:00:00 2001 From: Hoang Dinh Date: Fri, 2 May 2025 12:59:13 +1000 Subject: [PATCH 15/18] wip - clean up --- src/types/algo-http-client-with-retry.ts | 2 +- src/types/algokit-core-bridge.ts | 62 +------------------ .../algorand-client-transaction-sender.ts | 54 ++-------------- src/types/client-manager.ts | 32 ---------- 4 files changed, 7 insertions(+), 143 deletions(-) diff --git a/src/types/algo-http-client-with-retry.ts b/src/types/algo-http-client-with-retry.ts index cd1e17e9..7e15ff4c 100644 --- a/src/types/algo-http-client-with-retry.ts +++ b/src/types/algo-http-client-with-retry.ts @@ -124,7 +124,6 @@ export class AlgoHttpClientWithRetry extends URLTokenBaseHTTPClient { return response } - // TODO: verify error handling async post( relativePath: string, data: Uint8Array, @@ -223,6 +222,7 @@ function getAlgoKitCoreAlgodClient(baseUrl: string, tokenHeader: TokenHeader): a return new algodApi.AlgodApi(config) } +// This is a copy of URLTokenBaseHTTPError from algosdk class URLTokenBaseHTTPError extends Error implements BaseHTTPClientError { constructor( message: string, diff --git a/src/types/algokit-core-bridge.ts b/src/types/algokit-core-bridge.ts index a02085d2..1da5fd81 100644 --- a/src/types/algokit-core-bridge.ts +++ b/src/types/algokit-core-bridge.ts @@ -1,7 +1,6 @@ -import { AlgodApi, PendingTransactionResponse, RequestContext, SecurityAuthentication } from '@algorand/algod-client' +import { RequestContext, SecurityAuthentication } from '@algorand/algod-client' import { addressFromString, Transaction as AlgokitCoreTransaction, encodeTransactionRaw } from 'algokit_transact' import algosdk, { Address, TokenHeader } from 'algosdk' -import { toNumber } from '../util' function getAlgokitCoreAddress(address: string | Address) { return addressFromString(typeof address === 'string' ? address : address.toString()) @@ -73,62 +72,3 @@ export class TokenHeaderAuthenticationMethod implements SecurityAuthentication { context.setHeaderParam(this._header, this._key) } } - -/** - * Wait until the transaction is confirmed or rejected, or until `timeout` - * number of rounds have passed. - * - * @param algod An AlgoKit core algod client - * @param transactionId The transaction ID to wait for - * @param maxRoundsToWait Maximum number of rounds to wait - * - * @return Pending transaction information - * @throws Throws an error if the transaction is not confirmed or rejected in the next `timeout` rounds - */ -export const waitForConfirmation = async function ( - transactionId: string, - maxRoundsToWait: number | bigint, - algod: AlgodApi, -): Promise { - if (maxRoundsToWait < 0) { - throw new Error(`Invalid timeout, received ${maxRoundsToWait}, expected > 0`) - } - - // Get current round - const status = await algod.getStatus() - if (status === undefined) { - throw new Error('Unable to get node status') - } - - // Loop for up to `timeout` rounds looking for a confirmed transaction - const startRound = BigInt(status.lastRound) + 1n - let currentRound = startRound - while (currentRound < startRound + BigInt(maxRoundsToWait)) { - try { - const pendingInfo = await algod.pendingTransactionInformation(transactionId, 'msgpack') - - if (pendingInfo !== undefined) { - const confirmedRound = pendingInfo.confirmedRound - if (confirmedRound && confirmedRound > 0) { - return pendingInfo - } else { - const poolError = pendingInfo.poolError - if (poolError != null && poolError.length > 0) { - // If there was a pool error, then the transaction has been rejected! - throw new Error(`Transaction ${transactionId} was rejected; pool error: ${poolError}`) - } - } - } - } catch (e: unknown) { - if ((e as Error).name === 'URLTokenBaseHTTPError') { - currentRound++ - continue - } - } - - await algod.waitForBlock(toNumber(currentRound)) - currentRound++ - } - - throw new Error(`Transaction ${transactionId} not confirmed after ${maxRoundsToWait} rounds`) -} diff --git a/src/types/algorand-client-transaction-sender.ts b/src/types/algorand-client-transaction-sender.ts index 081f1e3e..16d059f4 100644 --- a/src/types/algorand-client-transaction-sender.ts +++ b/src/types/algorand-client-transaction-sender.ts @@ -1,9 +1,7 @@ -import { AlgodApi } from '@algorand/algod-client' import algosdk, { Address } from 'algosdk' import { Buffer } from 'buffer' import { Config } from '../config' import { asJson, defaultJsonValueReplacer } from '../util' -import { AlgoAmount } from './amount' import { SendAppCreateTransactionResult, SendAppTransactionResult, SendAppUpdateTransactionResult } from './app' import { AppManager } from './app-manager' import { AssetManager } from './asset-manager' @@ -18,7 +16,6 @@ import { AppUpdateParams, AssetCreateParams, AssetOptOutParams, - CommonTransactionParams, TransactionComposer, } from './composer' import { SendParams, SendSingleTransactionResult } from './transaction' @@ -40,24 +37,21 @@ export class AlgorandClientTransactionSender { private _newGroup: () => TransactionComposer private _assetManager: AssetManager private _appManager: AppManager - private _algoKitCoreAlgod?: AlgodApi /** * Creates a new `AlgorandClientSender` * @param newGroup A lambda that starts a new `TransactionComposer` transaction group * @param assetManager An `AssetManager` instance * @param appManager An `AppManager` instance - * @param algoKitCoreAlgod An optional `AlgodApi` instance for AlgoKit core * @example * ```typescript * const transactionSender = new AlgorandClientTransactionSender(() => new TransactionComposer(), assetManager, appManager) * ``` */ - constructor(newGroup: () => TransactionComposer, assetManager: AssetManager, appManager: AppManager, algoKitCoreAlgod?: AlgodApi) { + constructor(newGroup: () => TransactionComposer, assetManager: AssetManager, appManager: AppManager) { this._newGroup = newGroup this._assetManager = assetManager this._appManager = appManager - this._algoKitCoreAlgod = algoKitCoreAlgod } /** @@ -168,7 +162,6 @@ export class AlgorandClientTransactionSender { } /** - * Experimental feature: * Send a payment transaction to transfer Algo between accounts. * @param params The parameters for the payment transaction * @example Basic example @@ -208,47 +201,10 @@ export class AlgorandClientTransactionSender { * ``` * @returns The result of the payment transaction and the transaction that was sent */ - payment = async ( - params: CommonTransactionParams & { - receiver: string | Address - amount: AlgoAmount - closeRemainderTo?: string | Address - } & SendParams, - ): Promise => { - return this._send((c) => c.addPayment, { - preLog: (params, transaction) => - `Sending ${params.amount.microAlgo} µALGO from ${params.sender} to ${params.receiver} via transaction ${transaction.txID()}`, - })(params) - - // const composer = this._newGroup() - // composer.addPayment(params) - // const { atc, transactions } = await composer.build() - - // const transaction = transactions[0].txn - - // Config.getLogger(params?.suppressLog).debug( - // `AlgoKit core: sending ${params.amount.microAlgo} µALGO from ${params.sender} to ${params.receiver} via transaction ${transaction.txID()}`, - // ) - - // atc.buildGroup() - // const signedTxns = await atc.gatherSignatures() - - // const httpFile = new File(signedTxns, '') - // await this._algoKitCoreAlgod.rawTransaction(httpFile) - // // TODO: check the support for msgpack in pendingTransactionInformation - // // TODO: conversation about decoding generic types in Rust (whenever we have to do it with algosdk) - // const confirmation = await waitForConfirmation(transaction.txID(), params.maxRoundsToWaitForConfirmation ?? 5, this._algoKitCoreAlgod) - // // const confirmation = confirmation1 as algosdk.modelsv2.PendingTransactionResponse - - // return { - // txIds: [transaction.txID()], - // returns: undefined, - // confirmation: confirmation, - // transaction: transaction, - // confirmations: [confirmation], - // transactions: [transaction], - // } satisfies SendSingleTransactionResult - } + payment = this._send((c) => c.addPayment, { + preLog: (params, transaction) => + `Sending ${params.amount.microAlgo} µALGO from ${params.sender} to ${params.receiver} via transaction ${transaction.txID()}`, + }) /** * Create a new Algorand Standard Asset. diff --git a/src/types/client-manager.ts b/src/types/client-manager.ts index 98067da2..fef69ce2 100644 --- a/src/types/client-manager.ts +++ b/src/types/client-manager.ts @@ -1,7 +1,6 @@ import * as algodApi from '@algorand/algod-client' import algosdk, { SuggestedParams } from 'algosdk' import { AlgoHttpClientWithRetry } from './algo-http-client-with-retry' -import { TokenHeaderAuthenticationMethod } from './algokit-core-bridge' import { type AlgorandClient } from './algorand-client' import { AppClient, AppClientParams, ResolveAppClientByCreatorAndName } from './app-client' import { AppFactory, AppFactoryParams } from './app-factory' @@ -80,12 +79,10 @@ export class ClientManager { ? clientsOrConfig : { algod: ClientManager.getAlgodClient(clientsOrConfig.algodConfig), - algoKitCoreAlgod: ClientManager.getAlgoKitCoreAlgodClient(clientsOrConfig.algodConfig), indexer: clientsOrConfig.indexerConfig ? ClientManager.getIndexerClient(clientsOrConfig.indexerConfig) : undefined, kmd: clientsOrConfig.kmdConfig ? ClientManager.getKmdClient(clientsOrConfig.kmdConfig) : undefined, } this._algod = _clients.algod - this._algoKitCoreAlgod = 'algoKitCoreAlgod' in _clients ? _clients.algoKitCoreAlgod : undefined this._indexer = _clients.indexer this._kmd = _clients.kmd this._algorand = algorandClient @@ -603,35 +600,6 @@ export class ClientManager { return new algosdk.Algodv2(httpClientWithRetry, server) } - public static getAlgoKitCoreAlgodClient(algoClientConfig: AlgoClientConfig): algodApi.AlgodApi { - const { token, server, port } = algoClientConfig - - const authMethodConfig = - token === undefined - ? undefined - : typeof token === 'string' - ? new TokenHeaderAuthenticationMethod({ 'X-Algo-API-Token': token }) - : new TokenHeaderAuthenticationMethod(token) - - // Covers all auth methods included in your OpenAPI yaml definition - const authConfig: algodApi.AuthMethodsConfiguration = { - default: authMethodConfig, - } - - // Create configuration parameter object - const serverConfig = new algodApi.ServerConfiguration(`${server}:${port}`, {}) - const configurationParameters = { - httpApi: new algodApi.IsomorphicFetchHttpLibrary(), - baseServer: serverConfig, - authMethods: authConfig, - promiseMiddleware: [], - } - - // Convert to actual configuration - const config = algodApi.createConfiguration(configurationParameters) - return new algodApi.AlgodApi(config) - } - /** * Returns an algod SDK client that automatically retries on idempotent calls loaded from environment variables (expects to be called from a Node.js environment). * From 524941b70ae45a13e8efeb0868ea98925840315c Mon Sep 17 00:00:00 2001 From: Hoang Dinh Date: Fri, 2 May 2025 13:01:33 +1000 Subject: [PATCH 16/18] wip - clean up --- src/transaction/legacy-bridge.ts | 2 +- src/types/algorand-client-transaction-sender.ts | 1 - src/types/algorand-client.ts | 7 +------ src/types/client-manager.ts | 6 ------ src/types/transaction.ts | 2 +- 5 files changed, 3 insertions(+), 15 deletions(-) diff --git a/src/transaction/legacy-bridge.ts b/src/transaction/legacy-bridge.ts index afb92856..87970336 100644 --- a/src/transaction/legacy-bridge.ts +++ b/src/transaction/legacy-bridge.ts @@ -50,7 +50,7 @@ export async function legacySendTransactionBridge (suggestedParams ? { ...suggestedParams } : await algod.getTransactionParams().do()), appManager, }) - const transactionSender = new AlgorandClientTransactionSender(newGroup, new AssetManager(algod, newGroup), appManager, algod) + const transactionSender = new AlgorandClientTransactionSender(newGroup, new AssetManager(algod, newGroup), appManager) const transactionCreator = new AlgorandClientTransactionCreator(newGroup) if (sendParams.fee) { diff --git a/src/types/algorand-client-transaction-sender.ts b/src/types/algorand-client-transaction-sender.ts index 16d059f4..8f163bf1 100644 --- a/src/types/algorand-client-transaction-sender.ts +++ b/src/types/algorand-client-transaction-sender.ts @@ -205,7 +205,6 @@ export class AlgorandClientTransactionSender { preLog: (params, transaction) => `Sending ${params.amount.microAlgo} µALGO from ${params.sender} to ${params.receiver} via transaction ${transaction.txID()}`, }) - /** * Create a new Algorand Standard Asset. * diff --git a/src/types/algorand-client.ts b/src/types/algorand-client.ts index aa131f12..2e723c59 100644 --- a/src/types/algorand-client.ts +++ b/src/types/algorand-client.ts @@ -35,12 +35,7 @@ export class AlgorandClient { this._accountManager = new AccountManager(this._clientManager) this._appManager = new AppManager(this._clientManager.algod) this._assetManager = new AssetManager(this._clientManager.algod, () => this.newGroup()) - this._transactionSender = new AlgorandClientTransactionSender( - () => this.newGroup(), - this._assetManager, - this._appManager, - this._clientManager.algoKitCoreAlgod, - ) + this._transactionSender = new AlgorandClientTransactionSender(() => this.newGroup(), this._assetManager, this._appManager) this._transactionCreator = new AlgorandClientTransactionCreator(() => this.newGroup()) this._appDeployer = new AppDeployer(this._appManager, this._transactionSender, this._clientManager.indexerIfPresent) } diff --git a/src/types/client-manager.ts b/src/types/client-manager.ts index fef69ce2..cd11ae84 100644 --- a/src/types/client-manager.ts +++ b/src/types/client-manager.ts @@ -1,4 +1,3 @@ -import * as algodApi from '@algorand/algod-client' import algosdk, { SuggestedParams } from 'algosdk' import { AlgoHttpClientWithRetry } from './algo-http-client-with-retry' import { type AlgorandClient } from './algorand-client' @@ -48,7 +47,6 @@ export type ClientTypedAppFactoryParams = Expand & ConfirmedTransactionResult> +export type SendSingleTransactionResult = Expand /** The result of sending a transaction */ export interface SendTransactionResult { From 5ceed2ea566da69599322d6f0e1bb4d6bd6a9d11 Mon Sep 17 00:00:00 2001 From: Hoang Dinh Date: Fri, 2 May 2025 13:14:20 +1000 Subject: [PATCH 17/18] more clean up --- src/types/algo-http-client-with-retry.ts | 29 +++--------------------- src/types/algokit-core-bridge.ts | 27 +++++++++++++++++++--- 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/types/algo-http-client-with-retry.ts b/src/types/algo-http-client-with-retry.ts index 7e15ff4c..80164a4a 100644 --- a/src/types/algo-http-client-with-retry.ts +++ b/src/types/algo-http-client-with-retry.ts @@ -1,4 +1,3 @@ -import * as algodApi from '@algorand/algod-client' import { BaseHTTPClientError, decodeSignedTransaction, @@ -11,7 +10,7 @@ import { } from 'algosdk' import { BaseHTTPClientResponse, Query, URLTokenBaseHTTPClient } from 'algosdk/client' import { Config } from '../config' -import { TokenHeaderAuthenticationMethod } from './algokit-core-bridge' +import { buildAlgoKitCoreAlgodClient } from './algokit-core-bridge' /** A HTTP Client that wraps the Algorand SDK HTTP Client with retries */ export class AlgoHttpClientWithRetry extends URLTokenBaseHTTPClient { @@ -75,7 +74,7 @@ export class AlgoHttpClientWithRetry extends URLTokenBaseHTTPClient { const baseUrl = (this as any).baseURL as URL // eslint-disable-next-line @typescript-eslint/no-explicit-any const tokenHeader = (this as any).tokenHeader as TokenHeader - const algoKitCoreAlgod = getAlgoKitCoreAlgodClient(baseUrl.toString(), tokenHeader) + const algoKitCoreAlgod = buildAlgoKitCoreAlgodClient(baseUrl.toString(), tokenHeader) return await this.callWithRetry(async () => { const httpInfo = await algoKitCoreAlgod.pendingTransactionInformationResponse(possibleTxnId, 'msgpack') @@ -145,7 +144,7 @@ export class AlgoHttpClientWithRetry extends URLTokenBaseHTTPClient { // eslint-disable-next-line @typescript-eslint/no-explicit-any const tokenHeader = (this as any).tokenHeader as TokenHeader - const algoKitCoreAlgod = getAlgoKitCoreAlgodClient(baseUrl.toString(), tokenHeader) + const algoKitCoreAlgod = buildAlgoKitCoreAlgodClient(baseUrl.toString(), tokenHeader) return await this.callWithRetry(async () => { const responseContext = await algoKitCoreAlgod.rawTransactionResponse(new File([data], '')) @@ -200,28 +199,6 @@ export class AlgoHttpClientWithRetry extends URLTokenBaseHTTPClient { } } -function getAlgoKitCoreAlgodClient(baseUrl: string, tokenHeader: TokenHeader): algodApi.AlgodApi { - const authMethodConfig = new TokenHeaderAuthenticationMethod(tokenHeader) - // Covers all auth methods included in your OpenAPI yaml definition - const authConfig: algodApi.AuthMethodsConfiguration = { - default: authMethodConfig, - } - - // Create configuration parameter object - const fixedBaseUrl = baseUrl.replace(/\/+$/, '') - const serverConfig = new algodApi.ServerConfiguration(fixedBaseUrl, {}) - const configurationParameters = { - httpApi: new algodApi.IsomorphicFetchHttpLibrary(), - baseServer: serverConfig, - authMethods: authConfig, - promiseMiddleware: [], - } - - // Convert to actual configuration - const config = algodApi.createConfiguration(configurationParameters) - return new algodApi.AlgodApi(config) -} - // This is a copy of URLTokenBaseHTTPError from algosdk class URLTokenBaseHTTPError extends Error implements BaseHTTPClientError { constructor( diff --git a/src/types/algokit-core-bridge.ts b/src/types/algokit-core-bridge.ts index 1da5fd81..c930edfc 100644 --- a/src/types/algokit-core-bridge.ts +++ b/src/types/algokit-core-bridge.ts @@ -1,4 +1,4 @@ -import { RequestContext, SecurityAuthentication } from '@algorand/algod-client' +import * as algodApi from '@algorand/algod-client' import { addressFromString, Transaction as AlgokitCoreTransaction, encodeTransactionRaw } from 'algokit_transact' import algosdk, { Address, TokenHeader } from 'algosdk' @@ -54,7 +54,7 @@ export function buildPayment({ return algosdk.decodeUnsignedTransaction(encodeTransactionRaw(txnModel)) } -export class TokenHeaderAuthenticationMethod implements SecurityAuthentication { +export class TokenHeaderAuthenticationMethod implements algodApi.SecurityAuthentication { private _header: string private _key: string @@ -68,7 +68,28 @@ export class TokenHeaderAuthenticationMethod implements SecurityAuthentication { return 'custom_header' } - public applySecurityAuthentication(context: RequestContext) { + public applySecurityAuthentication(context: algodApi.RequestContext) { context.setHeaderParam(this._header, this._key) } } + +export function buildAlgoKitCoreAlgodClient(baseUrl: string, tokenHeader: TokenHeader): algodApi.AlgodApi { + const authMethodConfig = new TokenHeaderAuthenticationMethod(tokenHeader) + const authConfig: algodApi.AuthMethodsConfiguration = { + default: authMethodConfig, + } + + // Create configuration parameter object + const fixedBaseUrl = baseUrl.replace(/\/+$/, '') + const serverConfig = new algodApi.ServerConfiguration(fixedBaseUrl, {}) + const configurationParameters = { + httpApi: new algodApi.IsomorphicFetchHttpLibrary(), + baseServer: serverConfig, + authMethods: authConfig, + promiseMiddleware: [], + } + + // Convert to actual configuration + const config = algodApi.createConfiguration(configurationParameters) + return new algodApi.AlgodApi(config) +} From 8a32def260cda3ee8c9b350afca51f9796f0e0af Mon Sep 17 00:00:00 2001 From: Hoang Dinh Date: Fri, 2 May 2025 15:43:27 +1000 Subject: [PATCH 18/18] tests --- src/transaction/transaction.spec.ts | 22 +++++++++---- src/types/algo-http-client-with-retry.ts | 42 ++++++++++++++++-------- src/types/algokit-core-bridge.ts | 10 ++++-- 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/src/transaction/transaction.spec.ts b/src/transaction/transaction.spec.ts index c4ef04ab..8e385921 100644 --- a/src/transaction/transaction.spec.ts +++ b/src/transaction/transaction.spec.ts @@ -1160,12 +1160,21 @@ describe('abi return', () => { }) }) -// TODO: PD - fix this test, how??? describe('When create algorand client with config from environment', () => { - test('payment transactions are sent by algokit core algod client', async () => { + test('payment transactions are sent and waited for by algokit core algod client', async () => { const algorandClient = AlgorandClient.fromConfig(ClientManager.getConfigFromEnvironmentOrLocalNet()) - const algodSpy = vi.spyOn(algorandClient.client.algod, 'sendRawTransaction') - const algoKitCoreAlgodSpy = vi.spyOn(algorandClient.client.algoKitCoreAlgod!, 'rawTransaction') + + const sendRawTransactionWithAlgoKitCoreAlgod = vi.spyOn( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (algorandClient.client.algod.c as any).bc._algoKitCoreAlgod, + 'rawTransactionResponse', + ) + + const sendPendingTransactionInformationResponseWithAlgoKitCoreAlgod = vi.spyOn( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (algorandClient.client.algod.c as any).bc._algoKitCoreAlgod, + 'pendingTransactionInformationResponse', + ) const testAccount = await getTestAccount({ initialFunds: algos(10), suppressLog: true }, algorandClient) algorandClient.setSignerFromAccount(testAccount) @@ -1179,9 +1188,8 @@ describe('When create algorand client with config from environment', () => { const fee = (1).algo() const { confirmation } = await algorandClient.send.payment({ ...testPayTransaction, staticFee: fee }) - // expect(algodSpy).not.toHaveBeenCalled() - // expect(algorandClient.client.algoKitCoreAlgod).toBeDefined() - // expect(algoKitCoreAlgodSpy).toBeCalledTimes(2) + expect(sendRawTransactionWithAlgoKitCoreAlgod).toBeCalledTimes(2) + expect(sendPendingTransactionInformationResponseWithAlgoKitCoreAlgod).toBeCalled() expect(confirmation.txn.txn.fee).toBe(fee.microAlgo) }) }) diff --git a/src/types/algo-http-client-with-retry.ts b/src/types/algo-http-client-with-retry.ts index 80164a4a..3db4572f 100644 --- a/src/types/algo-http-client-with-retry.ts +++ b/src/types/algo-http-client-with-retry.ts @@ -1,3 +1,4 @@ +import { AlgodApi } from '@algorand/algod-client' import { BaseHTTPClientError, decodeSignedTransaction, @@ -14,6 +15,15 @@ import { buildAlgoKitCoreAlgodClient } from './algokit-core-bridge' /** A HTTP Client that wraps the Algorand SDK HTTP Client with retries */ export class AlgoHttpClientWithRetry extends URLTokenBaseHTTPClient { + private _algoKitCoreAlgod: AlgodApi + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(tokenHeader: TokenHeader, baseServer: string, port?: string | number, defaultHeaders?: Record) { + super(tokenHeader, baseServer, port, defaultHeaders) + + this._algoKitCoreAlgod = buildAlgoKitCoreAlgodClient(this.buildBaseServerUrl(baseServer, port), tokenHeader) + } + private static readonly MAX_TRIES = 5 private static readonly MAX_BACKOFF_MS = 10000 @@ -32,6 +42,21 @@ export class AlgoHttpClientWithRetry extends URLTokenBaseHTTPClient { 'EPROTO', // We get this intermittently with AlgoNode API ] + private buildBaseServerUrl(baseServer: string, port?: string | number) { + // This logic is copied from algosdk to make sure that we have the same base server config + + // Append a trailing slash so we can use relative paths. Without the trailing + // slash, the last path segment will be replaced by the relative path. See + // usage in `addressWithPath`. + const fixedBaseServer = baseServer.endsWith('/') ? baseServer : `${baseServer}/` + const baseServerURL = new URL(fixedBaseServer) + if (typeof port !== 'undefined') { + baseServerURL.port = port.toString() + } + + return baseServerURL + } + private async callWithRetry(func: () => Promise): Promise { let response: BaseHTTPClientResponse | undefined let numTries = 1 @@ -70,14 +95,8 @@ export class AlgoHttpClientWithRetry extends URLTokenBaseHTTPClient { const possibleTxnId = relativePath.replace('/v2/transactions/pending/', '').replace(/\/+$/, '') // TODO: test for possibleTxnId if (possibleTxnId) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const baseUrl = (this as any).baseURL as URL - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokenHeader = (this as any).tokenHeader as TokenHeader - const algoKitCoreAlgod = buildAlgoKitCoreAlgodClient(baseUrl.toString(), tokenHeader) - return await this.callWithRetry(async () => { - const httpInfo = await algoKitCoreAlgod.pendingTransactionInformationResponse(possibleTxnId, 'msgpack') + const httpInfo = await this._algoKitCoreAlgod.pendingTransactionInformationResponse(possibleTxnId, 'msgpack') const binary = await httpInfo.body.binary() const arrayBuffer = await binary.arrayBuffer() const uint8Array = new Uint8Array(arrayBuffer) @@ -139,20 +158,15 @@ export class AlgoHttpClientWithRetry extends URLTokenBaseHTTPClient { // Ignore errors here } if (signedTxn && signedTxn.txn.type === TransactionType.pay) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const baseUrl = (this as any).baseURL as URL - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokenHeader = (this as any).tokenHeader as TokenHeader - - const algoKitCoreAlgod = buildAlgoKitCoreAlgodClient(baseUrl.toString(), tokenHeader) return await this.callWithRetry(async () => { - const responseContext = await algoKitCoreAlgod.rawTransactionResponse(new File([data], '')) + const responseContext = await this._algoKitCoreAlgod.rawTransactionResponse(new File([data], '')) const binary = await responseContext.body.binary() const arrayBuffer = await binary.arrayBuffer() const uint8Array = new Uint8Array(arrayBuffer) if (responseContext.httpStatusCode !== 200) { + // This logic is copied from algosdk to make sure that we produce the same errors let bodyErrorMessage: string | undefined try { diff --git a/src/types/algokit-core-bridge.ts b/src/types/algokit-core-bridge.ts index c930edfc..30da438c 100644 --- a/src/types/algokit-core-bridge.ts +++ b/src/types/algokit-core-bridge.ts @@ -59,6 +59,10 @@ export class TokenHeaderAuthenticationMethod implements algodApi.SecurityAuthent private _key: string public constructor(tokenHeader: TokenHeader) { + if (Object.entries(tokenHeader).length === 0) { + throw new Error('Cannot construct empty token header auth') + } + const [header, key] = Object.entries(tokenHeader)[0] this._header = header this._key = key @@ -73,14 +77,14 @@ export class TokenHeaderAuthenticationMethod implements algodApi.SecurityAuthent } } -export function buildAlgoKitCoreAlgodClient(baseUrl: string, tokenHeader: TokenHeader): algodApi.AlgodApi { - const authMethodConfig = new TokenHeaderAuthenticationMethod(tokenHeader) +export function buildAlgoKitCoreAlgodClient(baseUrl: URL, tokenHeader: TokenHeader): algodApi.AlgodApi { + const authMethodConfig = Object.entries(tokenHeader).length > 0 ? new TokenHeaderAuthenticationMethod(tokenHeader) : undefined const authConfig: algodApi.AuthMethodsConfiguration = { default: authMethodConfig, } // Create configuration parameter object - const fixedBaseUrl = baseUrl.replace(/\/+$/, '') + const fixedBaseUrl = baseUrl.toString().replace(/\/+$/, '') const serverConfig = new algodApi.ServerConfiguration(fixedBaseUrl, {}) const configurationParameters = { httpApi: new algodApi.IsomorphicFetchHttpLibrary(),