From 48ded50a3b1f11d81595a7d7a7642b83633e6f0d Mon Sep 17 00:00:00 2001 From: p2arthur Date: Mon, 22 Sep 2025 22:00:44 -0700 Subject: [PATCH 1/8] fix(hazard_symbol): start fixing dual-package hazard symbol --- src/testing/test-logger.ts | 15 +++++++++++++++ src/types/account.ts | 28 ++++++++++++++++++++++++++++ src/types/app-deployer.ts | 14 ++++++++++++++ src/types/composer.ts | 14 ++++++++++++++ 4 files changed, 71 insertions(+) diff --git a/src/testing/test-logger.ts b/src/testing/test-logger.ts index 9ec5578b..5acd9d95 100644 --- a/src/testing/test-logger.ts +++ b/src/testing/test-logger.ts @@ -9,6 +9,21 @@ export class TestLogger implements Logger { private originalLogger: Logger | undefined private logs: string[] + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isTestLogger = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is a TestLogger, regardless of which module loaded it + */ + + static [Symbol.hasInstance](instance: any): boolean { + return instance && instance._isAlgosdkABIContract === true + } + /** * Create a new test logger that wraps the given logger if provided. * @param originalLogger The optional original logger to wrap. diff --git a/src/types/account.ts b/src/types/account.ts index ee87b8c6..d73d7f30 100644 --- a/src/types/account.ts +++ b/src/types/account.ts @@ -23,6 +23,20 @@ export class MultisigAccount { _addr: Address _signer: TransactionSigner + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isMultisigAccount = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is a MultisigAccount, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: any): boolean { + return instance && instance._isMultisigAccount === true + } + /** The parameters for the multisig account */ get params(): Readonly { return this._params @@ -81,6 +95,20 @@ export class SigningAccount implements Account { private _signer: TransactionSigner private _sender: Address + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isSigningAccount = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is an ABIContract, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: any): boolean { + return instance && instance._isSigningAccount === true + } + /** * Algorand address of the sender */ diff --git a/src/types/app-deployer.ts b/src/types/app-deployer.ts index 11db70a2..61594cec 100644 --- a/src/types/app-deployer.ts +++ b/src/types/app-deployer.ts @@ -115,6 +115,20 @@ export class AppDeployer { private _indexer?: algosdk.Indexer private _appLookups = new Map() + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAppDeployer = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is an AppDeployer, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: any): boolean { + return instance && instance._isAppDeployer === true + } + /** * Creates an `AppManager` * @param appManager An `AppManager` instance diff --git a/src/types/composer.ts b/src/types/composer.ts index 0a71fa86..156af504 100644 --- a/src/types/composer.ts +++ b/src/types/composer.ts @@ -548,6 +548,20 @@ export class TransactionComposer { /** Signer used to represent a lack of signer */ private static NULL_SIGNER: algosdk.TransactionSigner = algosdk.makeEmptyTransactionSigner() + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isTransactionComposer = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is an TransactionComposer, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: any): boolean { + return instance && instance._isTransactionComposer === true + } + /** The ATC used to compose the group */ private atc = new algosdk.AtomicTransactionComposer() From 579d4661e9acf0cafd8c38560708d47d5ba69d8b Mon Sep 17 00:00:00 2001 From: p2arthur Date: Mon, 22 Sep 2025 22:29:43 -0700 Subject: [PATCH 2/8] fix(lint) fix lint errors + new logic for checking instances of the classes --- src/testing/test-logger.ts | 14 ++++++++++++++ src/types/account.ts | 28 ++++++++++++++++++++++++++++ src/types/app-deployer.ts | 14 ++++++++++++++ src/types/composer.ts | 14 ++++++++++++++ 4 files changed, 70 insertions(+) diff --git a/src/testing/test-logger.ts b/src/testing/test-logger.ts index 9ec5578b..be1c72bd 100644 --- a/src/testing/test-logger.ts +++ b/src/testing/test-logger.ts @@ -9,6 +9,20 @@ export class TestLogger implements Logger { private originalLogger: Logger | undefined private logs: string[] + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isTestLogger = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is an ABIContract, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: unknown): boolean { + return !!(instance && (instance as TestLogger)._isTestLogger === true) + } + /** * Create a new test logger that wraps the given logger if provided. * @param originalLogger The optional original logger to wrap. diff --git a/src/types/account.ts b/src/types/account.ts index ee87b8c6..c2a658e8 100644 --- a/src/types/account.ts +++ b/src/types/account.ts @@ -23,6 +23,20 @@ export class MultisigAccount { _addr: Address _signer: TransactionSigner + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isMultisigAccount = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is an ABIContract, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: unknown): boolean { + return !!(instance && (instance as MultisigAccount)._isMultisigAccount === true) + } + /** The parameters for the multisig account */ get params(): Readonly { return this._params @@ -81,6 +95,20 @@ export class SigningAccount implements Account { private _signer: TransactionSigner private _sender: Address + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isSigningAccount = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is an ABIContract, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: unknown): boolean { + return !!(instance && (instance as SigningAccount)._isSigningAccount === true) + } + /** * Algorand address of the sender */ diff --git a/src/types/app-deployer.ts b/src/types/app-deployer.ts index 11db70a2..f8767d5c 100644 --- a/src/types/app-deployer.ts +++ b/src/types/app-deployer.ts @@ -115,6 +115,20 @@ export class AppDeployer { private _indexer?: algosdk.Indexer private _appLookups = new Map() + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAppDeployer = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is an ABIContract, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: unknown): boolean { + return !!(instance && (instance as AppDeployer)._isAppDeployer === true) + } + /** * Creates an `AppManager` * @param appManager An `AppManager` instance diff --git a/src/types/composer.ts b/src/types/composer.ts index 0a71fa86..38f131f0 100644 --- a/src/types/composer.ts +++ b/src/types/composer.ts @@ -578,6 +578,20 @@ export class TransactionComposer { private errorTransformers: ErrorTransformer[] + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isTransactionComposer = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is an ABIContract, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: unknown): boolean { + return !!(instance && (instance as TransactionComposer)._isTransactionComposer === true) + } + private async transformError(originalError: unknown): Promise { // Transformers only work with Error instances, so immediately return anything else if (!(originalError instanceof Error)) { From 0acfb5ce9979200d2c3d0409dbd8fbabb9f9b91c Mon Sep 17 00:00:00 2001 From: p2arthur Date: Tue, 30 Sep 2025 00:01:13 -0700 Subject: [PATCH 3/8] fix(dual package issue) fix dual package issue in more classes --- src/testing/test-logger.ts | 2 +- src/types/account-manager.ts | 14 ++++++++++ src/types/account.ts | 4 +-- .../algorand-client-transaction-sender.ts | 14 ++++++++++ src/types/app-arc56.ts | 14 ++++++++++ src/types/app-client.ts | 27 +++++++++++++++++++ src/types/app-deployer.ts | 2 +- src/types/app-manager.ts | 14 ++++++++++ src/types/composer.ts | 16 +---------- src/types/logic-error.ts | 14 ++++++++++ 10 files changed, 102 insertions(+), 19 deletions(-) diff --git a/src/testing/test-logger.ts b/src/testing/test-logger.ts index be1c72bd..2abf78c8 100644 --- a/src/testing/test-logger.ts +++ b/src/testing/test-logger.ts @@ -17,7 +17,7 @@ export class TestLogger implements Logger { /** * Custom Symbol.hasInstance to handle dual package hazard * @param instance - The instance to check - * @returns true if the instance is an ABIContract, regardless of which module loaded it + * @returns true if the instance is of the Type of the class, regardless of which module loaded it */ static [Symbol.hasInstance](instance: unknown): boolean { return !!(instance && (instance as TestLogger)._isTestLogger === true) diff --git a/src/types/account-manager.ts b/src/types/account-manager.ts index 476ae9a8..41a5a217 100644 --- a/src/types/account-manager.ts +++ b/src/types/account-manager.ts @@ -49,6 +49,20 @@ export class AccountManager { private _accounts: { [address: string]: TransactionSignerAccount } = {} private _defaultSigner?: algosdk.TransactionSigner + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAccountManager = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is of the Type of the class, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: unknown): boolean { + return !!(instance && (instance as AccountManager)._isAccountManager === true) + } + /** * Create a new account manager. * @param clientManager The ClientManager client to use for algod and kmd clients diff --git a/src/types/account.ts b/src/types/account.ts index c2a658e8..661db045 100644 --- a/src/types/account.ts +++ b/src/types/account.ts @@ -31,7 +31,7 @@ export class MultisigAccount { /** * Custom Symbol.hasInstance to handle dual package hazard * @param instance - The instance to check - * @returns true if the instance is an ABIContract, regardless of which module loaded it + * @returns true if the instance is of the Type of the class, regardless of which module loaded it */ static [Symbol.hasInstance](instance: unknown): boolean { return !!(instance && (instance as MultisigAccount)._isMultisigAccount === true) @@ -103,7 +103,7 @@ export class SigningAccount implements Account { /** * Custom Symbol.hasInstance to handle dual package hazard * @param instance - The instance to check - * @returns true if the instance is an ABIContract, regardless of which module loaded it + * @returns true if the instance is of the Type of the class, regardless of which module loaded it */ static [Symbol.hasInstance](instance: unknown): boolean { return !!(instance && (instance as SigningAccount)._isSigningAccount === true) diff --git a/src/types/algorand-client-transaction-sender.ts b/src/types/algorand-client-transaction-sender.ts index 8f163bf1..1c17fb2c 100644 --- a/src/types/algorand-client-transaction-sender.ts +++ b/src/types/algorand-client-transaction-sender.ts @@ -38,6 +38,20 @@ export class AlgorandClientTransactionSender { private _assetManager: AssetManager private _appManager: AppManager + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAlgorandClientTransactionSender = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is of the Type of the class, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: unknown): boolean { + return !!(instance && (instance as AlgorandClientTransactionSender)._isAlgorandClientTransactionSender === true) + } + /** * Creates a new `AlgorandClientSender` * @param newGroup A lambda that starts a new `TransactionComposer` transaction group diff --git a/src/types/app-arc56.ts b/src/types/app-arc56.ts index 51359b45..b5b72a04 100644 --- a/src/types/app-arc56.ts +++ b/src/types/app-arc56.ts @@ -24,6 +24,20 @@ export class Arc56Method extends algosdk.ABIMethod { override readonly args: Array override readonly returns: Arc56MethodReturnType + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isArc56Method = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is of the Type of the class, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: unknown): boolean { + return !!(instance && (instance as Arc56Method)._isArc56Method === true) + } + constructor(public method: Method) { super(method) this.args = method.args.map((arg) => ({ diff --git a/src/types/app-client.ts b/src/types/app-client.ts index ff3d6224..fdf6a3cc 100644 --- a/src/types/app-client.ts +++ b/src/types/app-client.ts @@ -502,6 +502,20 @@ export class AppClient { } private _lastCompiled: { clear?: Uint8Array; approval?: Uint8Array } + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAppClient = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is of the Type of the class, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: unknown): boolean { + return !!(instance && (instance as AppClient)._isAppClient === true) + } + /** * Create a new app client. * @param params The parameters to create the app client @@ -1807,6 +1821,19 @@ export class ApplicationClient { private _approvalSourceMap: SourceMap | undefined private _clearSourceMap: SourceMap | undefined + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isApplicationClient = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is of the Type of the class, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: unknown): boolean { + return !!(instance && (instance as ApplicationClient)._isApplicationClient === true) + } /** * @deprecated Use `AppClient` instead e.g. via `algorand.client.getAppClientById` or diff --git a/src/types/app-deployer.ts b/src/types/app-deployer.ts index f8767d5c..0bdb9201 100644 --- a/src/types/app-deployer.ts +++ b/src/types/app-deployer.ts @@ -123,7 +123,7 @@ export class AppDeployer { /** * Custom Symbol.hasInstance to handle dual package hazard * @param instance - The instance to check - * @returns true if the instance is an ABIContract, regardless of which module loaded it + * @returns true if the instance is of the Type of the class, regardless of which module loaded it */ static [Symbol.hasInstance](instance: unknown): boolean { return !!(instance && (instance as AppDeployer)._isAppDeployer === true) diff --git a/src/types/app-manager.ts b/src/types/app-manager.ts index 16aed9a6..2ca7c39e 100644 --- a/src/types/app-manager.ts +++ b/src/types/app-manager.ts @@ -99,6 +99,20 @@ export class AppManager { private _algod: algosdk.Algodv2 private _compilationResults: Record = {} + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAppManager = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is of the Type of the class, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: unknown): boolean { + return !!(instance && (instance as AppManager)._isAppManager === true) + } + /** * Creates an `AppManager` * @param algod An algod instance diff --git a/src/types/composer.ts b/src/types/composer.ts index 44508af2..09b4f310 100644 --- a/src/types/composer.ts +++ b/src/types/composer.ts @@ -548,20 +548,6 @@ export class TransactionComposer { /** Signer used to represent a lack of signer */ private static NULL_SIGNER: algosdk.TransactionSigner = algosdk.makeEmptyTransactionSigner() - /** - * Unique property marker for Symbol.hasInstance compatibility across module boundaries - */ - private readonly _isTransactionComposer = true - - /** - * Custom Symbol.hasInstance to handle dual package hazard - * @param instance - The instance to check - * @returns true if the instance is an TransactionComposer, regardless of which module loaded it - */ - static [Symbol.hasInstance](instance: any): boolean { - return instance && instance._isTransactionComposer === true - } - /** The ATC used to compose the group */ private atc = new algosdk.AtomicTransactionComposer() @@ -600,7 +586,7 @@ export class TransactionComposer { /** * Custom Symbol.hasInstance to handle dual package hazard * @param instance - The instance to check - * @returns true if the instance is an ABIContract, regardless of which module loaded it + * @returns true if the instance is of the Type of the class, regardless of which module loaded it */ static [Symbol.hasInstance](instance: unknown): boolean { return !!(instance && (instance as TransactionComposer)._isTransactionComposer === true) diff --git a/src/types/logic-error.ts b/src/types/logic-error.ts index 7018925b..aa6aec5b 100644 --- a/src/types/logic-error.ts +++ b/src/types/logic-error.ts @@ -20,6 +20,20 @@ export interface LogicErrorDetails { /** Wraps key functionality around processing logic errors */ export class LogicError extends Error { + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isLogicError = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is of the Type of the class, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: unknown): boolean { + return !!(instance && (instance as LogicError)._isLogicError === true) + } + /** Takes an error message and parses out the details of any logic errors in there. * @param error The error message to parse * @returns The logic error details if any, or undefined From 0f01ef39760756dbbb4f28585099c7d18bed7ffa Mon Sep 17 00:00:00 2001 From: p2arthur Date: Tue, 30 Sep 2025 01:42:21 -0700 Subject: [PATCH 4/8] fix(dual package issue) fix dual package issue in more classes --- src/types/algo-http-client-with-retry.ts | 14 ++++++++++++++ src/types/algorand-client-transaction-creator.ts | 14 ++++++++++++++ src/types/algorand-client.ts | 14 ++++++++++++++ src/types/async-event-emitter.ts | 14 ++++++++++++++ src/types/client-manager.ts | 14 ++++++++++++++ src/types/dispenser-client.ts | 14 ++++++++++++++ src/types/kmd-account-manager.ts | 14 ++++++++++++++ 7 files changed, 98 insertions(+) diff --git a/src/types/algo-http-client-with-retry.ts b/src/types/algo-http-client-with-retry.ts index 214e194d..d7efd115 100644 --- a/src/types/algo-http-client-with-retry.ts +++ b/src/types/algo-http-client-with-retry.ts @@ -22,6 +22,20 @@ export class AlgoHttpClientWithRetry extends URLTokenBaseHTTPClient { 'EPROTO', // We get this intermittently with AlgoNode API ] + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAlgoHttpClientWithRetry = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is of the Type of the class, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: unknown): boolean { + return !!(instance && (instance as AlgoHttpClientWithRetry)._isAlgoHttpClientWithRetry === true) + } + private async callWithRetry(func: () => Promise): Promise { let response: BaseHTTPClientResponse | undefined let numTries = 1 diff --git a/src/types/algorand-client-transaction-creator.ts b/src/types/algorand-client-transaction-creator.ts index 7a1bf216..2252e729 100644 --- a/src/types/algorand-client-transaction-creator.ts +++ b/src/types/algorand-client-transaction-creator.ts @@ -8,6 +8,20 @@ import Transaction = algosdk.Transaction export class AlgorandClientTransactionCreator { private _newGroup: () => TransactionComposer + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAlgorandClientTransactionCreator = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is of the Type of the class, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: unknown): boolean { + return !!(instance && (instance as AlgorandClientTransactionCreator)._isAlgorandClientTransactionCreator === true) + } + /** * Creates a new `AlgorandClientTransactionCreator` * @param newGroup A lambda that starts a new `TransactionComposer` transaction group diff --git a/src/types/algorand-client.ts b/src/types/algorand-client.ts index 24bd16f2..ee1a8b00 100644 --- a/src/types/algorand-client.ts +++ b/src/types/algorand-client.ts @@ -37,6 +37,20 @@ export class AlgorandClient { */ private _errorTransformers: Set = new Set() + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAlgorandClient = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is of the Type of the class, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: unknown): boolean { + return !!(instance && (instance as AlgorandClient)._isAlgorandClient === true) + } + private constructor(config: AlgoConfig | AlgoSdkClients) { this._clientManager = new ClientManager(config, this) this._accountManager = new AccountManager(this._clientManager) diff --git a/src/types/async-event-emitter.ts b/src/types/async-event-emitter.ts index 601635d9..c5601ad5 100644 --- a/src/types/async-event-emitter.ts +++ b/src/types/async-event-emitter.ts @@ -6,6 +6,20 @@ export class AsyncEventEmitter { private listenerWrapperMap = new WeakMap() private listenerMap: Record = {} + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAsyncEventEmitter = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is of the Type of the class, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: unknown): boolean { + return !!(instance && (instance as AsyncEventEmitter)._isAsyncEventEmitter === true) + } + async emitAsync(eventName: K, event: EventDataMap[K]): Promise async emitAsync(eventName: string | symbol, event: unknown): Promise async emitAsync(eventName: string | symbol, event: unknown): Promise { diff --git a/src/types/client-manager.ts b/src/types/client-manager.ts index cd11ae84..b027e3ed 100644 --- a/src/types/client-manager.ts +++ b/src/types/client-manager.ts @@ -51,6 +51,20 @@ export class ClientManager { private _kmd?: algosdk.Kmd private _algorand?: AlgorandClient + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isClientManager = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is of the Type of the class, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: unknown): boolean { + return !!(instance && (instance as ClientManager)._isClientManager === true) + } + /** * algosdk clients or config for interacting with the official Algorand APIs. * @param clientsOrConfig The clients or config to use diff --git a/src/types/dispenser-client.ts b/src/types/dispenser-client.ts index cc9a9b44..df39ba74 100644 --- a/src/types/dispenser-client.ts +++ b/src/types/dispenser-client.ts @@ -73,6 +73,20 @@ export class TestNetDispenserApiClient { private _authToken: string private _requestTimeout: number + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isTestNetDispenserApiClient = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is of the Type of the class, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: unknown): boolean { + return !!(instance && (instance as TestNetDispenserApiClient)._isTestNetDispenserApiClient === true) + } + constructor(params?: TestNetDispenserApiClientParams) { const authTokenFromEnv = process?.env?.[DISPENSER_ACCESS_TOKEN_KEY] diff --git a/src/types/kmd-account-manager.ts b/src/types/kmd-account-manager.ts index b92231b2..0317061f 100644 --- a/src/types/kmd-account-manager.ts +++ b/src/types/kmd-account-manager.ts @@ -11,6 +11,20 @@ export class KmdAccountManager { private _clientManager: Omit private _kmd?: algosdk.Kmd | null + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isKmdAccountManager = true + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is of the Type of the class, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: unknown): boolean { + return !!(instance && (instance as KmdAccountManager)._isKmdAccountManager === true) + } + /** * Create a new KMD manager. * @param clientManager A ClientManager client to use for algod and kmd clients From a4ab87fada756afebc123cf9af348089a9f6cfd2 Mon Sep 17 00:00:00 2001 From: p2arthur Date: Tue, 30 Sep 2025 01:43:43 -0700 Subject: [PATCH 5/8] test(dual package issue): start adding tests to dual package isuue --- src/types/dual-package-hazard.spec.ts | 60 +++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/types/dual-package-hazard.spec.ts diff --git a/src/types/dual-package-hazard.spec.ts b/src/types/dual-package-hazard.spec.ts new file mode 100644 index 00000000..e06d0192 --- /dev/null +++ b/src/types/dual-package-hazard.spec.ts @@ -0,0 +1,60 @@ +import * as algosdk from 'algosdk' +import assert from 'assert' + +import { beforeEach, describe, it } from 'vitest' +import { TestNetDispenserApiClient } from './dispenser-client' + +describe('Dual Package Hazard Solution', () => { + describe('TestNetDispenserApiClient Symbol.hasInstance', () => { + let client: TestNetDispenserApiClient + + beforeEach(() => { + client = new TestNetDispenserApiClient({ authToken: 'test-token', requestTimeout: 5 }) + }) + + it('should work with regular instanceof', () => { + assert.strictEqual(client instanceof TestNetDispenserApiClient, true) + }) + + it('should work with custom Symbol.hasInstance', () => { + assert.strictEqual(TestNetDispenserApiClient[Symbol.hasInstance](client), true) + }) + + it('should work with cross-module simulation', () => { + const mockClient = { + _isTestNetDispenserApiClient: true, // must match your hasInstance check + _authToken: 'other-token', + _requestTimeout: 15, + } as any + + assert.strictEqual(TestNetDispenserApiClient[Symbol.hasInstance](mockClient), true) + }) + + it('should reject objects without marker', () => { + const fakeClient = { + _authToken: 'no-marker', + _requestTimeout: 10, + } as any + + assert.strictEqual(TestNetDispenserApiClient[Symbol.hasInstance](fakeClient), false) + }) + }) + + describe('Edge cases', () => { + it('should handle primitive values', () => { + assert.strictEqual(algosdk.Address[Symbol.hasInstance]('string'), false) + assert.strictEqual(algosdk.Address[Symbol.hasInstance](123), false) + assert.strictEqual(algosdk.Address[Symbol.hasInstance](true), false) + }) + + it('should handle empty objects', () => { + assert.strictEqual(algosdk.Address[Symbol.hasInstance]({}), false) + assert.strictEqual(algosdk.Transaction[Symbol.hasInstance]({}), false) + }) + + it('should handle objects with wrong marker values', () => { + const wrongMarker = { _isAlgosdkAddress: 'true' } // string instead of boolean + assert.strictEqual(algosdk.Address[Symbol.hasInstance](wrongMarker), false) + }) + }) +}) From 80b8baf691051f7ed87ca113596d80378cee62d8 Mon Sep 17 00:00:00 2001 From: p2arthur Date: Thu, 2 Oct 2025 23:14:16 -0700 Subject: [PATCH 6/8] test(dual_package) add test to check for dual pack hazard for MultisigAccount --- src/types/dual-package-hazard.spec.ts | 115 +++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 2 deletions(-) diff --git a/src/types/dual-package-hazard.spec.ts b/src/types/dual-package-hazard.spec.ts index e06d0192..24d7c78d 100644 --- a/src/types/dual-package-hazard.spec.ts +++ b/src/types/dual-package-hazard.spec.ts @@ -2,6 +2,8 @@ import * as algosdk from 'algosdk' import assert from 'assert' import { beforeEach, describe, it } from 'vitest' +import { MultisigAccount } from './account' +import { AlgorandClientTransactionCreator } from './algorand-client-transaction-creator' import { TestNetDispenserApiClient } from './dispenser-client' describe('Dual Package Hazard Solution', () => { @@ -22,7 +24,7 @@ describe('Dual Package Hazard Solution', () => { it('should work with cross-module simulation', () => { const mockClient = { - _isTestNetDispenserApiClient: true, // must match your hasInstance check + _isTestNetDispenserApiClient: true, _authToken: 'other-token', _requestTimeout: 15, } as any @@ -40,6 +42,115 @@ describe('Dual Package Hazard Solution', () => { }) }) + describe('AlgorandClientTransactionCreator Symbol.hasInstance', () => { + let creator: AlgorandClientTransactionCreator + + beforeEach(() => { + const newGroup = () => ({}) as any + creator = new AlgorandClientTransactionCreator(newGroup) + }) + + it('should work with regular instanceof', () => { + assert.strictEqual(creator instanceof AlgorandClientTransactionCreator, true) + }) + + it('should work with custom Symbol.hasInstance', () => { + assert.strictEqual(AlgorandClientTransactionCreator[Symbol.hasInstance](creator), true) + }) + + it('should work with cross-module simulation', () => { + const mockCreator = { + _isAlgorandClientTransactionCreator: true, + _newGroup: () => ({}) as any, + } as any + + assert.strictEqual(AlgorandClientTransactionCreator[Symbol.hasInstance](mockCreator), true) + }) + + it('should reject objects without marker', () => { + const fakeCreator = { _newGroup: () => ({}) as any } as any + assert.strictEqual(AlgorandClientTransactionCreator[Symbol.hasInstance](fakeCreator), false) + }) + }) + + describe('SigningAccount Symbol.hasInstance', () => { + let creator: AlgorandClientTransactionCreator + + beforeEach(() => { + const newGroup = () => ({}) as any + creator = new AlgorandClientTransactionCreator(newGroup) + }) + + it('should work with regular instanceof', () => { + assert.strictEqual(creator instanceof AlgorandClientTransactionCreator, true) + }) + + it('should work with custom Symbol.hasInstance', () => { + assert.strictEqual(AlgorandClientTransactionCreator[Symbol.hasInstance](creator), true) + }) + + it('should work with cross-module simulation', () => { + const mockCreator = { + _isAlgorandClientTransactionCreator: true, + _newGroup: () => ({}) as any, + } as any + + assert.strictEqual(AlgorandClientTransactionCreator[Symbol.hasInstance](mockCreator), true) + }) + + it('should reject objects without marker', () => { + const fakeCreator = { _newGroup: () => ({}) as any } as any + assert.strictEqual(AlgorandClientTransactionCreator[Symbol.hasInstance](fakeCreator), false) + }) + }) + + describe('MultisigAccount Symbol.hasInstance', () => { + let msig: MultisigAccount + + beforeEach(() => { + const a1 = algosdk.generateAccount() + const a2 = algosdk.generateAccount() + const a3 = algosdk.generateAccount() + + const params: algosdk.MultisigMetadata = { + version: 1, + threshold: 2, + addrs: [a1.addr, a2.addr, a3.addr], + } + + const signingAccounts: algosdk.Account[] = [a1, a3] + + msig = new MultisigAccount(params, signingAccounts) + }) + + it('should work with regular instanceof', () => { + assert.strictEqual(msig instanceof MultisigAccount, true) + }) + + it('should work with custom Symbol.hasInstance', () => { + assert.strictEqual(MultisigAccount[Symbol.hasInstance](msig), true) + }) + + it('should work with cross-module simulation', () => { + const mockMsig = { + _isMultisigAccount: true, + _params: { version: 1, threshold: 1, addrs: [] }, + _signingAccounts: [], + _addr: 'FAKEADDR', + } as any + + assert.strictEqual(MultisigAccount[Symbol.hasInstance](mockMsig), true) + }) + + it('should reject objects without marker', () => { + const fakeMsig = { + _params: { version: 1, threshold: 1, addrs: [] }, + } as any + + assert.strictEqual(MultisigAccount[Symbol.hasInstance](fakeMsig), false) + }) + }) + describe('Edge cases', () => { it('should handle primitive values', () => { assert.strictEqual(algosdk.Address[Symbol.hasInstance]('string'), false) @@ -53,7 +164,7 @@ describe('Dual Package Hazard Solution', () => { }) it('should handle objects with wrong marker values', () => { - const wrongMarker = { _isAlgosdkAddress: 'true' } // string instead of boolean + const wrongMarker = { _isAlgosdkAddress: 'true' } assert.strictEqual(algosdk.Address[Symbol.hasInstance](wrongMarker), false) }) }) From f5fa57bc6752678ba13e50778f7bdb29d295a688 Mon Sep 17 00:00:00 2001 From: p2arthur Date: Thu, 2 Oct 2025 23:18:49 -0700 Subject: [PATCH 7/8] fix(lint) fix lint errors any type --- src/types/dual-package-hazard.spec.ts | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/types/dual-package-hazard.spec.ts b/src/types/dual-package-hazard.spec.ts index 24d7c78d..57976b4f 100644 --- a/src/types/dual-package-hazard.spec.ts +++ b/src/types/dual-package-hazard.spec.ts @@ -4,6 +4,7 @@ import assert from 'assert' import { beforeEach, describe, it } from 'vitest' import { MultisigAccount } from './account' import { AlgorandClientTransactionCreator } from './algorand-client-transaction-creator' +import { TransactionComposer } from './composer' import { TestNetDispenserApiClient } from './dispenser-client' describe('Dual Package Hazard Solution', () => { @@ -27,7 +28,7 @@ describe('Dual Package Hazard Solution', () => { _isTestNetDispenserApiClient: true, _authToken: 'other-token', _requestTimeout: 15, - } as any + } assert.strictEqual(TestNetDispenserApiClient[Symbol.hasInstance](mockClient), true) }) @@ -36,7 +37,7 @@ describe('Dual Package Hazard Solution', () => { const fakeClient = { _authToken: 'no-marker', _requestTimeout: 10, - } as any + } assert.strictEqual(TestNetDispenserApiClient[Symbol.hasInstance](fakeClient), false) }) @@ -46,7 +47,7 @@ describe('Dual Package Hazard Solution', () => { let creator: AlgorandClientTransactionCreator beforeEach(() => { - const newGroup = () => ({}) as any + const newGroup = () => ({}) as TransactionComposer creator = new AlgorandClientTransactionCreator(newGroup) }) @@ -61,14 +62,14 @@ describe('Dual Package Hazard Solution', () => { it('should work with cross-module simulation', () => { const mockCreator = { _isAlgorandClientTransactionCreator: true, - _newGroup: () => ({}) as any, - } as any + _newGroup: () => ({}), + } assert.strictEqual(AlgorandClientTransactionCreator[Symbol.hasInstance](mockCreator), true) }) it('should reject objects without marker', () => { - const fakeCreator = { _newGroup: () => ({}) as any } as any + const fakeCreator = { _newGroup: () => ({}) } assert.strictEqual(AlgorandClientTransactionCreator[Symbol.hasInstance](fakeCreator), false) }) }) @@ -77,7 +78,7 @@ describe('Dual Package Hazard Solution', () => { let creator: AlgorandClientTransactionCreator beforeEach(() => { - const newGroup = () => ({}) as any + const newGroup = () => ({}) as TransactionComposer creator = new AlgorandClientTransactionCreator(newGroup) }) @@ -92,14 +93,14 @@ describe('Dual Package Hazard Solution', () => { it('should work with cross-module simulation', () => { const mockCreator = { _isAlgorandClientTransactionCreator: true, - _newGroup: () => ({}) as any, - } as any + _newGroup: () => ({}), + } assert.strictEqual(AlgorandClientTransactionCreator[Symbol.hasInstance](mockCreator), true) }) it('should reject objects without marker', () => { - const fakeCreator = { _newGroup: () => ({}) as any } as any + const fakeCreator = { _newGroup: () => ({}) } assert.strictEqual(AlgorandClientTransactionCreator[Symbol.hasInstance](fakeCreator), false) }) }) @@ -137,7 +138,7 @@ describe('Dual Package Hazard Solution', () => { _params: { version: 1, threshold: 1, addrs: [] }, _signingAccounts: [], _addr: 'FAKEADDR', - } as any + } assert.strictEqual(MultisigAccount[Symbol.hasInstance](mockMsig), true) }) @@ -145,7 +146,7 @@ describe('Dual Package Hazard Solution', () => { it('should reject objects without marker', () => { const fakeMsig = { _params: { version: 1, threshold: 1, addrs: [] }, - } as any + } assert.strictEqual(MultisigAccount[Symbol.hasInstance](fakeMsig), false) }) From 8ab8b178c858bc6d87df6f1b8c9cde83ee684662 Mon Sep 17 00:00:00 2001 From: p2arthur Date: Fri, 3 Oct 2025 11:30:53 -0700 Subject: [PATCH 8/8] test(dual_package) add more tests for dual package hazard --- src/types/dual-package-hazard.spec.ts | 42 +++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/types/dual-package-hazard.spec.ts b/src/types/dual-package-hazard.spec.ts index 57976b4f..62dec108 100644 --- a/src/types/dual-package-hazard.spec.ts +++ b/src/types/dual-package-hazard.spec.ts @@ -4,6 +4,7 @@ import assert from 'assert' import { beforeEach, describe, it } from 'vitest' import { MultisigAccount } from './account' import { AlgorandClientTransactionCreator } from './algorand-client-transaction-creator' +import { AppManager } from './app-manager' import { TransactionComposer } from './composer' import { TestNetDispenserApiClient } from './dispenser-client' @@ -152,6 +153,47 @@ describe('Dual Package Hazard Solution', () => { }) }) + describe('AppManager Symbol.hasInstance', () => { + let manager: AppManager + + beforeEach(() => { + // Minimal fake that satisfies the constructor type; none of its methods are used here. + const fakeAlgod = {} as unknown as algosdk.Algodv2 + manager = new AppManager(fakeAlgod) + }) + + it('should work with regular instanceof', () => { + assert.strictEqual(manager instanceof AppManager, true) + }) + + it('should work with custom Symbol.hasInstance', () => { + assert.strictEqual(AppManager[Symbol.hasInstance](manager), true) + }) + + it('should work with cross-module simulation', () => { + // Object coming from a “different module”, but carrying the private marker flag shape + const mockManager = { + _isAppManager: true, + _algod: {} as unknown as algosdk.Algodv2, + } as unknown as AppManager + + assert.strictEqual(AppManager[Symbol.hasInstance](mockManager), true) + }) + + it('should reject objects without marker', () => { + const fakeManager = { + _algod: {} as unknown as algosdk.Algodv2, + } as unknown as AppManager + + assert.strictEqual(AppManager[Symbol.hasInstance](fakeManager), false) + }) + + it('should reject null/undefined safely', () => { + assert.strictEqual(AppManager[Symbol.hasInstance](null as unknown), false) + assert.strictEqual(AppManager[Symbol.hasInstance](undefined as unknown), false) + }) + }) + describe('Edge cases', () => { it('should handle primitive values', () => { assert.strictEqual(algosdk.Address[Symbol.hasInstance]('string'), false)