Skip to content

Commit 9412514

Browse files
committed
fix: ARC4 contracts should log the return value of an abi method, encoded as bytes and prefixed with a specific byte pattern detailed by arc4
1 parent fdbc13b commit 9412514

File tree

6 files changed

+58
-35
lines changed

6 files changed

+58
-35
lines changed

src/abi-metadata.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Contract } from '@algorandfoundation/algorand-typescript'
1+
import { BaseContract, Contract } from '@algorandfoundation/algorand-typescript'
22
import { AbiMethodConfig, BareMethodConfig, CreateOptions, OnCompleteActionStr } from '@algorandfoundation/algorand-typescript/arc4'
33
import { DeliberateAny } from './typescript-helpers'
44

@@ -50,7 +50,7 @@ export const hasAbiMetadata = <T extends Contract>(contract: T): boolean => {
5050
)
5151
}
5252

53-
export const getAbiMetadata = <T extends Contract>(contract: T, methodName: string): AbiMetadata => {
53+
export const getAbiMetadata = <T extends BaseContract>(contract: T, methodName: string): AbiMetadata => {
5454
const contractClass = contract.constructor as { new (): T }
5555
const s = Object.getOwnPropertySymbols(contractClass).find((s) => s.toString() === AbiMetaSymbol.toString())
5656
const metadatas: Record<string, AbiMetadata> = (

src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,5 @@ export const LOGIC_DATA_PREFIX = Bytes('ProgData')
3535

3636
//number: minimum transaction fee
3737
export const MIN_TXN_FEE = 1000
38+
39+
export const ABI_RETURN_VALUE_LOG_PREFIX = Bytes.fromHex('151F7C75')

src/decode-logs.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
import { internal } from '@algorandfoundation/algorand-typescript'
1+
import { bytes, op } from '@algorandfoundation/algorand-typescript'
22

33
export type LogDecoding = 'i' | 's' | 'b'
44

55
export type DecodedLog<T extends LogDecoding> = T extends 'i' ? bigint : T extends 's' ? string : Uint8Array
66
export type DecodedLogs<T extends [...LogDecoding[]]> = {
77
[Index in keyof T]: DecodedLog<T[Index]>
88
} & { length: T['length'] }
9-
export function decodeLogs<const T extends [...LogDecoding[]]>(logs: Uint8Array[], decoding: T): DecodedLogs<T> {
9+
export function decodeLogs<const T extends [...LogDecoding[]]>(logs: bytes[], decoding: T): DecodedLogs<T> {
1010
return logs.map((log, i) => {
1111
switch (decoding[i]) {
1212
case 'i':
13-
return internal.encodingUtil.uint8ArrayToBigInt(log)
13+
return op.btoi(log)
1414
case 's':
15-
return internal.encodingUtil.uint8ArrayToUtf8(log)
15+
return log.toString()
1616
default:
1717
return log
1818
}

src/impl/transactions.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
uint64,
1212
Uint64,
1313
} from '@algorandfoundation/algorand-typescript'
14-
import { MAX_ITEMS_IN_LOG } from '../constants'
14+
import { ABI_RETURN_VALUE_LOG_PREFIX, MAX_ITEMS_IN_LOG } from '../constants'
1515
import { lazyContext } from '../context-helpers/internal-context'
1616
import { Mutable, ObjectKeys } from '../typescript-helpers'
1717
import { asBytes, asMaybeBytesCls, asMaybeUint64Cls, asNumber, asUint64Cls, combineIntoMaxBytePages, getRandomBytes } from '../util'
@@ -334,6 +334,10 @@ export class ApplicationTransaction extends TransactionBase implements gtxn.Appl
334334
}
335335
this.#appLogs.push(asBytes(value))
336336
}
337+
/* @internal */
338+
logArc4ReturnValue(value: internal.primitives.StubBytesCompat): void {
339+
this.appendLog(ABI_RETURN_VALUE_LOG_PREFIX.concat(asBytes(value)))
340+
}
337341
}
338342

339343
export type Transaction =

src/subcontexts/contract-context.ts

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Account, Application, Asset, BaseContract, Bytes, bytes, Contract, internal } from '@algorandfoundation/algorand-typescript'
2-
import { getAbiMetadata, hasAbiMetadata } from '../abi-metadata'
2+
import { getAbiMetadata } from '../abi-metadata'
33
import { lazyContext } from '../context-helpers/internal-context'
44
import { AccountCls } from '../impl/account'
55
import { ApplicationCls } from '../impl/application'
@@ -96,11 +96,24 @@ function isTransaction(obj: unknown): obj is Transaction {
9696

9797
export class ContractContext {
9898
create<T extends BaseContract>(type: IConstructor<T>, ...args: DeliberateAny[]): T {
99-
const proxy = new Proxy(type, this.getContractProxyHandler<T>())
99+
Object.getPrototypeOf(type)
100+
const proxy = new Proxy(type, this.getContractProxyHandler<T>(this.isArc4(type)))
100101
return new proxy(...args)
101102
}
102103

103-
private getContractProxyHandler<T extends BaseContract>(): ProxyHandler<IConstructor<T>> {
104+
private isArc4<T extends BaseContract>(type: IConstructor<T>): boolean {
105+
const proto = Object.getPrototypeOf(type)
106+
if (proto === BaseContract) {
107+
return false
108+
} else if (proto === Contract) {
109+
return true
110+
} else if (proto === Object) {
111+
throw new Error('Cannot create a contract for class as it does not extend Contract or BaseContract')
112+
}
113+
return this.isArc4(proto)
114+
}
115+
116+
private getContractProxyHandler<T extends BaseContract>(isArc4: boolean): ProxyHandler<IConstructor<T>> {
104117
const onConstructed = (instance: BaseContract) => {
105118
const states = extractStates(instance)
106119

@@ -111,31 +124,35 @@ export class ContractContext {
111124
}
112125
return {
113126
construct(target, args) {
114-
let isArc4 = false
115127
const instance = new Proxy(new target(...args), {
116128
get(target, prop, receiver) {
117129
const orig = Reflect.get(target, prop, receiver)
118-
if (isArc4 || prop === 'approvalProgram' || prop === 'clearStateProgram') {
130+
const isProgramMethod = prop === 'approvalProgram' || prop === 'clearStateProgram'
131+
if (isArc4 || isProgramMethod) {
119132
return (...args: DeliberateAny[]): DeliberateAny => {
120133
const app = lazyContext.ledger.getApplicationForContract(receiver)
121134
const { transactions, ...appCallArgs } = extractArraysFromArgs(args)
122-
const abiMetadata = getAbiMetadata(receiver, prop as string)
123-
const txns = [
124-
...(transactions ?? []),
125-
lazyContext.any.txn.applicationCall({
126-
appId: app,
127-
...appCallArgs,
128-
onCompletion: (abiMetadata?.allowActions ?? [])[0],
129-
}),
130-
]
131-
return lazyContext.txn.ensureScope(txns).execute(() => (orig as DeliberateAny).apply(target, args))
135+
const abiMetadata = getAbiMetadata(target, prop as string)
136+
const appTxn = lazyContext.any.txn.applicationCall({
137+
appId: app,
138+
...appCallArgs,
139+
// TODO: This needs to be specifiable by the test code
140+
onCompletion: (abiMetadata?.allowActions ?? [])[0],
141+
})
142+
const txns = [...(transactions ?? []), appTxn]
143+
return lazyContext.txn.ensureScope(txns).execute(() => {
144+
const returnValue = (orig as DeliberateAny).apply(target, args)
145+
if (isArc4) {
146+
appTxn.logArc4ReturnValue(returnValue)
147+
}
148+
return returnValue
149+
})
132150
}
133151
}
134152
return orig
135153
},
136154
})
137155
onConstructed(instance)
138-
isArc4 = hasAbiMetadata(instance as Contract)
139156

140157
return instance
141158
},

src/subcontexts/transaction-context.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@ import algosdk from 'algosdk'
33
import { lazyContext } from '../context-helpers/internal-context'
44
import { DecodedLogs, decodeLogs, LogDecoding } from '../decode-logs'
55
import { testInvariant } from '../errors'
6+
import {
7+
ApplicationInnerTxn,
8+
AssetConfigInnerTxn,
9+
AssetFreezeInnerTxn,
10+
AssetTransferInnerTxn,
11+
createInnerTxn,
12+
KeyRegistrationInnerTxn,
13+
PaymentInnerTxn,
14+
} from '../impl/inner-transactions'
15+
import { InnerTxn, InnerTxnFields } from '../impl/itxn'
616
import {
717
AllTransactionFields,
818
ApplicationTransaction,
@@ -14,16 +24,6 @@ import {
1424
Transaction,
1525
} from '../impl/transactions'
1626
import { asBigInt, asNumber, asUint64 } from '../util'
17-
import { InnerTxn, InnerTxnFields } from '../impl/itxn'
18-
import {
19-
ApplicationInnerTxn,
20-
AssetConfigInnerTxn,
21-
AssetFreezeInnerTxn,
22-
AssetTransferInnerTxn,
23-
createInnerTxn,
24-
KeyRegistrationInnerTxn,
25-
PaymentInnerTxn,
26-
} from '../impl/inner-transactions'
2727

2828
function ScopeGenerator(dispose: () => void) {
2929
function* internal() {
@@ -119,8 +119,8 @@ export class TransactionContext {
119119
} else {
120120
logs = lazyContext.getApplicationData(appId).appLogs
121121
}
122-
const rawLogs = logs.map((l) => internal.primitives.toExternalValue(l))
123-
return decodeLogs(rawLogs, decoding)
122+
123+
return decodeLogs(logs, decoding)
124124
}
125125
}
126126

0 commit comments

Comments
 (0)