Skip to content

Commit 28eebfc

Browse files
authored
Merge pull request #100 from algorandfoundation/feat/abi-validation
feat: add support for ABI validation
2 parents 3f8b034 + ebc7954 commit 28eebfc

File tree

13 files changed

+331
-44
lines changed

13 files changed

+331
-44
lines changed

docs/testing-guide/concepts.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,8 @@ As explained in the [introduction](index.md), `algorand-typescript-testing` _inj
6464
3. **Mockable**: Not implemented, but can be mocked or patched. For example, `op.onlineStake` can be mocked to return specific values or behaviors; otherwise, it raises a `NotImplementedError`. This category covers cases where native or emulated implementation in a unit test context is impractical or overly complex.
6565

6666
For a full list of all public `algorand-typescript` types and their corresponding implementation category, refer to the [Coverage](../coverage.md) section.
67+
68+
## Data Validation
69+
70+
Algorand TypeScript and the puya compiler have functionality to perform validation of transaction inputs via the `--validate-abi-args`, `--validate-abi-return` CLI arguments, `arc4.abimethod({validateEncoding: ...})` decorator, and `validateEncoding(...)` method.
71+
The Algorand TypeScript Testing library does _NOT_ implement this validation behaviour, as you should test invalid inputs using an integrated test against a real Algorand network.

package-lock.json

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

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@
6666
"vitest": "3.2.4"
6767
},
6868
"dependencies": {
69-
"@algorandfoundation/algorand-typescript": "1.0.0-beta.65",
70-
"@algorandfoundation/puya-ts": "1.0.0-beta.65",
69+
"@algorandfoundation/algorand-typescript": "1.0.0-beta.73",
70+
"@algorandfoundation/puya-ts": "1.0.0-beta.73",
7171
"elliptic": "^6.6.1",
7272
"js-sha256": "^0.11.0",
7373
"js-sha3": "^0.9.3",

src/encoders.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ARC4Encoded } from '@algorandfoundation/algorand-typescript/arc4'
33
import { encodingUtil } from '@algorandfoundation/puya-ts'
44
import { InternalError } from './errors'
55
import { BytesBackedCls, Uint64BackedCls } from './impl/base'
6-
import { arc4Encoders, encodeArc4Impl, getArc4Encoder } from './impl/encoded-types'
6+
import { arc4Encoders, encodeArc4Impl, getArc4Encoder, tryArc4EncodedLengthImpl } from './impl/encoded-types'
77
import { BigUint, Uint64, type StubBytesCompat } from './impl/primitives'
88
import { AccountCls, ApplicationCls, AssetCls } from './impl/reference'
99
import type { DeliberateAny } from './typescript-helpers'
@@ -89,3 +89,8 @@ export const toBytes = (val: unknown): bytes => {
8989
}
9090
throw new InternalError(`Invalid type for bytes: ${nameOfType(val)}`)
9191
}
92+
93+
export const minLengthForType = (typeInfo: TypeInfo): number => {
94+
const minArc4StaticLength = tryArc4EncodedLengthImpl(typeInfo)
95+
return minArc4StaticLength ?? 0
96+
}

src/impl/encoded-types.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {
22
Account as AccountType,
33
BigUintCompat,
44
bytes,
5+
NTuple,
56
StringCompat,
67
uint64,
78
Uint64Compat,
@@ -417,8 +418,8 @@ export class StaticArrayImpl<TItem extends ARC4Encoded, TLength extends number>
417418
)
418419
}
419420

420-
get native(): TItem[] {
421-
return this.items
421+
get native(): NTuple<TItem, TLength> {
422+
return this.items as NTuple<TItem, TLength>
422423
}
423424

424425
static fromBytesImpl(
@@ -433,7 +434,7 @@ export class StaticArrayImpl<TItem extends ARC4Encoded, TLength extends number>
433434
}
434435
const result = new StaticArrayImpl(typeInfo)
435436
result.uint8ArrayValue = asUint8Array(bytesValue)
436-
return result
437+
return result as StaticArrayImpl<ARC4Encoded, number>
437438
}
438439

