Skip to content

Commit

Permalink
fix: return correct operations from coin and message inputs (#2782)
Browse files Browse the repository at this point in the history
Co-authored-by: Peter Smith <[email protected]>
Co-authored-by: Sérgio Torres <[email protected]>
  • Loading branch information
3 people committed Jul 30, 2024
1 parent 17edbba commit fc39124
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 27 deletions.
5 changes: 5 additions & 0 deletions .changeset/thin-pumpkins-sell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/account": patch
---

fix: return correct operations from coin and message inputs
30 changes: 28 additions & 2 deletions packages/account/src/providers/transaction-summary/input.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ZeroBytes32 } from '@fuel-ts/address/configs';
import type { InputCoin } from '@fuel-ts/transactions';
import { bn } from '@fuel-ts/math';
import type { InputCoin, InputMessage } from '@fuel-ts/transactions';
import { ASSET_A } from '@fuel-ts/utils/test-utils';

import {
Expand Down Expand Up @@ -116,8 +117,33 @@ describe('transaction-summary/input', () => {
expect(getInputFromAssetId([inputCoin1, inputCoin2], ZeroBytes32)).toStrictEqual(inputCoin1);
expect(getInputFromAssetId([inputCoin1, inputCoin2], ASSET_A)).toStrictEqual(inputCoin2);

expect(getInputFromAssetId([MOCK_INPUT_MESSAGE], ZeroBytes32)).toStrictEqual(
expect(getInputFromAssetId([MOCK_INPUT_MESSAGE], ZeroBytes32, true)).toStrictEqual(
MOCK_INPUT_MESSAGE
);
});

it('should ensure getInputFromAssetId returns the correct coinInput thats greater than 0 for default assetId', () => {
const coinInput1: InputCoin = {
...MOCK_INPUT_COIN,
amount: bn(100),
assetId: ZeroBytes32,
};

const coinInput2: InputCoin = {
...MOCK_INPUT_COIN,
amount: bn(0),
assetId: ZeroBytes32,
};

expect(getInputFromAssetId([coinInput1, coinInput2], ZeroBytes32)).toEqual(coinInput1);
});

it('Should return the correct input message for withdrawals', () => {
const inputMessage: InputMessage = {
...MOCK_INPUT_MESSAGE,
amount: bn(100),
};

expect(getInputFromAssetId([inputMessage], ZeroBytes32, true)).toEqual(inputMessage);
});
});
36 changes: 26 additions & 10 deletions packages/account/src/providers/transaction-summary/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function getInputsByType<T = Input>(inputs: Input[], type: InputType) {
}

