Skip to content

Commit ec2ac46

Browse files
authored
fix: contract operation summary for multi-calls (#3897)
1 parent b575c6e commit ec2ac46

File tree

6 files changed

+279
-165
lines changed

6 files changed

+279
-165
lines changed

.changeset/angry-donuts-enter.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@fuel-ts/account": patch
3+
---
4+
5+
fix: contract operation summary for multi-calls

packages/account/src/providers/transaction-summary/call.ts

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
1-
import { type JsonAbi, decodeScriptData } from '@fuel-ts/abi-coder';
1+
import { type JsonAbi, StdStringCoder } from '@fuel-ts/abi-coder';
22
import { Interface } from '@fuel-ts/abi-coder';
3-
import { FuelError, ErrorCode } from '@fuel-ts/errors';
4-
import type { BN } from '@fuel-ts/math';
3+
import { type BN } from '@fuel-ts/math';
54
import type { ReceiptCall } from '@fuel-ts/transactions';
6-
import { TransactionCoder } from '@fuel-ts/transactions';
7-
import { arrayify } from '@fuel-ts/utils';
85

96
type GetFunctionCallProps = {
107
abi: JsonAbi;
118
receipt: ReceiptCall;
12-
rawPayload: string;
13-
maxInputs: BN;
9+
offset: number;
10+
scriptData: Uint8Array;
1411
};
1512

1613
export interface FunctionCall {
@@ -23,39 +20,29 @@ export interface FunctionCall {
2320

2421
/**
2522
* Builds a FunctionCall object from a call receipt.
26-
*
27-
* Currently only supports the first function call, multicall is not supported.
28-
* This should be https://github.com/FuelLabs/fuels-ts/issues/3733.
2923
*/
3024
export const getFunctionCall = ({
3125
abi,
3226
receipt,
33-
rawPayload,
27+
offset,
28+
scriptData,
3429
}: GetFunctionCallProps): FunctionCall => {
35-
const [transaction] = new TransactionCoder().decode(arrayify(rawPayload), 0);
36-
37-
if (!transaction.scriptData) {
38-
throw new FuelError(
39-
ErrorCode.NOT_SUPPORTED,
40-
'Cannot get function calls for this transaction type.'
41-
);
42-
}
43-
44-
const { functionArgs, functionSelector } = decodeScriptData(
45-
arrayify(transaction.scriptData),
46-
abi
47-
);
30+
const [functionSelector, argumentsOffset] = new StdStringCoder().decode(scriptData, offset);
4831

4932
const abiInterface = new Interface(abi);
5033
const functionFragment = abiInterface.getFunction(functionSelector);
5134
const inputs = functionFragment.jsonFn.inputs;
5235

53-
let argumentsProvided;
36+
let argumentsProvided: Record<string, unknown> | undefined;
37+
38+
// Validate if the function has arguments before attempting to decode them
39+
if (inputs.length) {
40+
const functionArgsBytes = scriptData.slice(argumentsOffset);
41+
const decodedArguments = functionFragment.decodeArguments(functionArgsBytes);
5442

55-
if (functionArgs) {
56-
// put together decoded data with input names from abi
43+
// Put together decoded data with input names from abi
5744
argumentsProvided = inputs.reduce((prev, input, index) => {
58-
const value = functionArgs[index];
45+
const value = decodedArguments?.[index];
5946
const name = input.name;
6047

6148
if (name) {

packages/account/src/providers/transaction-summary/operations.test.ts

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { ReceiptType, TransactionType } from '@fuel-ts/transactions';
55
import { ASSET_A, ASSET_B } from '@fuel-ts/utils/test-utils';
66

77
import {
8-
CONTRACT_CALL_ABI,
98
MOCK_INPUT_COIN,
109
MOCK_INPUT_CONTRACT,
1110
MOCK_INPUT_MESSAGE,
@@ -21,7 +20,6 @@ import {
2120
MOCK_RECEIPT_RETURN_DATA_2,
2221
MOCK_RECEIPT_SCRIPT_RESULT,
2322
MOCK_RECEIPT_TRANSFER_OUT,
24-
MOCK_TRANSACTION_RAWPAYLOAD,
2523
} from '../../../test/fixtures/transaction-summary';
2624
import type {
2725
TransactionResultMessageOutReceipt,
@@ -97,69 +95,6 @@ describe('operations', () => {
9795
expect(operations[0]).toStrictEqual(expected);
9896
});
9997

100-
// TODO: Make getOperation tests to be e2e
101-
it.skip('should ensure getContractCallOperations return contract call operations with calls details', () => {
102-
const expected: Operation = {
103-
name: OperationName.contractCall,
104-
calls: [
105-
{
106-
functionName: 'mint_to_address',
107-
functionSignature: 'mint_to_address(u64,s(b256),u64)',
108-
argumentsProvided: {
109-
address: {
110-
value: '0xa5a77a7d97c6708b08de873528ae6879ef5e9900fbc2e3f3cb74e28917bf7038',
111-
},
112-
amount: '0x64',
113-
amount2: '0x64',
114-
},
115-
amount: bn('0x5f5e100'),
116-
assetId: ZeroBytes32,
117-
},
118-
],
119-
from: {
120-
type: AddressType.account,
121-
address: '0x3e7ddda4d0d3f8307ae5f1aed87623992c1c4decefec684936960775181b2302',
122-
},
123-
to: {
124-
type: AddressType.contract,
125-
address: '0x0a98320d39c03337401a4e46263972a9af6ce69ec2f35a5420b1bd35784c74b1',
126-
},
127-
assetsSent: [
128-
{
129-
amount: bn(100000000),
130-
assetId: ZeroBytes32,
131-
},
132-
],
133-
};
134-
135-
const receipts: TransactionResultReceipt[] = [
136-
MOCK_RECEIPT_CALL,
137-
{
138-
...MOCK_RECEIPT_CALL,
139-
to: '0x0000000000000000000000000000000000000000000000000000000000000000',
140-
},
141-
MOCK_RECEIPT_TRANSFER_OUT,
142-
MOCK_RECEIPT_RETURN_DATA_1,
143-
MOCK_RECEIPT_RETURN_DATA_2,
144-
MOCK_RECEIPT_SCRIPT_RESULT,
145-
];
146-
147-
const operations = getContractCallOperations({
148-
inputs: [MOCK_INPUT_CONTRACT, MOCK_INPUT_COIN],
149-
outputs: [MOCK_OUTPUT_CONTRACT, MOCK_OUTPUT_VARIABLE, MOCK_OUTPUT_CHANGE],
150-
receipts,
151-
abiMap: {
152-
'0x0a98320d39c03337401a4e46263972a9af6ce69ec2f35a5420b1bd35784c74b1': CONTRACT_CALL_ABI,
153-
},
154-
rawPayload: MOCK_TRANSACTION_RAWPAYLOAD,
155-
maxInputs: bn(255),
156-
baseAssetId: ZeroBytes32,
157-
});
158-
159-
expect(operations.length).toEqual(1);
160-
expect(operations[0]).toStrictEqual(expected);
161-
});
162-
16398
it('should ensure getContractCallOperations return empty', () => {
16499
const operations = getContractCallOperations({
165100
inputs: [],

packages/account/src/providers/transaction-summary/operations.ts

Lines changed: 70 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { ZeroBytes32 } from '@fuel-ts/address/configs';
22
import { ErrorCode, FuelError } from '@fuel-ts/errors';
33
import type { BN } from '@fuel-ts/math';
4-
import { bn } from '@fuel-ts/math';
5-
import { ReceiptType, TransactionType } from '@fuel-ts/transactions';
4+
import { bn, toBytes } from '@fuel-ts/math';
5+
import { ReceiptType, TransactionCoder, TransactionType } from '@fuel-ts/transactions';
66
import type { InputContract, Output, OutputChange, Input } from '@fuel-ts/transactions';
7+
import { arrayify, concat } from '@fuel-ts/utils';
78

89
import type {
910
TransactionResultReceipt,
@@ -13,7 +14,7 @@ import type {
1314
TransactionResultTransferReceipt,
1415
} from '../transaction-response';
1516

16-
import type { FunctionCall } from './call';
17+
import { getFunctionCall, type FunctionCall } from './call';
1718
import {
1819
getInputFromAssetId,
1920
getInputAccountAddress,
@@ -269,24 +270,68 @@ export function getWithdrawFromFuelOperations({
269270
return withdrawFromFuelOperations;
270271
}
271272

273+
/** @hidden */
274+
function findBytesSegmentIndex(whole: Uint8Array, segment: Uint8Array) {
275+
for (let i = 0; i <= whole.length - segment.length; i++) {
276+
let match = true;
277+
for (let j = 0; j < segment.length; j++) {
278+
if (whole[i + j] !== segment[j]) {
279+
match = false;
280+
break;
281+
}
282+
}
283+
if (match) {
284+
return i;
285+
}
286+
}
287+
return -1;
288+
}
289+
272290
/** @hidden */
273291
function getContractCalls(
274292
contractInput: InputContract,
275293
abiMap: AbiMap | undefined,
276-
_receipt: TransactionResultCallReceipt,
277-
_rawPayload: string,
278-
_maxInputs: BN
294+
receipt: TransactionResultCallReceipt,
295+
scriptData?: Uint8Array
279296
): FunctionCall[] {
297+
const calls: FunctionCall[] = [];
298+
280299
const abi = abiMap?.[contractInput.contractID];
281-
if (!abi) {
282-
return [];
300+
if (!abi || !scriptData) {
301+
return calls;
302+
}
303+
304+
const bytesSegment = concat([
305+
arrayify(receipt.to), // Contract ID (32 bytes)
306+
toBytes(receipt.param1.toHex(), 8), // Function selector offset (8 bytes)
307+
toBytes(receipt.param2.toHex(), 8), // Function args offset (8 bytes)
308+
]);
309+
310+
const segmentIndex = findBytesSegmentIndex(scriptData, bytesSegment);
311+
312+
/**
313+
* If the byte segment is not found, it likely indicates a non-standard contract call, such as:
314+
*
315+
* 1. Manual External Call: A direct call from a Sway script or contract using
316+
* `abi(abi_interface, contract_id)` built-in Sway function.
317+
*
318+
* 2. Inline ASM Call: A call made using the ASM `call` instruction in Sway,
319+
* without setting `param1` and `param2` offsets just like the SDKs do.
320+
*
321+
* In these cases, the function call cannot be decoded.
322+
*/
323+
const canDecodeFunctionCall = segmentIndex !== -1;
324+
325+
if (!canDecodeFunctionCall) {
326+
return calls;
283327
}
284328

285-
// Until we can successfully decode all operations, including multicall we
286-
// will just return an empty. This should then be reintroduced in
287-
// https://github.com/FuelLabs/fuels-ts/issues/3733
288-
return [];
289-
// return [ getFunctionCall({ abi, receipt, rawPayload, maxInputs }) ];
329+
const offset = segmentIndex + bytesSegment.length;
330+
331+
const call = getFunctionCall({ abi, receipt, offset, scriptData });
332+
calls.push(call);
333+
334+
return calls;
290335
}
291336

292337
/** @hidden */
@@ -307,8 +352,7 @@ function processCallReceipt(
307352
contractInput: InputContract,
308353
inputs: Input[],
309354
abiMap: AbiMap | undefined,
310-
rawPayload: string,
311-
maxInputs: BN,
355+
scriptData: Uint8Array | undefined,
312356
baseAssetId: string
313357
): Operation[] {
314358
const assetId = receipt.assetId === ZeroBytes32 ? baseAssetId : receipt.assetId;
@@ -318,7 +362,7 @@ function processCallReceipt(
318362
}
319363

320364
const inputAddress = getInputAccountAddress(input);
321-
const calls = getContractCalls(contractInput, abiMap, receipt, rawPayload, maxInputs);
365+
const calls = getContractCalls(contractInput, abiMap, receipt, scriptData);
322366

323367
return [
324368
{
@@ -345,7 +389,6 @@ export function getContractCallOperations({
345389
receipts,
346390
abiMap,
347391
rawPayload,
348-
maxInputs,
349392
baseAssetId,
350393
}: InputOutputParam &
351394
ReceiptParam &
@@ -360,18 +403,19 @@ export function getContractCallOperations({
360403
return [];
361404
}
362405

406+
let scriptData: Uint8Array | undefined;
407+
408+
if (rawPayload) {
409+
const [transaction] = new TransactionCoder().decode(arrayify(rawPayload), 0);
410+
if (transaction.type === TransactionType.Script) {
411+
scriptData = arrayify(transaction.scriptData as string);
412+
}
413+
}
414+
363415
return contractCallReceipts
364416
.filter((receipt) => receipt.to === contractInput.contractID)
365417
.flatMap((receipt) =>
366-
processCallReceipt(
367-
receipt,
368-
contractInput,
369-
inputs,
370-
abiMap,
371-
rawPayload as string,
372-
maxInputs,
373-
baseAssetId
374-
)
418+
processCallReceipt(receipt, contractInput, inputs, abiMap, scriptData, baseAssetId)
375419
);
376420
});
377421
}

0 commit comments

Comments
 (0)