Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: dynamic tuple encoding with boolean values #577

Merged
merged 11 commits into from
Dec 18, 2024
Binary file modified bun.lockb
Binary file not shown.
8 changes: 7 additions & 1 deletion src/lib/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2397,6 +2397,11 @@ export default class Compiler {
}
});

// If the last element is a bool, make sure to add the length
if (consecutiveBools > 0) {
totalLength += Math.ceil(consecutiveBools / 8);
}

return totalLength;
}

Expand Down Expand Up @@ -3515,7 +3520,7 @@ export default class Compiler {
if (consecutiveBools.length > 0) {
this.processBools(consecutiveBools);
if (!isStatic) this.pushVoid(parentNode, 'callsub *process_static_tuple_element');
if (consecutiveBools.length !== elements.length) this.pushVoid(parentNode, 'concat');
if (isStatic && consecutiveBools.length !== elements.length) this.pushVoid(parentNode, 'concat');
}

if (!isStatic) this.pushLines(parentNode, 'pop // pop head offset', 'concat // concat head and tail');
Expand Down Expand Up @@ -7488,6 +7493,7 @@ declare type AssetFreezeTxn = Required<AssetFreezeParams>;

if (this.isDynamicType(tVar.type) || isNumeric(tVar.type)) {
if (program === 'lsig' || (program === 'approval' && !dynamicTemplateWarning)) {
// eslint-disable-next-line no-console
console.warn(
`WARNING: Due to dynamic template variable type for ${tVar.name} (${typeInfoToABIString(
tVar.type
Expand Down
47 changes: 47 additions & 0 deletions tests/abi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -927,4 +927,51 @@ describe('ABI', function () {

expect(await runMethod(appClient, 'postBoolTupleOffset')).toEqual([true, 1n, 2n]);
});

test('nestedArrayInBox', async () => {
const { appClient } = await compileAndCreate('nestedArrayInBox');

expect(await runMethod(appClient, 'nestedArrayInBox')).toEqual([
[[...Buffer.from('abcd')], [...Buffer.from('efgh')]],
1n,
2n,
3n,
false,
]);
});

test('nestedArrayInBoxLast', async () => {
const { appClient } = await compileAndCreate('nestedArrayInBoxLast');

expect(await runMethod(appClient, 'nestedArrayInBoxLast')).toEqual([
1n,
2n,
3n,
false,
[[...Buffer.from('abcd')], [...Buffer.from('efgh')]],
]);
});

test('nestedArrayInBoxWithoutBool', async () => {
const { appClient } = await compileAndCreate('nestedArrayInBoxWithoutBool');

expect(await runMethod(appClient, 'nestedArrayInBoxWithoutBool')).toEqual([
[[...Buffer.from('abcd')], [...Buffer.from('efgh')]],
1n,
2n,
3n,
]);
});

test('nestedArrayAlongsideBoolean', async () => {
const { appClient } = await compileAndCreate('nestedArrayAlongsideBoolean');

expect(await runMethod(appClient, 'nestedArrayAlongsideBoolean')).toEqual([
[[...Buffer.from('abcd')], [...Buffer.from('efgh')]],
1n,
2n,
3n,
false,
]);
});
});
119 changes: 119 additions & 0 deletions tests/common.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-console */
/* eslint-disable func-names */

import fs from 'fs';
Expand Down Expand Up @@ -155,13 +156,15 @@ export async function runMethod({
callType = 'call',
fundAmount = 0,
fee = 1000,
skipEvalTrace = false,
}: {
appClient: ApplicationClient;
method: string;
methodArgs?: algosdk.ABIArgument[];
callType?: 'call' | 'optIn';
fundAmount?: number;
fee?: number;
skipEvalTrace?: boolean;
}) {
const params = {
method,
Expand All @@ -178,7 +181,47 @@ export async function runMethod({
}
return (await appClient[callType](params)).return?.returnValue;
} catch (e) {
if (skipEvalTrace) {
console.warn(e);
throw e;
}
// eslint-disable-next-line no-console
const abiMethod = appClient.getABIMethod(params.method)!;
const { appId } = await appClient.getAppReference();
const atc = new algosdk.AtomicTransactionComposer();

// @ts-expect-error sender is private but we need it
const senderAccount: algosdk.Account = appClient.sender;
atc.addMethodCall({
appID: Number(appId),
method: abiMethod,
methodArgs: params.methodArgs,
sender: senderAccount.addr,
suggestedParams: await algodClient.getTransactionParams().do(),
signer: algosdk.makeBasicAccountTransactionSigner(senderAccount),
});

const execTraceConfig = new algosdk.modelsv2.SimulateTraceConfig({
enable: true,
scratchChange: true,
stackChange: true,
stateChange: true,
});
const simReq = new algosdk.modelsv2.SimulateRequest({
txnGroups: [],
execTraceConfig,
});

const resp = await atc.simulate(algodClient, simReq);

// @ts-expect-error appSpec is private but we need it
const approvalProgramTeal = Buffer.from(appClient.appSpec.source.approval, 'base64').toString();

const trace = resp.simulateResponse.txnGroups[0].txnResults[0].execTrace!.approvalProgramTrace!;
// eslint-disable-next-line no-use-before-define
const fullTrace = await getFullTrace(trace, approvalProgramTeal, algodClient);
// eslint-disable-next-line no-use-before-define
printFullTrace(fullTrace);
console.warn(e);
throw e;
}
Expand All @@ -188,3 +231,79 @@ export function getErrorMessage(algodError: string, sourceInfo: { pc?: number[];
const pc = Number(algodError.match(/(?<=pc=)\d+/)?.[0]);
return sourceInfo.find((s) => s.pc?.includes(pc))?.errorMessage || `unknown error: ${algodError}`;
}

type FullTrace = {
teal: string;
pc: number;
scratchDelta?: { [slot: number]: string | number };
stack: (string | number)[];
}[];

// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function getFullTrace(simTrace: any[], teal: string, algod: algosdk.Algodv2): Promise<FullTrace> {
const result = await algod.compile(teal).sourcemap(true).do();

const srcMap = new algosdk.SourceMap(result.sourcemap);

let stack: (string | number)[] = [];

const fullTrace: FullTrace = [];

simTrace.forEach((t) => {
let newStack: (string | number)[] = [...stack];

if (t.stackPopCount) {
newStack = newStack.slice(0, -t.stackPopCount);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
t.stackAdditions?.forEach((s: any) => {
if (s.bytes) {
newStack.push(`0x${Buffer.from(s.bytes, 'base64').toString('hex')}`);
} else newStack.push(s.uint || 0);
});

const scratchDelta = t.scratchChanges?.map((s) => {
const delta = {};

const value = s.newValue;

if (s.bytes) {
delta[s.slot] = `0x${Buffer.from(value.bytes, 'base64').toString('hex')}`;
} else delta[s.slot] = value.uint || 0;

return delta;
});

fullTrace.push({
teal: teal.split('\n')[srcMap.pcToLine[t.pc as number]],
pc: t.pc,
stack: newStack,
scratchDelta,
});

stack = newStack;
});

return fullTrace;
}

function adjustWidth(line: string, width: number) {
if (line.length > width) {
return `${line.slice(0, width - 3)}...`;
}
if (line.length < width) {
return line.padEnd(width);
}
return line;
}

function printFullTrace(fullTrace: FullTrace, width: number = 30) {
console.warn(`${'TEAL'.padEnd(width)} | PC | STACK`);
console.warn(`${'-'.repeat(width)}-|------|${'-'.repeat(7)}`);
fullTrace.forEach((t) => {
const teal = adjustWidth(t.teal.trim(), width);
const pc = t.pc.toString().padEnd(4);
console.warn(`${teal} | ${pc} | [${t.stack}]`);
});
}
86 changes: 86 additions & 0 deletions tests/contracts/abi.algo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1625,3 +1625,89 @@ class ABITestPostBoolTupleOffset extends Contract {
return retVal;
}
}

type T10 = {
bytes4Array: bytes<4>[];
u64a: uint64;
u64b: uint64;
u64c: uint64;
boolValue: boolean;
};

class ABITestNestedArrayInBox extends Contract {
bMap = BoxMap<bytes, T10>();

nestedArrayInBox(): T10 {
this.bMap('bMap').value = {
bytes4Array: ['abcd' as bytes<4>, 'efgh' as bytes<4>],
u64a: 1,
u64b: 2,
u64c: 3,
boolValue: false,
};

return this.bMap('bMap').value;
}
}

type T11 = {
u64a: uint64;
u64b: uint64;
u64c: uint64;
boolValue: boolean;
bytes4Array: bytes<4>[];
};

class ABITestNestedArrayInBoxLast extends Contract {
bMap = BoxMap<bytes, T11>();

nestedArrayInBoxLast(): T11 {
this.bMap('bMap').value = {
bytes4Array: ['abcd' as bytes<4>, 'efgh' as bytes<4>],
u64a: 1,
u64b: 2,
u64c: 3,
boolValue: false,
};

return this.bMap('bMap').value;
}
}

type T12 = {
bytes4Array: bytes<4>[];
u64a: uint64;
u64b: uint64;
u64c: uint64;
};

class ABITestNestedArrayInBoxWithoutBool extends Contract {
bMap = BoxMap<bytes, T12>();

nestedArrayInBoxWithoutBool(): T12 {
this.bMap('bMap').value = {
bytes4Array: ['abcd' as bytes<4>, 'efgh' as bytes<4>],
u64a: 1,
u64b: 2,
u64c: 3,
};

return this.bMap('bMap').value;
}
}

class ABITestNestedArrayAlongsideBoolean extends Contract {
gMap = GlobalStateMap<bytes, T10>({ maxKeys: 1 });

nestedArrayAlongsideBoolean(): T10 {
const o: T10 = {
bytes4Array: ['abcd' as bytes<4>, 'efgh' as bytes<4>],
u64a: 1,
u64b: 2,
u64c: 3,
boolValue: false,
};

return o;
}
}
Loading
Loading