Skip to content

Commit

Permalink
feat: support latest ARC56 source mapping (including cblock offset)
Browse files Browse the repository at this point in the history
  • Loading branch information
joe-p committed Oct 22, 2024
1 parent fad7040 commit 4a7b1d6
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 16 deletions.
22 changes: 14 additions & 8 deletions src/types/app-arc56.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,9 +287,9 @@ export interface Arc56Contract {
/** Information about the TEAL programs */
sourceInfo?: {
/** Approval program information */
approval: SourceInfo[]
approval: ProgramSourceInfo
/** Clear program information */
clear: SourceInfo[]
clear: ProgramSourceInfo
}
/** The pre-compiled TEAL that may contain template variables. MUST be omitted if included as part of ARC23 */
source?: {
Expand Down Expand Up @@ -483,12 +483,18 @@ export interface StorageMap {
}

export interface SourceInfo {
/** The line of pre-compiled TEAL */
teal?: number
/** The program counter offset(s) that correspond to this line of TEAL */
pc?: Array<number>
pc: Array<number>
/** A human-readable string that describes the error when the program fails at this given line of TEAL */
errorMessage?: string
/** The line of the dissasembled TEAL this line of pre-compiled TEAL corresponds to */
disassembledTeal?: number
errorMessage: string
}

export interface ProgramSourceInfo {
/** The source information for the program */
sourceInfo: SourceInfo[]
/** How the program counter offset is calculated
* - none: The pc values in sourceInfo are not offset
* - cblocks: The pc values in sourceInfo are offset by the PC of the first op following the last cblock at the top of the program
*/
pcOffsetMethod: 'none' | 'cblocks'
}
76 changes: 68 additions & 8 deletions src/types/app-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,46 @@ export type ResolveAppClientByCreatorAndName = Expand<
/** Resolve an app client instance by looking up the current network. */
export type ResolveAppClientByNetwork = Expand<Omit<AppClientParams, 'appId'>>

const BYTE_CBLOCK = 38
const INT_CBLOCK = 32

function getConstantBlockOffsets(program: Uint8Array) {
const bytes = [...program]

const programSize = bytes.length
bytes.shift() // remove version
const offsets: { bytecblockOffset?: number; intcblockOffset?: number; cblocksOffset: number } = { cblocksOffset: 0 }

while (bytes.length > 0) {
const byte = bytes.shift()!

if (byte === BYTE_CBLOCK || byte === INT_CBLOCK) {
const isBytecblock = byte === BYTE_CBLOCK
const valuesRemaining = bytes.shift()!

for (let i = 0; i < valuesRemaining; i++) {
if (isBytecblock) {
// byte is the length of the next element
bytes.splice(0, bytes.shift()!)
} else {
// intcblock is a uvarint, so we need to keep reading until we find the end (MSB is not set)
while ((bytes.shift()! & 0x80) !== 0) {}

Check failure on line 432 in src/types/app-client.ts

View workflow job for this annotation

GitHub Actions / pull_request / node-ci

Empty block statement
}
}

offsets[isBytecblock ? 'bytecblockOffset' : 'intcblockOffset'] = programSize - bytes.length - 1

if (bytes[0] !== BYTE_CBLOCK && bytes[0] !== INT_CBLOCK) {
// if the next opcode isn't a constant block, we're done
break
}
}
}

offsets.cblocksOffset = Math.max(...Object.values(offsets))
return offsets
}

/** ARC-56/ARC-32 application client that allows you to manage calls and
* state for a specific deployed instance of an app (with a known app ID). */
export class AppClient {
Expand Down Expand Up @@ -710,11 +750,21 @@ export class AppClient {
* @param isClearStateProgram Whether or not the code was running the clear state program (defaults to approval program)
* @returns The new error, or if there was no logic error or source map then the wrapped error with source details
*/
public exposeLogicError(e: Error, isClearStateProgram?: boolean): Error {
public async exposeLogicError(e: Error, isClearStateProgram?: boolean): Promise<Error> {
const pcOffsetMethod = this._appSpec.sourceInfo?.[isClearStateProgram ? 'clear' : 'approval']?.pcOffsetMethod

let program: Uint8Array | undefined
if (pcOffsetMethod === 'cblocks') {
// TODO: Cache this if we deploy the app and it's not updateable
const appInfo = await this._algorand.app.getById(this.appId)
program = isClearStateProgram ? appInfo.clearStateProgram : appInfo.approvalProgram
}

return AppClient.exposeLogicError(e, this._appSpec, {
isClearStateProgram,
approvalSourceMap: this._approvalSourceMap,
clearSourceMap: this._clearSourceMap,
program,
})
}

Expand Down Expand Up @@ -809,16 +859,26 @@ export class AppClient {
/** Whether or not the code was running the clear state program (defaults to approval program) */ isClearStateProgram?: boolean
/** Approval program source map */ approvalSourceMap?: SourceMap
/** Clear state program source map */ clearSourceMap?: SourceMap
/** program bytes */ program?: Uint8Array
},
): Error {
const { isClearStateProgram, approvalSourceMap, clearSourceMap } = details
const { isClearStateProgram, approvalSourceMap, clearSourceMap, program } = details
if ((!isClearStateProgram && approvalSourceMap == undefined) || (isClearStateProgram && clearSourceMap == undefined)) return e

const errorDetails = LogicError.parseLogicError(e)

const errorMessage = (isClearStateProgram ? appSpec.sourceInfo?.clear : appSpec.sourceInfo?.approval)?.find((s) =>
s?.pc?.includes(errorDetails?.pc ?? -1),
)?.errorMessage
const programSourceInfo = isClearStateProgram ? appSpec.sourceInfo?.clear : appSpec.sourceInfo?.approval

let errorMessage: string | undefined

if (programSourceInfo?.pcOffsetMethod === 'none') {
errorMessage = programSourceInfo.sourceInfo.find((s) => s.pc.includes(errorDetails?.pc ?? -1))?.errorMessage
} else if (programSourceInfo?.pcOffsetMethod === 'cblocks' && program !== undefined && errorDetails?.pc !== undefined) {
const { cblocksOffset } = getConstantBlockOffsets(program)
const offsetPc = errorDetails.pc - cblocksOffset

errorMessage = programSourceInfo.sourceInfo.find((s) => s.pc.includes(offsetPc))?.errorMessage
}

if (errorDetails !== undefined && appSpec.source)
e = new LogicError(
Expand Down Expand Up @@ -1303,7 +1363,7 @@ export class AppClient {
try {
return await call()
} catch (e) {
throw this.exposeLogicError(e as Error)
throw await this.exposeLogicError(e as Error)
}
}

Expand Down Expand Up @@ -1779,7 +1839,7 @@ export class ApplicationClient {

return { ...result, ...({ compiledApproval: approvalCompiled, compiledClear: clearCompiled } as AppCompilationResult) }
} catch (e) {
throw this.exposeLogicError(e as Error)
throw await this.exposeLogicError(e as Error)
}
}

Expand Down Expand Up @@ -1820,7 +1880,7 @@ export class ApplicationClient {

return { ...result, ...({ compiledApproval: approvalCompiled, compiledClear: clearCompiled } as AppCompilationResult) }
} catch (e) {
throw this.exposeLogicError(e as Error)
throw await this.exposeLogicError(e as Error)
}
}

Expand Down

0 comments on commit 4a7b1d6

Please sign in to comment.