Skip to content

Commit

Permalink
fix: nested implicit generics get encoded properly (#1107)
Browse files Browse the repository at this point in the history
* fix: nested implicit generics get encoded properly
*refactor: move to ResolvedAbiType handling generic type resolution and getting of signature
  • Loading branch information
nedsalk committed Aug 8, 2023
1 parent c73afd6 commit 4a64634
Show file tree
Hide file tree
Showing 8 changed files with 627 additions and 430 deletions.
5 changes: 5 additions & 0 deletions .changeset/tasty-taxis-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/abi-coder": patch
---

encoding bugfix
161 changes: 39 additions & 122 deletions packages/abi-coder/src/abi-coder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,111 +24,38 @@ import {
tupleRegEx,
OPTION_CODER_TYPE,
VEC_CODER_TYPE,
genericRegEx,
} from './constants';
import type { JsonAbi, JsonAbiArgument, JsonAbiType } from './json-abi';
import type { JsonAbi, JsonAbiArgument } from './json-abi';
import { ResolvedAbiType } from './resolved-abi-type';
import { findOrThrow } from './utilities';

const logger = new Logger(versions.FUELS);

export abstract class AbiCoder {
private static getImplicitGenericTypeParameters(
abi: JsonAbi,
abiType: JsonAbiType,
implicitGenericParametersParam: number[] | undefined = undefined
): number[] {
const isExplicitGeneric = abiType.typeParameters !== null;
if (isExplicitGeneric || abiType.components === null) return [];

const implicitGenericParameters: number[] = implicitGenericParametersParam ?? [];

abiType.components.forEach((component) => {
const componentType = findOrThrow(abi.types, (t) => t.typeId === component.type);

const isGeneric = genericRegEx.test(componentType.type);

if (isGeneric) {
implicitGenericParameters.push(componentType.typeId);
return;
}

this.getImplicitGenericTypeParameters(abi, componentType, implicitGenericParameters);
});
static getCoder(abi: JsonAbi, argument: JsonAbiArgument): Coder {
const resolvedAbiType = new ResolvedAbiType(abi, argument);

return implicitGenericParameters;
return AbiCoder.getCoderImpl(resolvedAbiType);
}

private static resolveGenericArgs(
abi: JsonAbi,
args: readonly JsonAbiArgument[],
typeParametersAndArgsMap: Record<number, JsonAbiArgument> | undefined
): readonly JsonAbiArgument[] {
if (typeParametersAndArgsMap === undefined) return args;

return args.map((arg) => {
if (typeParametersAndArgsMap[arg.type] !== undefined) {
return {
...typeParametersAndArgsMap[arg.type],
name: arg.name,
};
}

if (arg.typeArguments !== null) {
return {
...structuredClone(arg),
typeArguments: this.resolveGenericArgs(abi, arg.typeArguments, typeParametersAndArgsMap),
};
}

const abiType = findOrThrow(abi.types, (x) => x.typeId === arg.type);
if (abiType.components === null) return arg;
const implicitGenericTypeParameters = this.getImplicitGenericTypeParameters(abi, abiType);
if (implicitGenericTypeParameters.length === 0) return arg;

return {
...structuredClone(arg),
typeArguments: implicitGenericTypeParameters.map((tp) => typeParametersAndArgsMap[tp]),
};
});
static encode(abi: JsonAbi, argument: JsonAbiArgument, value: InputValue) {
return this.getCoder(abi, argument).encode(value);
}

static resolveGenericComponents(abi: JsonAbi, arg: JsonAbiArgument): readonly JsonAbiArgument[] {
let abiType = findOrThrow(abi.types, (t) => t.typeId === arg.type);

const implicitGenericTypeParameters = this.getImplicitGenericTypeParameters(abi, abiType);
if (implicitGenericTypeParameters.length > 0) {
abiType = { ...structuredClone(abiType), typeParameters: implicitGenericTypeParameters };
}

const typeParametersAndArgsMap = abiType.typeParameters?.reduce(
(obj, typeParameter, typeParameterIndex) => {
const o: Record<number, JsonAbiArgument> = { ...obj };
o[typeParameter] = structuredClone(arg.typeArguments?.[typeParameterIndex]);
return o;
},
{} as Record<number, JsonAbiArgument>
);

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.resolveGenericArgs(abi, abiType.components!, typeParametersAndArgsMap);
static decode(
abi: JsonAbi,
argument: JsonAbiArgument,
data: Uint8Array,
offset: number
): [DecodedValue | undefined, number] {
return this.getCoder(abi, argument).decode(data, offset) as [DecodedValue | undefined, number];
}

static getCoder(abi: JsonAbi, argument: JsonAbiArgument): Coder {
const abiType = findOrThrow(
abi.types,
(t) => t.typeId === argument.type,
() =>
logger.throwArgumentError('Type does not exist in the provided abi', 'type', {
argument,
abi,
})
);

switch (abiType.type) {
private static getCoderImpl(resolvedAbiType: ResolvedAbiType): Coder {
switch (resolvedAbiType.type) {
case 'u8':
case 'u16':
case 'u32':
return new NumberCoder(abiType.type);
return new NumberCoder(resolvedAbiType.type);
case 'u64':
case 'raw untyped ptr':
return new U64Coder();
Expand All @@ -144,88 +71,78 @@ export abstract class AbiCoder {
break;
}

const stringMatch = stringRegEx.exec(abiType.type)?.groups;
const stringMatch = stringRegEx.exec(resolvedAbiType.type)?.groups;
if (stringMatch) {
const length = parseInt(stringMatch.length, 10);

return new StringCoder(length);
}

if (['raw untyped slice'].includes(abiType.type)) {
if (['raw untyped slice'].includes(resolvedAbiType.type)) {
const length = 0;
const itemCoder = new U64Coder();
return new ArrayCoder(itemCoder, length);
}

// ABI types underneath MUST have components by definition
const components = this.resolveGenericComponents(abi, argument);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const components = resolvedAbiType.components!;

const arrayMatch = arrayRegEx.exec(abiType.type)?.groups;
const arrayMatch = arrayRegEx.exec(resolvedAbiType.type)?.groups;
if (arrayMatch) {
const length = parseInt(arrayMatch.length, 10);
const arg = components[0];
if (!arg) {
throw new Error('Expected array type to have an item component');
}

const arrayElementCoder = this.getCoder(abi, arg);
const arrayElementCoder = AbiCoder.getCoderImpl(arg);
return new ArrayCoder(arrayElementCoder, length);
}

if (abiType.type === VEC_CODER_TYPE) {
const typeArgument = components.find((x) => x.name === 'buf')?.typeArguments?.[0];
if (!typeArgument) {
if (resolvedAbiType.type === VEC_CODER_TYPE) {
const arg = findOrThrow(components, (c) => c.name === 'buf').originalTypeArguments?.[0];
if (!arg) {
throw new Error('Expected Vec type to have a type argument');
}
const itemCoder = this.getCoder(abi, typeArgument);
const argType = new ResolvedAbiType(resolvedAbiType.abi, arg);

const itemCoder = AbiCoder.getCoderImpl(argType);
return new VecCoder(itemCoder);
}

const structMatch = structRegEx.exec(abiType.type)?.groups;
const structMatch = structRegEx.exec(resolvedAbiType.type)?.groups;
if (structMatch) {
const coders = this.getCoders(components, abi);
const coders = AbiCoder.getCoders(components);
return new StructCoder(structMatch.name, coders);
}

const enumMatch = enumRegEx.exec(abiType.type)?.groups;
const enumMatch = enumRegEx.exec(resolvedAbiType.type)?.groups;
if (enumMatch) {
const coders = this.getCoders(components, abi);
const coders = AbiCoder.getCoders(components);

const isOptionEnum = abiType.type === OPTION_CODER_TYPE;
const isOptionEnum = resolvedAbiType.type === OPTION_CODER_TYPE;
if (isOptionEnum) {
return new OptionCoder(enumMatch.name, coders);
}
return new EnumCoder(enumMatch.name, coders);
}

const tupleMatch = tupleRegEx.exec(abiType.type)?.groups;
const tupleMatch = tupleRegEx.exec(resolvedAbiType.type)?.groups;
if (tupleMatch) {
const coders = components.map((component) => this.getCoder(abi, component));
const coders = components.map((component) => AbiCoder.getCoderImpl(component));
return new TupleCoder(coders);
}

return logger.throwArgumentError('Coder not found', 'type', { abiType, abi });
return logger.throwArgumentError('Coder not found', 'abiType', { abiType: resolvedAbiType });
}

private static getCoders(components: readonly JsonAbiArgument[], abi: JsonAbi) {
private static getCoders(components: readonly ResolvedAbiType[]) {
return components.reduce((obj, component) => {
const o: Record<string, Coder> = obj;

o[component.name] = this.getCoder(abi, component);
o[component.name] = AbiCoder.getCoderImpl(component);
return o;
}, {});
}

static encode(abi: JsonAbi, argument: JsonAbiArgument, value: InputValue) {
return this.getCoder(abi, argument).encode(value);
}

static decode(
abi: JsonAbi,
arg: JsonAbiArgument,
data: Uint8Array,
offset: number
): [DecodedValue | undefined, number] {
return this.getCoder(abi, arg).decode(data, offset) as [DecodedValue | undefined, number];
}
}
Loading

0 comments on commit 4a64634

Please sign in to comment.