Skip to content

Commit c414b2f

Browse files
committed
fix: parsing of apt function args
Ticket: COIN-5733
1 parent ce0e0af commit c414b2f

File tree

2 files changed

+113
-43
lines changed

2 files changed

+113
-43
lines changed

modules/sdk-coin-apt/src/lib/transaction/customTransaction.ts

Lines changed: 71 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from '@aptos-labs/ts-sdk';
2121
import { CustomTransactionParams } from '../iface';
2222
import { validateModuleName, validateFunctionName } from '../utils/validation';
23+
import utils from '../utils';
2324

2425
/**
2526
* Transaction class for custom Aptos transactions.
@@ -69,6 +70,7 @@ export class CustomTransaction extends Transaction {
6970

7071
/**
7172
* Parse a transaction payload to extract the custom transaction data
73+
* Requires ABI information for proper type-aware conversion
7274
*
7375
* @param {TransactionPayload} payload - The transaction payload to parse
7476
*/
@@ -94,16 +96,23 @@ export class CustomTransaction extends Transaction {
9496
this._functionName = functionIdentifier;
9597
this._typeArguments = entryFunction.type_args.map((typeArg) => typeArg.toString());
9698

97-
this._functionArguments = entryFunction.args.map((arg: any) => {
98-
if (typeof arg === 'string' || typeof arg === 'number' || typeof arg === 'boolean') {
99-
return arg;
100-
}
101-
if (arg && typeof arg === 'object' && 'data' in arg && arg.data) {
102-
const bytes = Array.from(arg.data) as number[];
103-
return '0x' + bytes.map((b: number) => b.toString(16).padStart(2, '0')).join('');
104-
}
105-
return arg;
106-
});
99+
// Parse arguments using ABI information
100+
// If ABI is available, parse with type awareness; otherwise store raw args for later processing
101+
if (this._entryFunctionAbi?.parameters) {
102+
this._functionArguments = entryFunction.args.map((arg: any, index: number) => {
103+
const paramType = this._entryFunctionAbi?.parameters?.[index];
104+
if (!paramType) {
105+
throw new InvalidTransactionError(
106+
`Missing ABI parameter type for argument ${index} in ${this._moduleName}::${this._functionName}. ` +
107+
'ABI parameter count mismatch.'
108+
);
109+
}
110+
return this.convertArgumentByABI(arg, paramType);
111+
});
112+
} else {
113+
// Store raw arguments temporarily - transaction will be re-parsed when ABI is provided
114+
this._functionArguments = entryFunction.args.map((arg: any) => arg);
115+
}
107116
}
108117

109118
/**
@@ -112,24 +121,8 @@ export class CustomTransaction extends Transaction {
112121
protected getTransactionPayloadData(): InputGenerateTransactionPayloadData {
113122
const functionName = this.getValidatedFullFunctionName();
114123

115-
// Convert arguments based on ABI information if available
116-
const processedArguments = this._functionArguments.map((arg: any, index: number) => {
117-
// Use ABI to identify the expected type for this argument
118-
const paramType = this._entryFunctionAbi?.parameters?.[index];
119-
if (paramType) {
120-
return this.convertArgumentByABI(arg, paramType);
121-
}
122-
123-
// Fallback: basic conversion for common cases
124-
if (typeof arg === 'string' && arg.startsWith('0x') && arg.length === 66) {
125-
try {
126-
return AccountAddress.fromString(arg);
127-
} catch {
128-
return arg;
129-
}
130-
}
131-
return arg;
132-
});
124+
// Arguments are pre-processed during parsing phase
125+
const processedArguments = this._functionArguments;
133126

134127
return {
135128
function: functionName,
@@ -184,6 +177,16 @@ export class CustomTransaction extends Transaction {
184177

185178
// Handle nested BCS structures with 'value' property
186179
if (arg && typeof arg === 'object' && 'value' in arg && arg.value) {
180+
// Check if inner value is a Uint8Array (common for U64 arguments)
181+
if (arg.value.value && arg.value.value instanceof Uint8Array) {
182+
const bytes = Array.from(arg.value.value) as number[];
183+
if (this.isNumericType(paramType)) {
184+
return this.convertNumericArgument(bytes, paramType);
185+
}
186+
// For non-numeric types, convert to hex string
187+
return bytesToHex(bytes);
188+
}
189+
187190
// Simple value wrapper
188191
if (!('value' in arg.value) || typeof arg.value.value !== 'object') {
189192
return this.convertArgumentByABI(arg.value, paramType);
@@ -200,24 +203,13 @@ export class CustomTransaction extends Transaction {
200203
const bytes = keys.map((k) => bytesObj[k]);
201204
let extractedValue: any;
202205

203-
// Convert bytes based on parameter type
204-
if (
205-
paramType instanceof TypeTagAddress ||
206-
paramType instanceof TypeTagU64 ||
207-
paramType instanceof TypeTagU128 ||
208-
paramType instanceof TypeTagU256
209-
) {
206+
// Convert bytes based on parameter type using unified approach
207+
if (this.isNumericType(paramType)) {
208+
extractedValue = this.convertNumericArgument(bytes, paramType);
209+
} else if (paramType instanceof TypeTagAddress) {
210210
extractedValue = bytesToHex(bytes);
211211
} else if (paramType instanceof TypeTagBool) {
212212
extractedValue = bytes[0] === 1;
213-
} else if (paramType instanceof TypeTagU8 || paramType instanceof TypeTagU16 || paramType instanceof TypeTagU32) {
214-
// Convert little-endian bytes to number using the original algorithm
215-
// to ensure consistent behavior with large numbers
216-
let result = 0;
217-
for (let i = bytes.length - 1; i >= 0; i--) {
218-
result = result * 256 + bytes[i];
219-
}
220-
extractedValue = result;
221213
} else {
222214
extractedValue = bytesToHex(bytes);
223215
}
@@ -274,4 +266,40 @@ export class CustomTransaction extends Transaction {
274266

275267
return fullName as `${string}::${string}::${string}`;
276268
}
269+
270+
/**
271+
* Check if a parameter type is a numeric type
272+
*/
273+
private isNumericType(paramType: any): boolean {
274+
return (
275+
paramType instanceof TypeTagU8 ||
276+
paramType instanceof TypeTagU16 ||
277+
paramType instanceof TypeTagU32 ||
278+
paramType instanceof TypeTagU64 ||
279+
paramType instanceof TypeTagU128 ||
280+
paramType instanceof TypeTagU256
281+
);
282+
}
283+
284+
/**
285+
* Convert numeric argument using consistent little-endian byte handling
286+
*/
287+
private convertNumericArgument(bytes: number[], paramType: any): any {
288+
if (paramType instanceof TypeTagU8 || paramType instanceof TypeTagU16 || paramType instanceof TypeTagU32) {
289+
// Small integers: use Number for compatibility
290+
let result = 0;
291+
for (let i = bytes.length - 1; i >= 0; i--) {
292+
result = result * 256 + bytes[i];
293+
}
294+
return result;
295+
}
296+
297+
if (paramType instanceof TypeTagU64 || paramType instanceof TypeTagU128 || paramType instanceof TypeTagU256) {
298+
// Large integers: reuse the existing utility method
299+
return utils.getAmountFromPayloadArgs(new Uint8Array(bytes));
300+
}
301+
302+
// Fallback for unexpected numeric types
303+
return '0x' + bytes.map((b) => b.toString(16).padStart(2, '0')).join('');
304+
}
277305
}

