Skip to content

Commit

Permalink
Merge pull request #18 from algorandfoundation/feat-encode-decode-arc4
Browse files Browse the repository at this point in the history
feat: implement stubs for decodeArc4 and encodeArc4 functions
  • Loading branch information
boblat authored Dec 23, 2024
2 parents bd713dc + 73dd3e7 commit a94df7c
Show file tree
Hide file tree
Showing 9 changed files with 393 additions and 146 deletions.
237 changes: 111 additions & 126 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
},
"dependencies": {
"@algorandfoundation/algorand-typescript": "^0.0.1-alpha.23",
"@algorandfoundation/puya-ts": "^1.0.0-alpha.35",
"@algorandfoundation/puya-ts": "^1.0.0-alpha.36",
"elliptic": "^6.5.7",
"js-sha256": "^0.11.0",
"js-sha3": "^0.9.3",
Expand Down
132 changes: 120 additions & 12 deletions src/impl/encoded-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,20 @@ import {
BITS_IN_BYTE,
UINT64_SIZE,
} from '../constants'
import { lazyContext } from '../context-helpers/internal-context'
import { fromBytes, TypeInfo } from '../encoders'
import { DeliberateAny } from '../typescript-helpers'
import { asBigUint, asBigUintCls, asBytesCls, asUint64, asUint8Array, conactUint8Arrays, uint8ArrayToNumber } from '../util'
import { asBigInt, asBigUint, asBigUintCls, asBytesCls, asUint64, asUint8Array, conactUint8Arrays, uint8ArrayToNumber } from '../util'
import { AccountCls } from './account'
import { ApplicationCls } from './application'
import { AssetCls } from './asset'
import { ApplicationTransaction } from './transactions'

const ABI_LENGTH_SIZE = 2
const maxBigIntValue = (bitSize: number) => 2n ** BigInt(bitSize) - 1n
const maxBytesLength = (bitSize: number) => Math.floor(bitSize / BITS_IN_BYTE)
const encodeLength = (length: number) => new internal.primitives.BytesCls(encodingUtil.bigIntToUint8Array(BigInt(length), ABI_LENGTH_SIZE))

type CompatForArc4Int<N extends BitSize> = N extends 8 | 16 | 32 | 64 ? Uint64Compat : BigUintCompat
export class UintNImpl<N extends BitSize> extends UintN<N> {
private value: Uint8Array
Expand Down Expand Up @@ -99,7 +105,7 @@ export class UFixedNxMImpl<N extends BitSize, M extends number> extends UFixedNx
private value: Uint8Array
private bitSize: N
private precision: M
private typeInfo: TypeInfo
typeInfo: TypeInfo

constructor(typeInfo: TypeInfo | string, v: `${number}.${number}`) {
super(v)
Expand Down Expand Up @@ -163,7 +169,10 @@ export class UFixedNxMImpl<N extends BitSize, M extends number> extends UFixedNx
export class ByteImpl extends Byte {
private value: UintNImpl<8>

constructor(typeInfo: TypeInfo | string, v?: CompatForArc4Int<8>) {
constructor(
public typeInfo: TypeInfo | string,
v?: CompatForArc4Int<8>,
) {
super(v)
this.value = new UintNImpl<8>(typeInfo, v)
}
Expand Down Expand Up @@ -202,7 +211,10 @@ export class ByteImpl extends Byte {
export class StrImpl extends Str {
private value: Uint8Array

constructor(_typeInfo: TypeInfo | string, s?: StringCompat) {
constructor(
public typeInfo: TypeInfo | string,
s?: StringCompat,
) {
super()
const bytesValue = asBytesCls(s ?? '')
const bytesLength = encodeLength(bytesValue.length.asNumber())
Expand Down Expand Up @@ -244,7 +256,10 @@ const FALSE_BIGINT_VALUE = 0n
export class BoolImpl extends Bool {
private value: Uint8Array

constructor(_typeInfo: TypeInfo | string, v?: boolean) {
constructor(
public typeInfo: TypeInfo | string,
v?: boolean,
) {
super(v)
this.value = encodingUtil.bigIntToUint8Array(v ? TRUE_BIGINT_VALUE : FALSE_BIGINT_VALUE, 1)
}
Expand Down Expand Up @@ -320,8 +335,8 @@ const arrayProxyHandler = <TItem>() => ({
export class StaticArrayImpl<TItem extends ARC4Encoded, TLength extends number> extends StaticArray<TItem, TLength> {
private value?: TItem[]
private uint8ArrayValue?: Uint8Array
private typeInfo: TypeInfo
private size: number
typeInfo: TypeInfo
genericArgs: StaticArrayGenericArgs

constructor(typeInfo: TypeInfo | string, ...items: TItem[] & { length: TLength })
Expand Down Expand Up @@ -383,6 +398,10 @@ export class StaticArrayImpl<TItem extends ARC4Encoded, TLength extends number>
return StaticArrayImpl.fromBytesImpl(this.bytes, JSON.stringify(this.typeInfo)) as StaticArrayImpl<TItem, TLength>
}

get native(): TItem[] {
return this.items
}

static fromBytesImpl(
value: internal.primitives.StubBytesCompat | Uint8Array,
typeInfo: string | TypeInfo,
Expand Down Expand Up @@ -425,7 +444,7 @@ export class StaticArrayImpl<TItem extends ARC4Encoded, TLength extends number>
}

export class AddressImpl extends Address {
private typeInfo: TypeInfo
typeInfo: TypeInfo
private value: StaticArrayImpl<ByteImpl, 32>

constructor(typeInfo: TypeInfo | string, value?: Account | string | bytes) {
Expand Down Expand Up @@ -499,7 +518,7 @@ const readLength = (value: Uint8Array): readonly [number, Uint8Array] => {
export class DynamicArrayImpl<TItem extends ARC4Encoded> extends DynamicArray<TItem> {
private value?: TItem[]
private uint8ArrayValue?: Uint8Array
private typeInfo: TypeInfo
typeInfo: TypeInfo
genericArgs: DynamicArrayGenericArgs

constructor(typeInfo: TypeInfo | string, ...items: TItem[]) {
Expand Down Expand Up @@ -554,6 +573,10 @@ export class DynamicArrayImpl<TItem extends ARC4Encoded> extends DynamicArray<TI
return DynamicArrayImpl.fromBytesImpl(this.bytes, JSON.stringify(this.typeInfo)) as DynamicArrayImpl<TItem>
}

get native(): TItem[] {
return this.items
}

push(...values: TItem[]) {
const items = this.items
items.push(...values)
Expand Down Expand Up @@ -594,7 +617,7 @@ export class DynamicArrayImpl<TItem extends ARC4Encoded> extends DynamicArray<TI
export class TupleImpl<TTuple extends [ARC4Encoded, ...ARC4Encoded[]]> extends Tuple<TTuple> {
private value?: TTuple
private uint8ArrayValue?: Uint8Array
private typeInfo: TypeInfo
typeInfo: TypeInfo
genericArgs: TypeInfo[]

constructor(typeInfo: TypeInfo | string)
Expand Down Expand Up @@ -691,7 +714,6 @@ export class TupleImpl<TTuple extends [ARC4Encoded, ...ARC4Encoded[]]> extends T
type StructConstraint = Record<string, ARC4Encoded>
export class StructImpl<T extends StructConstraint> extends (Struct<StructConstraint> as DeliberateAny) {
private uint8ArrayValue?: Uint8Array
private typeInfo: TypeInfo
genericArgs: Record<string, TypeInfo>

constructor(typeInfo: TypeInfo | string, value: T = {} as T) {
Expand Down Expand Up @@ -737,6 +759,10 @@ export class StructImpl<T extends StructConstraint> extends (Struct<StructConstr
return result as T
}

get native(): T {
return this.items
}

private decodeAsProperties() {
if (this.uint8ArrayValue) {
const values = decode(this.uint8ArrayValue, Object.values(this.genericArgs))
Expand Down Expand Up @@ -769,7 +795,7 @@ export class StructImpl<T extends StructConstraint> extends (Struct<StructConstr
}

export class DynamicBytesImpl extends DynamicBytes {
private typeInfo: TypeInfo
typeInfo: TypeInfo
private value: DynamicArrayImpl<ByteImpl>

constructor(typeInfo: TypeInfo | string, value?: bytes | string) {
Expand Down Expand Up @@ -825,7 +851,7 @@ export class DynamicBytesImpl extends DynamicBytes {

export class StaticBytesImpl extends StaticBytes {
private value: StaticArrayImpl<ByteImpl, number>
private typeInfo: TypeInfo
typeInfo: TypeInfo

constructor(typeInfo: TypeInfo | string, value?: bytes | string) {
super(value)
Expand Down Expand Up @@ -1161,3 +1187,85 @@ export const getArc4TypeName = (typeInfo: TypeInfo): string | undefined => {
else if (typeof name === 'function') return name(typeInfo)
return undefined
}

export function decodeArc4Impl<T>(sourceTypeInfoString: string, bytes: internal.primitives.StubBytesCompat): T {
const sourceTypeInfo = JSON.parse(sourceTypeInfoString)
const encoder = getArc4Encoder(sourceTypeInfo)
const source = encoder(bytes, sourceTypeInfo)

return getNativeValue(source) as T
}

export function encodeArc4Impl<T>(_targetTypeInfoString: string, source: T): bytes {
const arc4Encoded = getArc4Encoded(source)
return arc4Encoded.bytes
}

const getNativeValue = (value: DeliberateAny): DeliberateAny => {
const native = (value as DeliberateAny).native
if (Array.isArray(native)) {
return native.map((item) => getNativeValue(item))
} else if (native instanceof internal.primitives.AlgoTsPrimitiveCls) {
return native
} else if (typeof native === 'object') {
return Object.fromEntries(Object.entries(native).map(([key, value]) => [key, getNativeValue(value)]))
}
return native
}

const getArc4Encoded = (value: DeliberateAny): ARC4Encoded => {
if (value instanceof ARC4Encoded) {
return value
}
if (value instanceof AccountCls) {
const index = (lazyContext.activeGroup.activeTransaction as ApplicationTransaction).apat.indexOf(value)
return new UintNImpl({ name: 'UintN<64>', genericArgs: [{ name: '64' }] }, asBigInt(index))
}
if (value instanceof AssetCls) {
const index = (lazyContext.activeGroup.activeTransaction as ApplicationTransaction).apas.indexOf(value)
return new UintNImpl({ name: 'UintN<64>', genericArgs: [{ name: '64' }] }, asBigInt(index))
}
if (value instanceof ApplicationCls) {
const index = (lazyContext.activeGroup.activeTransaction as ApplicationTransaction).apfa.indexOf(value)
return new UintNImpl({ name: 'UintN<64>', genericArgs: [{ name: '64' }] }, asBigInt(index))
}
if (typeof value === 'boolean') {
return new BoolImpl({ name: 'Bool' }, value)
}
if (value instanceof internal.primitives.Uint64Cls || typeof value === 'number') {
return new UintNImpl({ name: 'UintN<64>', genericArgs: [{ name: '64' }] }, asBigInt(value))
}
if (value instanceof internal.primitives.BigUintCls) {
return new UintNImpl({ name: 'UintN<512>', genericArgs: [{ name: '512' }] }, value.asBigInt())
}
if (typeof value === 'bigint') {
return new UintNImpl({ name: 'UintN<512>', genericArgs: [{ name: '512' }] }, value)
}
if (value instanceof internal.primitives.BytesCls) {
return new DynamicBytesImpl(
{ name: 'DynamicBytes', genericArgs: { elementType: { name: 'Byte', genericArgs: [{ name: '8' }] } } },
value.asAlgoTs(),
)
}
if (typeof value === 'string') {
return new StrImpl({ name: 'Str' }, value)
}
if (Array.isArray(value)) {
const result: ARC4Encoded[] = value.reduce((acc: ARC4Encoded[], cur: DeliberateAny) => {
return acc.concat(getArc4Encoded(cur))
}, [])
const genericArgs: TypeInfo[] = result.map((x) => (x as DeliberateAny).typeInfo)
const typeInfo = { name: `Tuple<[${genericArgs.map((x) => x.name).join(',')}]>`, genericArgs }
return new TupleImpl(typeInfo, ...(result as []))
}
if (typeof value === 'object') {
const result = Object.values(value).reduce((acc: ARC4Encoded[], cur: DeliberateAny) => {
return acc.concat(getArc4Encoded(cur))
}, [])
const genericArgs: TypeInfo[] = result.map((x) => (x as DeliberateAny).typeInfo)
const typeInfo = { name: 'Struct', genericArgs: Object.fromEntries(Object.keys(value).map((x, i) => [x, genericArgs[i]])) }
return new StructImpl(typeInfo, Object.fromEntries(Object.keys(value).map((x, i) => [x, result[i]])))
}

throw internal.errors.codeError(`Unsupported type for encoding: ${typeof value}`)
}
9 changes: 9 additions & 0 deletions src/impl/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,15 @@ export class ApplicationTransaction extends TransactionBase implements gtxn.Appl
get lastLog() {
return this.#appLogs.at(-1) ?? lazyContext.getApplicationData(this.appId.id).appLogs.at(-1) ?? Bytes()
}
get apat() {
return this.#accounts
}
get apas() {
return this.#assets
}
get apfa() {
return this.#apps
}
appArgs(index: internal.primitives.StubUint64Compat): bytes {
return toBytes(this.#appArgs[asNumber(index)])
}
Expand Down
1 change: 1 addition & 0 deletions src/runtime-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { nameOfType } from './util'

export { attachAbiMetadata } from './abi-metadata'
export * from './impl/encoded-types'
export { decodeArc4Impl, encodeArc4Impl } from './impl/encoded-types'
export { ensureBudgetImpl } from './impl/ensure-budget'
export { TemplateVarImpl } from './impl/template-var'

Expand Down
10 changes: 6 additions & 4 deletions src/test-transformer/node-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,17 +94,19 @@ export const nodeFactory = {
)
},

callStubbedFunction(functionName: string, node: ts.CallExpression, typeInfo?: TypeInfo) {
const infoString = JSON.stringify(typeInfo)
callStubbedFunction(functionName: string, node: ts.CallExpression, ...typeInfos: (TypeInfo | undefined)[]) {
const infoStringArray = typeInfos.length ? typeInfos.map((typeInfo) => JSON.stringify(typeInfo)) : undefined
const updatedPropertyAccessExpression = factory.createPropertyAccessExpression(
factory.createIdentifier('runtimeHelpers'),
`${functionName}Impl`,
)

const typeInfoArgs = infoStringArray
? infoStringArray?.filter((s) => !!s).map((infoString) => factory.createStringLiteral(infoString))
: undefined
return factory.createCallExpression(
updatedPropertyAccessExpression,
node.typeArguments,
[infoString ? factory.createStringLiteral(infoString) : undefined, ...(node.arguments ?? [])].filter((arg) => !!arg),
[...(typeInfoArgs ?? []), ...(node.arguments ?? [])].filter((arg) => !!arg),
)
},
} satisfies Record<string, (...args: DeliberateAny[]) => ts.Node>
11 changes: 10 additions & 1 deletion src/test-transformer/visitors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,13 @@ class ExpressionVisitor {
}
if (ts.isCallExpression(updatedNode)) {
const stubbedFunctionName = tryGetStubbedFunctionName(updatedNode, this.helper)
updatedNode = stubbedFunctionName ? nodeFactory.callStubbedFunction(stubbedFunctionName, updatedNode, info) : updatedNode
const infos = [info]
if (isCallingDecodeArc4(stubbedFunctionName)) {
const targetType = ptypes.ptypeToArc4EncodedType(type, this.helper.sourceLocation(node))
const targetTypeInfo = getGenericTypeInfo(targetType)
infos[0] = targetTypeInfo
}
updatedNode = stubbedFunctionName ? nodeFactory.callStubbedFunction(stubbedFunctionName, updatedNode, ...infos) : updatedNode
}
return isGeneric
? nodeFactory.captureGenericTypeInfo(ts.visitEachChild(updatedNode, this.visit, this.context), JSON.stringify(info))
Expand Down Expand Up @@ -289,6 +295,7 @@ const isGenericType = (type: ptypes.PType): boolean =>
ptypes.StaticArrayType,
ptypes.UFixedNxMType,
ptypes.UintNType,
ptypes.TuplePType,
)

const isArc4EncodedType = (type: ptypes.PType): boolean =>
Expand Down Expand Up @@ -356,3 +363,5 @@ const tryGetStubbedFunctionName = (node: ts.CallExpression, helper: VisitorHelpe
const stubbedFunctionNames = ['interpretAsArc4', 'decodeArc4', 'encodeArc4', 'TemplateVar', 'ensureBudget']
return stubbedFunctionNames.includes(functionName) ? functionName : undefined
}

const isCallingDecodeArc4 = (functionName: string | undefined): boolean => ['decodeArc4', 'encodeArc4'].includes(functionName ?? '')
7 changes: 5 additions & 2 deletions src/value-generators/arc4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ export class Arc4ValueGenerator {
* */
address(): arc4.Address {
const source = new AvmValueGenerator().account()
const result = new AddressImpl({ name: 'StaticArray', genericArgs: { elementType: { name: 'Byte' }, size: { name: '32' } } }, source)
const result = new AddressImpl(
{ name: 'StaticArray', genericArgs: { elementType: { name: 'Byte', genericArgs: [{ name: '8' }] }, size: { name: '32' } } },
source,
)
return result
}

Expand Down Expand Up @@ -93,7 +96,7 @@ export class Arc4ValueGenerator {
* */
dynamicBytes(n: number): arc4.DynamicBytes {
return new DynamicBytesImpl(
{ name: 'DynamicArray', genericArgs: { elementType: { name: 'Byte' } } },
{ name: 'DynamicBytes', genericArgs: { elementType: { name: 'Byte', genericArgs: [{ name: '8' }] } } },
getRandomBytes(n / BITS_IN_BYTE).asAlgoTs(),
)
}
Expand Down
Loading

0 comments on commit a94df7c

Please sign in to comment.