Skip to content

Commit 78e24a6

Browse files
committed
refactor: move all implementation logic from algo-ts to algo-ts-testing
1 parent 4fd3974 commit 78e24a6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+1858
-1142
lines changed

examples/calculator/contract.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { internal, Uint64 } from '@algorandfoundation/algorand-typescript'
2-
import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
1+
import { Uint64 } from '@algorandfoundation/algorand-typescript'
2+
import { AvmError, TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
33
import { afterEach, describe, expect, it } from 'vitest'
44
import MyContract from './contract.algo'
55

@@ -19,7 +19,7 @@ describe('Calculator', () => {
1919
}),
2020
])
2121
.execute(() => {
22-
expect(() => contract.approvalProgram()).toThrowError(new internal.errors.AvmError('Unknown operation'))
22+
expect(() => contract.approvalProgram()).toThrowError(new AvmError('Unknown operation'))
2323
})
2424
})
2525
})

examples/voting/contract.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Bytes, Uint64 } from '@algorandfoundation/algorand-typescript'
2-
import { encodingUtil, TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
2+
import { TestExecutionContext, toExternalValue } from '@algorandfoundation/algorand-typescript-testing'
33
import { DynamicArray, UintN8 } from '@algorandfoundation/algorand-typescript/arc4'
44
import nacl from 'tweetnacl'
55
import { afterEach, describe, expect, it } from 'vitest'
@@ -50,7 +50,7 @@ describe('VotingRoundApp', () => {
5050
contract.bootstrap(ctx.any.txn.payment({ receiver: app.address, amount: boostrapMinBalanceReq }))
5151

5252
const account = ctx.any.account()
53-
const signature = nacl.sign.detached(encodingUtil.toExternalValue(account.bytes), keyPair.secretKey)
53+
const signature = nacl.sign.detached(toExternalValue(account.bytes), keyPair.secretKey)
5454
ctx.txn.createScope([ctx.any.txn.applicationCall({ sender: account })]).execute(() => {
5555
const preconditions = contract.getPreconditions(Bytes(signature))
5656

@@ -67,7 +67,7 @@ describe('VotingRoundApp', () => {
6767
contract.bootstrap(ctx.any.txn.payment({ receiver: app.address, amount: boostrapMinBalanceReq }))
6868

6969
const account = ctx.any.account()
70-
const signature = nacl.sign.detached(encodingUtil.toExternalValue(account.bytes), keyPair.secretKey)
70+
const signature = nacl.sign.detached(toExternalValue(account.bytes), keyPair.secretKey)
7171
const answerIds = new DynamicArray<UintN8>(
7272
...Array(13)
7373
.fill(0)

src/collections/custom-key-map.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Account } from '@algorandfoundation/algorand-typescript'
2-
import { internal } from '@algorandfoundation/algorand-typescript'
2+
import { internalError } from '../errors'
3+
import type { StubBytesCompat, StubUint64Compat } from '../impl/primitives'
34
import type { DeliberateAny } from '../typescript-helpers'
45
import { asBytesCls, asUint64Cls } from '../util'
56

@@ -29,7 +30,7 @@ export abstract class CustomKeyMap<TKey, TValue> implements Map<TKey, TValue> {
2930
getOrFail(key: TKey): TValue {
3031
const value = this.get(key)
3132
if (value === undefined) {
32-
throw internal.errors.internalError('Key not found')
33+
throw internalError('Key not found')
3334
}
3435
return value
3536
}
@@ -74,13 +75,13 @@ export class AccountMap<TValue> extends CustomKeyMap<Account, TValue> {
7475
}
7576
}
7677

77-
export class BytesMap<TValue> extends CustomKeyMap<internal.primitives.StubBytesCompat, TValue> {
78+
export class BytesMap<TValue> extends CustomKeyMap<StubBytesCompat, TValue> {
7879
constructor() {
7980
super((bytes) => asBytesCls(bytes).valueOf())
8081
}
8182
}
8283

83-
export class Uint64Map<TValue> extends CustomKeyMap<internal.primitives.StubUint64Compat, TValue> {
84+
export class Uint64Map<TValue> extends CustomKeyMap<StubUint64Compat, TValue> {
8485
constructor() {
8586
super((uint64) => asUint64Cls(uint64).valueOf())
8687
}

src/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Bytes } from '@algorandfoundation/algorand-typescript'
1+
import { Bytes } from './impl/primitives'
22

33
export const UINT64_SIZE = 64
44
export const UINT512_SIZE = 512
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { TestExecutionContext } from '../test-execution-context'
2+
3+
declare global {
4+
// eslint-disable-next-line no-var
5+
var puyaTsExecutionContext: TestExecutionContext | undefined
6+
}
7+
8+
export const ctxMgr = {
9+
set instance(ctx: TestExecutionContext) {
10+
const instance = global.puyaTsExecutionContext
11+
if (instance != undefined) throw new Error('Execution context has already been set')
12+
global.puyaTsExecutionContext = ctx
13+
},
14+
get instance() {
15+
const instance = global.puyaTsExecutionContext
16+
if (instance == undefined) throw new Error('No execution context has been set')
17+
return instance
18+
},
19+
reset() {
20+
global.puyaTsExecutionContext = undefined
21+
},
22+
}
Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
11
import type { uint64 } from '@algorandfoundation/algorand-typescript'
2-
import { internal } from '@algorandfoundation/algorand-typescript'
32
import type { AbiMetadata } from '../abi-metadata'
3+
import { CodeError } from '../errors'
44
import { ApplicationTransaction } from '../impl/transactions'
55
import { lazyContext } from './internal-context'
66

77
export const checkRoutingConditions = (appId: uint64, metadata: AbiMetadata) => {
88
const appData = lazyContext.getApplicationData(appId)
99
const isCreating = appData.isCreating
1010
if (isCreating && metadata.onCreate === 'disallow') {
11-
throw new internal.errors.CodeError('method can not be called while creating')
11+
throw new CodeError('method can not be called while creating')
1212
}
1313
if (!isCreating && metadata.onCreate === 'require') {
14-
throw new internal.errors.CodeError('method can only be called while creating')
14+
throw new CodeError('method can only be called while creating')
1515
}
1616
const txn = lazyContext.activeGroup.activeTransaction
1717
if (txn instanceof ApplicationTransaction && metadata.allowActions && !metadata.allowActions.includes(txn.onCompletion)) {
18-
throw new internal.errors.CodeError(
19-
`method can only be called with one of the following on_completion values: ${metadata.allowActions.join(', ')}`,
20-
)
18+
throw new CodeError(`method can only be called with one of the following on_completion values: ${metadata.allowActions.join(', ')}`)
2119
}
2220
}

src/context-helpers/internal-context.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import type { Account } from '@algorandfoundation/algorand-typescript'
2-
import { BaseContract, internal } from '@algorandfoundation/algorand-typescript'
2+
import { internalError } from '../errors'
3+
import { BaseContract } from '../impl/base-contract'
4+
import type { StubUint64Compat } from '../impl/primitives'
5+
import { Uint64Cls } from '../impl/primitives'
36
import type { AccountData, ApplicationData, AssetData } from '../impl/reference'
47
import type { VoterData } from '../impl/voter-params'
58
import type { TransactionGroup } from '../subcontexts/transaction-context'
69
import type { TestExecutionContext } from '../test-execution-context'
10+
import { ctxMgr } from './context-manager'
711

812
/**
913
* For accessing implementation specific functions, with a convenient single entry
@@ -12,7 +16,7 @@ import type { TestExecutionContext } from '../test-execution-context'
1216
*/
1317
class InternalContext {
1418
get value() {
15-
return internal.ctxMgr.instance as TestExecutionContext
19+
return ctxMgr.instance as TestExecutionContext
1620
}
1721

1822
get defaultSender() {
@@ -46,34 +50,33 @@ class InternalContext {
4650
getAccountData(account: Account): AccountData {
4751
const data = this.ledger.accountDataMap.get(account)
4852
if (!data) {
49-
throw internal.errors.internalError('Unknown account, check correct testing context is active')
53+
throw internalError('Unknown account, check correct testing context is active')
5054
}
5155
return data
5256
}
5357

54-
getAssetData(id: internal.primitives.StubUint64Compat): AssetData {
55-
const key = internal.primitives.Uint64Cls.fromCompat(id)
58+
getAssetData(id: StubUint64Compat): AssetData {
59+
const key = Uint64Cls.fromCompat(id)
5660
const data = this.ledger.assetDataMap.get(key.asBigInt())
5761
if (!data) {
58-
throw internal.errors.internalError('Unknown asset, check correct testing context is active')
62+
throw internalError('Unknown asset, check correct testing context is active')
5963
}
6064
return data
6165
}
6266

63-
getApplicationData(id: internal.primitives.StubUint64Compat | BaseContract): ApplicationData {
64-
const uint64Id =
65-
id instanceof BaseContract ? this.ledger.getApplicationForContract(id).id : internal.primitives.Uint64Cls.fromCompat(id)
67+
getApplicationData(id: StubUint64Compat | BaseContract): ApplicationData {
68+
const uint64Id = id instanceof BaseContract ? this.ledger.getApplicationForContract(id).id : Uint64Cls.fromCompat(id)
6669
const data = this.ledger.applicationDataMap.get(uint64Id)
6770
if (!data) {
68-
throw internal.errors.internalError('Unknown application, check correct testing context is active')
71+
throw internalError('Unknown application, check correct testing context is active')
6972
}
7073
return data
7174
}
7275

7376
getVoterData(account: Account): VoterData {
7477
const data = this.ledger.voterDataMap.get(account)
7578
if (!data) {
76-
throw internal.errors.internalError('Unknown voter, check correct testing context is active')
79+
throw internalError('Unknown voter, check correct testing context is active')
7780
}
7881
return data
7982
}

src/encoders.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import type { biguint, bytes, TransactionType, uint64 } from '@algorandfoundation/algorand-typescript'
2-
import { BigUint, internal, Uint64 } from '@algorandfoundation/algorand-typescript'
32
import type { OnCompleteAction } from '@algorandfoundation/algorand-typescript/arc4'
43
import { ARC4Encoded } from '@algorandfoundation/algorand-typescript/arc4'
4+
import { uint8ArrayToBigInt } from './encoding-util'
5+
import { internalError } from './errors'
56
import { BytesBackedCls, Uint64BackedCls } from './impl/base'
67
import { arc4Encoders, encodeArc4Impl, getArc4Encoder } from './impl/encoded-types'
8+
import { BigUint, Uint64, type StubBytesCompat } from './impl/primitives'
79
import { AccountCls, ApplicationCls, AssetCls } from './impl/reference'
810
import type { DeliberateAny } from './typescript-helpers'
911
import { asBytes, asMaybeBigUintCls, asMaybeBytesCls, asMaybeUint64Cls, asUint64Cls, asUint8Array, nameOfType } from './util'
@@ -13,14 +15,14 @@ export type TypeInfo = {
1315
genericArgs?: TypeInfo[] | Record<string, TypeInfo>
1416
}
1517

16-
export type fromBytes<T> = (val: Uint8Array | internal.primitives.StubBytesCompat, typeInfo: TypeInfo, prefix?: 'none' | 'log') => T
18+
export type fromBytes<T> = (val: Uint8Array | StubBytesCompat, typeInfo: TypeInfo, prefix?: 'none' | 'log') => T
1719

1820
const booleanFromBytes: fromBytes<boolean> = (val) => {
19-
return internal.encodingUtil.uint8ArrayToBigInt(asUint8Array(val)) > 0n
21+
return uint8ArrayToBigInt(asUint8Array(val)) > 0n
2022
}
2123

2224
const bigUintFromBytes: fromBytes<biguint> = (val) => {
23-
return BigUint(internal.encodingUtil.uint8ArrayToBigInt(asUint8Array(val)))
25+
return BigUint(uint8ArrayToBigInt(asUint8Array(val)))
2426
}
2527

2628
const bytesFromBytes: fromBytes<bytes> = (val) => {
@@ -32,15 +34,15 @@ const stringFromBytes: fromBytes<string> = (val) => {
3234
}
3335

3436
const uint64FromBytes: fromBytes<uint64> = (val) => {
35-
return Uint64(internal.encodingUtil.uint8ArrayToBigInt(asUint8Array(val)))
37+
return Uint64(uint8ArrayToBigInt(asUint8Array(val)))
3638
}
3739

3840
const onCompletionFromBytes: fromBytes<OnCompleteAction> = (val) => {
39-
return Uint64(internal.encodingUtil.uint8ArrayToBigInt(asUint8Array(val))) as OnCompleteAction
41+
return Uint64(uint8ArrayToBigInt(asUint8Array(val))) as OnCompleteAction
4042
}
4143

4244
const transactionTypeFromBytes: fromBytes<TransactionType> = (val) => {
43-
return Uint64(internal.encodingUtil.uint8ArrayToBigInt(asUint8Array(val))) as TransactionType
45+
return Uint64(uint8ArrayToBigInt(asUint8Array(val))) as TransactionType
4446
}
4547

4648
export const encoders: Record<string, fromBytes<DeliberateAny>> = {
@@ -86,5 +88,5 @@ export const toBytes = (val: unknown): bytes => {
8688
if (Array.isArray(val) || typeof val === 'object') {
8789
return encodeArc4Impl('', val)
8890
}
89-
internal.errors.internalError(`Invalid type for bytes: ${nameOfType(val)}`)
91+
internalError(`Invalid type for bytes: ${nameOfType(val)}`)
9092
}

src/encoding-util.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { AvmError } from './errors'
2+
3+
export const uint8ArrayToNumber = (value: Uint8Array): number => {
4+
return value.reduce((acc, x) => acc * 256 + x, 0)
5+
}
6+
7+
export const uint8ArrayToBigInt = (v: Uint8Array): bigint => {
8+
// Assume big-endian
9+
return Array.from(v)
10+
.toReversed()
11+
.map((byte_value, i): bigint => BigInt(byte_value) << BigInt(i * 8))
12+
.reduce((a, b) => a + b, 0n)
13+
}
14+
15+
export const hexToUint8Array = (value: string): Uint8Array => {
16+
if (value.length % 2 !== 0) {
17+
throw new AvmError('Hex string must have even number of characters')
18+
}
19+
return Uint8Array.from(Buffer.from(value, 'hex'))
20+
}
21+
22+
export const base64ToUint8Array = (value: string): Uint8Array => {
23+
return Uint8Array.from(Buffer.from(value, 'base64'))
24+
}
25+
26+
export const bigIntToUint8Array = (val: bigint, fixedSize: number | 'dynamic' = 'dynamic'): Uint8Array => {
27+
if (val === 0n && fixedSize === 'dynamic') {
28+
return new Uint8Array(0)
29+
}
30+
const maxBytes = fixedSize === 'dynamic' ? undefined : fixedSize
31+
32+
let hex = val.toString(16)
33+
34+
// Pad the hex with zeros so it matches the size in bytes
35+
if (fixedSize !== 'dynamic' && hex.length !== fixedSize * 2) {
36+
hex = hex.padStart(fixedSize * 2, '0')
37+
} else if (hex.length % 2 == 1) {
38+
// Pad to 'whole' byte
39+
hex = `0${hex}`
40+
}
41+
if (maxBytes && hex.length > maxBytes * 2) {
42+
throw new AvmError(`Cannot encode ${val} as ${maxBytes} bytes as it would overflow`)
43+
}
44+
const byteArray = new Uint8Array(hex.length / 2)
45+
for (let i = 0, j = 0; i < hex.length / 2; i++, j += 2) {
46+
byteArray[i] = parseInt(hex.slice(j, j + 2), 16)
47+
}
48+
return byteArray
49+
}
50+
51+
export const utf8ToUint8Array = (value: string): Uint8Array => {
52+
const encoder = new TextEncoder()
53+
return encoder.encode(value)
54+
}
55+
56+
export const uint8ArrayToUtf8 = (value: Uint8Array): string => {
57+
const decoder = new TextDecoder()
58+
return decoder.decode(value)
59+
}
60+
61+
export const uint8ArrayToHex = (value: Uint8Array): string => Buffer.from(value).toString('hex')
62+
63+
export const uint8ArrayToBase64 = (value: Uint8Array): string => Buffer.from(value).toString('base64')
64+
65+
export const uint8ArrayToBase64Url = (value: Uint8Array): string => Buffer.from(value).toString('base64url')
66+
67+
export { uint8ArrayToBase32 } from './impl/base-32'

src/errors.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,55 @@
1-
import { internal } from '@algorandfoundation/algorand-typescript'
1+
/**
2+
* Raised when an `err` op is encountered, or when the testing VM is asked to do something that would cause
3+
* the AVM to fail.
4+
*/
5+
export class AvmError extends Error {
6+
constructor(message: string) {
7+
super(message)
8+
}
9+
}
10+
export function avmError(message: string): never {
11+
throw new AvmError(message)
12+
}
13+
14+
export function avmInvariant(condition: unknown, message: string): asserts condition {
15+
if (!condition) {
16+
throw new AvmError(message)
17+
}
18+
}
19+
/**
20+
* Raised when an assertion fails
21+
*/
22+
export class AssertError extends AvmError {
23+
constructor(message: string) {
24+
super(message)
25+
}
26+
}
27+
28+
/**
29+
* Raised when testing code errors
30+
*/
31+
export class InternalError extends Error {
32+
constructor(message: string, options?: ErrorOptions) {
33+
super(message, options)
34+
}
35+
}
36+
37+
export function internalError(message: string): never {
38+
throw new InternalError(message)
39+
}
40+
41+
/**
42+
* Raised when unsupported user code is encountered
43+
*/
44+
export class CodeError extends Error {
45+
constructor(message: string, options?: ErrorOptions) {
46+
super(message, options)
47+
}
48+
}
49+
50+
export function codeError(message: string): never {
51+
throw new CodeError(message)
52+
}
253

354
export class NotImplementedError extends Error {
455
constructor(feature: string) {
@@ -12,6 +63,6 @@ export function notImplementedError(feature: string): never {
1263

1364
export function testInvariant(condition: unknown, message: string): asserts condition {
1465
if (!condition) {
15-
throw new internal.errors.InternalError(message)
66+
throw new InternalError(message)
1667
}
1768
}

0 commit comments

Comments
 (0)