diff --git a/src/testing/test-logger.ts b/src/testing/test-logger.ts index 9ec5578b..2abf78c8 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 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) + } + /** * 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-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 ee87b8c6..661db045 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 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) + } + /** 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 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) + } + /** * Algorand address of the sender */ 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-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/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/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 11db70a2..0bdb9201 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 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) + } + /** * Creates an `AppManager` * @param appManager An `AppManager` instance 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/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/composer.ts b/src/types/composer.ts index 0a71fa86..09b4f310 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 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) + } + private async transformError(originalError: unknown): Promise { // Transformers only work with Error instances, so immediately return anything else if (!(originalError instanceof Error)) { 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/dual-package-hazard.spec.ts b/src/types/dual-package-hazard.spec.ts new file mode 100644 index 00000000..62dec108 --- /dev/null +++ b/src/types/dual-package-hazard.spec.ts @@ -0,0 +1,214 @@ +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 { AppManager } from './app-manager' +import { TransactionComposer } from './composer' +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, + _authToken: 'other-token', + _requestTimeout: 15, + } + + assert.strictEqual(TestNetDispenserApiClient[Symbol.hasInstance](mockClient), true) + }) + + it('should reject objects without marker', () => { + const fakeClient = { + _authToken: 'no-marker', + _requestTimeout: 10, + } + + assert.strictEqual(TestNetDispenserApiClient[Symbol.hasInstance](fakeClient), false) + }) + }) + + describe('AlgorandClientTransactionCreator Symbol.hasInstance', () => { + let creator: AlgorandClientTransactionCreator + + beforeEach(() => { + const newGroup = () => ({}) as TransactionComposer + 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: () => ({}), + } + + assert.strictEqual(AlgorandClientTransactionCreator[Symbol.hasInstance](mockCreator), true) + }) + + it('should reject objects without marker', () => { + const fakeCreator = { _newGroup: () => ({}) } + assert.strictEqual(AlgorandClientTransactionCreator[Symbol.hasInstance](fakeCreator), false) + }) + }) + + describe('SigningAccount Symbol.hasInstance', () => { + let creator: AlgorandClientTransactionCreator + + beforeEach(() => { + const newGroup = () => ({}) as TransactionComposer + 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: () => ({}), + } + + assert.strictEqual(AlgorandClientTransactionCreator[Symbol.hasInstance](mockCreator), true) + }) + + it('should reject objects without marker', () => { + const fakeCreator = { _newGroup: () => ({}) } + 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', + } + + assert.strictEqual(MultisigAccount[Symbol.hasInstance](mockMsig), true) + }) + + it('should reject objects without marker', () => { + const fakeMsig = { + _params: { version: 1, threshold: 1, addrs: [] }, + } + + assert.strictEqual(MultisigAccount[Symbol.hasInstance](fakeMsig), false) + }) + }) + + 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) + 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' } + assert.strictEqual(algosdk.Address[Symbol.hasInstance](wrongMarker), false) + }) + }) +}) 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 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