/** @hidden */
export function getInputsCoin(inputs: Input[]) {
export function getInputsCoin(inputs: Input[]): InputCoin[] {
return getInputsByType<InputCoin>(inputs, InputType.Coin);
}

Expand All @@ -33,16 +33,32 @@ export function getInputsContract(inputs: Input[]) {
}

/** @hidden */
export function getInputFromAssetId(inputs: Input[], assetId: string) {
function findCoinInput(inputs: Input[], assetId: string): InputCoin | undefined {
const coinInputs = getInputsCoin(inputs);
const messageInputs = getInputsMessage(inputs);
const coinInput = coinInputs.find((i) => i.assetId === assetId);
// TODO: should include assetId in InputMessage as well. for now we're mocking ETH
const messageInput = messageInputs.find(
(_) => assetId === '0x0000000000000000000000000000000000000000000000000000000000000000'
);

return coinInput || messageInput;
return coinInputs.find((i) => i.assetId === assetId);
}

/** @hidden */
function findMessageInput(inputs: Input[]): InputMessage | undefined {
return getInputsMessage(inputs)?.[0];
}
/** @hidden */
export function getInputFromAssetId(
inputs: Input[],
assetId: string,
isBaseAsset = false
): InputCoin | InputMessage | undefined {
const coinInput = findCoinInput(inputs, assetId);
if (coinInput) {
return coinInput;
}

if (isBaseAsset) {
return findMessageInput(inputs);
}

// #TODO: we should throw an error here if we are unable to return a valid input
return undefined;
}

/** @hidden */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ describe('operations', () => {
outputs: [MOCK_OUTPUT_CONTRACT, MOCK_OUTPUT_VARIABLE, MOCK_OUTPUT_CHANGE],
receipts,
maxInputs: bn(255),
baseAssetId: ZeroBytes32,
});

expect(operations.length).toEqual(1);
Expand Down Expand Up @@ -150,6 +151,7 @@ describe('operations', () => {
},
rawPayload: MOCK_TRANSACTION_RAWPAYLOAD,
maxInputs: bn(255),
baseAssetId: ZeroBytes32,
});

expect(operations.length).toEqual(1);
Expand All @@ -162,6 +164,7 @@ describe('operations', () => {
outputs: [MOCK_OUTPUT_COIN, MOCK_OUTPUT_CHANGE],
receipts: [MOCK_RECEIPT_RETURN, MOCK_RECEIPT_SCRIPT_RESULT],
maxInputs: bn(255),
baseAssetId: ZeroBytes32,
});

expect(operations.length).toEqual(0);
Expand Down
11 changes: 8 additions & 3 deletions packages/account/src/providers/transaction-summary/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ export function getWithdrawFromFuelOperations({

const withdrawFromFuelOperations = messageOutReceipts.reduce(
(prevWithdrawFromFuelOps, receipt) => {
const input = getInputFromAssetId(inputs, baseAssetId);
const input = getInputFromAssetId(inputs, baseAssetId, true);
if (input) {
const inputAddress = getInputAccountAddress(input);
const newWithdrawFromFuelOps = addOperation(prevWithdrawFromFuelOps, {
Expand Down Expand Up @@ -239,9 +239,10 @@ export function getContractCallOperations({
abiMap,
rawPayload,
maxInputs,
baseAssetId,
}: InputOutputParam &
ReceiptParam &
Pick<GetOperationParams, 'abiMap' | 'maxInputs'> &
Pick<GetOperationParams, 'abiMap' | 'maxInputs' | 'baseAssetId'> &
RawPayloadParam): Operation[] {
const contractCallReceipts = getReceiptsCall(receipts);
const contractOutputs = getOutputsContract(outputs);
Expand All @@ -252,7 +253,10 @@ export function getContractCallOperations({
if (contractInput) {
const newCallOps = contractCallReceipts.reduce((prevContractCallOps, receipt) => {
if (receipt.to === contractInput.contractID) {
const input = getInputFromAssetId(inputs, receipt.assetId);
// # TODO: This is a temporary fix to ensure that the base assetId is used when the assetId is ZeroBytes32
// The assetId is returned as ZeroBytes32 if the contract call has no assets in it (see https://github.com/FuelLabs/fuel-core/issues/1941)
const assetId = receipt.assetId === ZeroBytes32 ? baseAssetId : receipt.assetId;
const input = getInputFromAssetId(inputs, assetId, assetId === baseAssetId);
if (input) {
const inputAddress = getInputAccountAddress(input);
const calls = [];
Expand Down Expand Up @@ -497,6 +501,7 @@ export function getOperations({
abiMap,
rawPayload,
maxInputs,
baseAssetId,
}),
...getWithdrawFromFuelOperations({ inputs, receipts, baseAssetId }),
];
Expand Down
10 changes: 1 addition & 9 deletions packages/account/src/test-utils/wallet-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,7 @@ describe('WalletsConfig', () => {
() => new WalletsConfig(hexlify(randomBytes(32)), { ...configOptions, amountPerCoin: -1 }),
new FuelError(
FuelError.CODES.INVALID_INPUT_PARAMETERS,
'Amount per coin must be greater than zero.'
)
);

await expectToThrowFuelError(
() => new WalletsConfig(hexlify(randomBytes(32)), { ...configOptions, amountPerCoin: 0 }),
new FuelError(
FuelError.CODES.INVALID_INPUT_PARAMETERS,
'Amount per coin must be greater than zero.'
'Amount per coin must be greater than or equal to zero.'
)
);
});
Expand Down
4 changes: 2 additions & 2 deletions packages/account/src/test-utils/wallet-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,10 @@ export class WalletsConfig {
'Number of coins per asset must be greater than zero.'
);
}
if (amountPerCoin <= 0) {
if (amountPerCoin < 0) {
throw new FuelError(
FuelError.CODES.INVALID_INPUT_PARAMETERS,
'Amount per coin must be greater than zero.'
'Amount per coin must be greater than or equal to zero.'
);
}
}
Expand Down
42 changes: 41 additions & 1 deletion packages/fuel-gauge/src/transaction-summary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
AddressType,
OperationName,
} from 'fuels';
import { ASSET_A, ASSET_B, launchTestNode } from 'fuels/test-utils';
import { ASSET_A, ASSET_B, launchTestNode, TestMessage } from 'fuels/test-utils';

import { MultiTokenContractAbi__factory, TokenContractAbi__factory } from '../test/typegen';
import MultiTokenContractAbiHex from '../test/typegen/contracts/MultiTokenContractAbi.hex';
Expand Down Expand Up @@ -591,5 +591,45 @@ describe('TransactionSummary', () => {
recipients: allRecipients,
});
});

it('should ensure that transfer operations are assembled correctly if only seeded with a MessageInput (SPENDABLE MESSAGE)', async () => {
const testMessage = new TestMessage({ amount: 1000000, data: '' });

using launched = await launchTestNode({
contractsConfigs: [
{
deployer: MultiTokenContractAbi__factory,
bytecode: MultiTokenContractAbiHex,
},
],
walletsConfig: {
amountPerCoin: 0,
messages: [testMessage],
},
});
const {
contracts: [contract],
provider,
wallets: [wallet],
} = launched;

const amount = 100;

const tx1 = await wallet.transferToContract(contract.id, amount);

const { operations } = await tx1.waitForResult();

expect(operations).toHaveLength(1);

validateTransferOperation({
operations,
sender: wallet.address,
fromType: AddressType.account,
toType: AddressType.contract,
recipients: [
{ address: contract.id, quantities: [{ amount, assetId: provider.getBaseAssetId() }] },
],
});
});
});
});

0 comments on commit fc39124

Please sign in to comment.