Skip to content

Commit c92edef

Browse files
committed
feat: Add test equality comparers to enable test assertions with algo-ts types and update tests to use these
1 parent db9b74a commit c92edef

28 files changed

+897
-734
lines changed

package-lock.json

Lines changed: 260 additions & 249 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"@commitlint/cli": "^19.5.0",
2828
"@commitlint/config-conventional": "^19.5.0",
2929
"@eslint/eslintrc": "3.1.0",
30-
"@eslint/js": "9.11.1",
30+
"@eslint/js": "9.12.0",
3131
"@makerx/eslint-config": "4.0.0",
3232
"@makerx/prettier-config": "2.0.1",
3333
"@makerx/ts-toolkit": "4.0.0-beta.21",
@@ -37,32 +37,32 @@
3737
"@rollup/plugin-typescript": "^12.1.0",
3838
"@tsconfig/node20": "20.1.4",
3939
"@types/elliptic": "^6.4.18",
40-
"@types/node": "22.6.1",
41-
"@typescript-eslint/eslint-plugin": "8.7.0",
42-
"@typescript-eslint/parser": "8.7.0",
43-
"@vitest/coverage-v8": "2.1.1",
40+
"@types/node": "22.7.5",
41+
"@typescript-eslint/eslint-plugin": "8.8.1",
42+
"@typescript-eslint/parser": "8.8.1",
43+
"@vitest/coverage-v8": "2.1.2",
4444
"better-npm-audit": "3.11.0",
4545
"conventional-changelog-conventionalcommits": "^8.0.0",
4646
"copyfiles": "2.4.1",
47-
"eslint": "9.11.1",
47+
"eslint": "9.12.0",
4848
"eslint-config-prettier": "^9.1.0",
4949
"eslint-plugin-prettier": "^5.2.1",
5050
"npm-run-all": "4.1.5",
5151
"prettier": "3.3.3",
5252
"rimraf": "6.0.1",
53-
"rollup": "^4.22.4",
54-
"semantic-release": "^24.1.1",
53+
"rollup": "^4.24.0",
54+
"semantic-release": "^24.1.2",
5555
"tsx": "4.19.1",
5656
"typescript": "^5.6.2",
57-
"vitest": "2.1.1"
57+
"vitest": "2.1.2"
5858
},
5959
"peerDependencies": {
6060
"tslib": "^2.6.2"
6161
},
6262
"dependencies": {
63-
"@algorandfoundation/algorand-typescript": "^0.0.1-alpha.2",
63+
"@algorandfoundation/algorand-typescript": "^0.0.1-alpha.9",
6464
"@algorandfoundation/algokit-utils": "^6.2.1",
65-
"@algorandfoundation/puya-ts": "^1.0.0-alpha.5",
65+
"@algorandfoundation/puya-ts": "^1.0.0-alpha.14",
6666
"algosdk": "^2.9.0",
6767
"elliptic": "^6.5.7",
6868
"js-sha256": "^0.11.0",

readme.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
# @algorandfoundation/algorand-typescript-testing
22

3-
A library which allows you to execute Algorand TypeScript code locally under a test context either emulating or mocking AVM behaviour.
3+
A library which allows you to execute Algorand TypeScript code locally under a test context either emulating or mocking AVM behaviour.
4+
5+
## Getting started
6+
7+
Install package
8+
`npm i -D @algorandfoundation/algorand-typescript-testing`

src/collections/custom-key-map.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Account, internal } from '@algorandfoundation/algorand-typescript'
2+
import { encodingUtil } from '@algorandfoundation/puya-ts'
3+
import { DeliberateAny } from '../typescript-helpers'
4+
5+
type Primitive = number | bigint | string | boolean
6+
export abstract class CustomKeyMap<TKey, TValue> implements Map<TKey, TValue> {
7+
#keySerializer: (key: TKey) => Primitive
8+
#map = new Map<Primitive, [TKey, TValue]>()
9+
10+
constructor(keySerializer: (key: TKey) => number | bigint | string) {
11+
this.#keySerializer = keySerializer
12+
}
13+
14+
clear(): void {
15+
this.#map.clear()
16+
}
17+
delete(key: TKey): boolean {
18+
return this.#map.delete(this.#keySerializer(key))
19+
}
20+
forEach(callbackfn: (value: TValue, key: TKey, map: Map<TKey, TValue>) => void, thisArg?: DeliberateAny): void {
21+
for (const [key, value] of this.#map.values()) {
22+
callbackfn.call(thisArg ?? this, value, key, this)
23+
}
24+
}
25+
get(key: TKey): TValue | undefined {
26+
return this.#map.get(this.#keySerializer(key))?.[1]
27+
}
28+
has(key: TKey): boolean {
29+
return this.#map.has(this.#keySerializer(key))
30+
}
31+
set(key: TKey, value: TValue): this {
32+
this.#map.set(this.#keySerializer(key), [key, value])
33+
return this
34+
}
35+
get size(): number {
36+
return this.#map.size
37+
}
38+
entries(): MapIterator<[TKey, TValue]> {
39+
return this.#map.values()
40+
}
41+
*keys(): MapIterator<TKey> {
42+
for (const [key] of this.#map.values()) {
43+
yield key
44+
}
45+
}
46+
*values(): MapIterator<TValue> {
47+
for (const [, value] of this.#map.values()) {
48+
yield value
49+
}
50+
}
51+
[Symbol.iterator](): MapIterator<[TKey, TValue]> {
52+
return this.#map.values()
53+
}
54+
get [Symbol.toStringTag](): string {
55+
return this.constructor.name
56+
}
57+
}
58+
59+
export class AccountMap<TValue> extends CustomKeyMap<Account, TValue> {
60+
constructor() {
61+
super(AccountMap.getAddressStrFromAccount)
62+
}
63+
64+
private static getAddressStrFromAccount = (acc: Account) => {
65+
return encodingUtil.uint8ArrayToHex(internal.primitives.BytesCls.fromCompat(acc.bytes).asUint8Array())
66+
}
67+
}