439440
static getMaxBytesLength(typeInfo: TypeInfo): number {
@@ -1086,6 +1087,8 @@ const getMaxLengthOfStaticContentType = (type: TypeInfo): number => {
10861087
case 'biguint':
10871088
return UINT512_SIZE / BITS_IN_BYTE
10881089
case 'boolean':
1090+
return 8
1091+
case 'Bool':
10891092
return 1
10901093
case 'Address':
10911094
return AddressImpl.getMaxBytesLength(type)
@@ -1103,8 +1106,9 @@ const getMaxLengthOfStaticContentType = (type: TypeInfo): number => {
11031106
return TupleImpl.getMaxBytesLength(type)
11041107
case 'Struct':
11051108
return StructImpl.getMaxBytesLength(type)
1109+
default:
1110+
throw new CodeError(`unsupported type ${type.name}`)
11061111
}
1107-
throw new CodeError(`unsupported type ${type.name}`)
11081112
}
11091113

11101114
const encode = (values: ARC4Encoded[]) => {
@@ -1359,3 +1363,17 @@ export const arc4EncodedLengthImpl = (typeInfoString: string): uint64 => {
13591363
const typeInfo = JSON.parse(typeInfoString)
13601364
return getMaxLengthOfStaticContentType(typeInfo)
13611365
}
1366+
1367+
export const tryArc4EncodedLengthImpl = (typeInfoString: string | TypeInfo): uint64 | undefined => {
1368+
const typeInfo = typeof typeInfoString === 'string' ? JSON.parse(typeInfoString) : typeInfoString
1369+
1370+
try {
1371+
return getMaxLengthOfStaticContentType(typeInfo)
1372+
} catch (e) {
1373+
if (e instanceof CodeError && e.message.startsWith('unsupported type')) {
1374+
return undefined
1375+
}
1376+
1377+
throw e
1378+
}
1379+
}

src/impl/pure.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export const bsqrt = (a: StubBigUintCompat): biguint => {
4545
export const btoi = (a: StubBytesCompat): uint64 => {
4646
const bytesValue = BytesCls.fromCompat(a)
4747
if (bytesValue.length.asAlgoTs() > BITS_IN_BYTE) {
48-
throw new AvmError(`btoi arg too long, got [${bytesValue.length.valueOf()}]bytes`)
48+
throw new AvmError(`btoi arg too long, got ${bytesValue.length.valueOf()} bytes`)
4949
}
5050
return bytesValue.toUint64().asAlgoTs()
5151
}

src/impl/state.ts

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ import { AccountMap } from '../collections/custom-key-map'
1515
import { MAX_BOX_SIZE } from '../constants'
1616
import { lazyContext } from '../context-helpers/internal-context'
1717
import type { TypeInfo } from '../encoders'
18-
import { getEncoder, toBytes } from '../encoders'
19-
import { AssertError, InternalError } from '../errors'
20-
import { getGenericTypeInfo } from '../runtime-helpers'
18+
import { getEncoder, minLengthForType, toBytes } from '../encoders'
19+
import { AssertError, CodeError, InternalError } from '../errors'
20+
import { getGenericTypeInfo, tryArc4EncodedLengthImpl } from '../runtime-helpers'
2121
import { asBytes, asBytesCls, asNumber, asUint8Array, conactUint8Arrays } from '../util'
2222
import type { StubBytesCompat, StubUint64Compat } from './primitives'
2323
import { Bytes, Uint64, Uint64Cls } from './primitives'
@@ -136,6 +136,17 @@ export class BoxCls<TValue> {
136136

137137
private readonly _type: string = BoxCls.name
138138

139+
private get valueType(): TypeInfo {
140+
if (this.#valueType === undefined) {
141+
const typeInfo = getGenericTypeInfo(this)
142+
if (typeInfo === undefined || typeInfo.genericArgs === undefined || typeInfo.genericArgs.length !== 1) {
143+
throw new InternalError('Box value type is not set')
144+
}
145+
this.#valueType = (typeInfo.genericArgs as TypeInfo[])[0]
146+
}
147+
return this.#valueType
148+
}
149+
139150
static [Symbol.hasInstance](x: unknown): x is BoxCls<unknown> {
140151
return x instanceof Object && '_type' in x && (x as { _type: string })['_type'] === BoxCls.name
141152
}
@@ -151,6 +162,35 @@ export class BoxCls<TValue> {
151162
return (val: Uint8Array) => getEncoder<TValue>(valueType)(val, valueType)
152163
}
153164

165+
create(options?: { size?: StubUint64Compat }): boolean {
166+
const optionSize = options?.size !== undefined ? asNumber(options.size) : undefined
167+
const valueTypeSize = tryArc4EncodedLengthImpl(this.valueType)
168+
169+
if (valueTypeSize === undefined && optionSize === undefined) {
170+
throw new InternalError(`${this.valueType.name} does not have a fixed byte size. Please specify a size argument`)
171+
}
172+
173+
if (valueTypeSize !== undefined && optionSize !== undefined) {
174+
if (optionSize < valueTypeSize) {
175+
throw new InternalError(`Box size cannot be less than ${valueTypeSize}`)
176+
}
177+
178+
if (optionSize > valueTypeSize) {
179+
process.emitWarning(
180+
`Box size is set to ${optionSize} but the value type ${this.valueType.name} has a fixed size of ${valueTypeSize}`,
181+
)
182+
}
183+
}
184+
185+
lazyContext.ledger.setBox(
186+
this.#app,
187+
this.key,
188+
new Uint8Array(Math.max(asNumber(options?.size ?? 0), this.valueType ? minLengthForType(this.valueType) : 0)),
189+
)
190+
191+
return true
192+
}
193+
154194
get value(): TValue {
155195
if (!this.exists) {
156196
throw new InternalError('Box has not been created')
@@ -164,8 +204,17 @@ export class BoxCls<TValue> {
164204
lazyContext.ledger.setMatrialisedBox(this.#app, this.key, materialised)
165205
return materialised
166206
}
207+
167208
set value(v: TValue) {
168-
lazyContext.ledger.setBox(this.#app, this.key, asUint8Array(toBytes(v)))
209+
const isStaticValueType = tryArc4EncodedLengthImpl(this.valueType) !== undefined
210+
const newValueBytes = asUint8Array(toBytes(v))
211+
if (isStaticValueType && this.exists) {
212+
const originalValueBytes = lazyContext.ledger.getBox(this.#app, this.key)
213+
if (originalValueBytes.length !== newValueBytes.length) {
214+
throw new CodeError(`attempt to box_put wrong size ${originalValueBytes.length} != ${newValueBytes.length}`)
215+
}
216+
}
217+
lazyContext.ledger.setBox(this.#app, this.key, newValueBytes)
169218
lazyContext.ledger.setMatrialisedBox(this.#app, this.key, v)
170219
}
171220

src/impl/validate-encoding.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/** @internal */
2+
export function validateEncoding<T>(_value: T) {}

src/internal/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export { Box, BoxMap, BoxRef, GlobalState, LocalState } from '../impl/state'
1313
export { TemplateVarImpl as TemplateVar } from '../impl/template-var'
1414
export { Txn } from '../impl/txn'
1515
export { urangeImpl as urange } from '../impl/urange'
16+
export { validateEncoding } from '../impl/validate-encoding'
1617
export { assert, err } from '../util'
1718
export * as arc4 from './arc4'
1819
export * as op from './op'

tests/arc4/encode-decode-arc4.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ describe('arc4EncodedLength', () => {
145145
test('should return the correct length', () => {
146146
expect(arc4EncodedLength<uint64>()).toEqual(8)
147147
expect(arc4EncodedLength<biguint>()).toEqual(64)
148-
expect(arc4EncodedLength<boolean>()).toEqual(1)
148+
expect(arc4EncodedLength<Bool>()).toEqual(1)
149+
expect(arc4EncodedLength<boolean>()).toEqual(8)
149150
expect(arc4EncodedLength<UintN<512>>()).toEqual(64)
150151
expect(arc4EncodedLength<[uint64, uint64, boolean]>()).toEqual(17)
151152
expect(arc4EncodedLength<[uint64, uint64, boolean, boolean]>()).toEqual(17)

0 commit comments

Comments
 (0)