Skip to content

Commit db88e98

Browse files
[enums] Add partial support for enum types (#649)
Add some support for enum types
1 parent eb8fc97 commit db88e98

File tree

4 files changed

+151
-41
lines changed

4 files changed

+151
-41
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. T
99
- Update simulation for MultiKeyAccount to use signatures of the same type as the corresponding public key.
1010
- Add `truncateAddress` helper function to truncate an address at the middle with an ellipsis.
1111
- Fix scriptComposer addBatchedCalls more typeArguments error
12+
- Add support for skipping struct type tag validation.
13+
- Add support for known enum structs: DelegationKey and RateLimiter.
14+
- Deprecated `fetchMoveFunctionAbi` and `convertCallArgument`
1215

1316
# 1.35.0 (2025-02-11)
1417

src/transactions/scriptComposer/index.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { ScriptComposerWasm } from "@aptos-labs/script-composer-pack";
55
import { AptosApiType, getFunctionParts } from "../../utils";
66
import { AptosConfig } from "../../api/aptosConfig";
77
import { InputBatchedFunctionData } from "../types";
8-
import { fetchMoveFunctionAbi, standardizeTypeTags } from "../transactionBuilder";
8+
import { standardizeTypeTags } from "../transactionBuilder";
99
import { CallArgument } from "../../types";
10-
import { convertCallArgument } from "../transactionBuilder/remoteAbi";
10+
import { convertArgument, fetchModuleAbi } from "../transactionBuilder/remoteAbi";
1111

1212
/**
1313
* A wrapper class around TransactionComposer, which is a WASM library compiled
@@ -63,16 +63,29 @@ export class AptosScriptComposer {
6363
}
6464
}
6565
const typeArguments = standardizeTypeTags(input.typeArguments);
66-
const functionAbi = await fetchMoveFunctionAbi(moduleAddress, moduleName, functionName, this.config);
66+
const moduleAbi = await fetchModuleAbi(moduleAddress, moduleName, this.config);
67+
if (!moduleAbi) {
68+
throw new Error(`Could not find module ABI for '${moduleAddress}::${moduleName}'`);
69+
}
70+
6771
// Check the type argument count against the ABI
68-
if (typeArguments.length !== functionAbi.typeParameters.length) {
72+
const functionAbi = moduleAbi?.exposed_functions.find((func) => func.name === functionName);
73+
if (!functionAbi) {
74+
throw new Error(`Could not find function ABI for '${moduleAddress}::${moduleName}::${functionName}'`);
75+
}
76+
77+
if (typeArguments.length !== functionAbi.generic_type_params.length) {
6978
throw new Error(
70-
`Type argument count mismatch, expected ${functionAbi.typeParameters.length}, received ${typeArguments.length}`,
79+
`Type argument count mismatch, expected ${functionAbi?.generic_type_params.length}, received ${typeArguments.length}`,
7180
);
7281
}
7382

7483
const functionArguments: CallArgument[] = input.functionArguments.map((arg, i) =>
75-
convertCallArgument(arg, functionName, functionAbi, i, typeArguments),
84+
arg instanceof CallArgument
85+
? arg
86+
: CallArgument.newBytes(
87+
convertArgument(functionName, moduleAbi, arg, i, typeArguments, { allowUnknownStructs: true }).bcsToBytes(),
88+
),
7689
);
7790

7891
return this.builder.add_batched_call(

src/transactions/transactionBuilder/remoteAbi.ts

Lines changed: 107 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ import {
2323
FunctionABI,
2424
TypeArgument,
2525
} from "../types";
26-
import { Bool, MoveOption, MoveString, MoveVector, U128, U16, U256, U32, U64, U8 } from "../../bcs";
26+
import { Bool, FixedBytes, MoveOption, MoveString, MoveVector, U128, U16, U256, U32, U64, U8 } from "../../bcs";
2727
import { AccountAddress } from "../../core";
28-
import { getModule } from "../../internal/utils";
28+
import { getModule } from "../../internal/account";
2929
import {
3030
findFirstNonSignerArg,
3131
isBcsAddress,
@@ -45,7 +45,7 @@ import {
4545
throwTypeMismatch,
4646
convertNumber,
4747
} from "./helpers";
48-
import { CallArgument, MoveFunction } from "../../types";
48+
import { CallArgument, MoveFunction, MoveModule } from "../../types";
4949

5050
const TEXT_ENCODER = new TextEncoder();
5151

@@ -69,6 +69,24 @@ export function standardizeTypeTags(typeArguments?: Array<TypeArgument>): Array<
6969
);
7070
}
7171

72+
/**
73+
* Fetches the ABI of a specified module from the on-chain module ABI.
74+
*
75+
* @param moduleAddress - The address of the module from which to fetch the ABI.
76+
* @param moduleName - The name of the module containing the ABI.
77+
* @param aptosConfig - The configuration settings for Aptos.
78+
* @group Implementation
79+
* @category Transactions
80+
*/
81+
export async function fetchModuleAbi(
82+
moduleAddress: string,
83+
moduleName: string,
84+
aptosConfig: AptosConfig,
85+
): Promise<MoveModule | undefined> {
86+
const moduleBytecode = await getModule({ aptosConfig, accountAddress: moduleAddress, moduleName });
87+
return moduleBytecode.abi;
88+
}
89+
7290
/**
7391
* Fetches the ABI of a specified function from the on-chain module ABI. This function allows you to access the details of a
7492
* specific function within a module.
@@ -86,22 +104,13 @@ export async function fetchFunctionAbi(
86104
functionName: string,
87105
aptosConfig: AptosConfig,
88106
): Promise<MoveFunction | undefined> {
89-
// This fetch from the API is currently cached
90-
const module = await getModule({ aptosConfig, accountAddress: moduleAddress, moduleName });
91-
92-
if (module.abi) {
93-
return module.abi.exposed_functions.find((func) => func.name === functionName);
94-
}
95-
96-
return undefined;
107+
const moduleAbi = await fetchModuleAbi(moduleAddress, moduleName, aptosConfig);
108+
if (!moduleAbi) throw new Error(`Could not find module ABI for '${moduleAddress}::${moduleName}'`);
109+
return moduleAbi.exposed_functions.find((func) => func.name === functionName);
97110
}
98111

99112
/**
100-
* Fetches a function ABI from the on-chain module ABI. It doesn't validate whether it's a view or entry function.
101-
* @param moduleAddress
102-
* @param moduleName
103-
* @param functionName
104-
* @param aptosConfig
113+
* @deprecated Use `fetchFunctionAbi` instead and manually parse the type tags.
105114
*/
106115
export async function fetchMoveFunctionAbi(
107116
moduleAddress: string,
@@ -220,15 +229,14 @@ export async function fetchViewFunctionAbi(
220229
}
221230

222231
/**
223-
* Converts a entry function argument into CallArgument, if necessary.
224-
* This function checks the provided argument against the expected parameter type and converts it accordingly.
232+
* @deprecated Handle this inline
225233
*
226-
* @param functionName - The name of the function for which the argument is being converted.
227-
* @param functionAbi - The ABI (Application Binary Interface) of the function, which defines its parameters.
228-
* @param argument - The argument to be converted, which can be of various types. If the argument is already
229-
* CallArgument returned from TransactionComposer it would be returned immediately.
230-
* @param position - The index of the argument in the function's parameter list.
231-
* @param genericTypeParams - An array of type tags for any generic type parameters.
234+
* @example
235+
* ```typescript
236+
* const callArgument = argument instanceof CallArgument ? argument : CallArgument.newBytes(
237+
* convertArgument(functionName, functionAbi, argument, position, genericTypeParams).bcsToBytes()
238+
* );
239+
* ```
232240
*/
233241
export function convertCallArgument(
234242
argument: CallArgument | EntryFunctionArgumentTypes | SimpleEntryFunctionArgumentTypes,
@@ -250,27 +258,54 @@ export function convertCallArgument(
250258
* This function checks the provided argument against the expected parameter type and converts it accordingly.
251259
*
252260
* @param functionName - The name of the function for which the argument is being converted.
253-
* @param functionAbi - The ABI (Application Binary Interface) of the function, which defines its parameters.
261+
* @param functionAbiOrModuleAbi - The ABI (Application Binary Interface) of the function, which defines its parameters.
254262
* @param arg - The argument to be converted, which can be of various types.
255263
* @param position - The index of the argument in the function's parameter list.
256264
* @param genericTypeParams - An array of type tags for any generic type parameters.
265+
* @param options - Options for the conversion process.
266+
* @param options.allowUnknownStructs - If true, unknown structs will be allowed and converted to a `FixedBytes`.
257267
* @group Implementation
258268
* @category Transactions
259269
*/
260270
export function convertArgument(
261271
functionName: string,
262-
functionAbi: FunctionABI,
272+
functionAbiOrModuleAbi: MoveModule | FunctionABI,
263273
arg: EntryFunctionArgumentTypes | SimpleEntryFunctionArgumentTypes,
264274
position: number,
265275
genericTypeParams: Array<TypeTag>,
276+
options?: { allowUnknownStructs?: boolean },
266277
) {
267-
// Ensure not too many arguments
268-
if (position >= functionAbi.parameters.length) {
269-
throw new Error(`Too many arguments for '${functionName}', expected ${functionAbi.parameters.length}`);
278+
let param: TypeTag;
279+
280+
if ("exposed_functions" in functionAbiOrModuleAbi) {
281+
const functionAbi = functionAbiOrModuleAbi.exposed_functions.find((func) => func.name === functionName);
282+
if (!functionAbi) {
283+
throw new Error(
284+
`Could not find function ABI for '${functionAbiOrModuleAbi.address}::${functionAbiOrModuleAbi.name}::${functionName}'`,
285+
);
286+
}
287+
288+
if (position >= functionAbi.params.length) {
289+
throw new Error(`Too many arguments for '${functionName}', expected ${functionAbi.params.length}`);
290+
}
291+
292+
param = parseTypeTag(functionAbi.params[position], { allowGenerics: true });
293+
} else {
294+
if (position >= functionAbiOrModuleAbi.parameters.length) {
295+
throw new Error(`Too many arguments for '${functionName}', expected ${functionAbiOrModuleAbi.parameters.length}`);
296+
}
297+
298+
param = functionAbiOrModuleAbi.parameters[position];
270299
}
271300

272-
const param = functionAbi.parameters[position];
273-
return checkOrConvertArgument(arg, param, position, genericTypeParams);
301+
return checkOrConvertArgument(
302+
arg,
303+
param,
304+
position,
305+
genericTypeParams,
306+
"exposed_functions" in functionAbiOrModuleAbi ? functionAbiOrModuleAbi : undefined,
307+
options,
308+
);
274309
}
275310

276311
/**
@@ -289,6 +324,8 @@ export function checkOrConvertArgument(
289324
param: TypeTag,
290325
position: number,
291326
genericTypeParams: Array<TypeTag>,
327+
moduleAbi?: MoveModule,
328+
options?: { allowUnknownStructs?: boolean },
292329
) {
293330
// If the argument is bcs encoded, we can just use it directly
294331
if (isEncodedEntryFunctionArgument(arg)) {
@@ -301,6 +338,8 @@ export function checkOrConvertArgument(
301338
* @param typeArgs - The expected type arguments.
302339
* @param arg - The argument to be checked.
303340
* @param position - The position of the argument in the context of the check.
341+
* @param moduleAbi - The ABI of the module containing the function, used for type checking.
342+
* This will typically have information about structs, enums, and other types.
304343
* @group Implementation
305344
* @category Transactions
306345
*/
@@ -309,7 +348,7 @@ export function checkOrConvertArgument(
309348
}
310349

311350
// If it is not BCS encoded, we will need to convert it with the ABI
312-
return parseArg(arg, param, position, genericTypeParams);
351+
return parseArg(arg, param, position, genericTypeParams, moduleAbi, options);
313352
}
314353

315354
/**
@@ -321,6 +360,10 @@ export function checkOrConvertArgument(
321360
* @param param - The type tag that defines the expected type of the argument.
322361
* @param position - The position of the argument in the function call, used for error reporting.
323362
* @param genericTypeParams - An array of type tags for generic type parameters, used when the parameter type is generic.
363+
* @param moduleAbi - The ABI of the module containing the function, used for type checking.
364+
* This will typically have information about structs, enums, and other types.
365+
* @param options - Options for the conversion process.
366+
* @param options.allowUnknownStructs - If true, unknown structs will be allowed and converted to a `FixedBytes`.
324367
* @group Implementation
325368
* @category Transactions
326369
*/
@@ -329,6 +372,8 @@ function parseArg(
329372
param: TypeTag,
330373
position: number,
331374
genericTypeParams: Array<TypeTag>,
375+
moduleAbi?: MoveModule,
376+
options?: { allowUnknownStructs?: boolean },
332377
): EntryFunctionArgumentTypes {
333378
if (param.isBool()) {
334379
if (isBool(arg)) {
@@ -403,7 +448,7 @@ function parseArg(
403448
throw new Error(`Generic argument ${param.toString()} is invalid for argument ${position}`);
404449
}
405450

406-
return checkOrConvertArgument(arg, genericTypeParams[genericIndex], position, genericTypeParams);
451+
return checkOrConvertArgument(arg, genericTypeParams[genericIndex], position, genericTypeParams, moduleAbi);
407452
}
408453

409454
// We have to special case some vectors for Vector<u8>
@@ -433,7 +478,9 @@ function parseArg(
433478
// TODO: Support Uint16Array, Uint32Array, BigUint64Array?
434479

435480
if (Array.isArray(arg)) {
436-
return new MoveVector(arg.map((item) => checkOrConvertArgument(item, param.value, position, genericTypeParams)));
481+
return new MoveVector(
482+
arg.map((item) => checkOrConvertArgument(item, param.value, position, genericTypeParams, moduleAbi)),
483+
);
437484
}
438485

439486
throw new Error(`Type mismatch for argument ${position}, type '${param.toString()}'`);
@@ -454,6 +501,13 @@ function parseArg(
454501
}
455502
throwTypeMismatch("string | AccountAddress", position);
456503
}
504+
// Handle known enum types from Aptos framework
505+
if (param.isDelegationKey() || param.isRateLimiter()) {
506+
if (arg instanceof Uint8Array) {
507+
return new FixedBytes(arg);
508+
}
509+
throwTypeMismatch("Uint8Array", position);
510+
}
457511

458512
if (param.isOption()) {
459513
if (isEmptyOption(arg)) {
@@ -490,7 +544,25 @@ function parseArg(
490544
return new MoveOption<MoveString>(null);
491545
}
492546

493-
return new MoveOption(checkOrConvertArgument(arg, param.value.typeArgs[0], position, genericTypeParams));
547+
return new MoveOption(
548+
checkOrConvertArgument(arg, param.value.typeArgs[0], position, genericTypeParams, moduleAbi),
549+
);
550+
}
551+
552+
// We are assuming that fieldless structs are enums, and therefore we cannot typecheck any further due
553+
// to limited information from the ABI. This does not work for structs on other modules.
554+
const structDefinition = moduleAbi?.structs.find((s) => s.name === param.value.name.identifier);
555+
if (structDefinition?.fields.length === 0 && arg instanceof Uint8Array) {
556+
return new FixedBytes(arg);
557+
}
558+
559+
if (arg instanceof Uint8Array && options?.allowUnknownStructs) {
560+
// eslint-disable-next-line no-console
561+
console.warn(
562+
// eslint-disable-next-line max-len
563+
`Unsupported struct input type for argument ${position}. Continuing since 'allowUnknownStructs' is enabled.`,
564+
);
565+
return new FixedBytes(arg);
494566
}
495567

496568
throw new Error(`Unsupported struct input type for argument ${position}, type '${param.toString()}'`);

src/transactions/typeTag/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,28 @@ export class TypeTagStruct extends TypeTag {
639639
isObject(): boolean {
640640
return this.isTypeTag(AccountAddress.ONE, "object", "Object");
641641
}
642+
643+
/**
644+
* Checks if the provided value is a 'DelegationKey' for permissioned signers.
645+
*
646+
* @returns {boolean} Returns true if the value is a DelegationKey, otherwise false.
647+
* @group Implementation
648+
* @category Transactions
649+
*/
650+
isDelegationKey(): boolean {
651+
return this.isTypeTag(AccountAddress.ONE, "permissioned_delegation", "DelegationKey");
652+
}
653+
654+
/**
655+
* Checks if the provided value is of type `RateLimiter`.
656+
*
657+
* @returns {boolean} Returns true if the value is a RateLimiter, otherwise false.
658+
* @group Implementation
659+
* @category Transactions
660+
*/
661+
isRateLimiter(): boolean {
662+
return this.isTypeTag(AccountAddress.ONE, "rate_limiter", "RateLimiter");
663+
}
642664
}
643665

644666
/**

0 commit comments

Comments
 (0)