src/constants.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ export const DEFAULT_GLOBAL_GENESIS_HASH = Bytes(
2222
)
2323

2424
// algorand encoded address of 32 zero bytes
25-
export const ZERO_ADDRESS = Bytes('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ')
25+
export const ZERO_ADDRESS_B32 = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ'
26+
export const ZERO_ADDRESS = Bytes.fromBase32(ZERO_ADDRESS_B32)
2627

2728
/**
2829
"\x09" # pragma version 9
@@ -37,3 +38,6 @@ export const LOGIC_DATA_PREFIX = Bytes('ProgData')
3738
export const MIN_TXN_FEE = 1000
3839

3940
export const ABI_RETURN_VALUE_LOG_PREFIX = Bytes.fromHex('151F7C75')
41+
42+
export const UINT64_OVERFLOW_UNDERFLOW_MESSAGE = 'Uint64 overflow or underflow'
43+
export const BIGUINT_OVERFLOW_UNDERFLOW_MESSAGE = 'BigUint overflow or underflow'

src/context-helpers/internal-context.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { internal } from '@algorandfoundation/algorand-typescript'
1+
import { Account, internal } from '@algorandfoundation/algorand-typescript'
22
import { AccountData } from '../impl/account'
33
import { ApplicationData } from '../impl/application'
44
import { AssetData } from '../impl/asset'
@@ -43,9 +43,8 @@ class InternalContext {
4343
return this.value.txn.activeGroup
4444
}
4545

46-
getAccountData(accountPublicKey: internal.primitives.StubBytesCompat): AccountData {
47-
const key = internal.primitives.BytesCls.fromCompat(accountPublicKey)
48-
const data = this.ledger.accountDataMap.get(key.toString())
46+
getAccountData(account: Account): AccountData {
47+
const data = this.ledger.accountDataMap.get(account)
4948
if (!data) {
5049
throw internal.errors.internalError('Unknown account, check correct testing context is active')
5150
}

src/impl/account.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import { Account, Application, Asset, bytes, internal, uint64 } from '@algorandf
22
import { DEFAULT_ACCOUNT_MIN_BALANCE, ZERO_ADDRESS } from '../constants'
33
import { lazyContext } from '../context-helpers/internal-context'
44
import { Mutable } from '../typescript-helpers'
5-
import { asBytes, asUint64, asUint64Cls } from '../util'
5+
import { asUint64, asUint64Cls } from '../util'
66
import { ApplicationCls } from './application'
77
import { AssetCls } from './asset'
8+
import { BytesBackedCls } from './base'
89

910
export class AssetHolding {
1011
balance: uint64
@@ -38,15 +39,17 @@ export class AccountData {
3839
}
3940
}
4041

41-
export class AccountCls implements Account {
42-
readonly bytes: bytes
43-
44-
constructor(address?: internal.primitives.StubBytesCompat) {
45-
this.bytes = asBytes(address ?? ZERO_ADDRESS)
42+
export class AccountCls extends BytesBackedCls implements Account {
43+
constructor(address?: bytes) {
44+
const addressBytes = address ?? ZERO_ADDRESS
45+
if (![32n, 36n].includes(asUint64Cls(addressBytes.length).valueOf())) {
46+
throw new internal.errors.AvmError('Address must be 32 bytes long, or 36 bytes including checksum')
47+
}
48+
super(addressBytes.slice(0, 32))
4649
}
4750

4851
private get data(): AccountData {
49-
return lazyContext.getAccountData(this.bytes)
52+
return lazyContext.getAccountData(this)
5053
}
5154

5255
get balance(): uint64 {

src/impl/application.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ALWAYS_APPROVE_TEAL_PROGRAM } from '../constants'
44
import { lazyContext } from '../context-helpers/internal-context'
55
import { Mutable } from '../typescript-helpers'
66
import { asBigInt, asUint64 } from '../util'
7+
import { Uint64BackedCls } from './base'
78

89
export class ApplicationData {
910
application: Mutable<Omit<Application, 'id' | 'address'>> & { appLogs: bytes[] }
@@ -28,11 +29,13 @@ export class ApplicationData {
2829
}
2930
}
3031

31-
export class ApplicationCls implements Application {
32-
readonly id: uint64
32+
export class ApplicationCls extends Uint64BackedCls implements Application {
33+
get id() {
34+
return this.uint64
35+
}
3336

3437
constructor(id?: uint64) {
35-
this.id = asUint64(id ?? 0)
38+
super(asUint64(id ?? 0))
3639
}
3740

3841
private get data(): ApplicationData {
@@ -64,6 +67,6 @@ export class ApplicationCls implements Application {
6467
}
6568
get address(): Account {
6669
const addr = algosdk.getApplicationAddress(asBigInt(this.id))
67-
return Account(Bytes(addr))
70+
return Account(Bytes.fromBase32(addr))
6871
}
6972
}

src/impl/asset-holding.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const getAssetHolding = (
1515
return undefined
1616
}
1717

18-
const accountData = lazyContext.getAccountData(account.bytes)
18+
const accountData = lazyContext.getAccountData(account)
1919
const holding = accountData.optedAssets.get(asBigInt(asset.id))
2020
if (holding === undefined) {
2121
return undefined

src/impl/asset.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import { Account, Asset, bytes, internal, uint64 } from '@algorandfoundation/algorand-typescript'
22
import { lazyContext } from '../context-helpers/internal-context'
3-
import { asBigInt, asUint64 } from '../util'
43
import { Mutable } from '../typescript-helpers'
4+
import { asBigInt, asUint64 } from '../util'
55
import { AssetHolding } from './account'
6+
import { Uint64BackedCls } from './base'
67

78
export type AssetData = Mutable<Omit<Asset, 'id' | 'balance' | 'frozen'>>
89

9-
export class AssetCls implements Asset {
10-
readonly id: uint64
10+
export class AssetCls extends Uint64BackedCls implements Asset {
11+
get id(): uint64 {
12+
return this.uint64
13+
}
1114

1215
constructor(id?: internal.primitives.StubUint64Compat) {
13-
this.id = asUint64(id ?? 0)
16+
super(asUint64(id ?? 0))
1417
}
1518

1619
private get data(): AssetData {
@@ -61,7 +64,7 @@ export class AssetCls implements Asset {
6164
}
6265

6366
private getAssetHolding(account: Account): AssetHolding {
64-
const accountData = lazyContext.getAccountData(account.bytes)
67+
const accountData = lazyContext.getAccountData(account)
6568
if (!accountData.optedAssets.has(asBigInt(this.id))) {
6669
internal.errors.internalError(
6770
'The asset is not opted into the account! Use `ctx.any.account(opted_asset_balances={{ASSET_ID: VALUE}})` to set emulated opted asset into the account.',

0 commit comments

Comments
 (0)