From 07e264acf9bc7686ce9309f3fa93f335eb52bfd3 Mon Sep 17 00:00:00 2001 From: David Rojas Date: Mon, 23 Jun 2025 17:47:10 -0400 Subject: [PATCH 1/4] feat: add custom Symbol.hasInstance to solve dual package hazard Add Symbol.hasInstance methods and unique markers to core classes (Address, Transaction, SignedTransaction, LogicSig, LogicSigAccount, AtomicTransactionComposer, ABIContract) to enable instanceof checks across module boundaries when CommonJS and ESM load separate SDK instances. --- .eslintrc.js | 14 ++++++++++++++ src/abi/contract.ts | 14 ++++++++++++++ src/composer.ts | 16 +++++++++++++++- src/encoding/address.ts | 14 ++++++++++++++ src/logicsig.ts | 28 ++++++++++++++++++++++++++++ src/signedTransaction.ts | 14 ++++++++++++++ src/transaction.ts | 14 ++++++++++++++ 7 files changed, 113 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index 6c76ac01f..8d800c66c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -41,6 +41,20 @@ module.exports = { '@typescript-eslint/no-redeclare': ['error'], 'no-shadow': 'off', '@typescript-eslint/no-shadow': ['error'], + 'no-underscore-dangle': [ + 'error', + { + allow: [ + '_isAlgosdkAddress', + '_isAlgosdkTransaction', + '_isAlgosdkSignedTransaction', + '_isAlgosdkLogicSig', + '_isAlgosdkLogicSigAccount', + '_isAlgosdkAtomicTransactionComposer', + '_isAlgosdkABIContract', + ], + }, + ], }, overrides: [ { diff --git a/src/abi/contract.ts b/src/abi/contract.ts index a3613ff08..337449f43 100644 --- a/src/abi/contract.ts +++ b/src/abi/contract.ts @@ -25,6 +25,20 @@ export class ABIContract { /** [ARC-28](https://arc.algorand.foundation/ARCs/arc-0028) events that MAY be emitted by this contract */ public readonly events?: ARC28Event[]; + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAlgosdkABIContract = 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._isAlgosdkABIContract === true; + } + constructor(params: ABIContractParams) { if ( typeof params.name !== 'string' || diff --git a/src/composer.ts b/src/composer.ts index e4d1ec180..4fe64f9bc 100644 --- a/src/composer.ts +++ b/src/composer.ts @@ -124,6 +124,20 @@ export class AtomicTransactionComposer { /** The maximum size of an atomic transaction group. */ static MAX_GROUP_SIZE: number = 16; + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAlgosdkAtomicTransactionComposer = true; + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is an AtomicTransactionComposer, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: any): boolean { + return instance && instance._isAlgosdkAtomicTransactionComposer === true; + } + private status = AtomicTransactionComposerStatus.BUILDING; private transactions: TransactionWithSigner[] = []; private methodCalls: Map = new Map(); @@ -632,7 +646,7 @@ export class AtomicTransactionComposer { * * @param client - An Algodv2 client * @param request - SimulateRequest with options in simulation. - * If provided, the request's transaction group will be overrwritten by the composer's group, + * If provided, the request's transaction group will be overwritten by the composer's group, * only simulation related options will be used. * * @returns A promise that, upon success, resolves to an object containing an diff --git a/src/encoding/address.ts b/src/encoding/address.ts index 77a2f7509..d91cb0919 100644 --- a/src/encoding/address.ts +++ b/src/encoding/address.ts @@ -33,6 +33,20 @@ export class Address { */ public readonly publicKey: Uint8Array; + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAlgosdkAddress = true; + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is an Address, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: any): boolean { + return instance && instance._isAlgosdkAddress === true; + } + /** * Create a new Address object from its binary form. * @param publicKey - The binary form of the address. Must be 32 bytes. diff --git a/src/logicsig.ts b/src/logicsig.ts index 3267c24d9..9b071c0b3 100644 --- a/src/logicsig.ts +++ b/src/logicsig.ts @@ -88,6 +88,20 @@ export class LogicSig implements encoding.Encodable { ]) ); + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAlgosdkLogicSig = true; + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is a LogicSig, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: any): boolean { + return instance && instance._isAlgosdkLogicSig === true; + } + logic: Uint8Array; args: Uint8Array[]; sig?: Uint8Array; @@ -268,6 +282,20 @@ export class LogicSigAccount implements encoding.Encodable { ]) ); + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAlgosdkLogicSigAccount = true; + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is a LogicSigAccount, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: any): boolean { + return instance && instance._isAlgosdkLogicSigAccount === true; + } + lsig: LogicSig; sigkey?: Uint8Array; diff --git a/src/signedTransaction.ts b/src/signedTransaction.ts index 03bf4d44f..e9a022020 100644 --- a/src/signedTransaction.ts +++ b/src/signedTransaction.ts @@ -46,6 +46,20 @@ export class SignedTransaction implements Encodable { ]) ); + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAlgosdkSignedTransaction = true; + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is a SignedTransaction, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: any): boolean { + return instance && instance._isAlgosdkSignedTransaction === true; + } + /** * The transaction that was signed */ diff --git a/src/transaction.ts b/src/transaction.ts index bd9864ec2..9c61091d2 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -453,6 +453,20 @@ export class Transaction implements encoding.Encodable { ]) ); + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAlgosdkTransaction = true; + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is a Transaction, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: any): boolean { + return instance && instance._isAlgosdkTransaction === true; + } + /** common */ public readonly type: TransactionType; public readonly sender: Address; From decf5ecb671e291fb5783a7eaaf25ddc954aa717 Mon Sep 17 00:00:00 2001 From: David Rojas Date: Mon, 23 Jun 2025 17:58:56 -0400 Subject: [PATCH 2/4] test: add comprehensive tests for dual package hazard solution - Add tests for all classes with Symbol.hasInstance implementation - Test regular instanceof, custom Symbol.hasInstance, and cross-module simulation - Test edge cases with null, undefined, and wrong marker values --- src/abi/contract.ts | 2 +- src/composer.ts | 4 +- src/encoding/address.ts | 2 +- src/logicsig.ts | 4 +- src/signedTransaction.ts | 2 +- src/transaction.ts | 2 +- tests/11.DualPackageHazard.ts | 288 ++++++++++++++++++++++++++++++++++ 7 files changed, 297 insertions(+), 7 deletions(-) create mode 100644 tests/11.DualPackageHazard.ts diff --git a/src/abi/contract.ts b/src/abi/contract.ts index 337449f43..ae345f09a 100644 --- a/src/abi/contract.ts +++ b/src/abi/contract.ts @@ -36,7 +36,7 @@ export class ABIContract { * @returns true if the instance is an ABIContract, regardless of which module loaded it */ static [Symbol.hasInstance](instance: any): boolean { - return instance && instance._isAlgosdkABIContract === true; + return !!(instance && instance._isAlgosdkABIContract === true); } constructor(params: ABIContractParams) { diff --git a/src/composer.ts b/src/composer.ts index 4fe64f9bc..9186f2010 100644 --- a/src/composer.ts +++ b/src/composer.ts @@ -135,7 +135,9 @@ export class AtomicTransactionComposer { * @returns true if the instance is an AtomicTransactionComposer, regardless of which module loaded it */ static [Symbol.hasInstance](instance: any): boolean { - return instance && instance._isAlgosdkAtomicTransactionComposer === true; + return !!( + instance && instance._isAlgosdkAtomicTransactionComposer === true + ); } private status = AtomicTransactionComposerStatus.BUILDING; diff --git a/src/encoding/address.ts b/src/encoding/address.ts index d91cb0919..ac2a39b85 100644 --- a/src/encoding/address.ts +++ b/src/encoding/address.ts @@ -44,7 +44,7 @@ export class Address { * @returns true if the instance is an Address, regardless of which module loaded it */ static [Symbol.hasInstance](instance: any): boolean { - return instance && instance._isAlgosdkAddress === true; + return !!(instance && instance._isAlgosdkAddress === true); } /** diff --git a/src/logicsig.ts b/src/logicsig.ts index 9b071c0b3..a2053caaa 100644 --- a/src/logicsig.ts +++ b/src/logicsig.ts @@ -99,7 +99,7 @@ export class LogicSig implements encoding.Encodable { * @returns true if the instance is a LogicSig, regardless of which module loaded it */ static [Symbol.hasInstance](instance: any): boolean { - return instance && instance._isAlgosdkLogicSig === true; + return !!(instance && instance._isAlgosdkLogicSig === true); } logic: Uint8Array; @@ -293,7 +293,7 @@ export class LogicSigAccount implements encoding.Encodable { * @returns true if the instance is a LogicSigAccount, regardless of which module loaded it */ static [Symbol.hasInstance](instance: any): boolean { - return instance && instance._isAlgosdkLogicSigAccount === true; + return !!(instance && instance._isAlgosdkLogicSigAccount === true); } lsig: LogicSig; diff --git a/src/signedTransaction.ts b/src/signedTransaction.ts index e9a022020..4299c3301 100644 --- a/src/signedTransaction.ts +++ b/src/signedTransaction.ts @@ -57,7 +57,7 @@ export class SignedTransaction implements Encodable { * @returns true if the instance is a SignedTransaction, regardless of which module loaded it */ static [Symbol.hasInstance](instance: any): boolean { - return instance && instance._isAlgosdkSignedTransaction === true; + return !!(instance && instance._isAlgosdkSignedTransaction === true); } /** diff --git a/src/transaction.ts b/src/transaction.ts index 9c61091d2..4acd80936 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -464,7 +464,7 @@ export class Transaction implements encoding.Encodable { * @returns true if the instance is a Transaction, regardless of which module loaded it */ static [Symbol.hasInstance](instance: any): boolean { - return instance && instance._isAlgosdkTransaction === true; + return !!(instance && instance._isAlgosdkTransaction === true); } /** common */ diff --git a/tests/11.DualPackageHazard.ts b/tests/11.DualPackageHazard.ts new file mode 100644 index 000000000..699d81cfd --- /dev/null +++ b/tests/11.DualPackageHazard.ts @@ -0,0 +1,288 @@ +/* eslint-env mocha */ +import assert from 'assert'; +import algosdk from '../src/index.js'; +import { ABIContract } from '../src/abi/index.js'; + +describe('Dual Package Hazard Solution', () => { + describe('Address Symbol.hasInstance', () => { + it('should work with regular instanceof', () => { + const address = new algosdk.Address(new Uint8Array(32)); + assert.strictEqual(address instanceof algosdk.Address, true); + }); + + it('should work with custom Symbol.hasInstance', () => { + const address = new algosdk.Address(new Uint8Array(32)); + assert.strictEqual(algosdk.Address[Symbol.hasInstance](address), true); + }); + + it('should work with cross-module simulation', () => { + // Simulate an Address object from a different module instance + const mockAddress = { + _isAlgosdkAddress: true, + publicKey: new Uint8Array(32), + }; + assert.strictEqual( + algosdk.Address[Symbol.hasInstance](mockAddress), + true + ); + }); + + it('should reject objects without marker', () => { + const fakeAddress = { publicKey: new Uint8Array(32) }; + assert.strictEqual( + algosdk.Address[Symbol.hasInstance](fakeAddress), + false + ); + }); + + it('should handle null and undefined', () => { + assert.strictEqual(algosdk.Address[Symbol.hasInstance](null), false); + assert.strictEqual(algosdk.Address[Symbol.hasInstance](undefined), false); + }); + }); + + describe('Transaction Symbol.hasInstance', () => { + let transaction: algosdk.Transaction; + + beforeEach(() => { + const sender = algosdk.encodeAddress(new Uint8Array(32)); + const receiver = algosdk.encodeAddress(new Uint8Array(32)); + transaction = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ + sender, + receiver, + amount: 1000, + suggestedParams: { + fee: 1000, + firstValid: 1, + lastValid: 100, + genesisHash: new Uint8Array(32), + genesisID: 'test', + minFee: 1000, + }, + }); + }); + + it('should work with regular instanceof', () => { + assert.strictEqual(transaction instanceof algosdk.Transaction, true); + }); + + it('should work with custom Symbol.hasInstance', () => { + assert.strictEqual( + algosdk.Transaction[Symbol.hasInstance](transaction), + true + ); + }); + + it('should work with cross-module simulation', () => { + const mockTransaction = { + _isAlgosdkTransaction: true, + type: 'pay' as any, // Mock object doesn't need strict typing + }; + assert.strictEqual( + algosdk.Transaction[Symbol.hasInstance](mockTransaction), + true + ); + }); + + it('should reject objects without marker', () => { + const fakeTransaction = { type: 'pay' as any }; + assert.strictEqual( + algosdk.Transaction[Symbol.hasInstance](fakeTransaction), + false + ); + }); + }); + + describe('SignedTransaction Symbol.hasInstance', () => { + let signedTransaction: algosdk.SignedTransaction; + + beforeEach(() => { + const sender = algosdk.encodeAddress(new Uint8Array(32)); + const receiver = algosdk.encodeAddress(new Uint8Array(32)); + const txn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ + sender, + receiver, + amount: 1000, + suggestedParams: { + fee: 1000, + firstValid: 1, + lastValid: 100, + genesisHash: new Uint8Array(32), + genesisID: 'test', + minFee: 1000, + }, + }); + signedTransaction = new algosdk.SignedTransaction({ txn }); + }); + + it('should work with regular instanceof', () => { + assert.strictEqual( + signedTransaction instanceof algosdk.SignedTransaction, + true + ); + }); + + it('should work with custom Symbol.hasInstance', () => { + assert.strictEqual( + algosdk.SignedTransaction[Symbol.hasInstance](signedTransaction), + true + ); + }); + + it('should work with cross-module simulation', () => { + const mockSignedTransaction = { + _isAlgosdkSignedTransaction: true, + txn: {}, + }; + assert.strictEqual( + algosdk.SignedTransaction[Symbol.hasInstance](mockSignedTransaction), + true + ); + }); + }); + + describe('LogicSig Symbol.hasInstance', () => { + let logicSig: algosdk.LogicSig; + + beforeEach(() => { + const program = new Uint8Array([1, 32, 1, 1, 34]); // Simple program + logicSig = new algosdk.LogicSig(program); + }); + + it('should work with regular instanceof', () => { + assert.strictEqual(logicSig instanceof algosdk.LogicSig, true); + }); + + it('should work with custom Symbol.hasInstance', () => { + assert.strictEqual(algosdk.LogicSig[Symbol.hasInstance](logicSig), true); + }); + + it('should work with cross-module simulation', () => { + const mockLogicSig = { + _isAlgosdkLogicSig: true, + logic: new Uint8Array([1, 32, 1, 1, 34]), + args: [], + }; + assert.strictEqual( + algosdk.LogicSig[Symbol.hasInstance](mockLogicSig), + true + ); + }); + }); + + describe('LogicSigAccount Symbol.hasInstance', () => { + let logicSigAccount: algosdk.LogicSigAccount; + + beforeEach(() => { + const program = new Uint8Array([1, 32, 1, 1, 34]); // Simple program + logicSigAccount = new algosdk.LogicSigAccount(program); + }); + + it('should work with regular instanceof', () => { + assert.strictEqual( + logicSigAccount instanceof algosdk.LogicSigAccount, + true + ); + }); + + it('should work with custom Symbol.hasInstance', () => { + assert.strictEqual( + algosdk.LogicSigAccount[Symbol.hasInstance](logicSigAccount), + true + ); + }); + + it('should work with cross-module simulation', () => { + const mockLogicSigAccount = { + _isAlgosdkLogicSigAccount: true, + lsig: {}, + }; + assert.strictEqual( + algosdk.LogicSigAccount[Symbol.hasInstance](mockLogicSigAccount), + true + ); + }); + }); + + describe('AtomicTransactionComposer Symbol.hasInstance', () => { + let composer: algosdk.AtomicTransactionComposer; + + beforeEach(() => { + composer = new algosdk.AtomicTransactionComposer(); + }); + + it('should work with regular instanceof', () => { + assert.strictEqual( + composer instanceof algosdk.AtomicTransactionComposer, + true + ); + }); + + it('should work with custom Symbol.hasInstance', () => { + assert.strictEqual( + algosdk.AtomicTransactionComposer[Symbol.hasInstance](composer), + true + ); + }); + + it('should work with cross-module simulation', () => { + const mockComposer = { + _isAlgosdkAtomicTransactionComposer: true, + status: 'BUILDING', + }; + assert.strictEqual( + algosdk.AtomicTransactionComposer[Symbol.hasInstance](mockComposer), + true + ); + }); + }); + + describe('ABIContract Symbol.hasInstance', () => { + let contract: ABIContract; + + beforeEach(() => { + contract = new ABIContract({ + name: 'TestContract', + methods: [{ name: 'test', args: [], returns: { type: 'void' } }], + }); + }); + + it('should work with regular instanceof', () => { + assert.strictEqual(contract instanceof ABIContract, true); + }); + + it('should work with custom Symbol.hasInstance', () => { + assert.strictEqual(ABIContract[Symbol.hasInstance](contract), true); + }); + + it('should work with cross-module simulation', () => { + const mockContract = { + _isAlgosdkABIContract: true, + name: 'MockContract', + methods: [], + }; + assert.strictEqual(ABIContract[Symbol.hasInstance](mockContract), true); + }); + }); + + 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 7d50a2a73ea5e763ba9cd1a40ca9e886b0662594 Mon Sep 17 00:00:00 2001 From: David Rojas Date: Tue, 24 Jun 2025 15:25:18 -0400 Subject: [PATCH 3/4] feat: extend dual package hazard solution to all ABI type classes - Add custom Symbol.hasInstance to all the ABITypes classes - Added tests --- .eslintrc.js | 9 +++ src/abi/abi_type.ts | 126 ++++++++++++++++++++++++++++++++++ tests/11.DualPackageHazard.ts | 123 +++++++++++++++++++++++++++++++++ 3 files changed, 258 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index 8d800c66c..2329d391e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -52,6 +52,15 @@ module.exports = { '_isAlgosdkLogicSigAccount', '_isAlgosdkAtomicTransactionComposer', '_isAlgosdkABIContract', + '_isAlgosdkABIUintType', + '_isAlgosdkABIUfixedType', + '_isAlgosdkABIAddressType', + '_isAlgosdkABIBoolType', + '_isAlgosdkABIByteType', + '_isAlgosdkABIStringType', + '_isAlgosdkABIArrayStaticType', + '_isAlgosdkABIArrayDynamicType', + '_isAlgosdkABITupleType', ], }, ], diff --git a/src/abi/abi_type.ts b/src/abi/abi_type.ts index bd467a90d..15e820471 100644 --- a/src/abi/abi_type.ts +++ b/src/abi/abi_type.ts @@ -128,6 +128,20 @@ export abstract class ABIType { export class ABIUintType extends ABIType { bitSize: number; + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAlgosdkABIUintType = true; + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is an ABIUintType, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: any): boolean { + return !!(instance && instance._isAlgosdkABIUintType === true); + } + constructor(size: number) { super(); if (size % 8 !== 0 || size < 8 || size > 512) { @@ -181,6 +195,20 @@ export class ABIUfixedType extends ABIType { bitSize: number; precision: number; + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAlgosdkABIUfixedType = true; + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is an ABIUfixedType, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: any): boolean { + return !!(instance && instance._isAlgosdkABIUfixedType === true); + } + constructor(size: number, denominator: number) { super(); if (size % 8 !== 0 || size < 8 || size > 512) { @@ -239,6 +267,20 @@ export class ABIUfixedType extends ABIType { } export class ABIAddressType extends ABIType { + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAlgosdkABIAddressType = true; + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is an ABIAddressType, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: any): boolean { + return !!(instance && instance._isAlgosdkABIAddressType === true); + } + toString() { return 'address'; } @@ -285,6 +327,20 @@ export class ABIAddressType extends ABIType { } export class ABIBoolType extends ABIType { + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAlgosdkABIBoolType = true; + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is an ABIBoolType, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: any): boolean { + return !!(instance && instance._isAlgosdkABIBoolType === true); + } + toString() { return 'bool'; } @@ -327,6 +383,20 @@ export class ABIBoolType extends ABIType { } export class ABIByteType extends ABIType { + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAlgosdkABIByteType = true; + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is an ABIByteType, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: any): boolean { + return !!(instance && instance._isAlgosdkABIByteType === true); + } + toString() { return 'byte'; } @@ -366,6 +436,20 @@ export class ABIByteType extends ABIType { } export class ABIStringType extends ABIType { + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAlgosdkABIStringType = true; + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is an ABIStringType, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: any): boolean { + return !!(instance && instance._isAlgosdkABIStringType === true); + } + toString() { return 'string'; } @@ -433,6 +517,20 @@ export class ABIArrayStaticType extends ABIType { childType: ABIType; staticLength: number; + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAlgosdkABIArrayStaticType = true; + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is an ABIArrayStaticType, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: any): boolean { + return !!(instance && instance._isAlgosdkABIArrayStaticType === true); + } + constructor(argType: ABIType, arrayLength: number) { super(); if (arrayLength < 0) { @@ -493,6 +591,20 @@ export class ABIArrayStaticType extends ABIType { export class ABIArrayDynamicType extends ABIType { childType: ABIType; + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAlgosdkABIArrayDynamicType = true; + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is an ABIArrayDynamicType, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: any): boolean { + return !!(instance && instance._isAlgosdkABIArrayDynamicType === true); + } + constructor(argType: ABIType) { super(); this.childType = argType; @@ -548,6 +660,20 @@ export class ABIArrayDynamicType extends ABIType { export class ABITupleType extends ABIType { childTypes: ABIType[]; + /** + * Unique property marker for Symbol.hasInstance compatibility across module boundaries + */ + private readonly _isAlgosdkABITupleType = true; + + /** + * Custom Symbol.hasInstance to handle dual package hazard + * @param instance - The instance to check + * @returns true if the instance is an ABITupleType, regardless of which module loaded it + */ + static [Symbol.hasInstance](instance: any): boolean { + return !!(instance && instance._isAlgosdkABITupleType === true); + } + constructor(argTypes: ABIType[]) { super(); if (argTypes.length >= MAX_LEN) { diff --git a/tests/11.DualPackageHazard.ts b/tests/11.DualPackageHazard.ts index 699d81cfd..a049811a8 100644 --- a/tests/11.DualPackageHazard.ts +++ b/tests/11.DualPackageHazard.ts @@ -265,6 +265,129 @@ describe('Dual Package Hazard Solution', () => { }); }); + describe('ABI Type Symbol.hasInstance', () => { + it('should work for ABIUintType', () => { + const uintType = new algosdk.ABIUintType(64); + assert.strictEqual(uintType instanceof algosdk.ABIUintType, true); + assert.strictEqual( + algosdk.ABIUintType[Symbol.hasInstance](uintType), + true + ); + + const mockUintType = { _isAlgosdkABIUintType: true, bitSize: 64 }; + assert.strictEqual( + algosdk.ABIUintType[Symbol.hasInstance](mockUintType), + true + ); + }); + + it('should work for ABIAddressType', () => { + const addressType = new algosdk.ABIAddressType(); + assert.strictEqual(addressType instanceof algosdk.ABIAddressType, true); + assert.strictEqual( + algosdk.ABIAddressType[Symbol.hasInstance](addressType), + true + ); + + const mockAddressType = { _isAlgosdkABIAddressType: true }; + assert.strictEqual( + algosdk.ABIAddressType[Symbol.hasInstance](mockAddressType), + true + ); + }); + + it('should work for ABIBoolType', () => { + const boolType = new algosdk.ABIBoolType(); + assert.strictEqual(boolType instanceof algosdk.ABIBoolType, true); + assert.strictEqual( + algosdk.ABIBoolType[Symbol.hasInstance](boolType), + true + ); + + const mockBoolType = { _isAlgosdkABIBoolType: true }; + assert.strictEqual( + algosdk.ABIBoolType[Symbol.hasInstance](mockBoolType), + true + ); + }); + + it('should work for ABIStringType', () => { + const stringType = new algosdk.ABIStringType(); + assert.strictEqual(stringType instanceof algosdk.ABIStringType, true); + assert.strictEqual( + algosdk.ABIStringType[Symbol.hasInstance](stringType), + true + ); + + const mockStringType = { _isAlgosdkABIStringType: true }; + assert.strictEqual( + algosdk.ABIStringType[Symbol.hasInstance](mockStringType), + true + ); + }); + + it('should work for ABIArrayStaticType', () => { + const elementType = new algosdk.ABIUintType(64); + const arrayType = new algosdk.ABIArrayStaticType(elementType, 5); + assert.strictEqual(arrayType instanceof algosdk.ABIArrayStaticType, true); + assert.strictEqual( + algosdk.ABIArrayStaticType[Symbol.hasInstance](arrayType), + true + ); + + const mockArrayType = { + _isAlgosdkABIArrayStaticType: true, + childType: {}, + staticLength: 5, + }; + assert.strictEqual( + algosdk.ABIArrayStaticType[Symbol.hasInstance](mockArrayType), + true + ); + }); + + it('should work for ABIArrayDynamicType', () => { + const elementType = new algosdk.ABIUintType(64); + const arrayType = new algosdk.ABIArrayDynamicType(elementType); + assert.strictEqual( + arrayType instanceof algosdk.ABIArrayDynamicType, + true + ); + assert.strictEqual( + algosdk.ABIArrayDynamicType[Symbol.hasInstance](arrayType), + true + ); + + const mockArrayType = { + _isAlgosdkABIArrayDynamicType: true, + childType: {}, + }; + assert.strictEqual( + algosdk.ABIArrayDynamicType[Symbol.hasInstance](mockArrayType), + true + ); + }); + + it('should work for ABITupleType', () => { + const childTypes = [ + new algosdk.ABIUintType(64), + new algosdk.ABIBoolType(), + ]; + const tupleType = new algosdk.ABITupleType(childTypes); + assert.strictEqual(tupleType instanceof algosdk.ABITupleType, true); + assert.strictEqual( + algosdk.ABITupleType[Symbol.hasInstance](tupleType), + true + ); + + const mockTupleType = { _isAlgosdkABITupleType: true, childTypes: [] }; + assert.strictEqual( + algosdk.ABITupleType[Symbol.hasInstance](mockTupleType), + true + ); + }); + }); + describe('Edge cases', () => { it('should handle primitive values', () => { assert.strictEqual(algosdk.Address[Symbol.hasInstance]('string'), false); From c6c802e18762d7f05b836d7fe251fa93a7d77c34 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Mon, 4 Aug 2025 20:31:27 -0400 Subject: [PATCH 4/4] test: use typehint to prevent CI error Think it should fix the browser tests in https://github.com/algorand/js-algorand-sdk/actions/runs/16733457099/job/47366838665?pr=990 --- tests/10.ABI.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/10.ABI.ts b/tests/10.ABI.ts index 0ff7f8c93..256c24f1c 100644 --- a/tests/10.ABI.ts +++ b/tests/10.ABI.ts @@ -29,7 +29,7 @@ import { decodeAddress } from '../src/encoding/address'; describe('ABI type checking', () => { it('should create the correct type from the string', () => { for (let i = 8; i < 513; i += 8) { - let expected = new ABIUintType(i); + let expected: ABIType = new ABIUintType(i); let actual = ABIType.from(`uint${i}`); assert.deepStrictEqual(actual, expected); for (let j = 1; j < 161; j++) {