modules/sdk-coin-apt/test/unit/transactionBuilder/customTransactionBuilder.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,48 @@ describe('Apt Custom Transaction Builder', () => {
197197
const rawTx = tx.toBroadcastFormat();
198198
should.equal(txBuilder.isValidRawTransaction(rawTx), true);
199199
});
200+
201+
it('should build and rebuild custom transaction with correct U64 amount parsing', async function () {
202+
// Use the provided serialized hex with amount 1000
203+
const serializedHex =
204+
'0xf22fa009b6473cdc539ecbd570d00d0585682f5939ffe256a90ddee0d616292f000000000000000002000000000000000000000000000000000000000000000000000000000000000104636f696e087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e000220da22bdc19f873fd6ce48c32912965c8a9dde578b2a3cf4fae3dd85dfaac784c908e803000000000000a8610000000000006400000000000000901cdd680000000002030020000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000adba44b46722b8da1797645f71ddcdab28626c06acd36b88d5198a684966306c0020453ed5aa75b01f3eb03ab1d3030be10996206e5a073a74983ea3fcd013ffe1ea4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000';
205+
206+
const transferCoinsAbi = {
207+
typeParameters: [{ constraints: [] }],
208+
parameters: [new TypeTagAddress(), new TypeTagU64()],
209+
};
210+
211+
// Build transaction from serialized hex
212+
const txBuilder = factory.from(serializedHex, transferCoinsAbi);
213+
const tx = (await txBuilder.build()) as CustomTransaction;
214+
215+
// Validate the transaction was parsed correctly
216+
should.equal(tx.type, TransactionType.CustomTx);
217+
should.exist(tx.fullFunctionName);
218+
should.equal(tx.fullFunctionName, '0x1::coin::transfer');
219+
220+
// Get the custom transaction parameters
221+
const customParams = tx.getCustomTransactionParams();
222+
should.exist(customParams);
223+
should.equal(customParams.moduleName, '0x1::coin');
224+
should.equal(customParams.functionName, 'transfer');
225+
should.exist(customParams.functionArguments);
226+
should.equal(customParams.functionArguments!.length, 2);
227+
228+
// Validate the amount argument was parsed correctly as 1000
229+
const parsedAmount = customParams.functionArguments![1];
230+
should.equal(parsedAmount, '1000', 'Amount should be correctly parsed as 1000 from U64 bytes');
231+
232+
// Validate the address argument (first argument)
233+
const parsedAddress = customParams.functionArguments![0];
234+
should.exist(parsedAddress);
235+
should.equal(typeof parsedAddress, 'string', 'Address should be a string');
236+
(parsedAddress as string).should.match(/^0x[a-fA-F0-9]+$/, 'Address should be valid hex format');
237+
238+
// Ensure the transaction can be rebuilt (round-trip test)
239+
const rawTx = tx.toBroadcastFormat();
240+
should.equal(txBuilder.isValidRawTransaction(rawTx), true);
241+
});
200242
});
201243

202244
describe('Fail', () => {

0 commit comments

Comments
 (0)