diff --git a/.changeset/thin-pumpkins-sell.md b/.changeset/thin-pumpkins-sell.md new file mode 100644 index 00000000000..bfa20d2ff25 --- /dev/null +++ b/.changeset/thin-pumpkins-sell.md @@ -0,0 +1,5 @@ +--- +"@fuel-ts/account": patch +--- + +fix: return correct operations from coin and message inputs \ No newline at end of file diff --git a/packages/account/src/providers/transaction-summary/input.test.ts b/packages/account/src/providers/transaction-summary/input.test.ts index 591ea4113f8..8489f019b59 100644 --- a/packages/account/src/providers/transaction-summary/input.test.ts +++ b/packages/account/src/providers/transaction-summary/input.test.ts @@ -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 { @@ -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); + }); }); diff --git a/packages/account/src/providers/transaction-summary/input.ts b/packages/account/src/providers/transaction-summary/input.ts index 6acb9ece782..ac08c85e502 100644 --- a/packages/account/src/providers/transaction-summary/input.ts +++ b/packages/account/src/providers/transaction-summary/input.ts @@ -13,7 +13,7 @@ export function getInputsByType(inputs: Input[], type: InputType) { } /** @hidden */ -export function getInputsCoin(inputs: Input[]) { +export function getInputsCoin(inputs: Input[]): InputCoin[] { return getInputsByType(inputs, InputType.Coin); } @@ -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 */ diff --git a/packages/account/src/providers/transaction-summary/operations.test.ts b/packages/account/src/providers/transaction-summary/operations.test.ts index cf20482aca0..32337158047 100644 --- a/packages/account/src/providers/transaction-summary/operations.test.ts +++ b/packages/account/src/providers/transaction-summary/operations.test.ts @@ -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); @@ -150,6 +151,7 @@ describe('operations', () => { }, rawPayload: MOCK_TRANSACTION_RAWPAYLOAD, maxInputs: bn(255), + baseAssetId: ZeroBytes32, }); expect(operations.length).toEqual(1); @@ -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); diff --git a/packages/account/src/providers/transaction-summary/operations.ts b/packages/account/src/providers/transaction-summary/operations.ts index 17e4f677302..237c3937845 100644 --- a/packages/account/src/providers/transaction-summary/operations.ts +++ b/packages/account/src/providers/transaction-summary/operations.ts @@ -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, { @@ -239,9 +239,10 @@ export function getContractCallOperations({ abiMap, rawPayload, maxInputs, + baseAssetId, }: InputOutputParam & ReceiptParam & - Pick & + Pick & RawPayloadParam): Operation[] { const contractCallReceipts = getReceiptsCall(receipts); const contractOutputs = getOutputsContract(outputs); @@ -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 = []; @@ -497,6 +501,7 @@ export function getOperations({ abiMap, rawPayload, maxInputs, + baseAssetId, }), ...getWithdrawFromFuelOperations({ inputs, receipts, baseAssetId }), ]; diff --git a/packages/account/src/test-utils/wallet-config.test.ts b/packages/account/src/test-utils/wallet-config.test.ts index 86598f10562..0c292bf4ffb 100644 --- a/packages/account/src/test-utils/wallet-config.test.ts +++ b/packages/account/src/test-utils/wallet-config.test.ts @@ -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.' ) ); }); diff --git a/packages/account/src/test-utils/wallet-config.ts b/packages/account/src/test-utils/wallet-config.ts index 334226be0e6..7b76326eb22 100644 --- a/packages/account/src/test-utils/wallet-config.ts +++ b/packages/account/src/test-utils/wallet-config.ts @@ -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.' ); } } diff --git a/packages/fuel-gauge/src/transaction-summary.test.ts b/packages/fuel-gauge/src/transaction-summary.test.ts index 691f7435b71..4c47df9aef1 100644 --- a/packages/fuel-gauge/src/transaction-summary.test.ts +++ b/packages/fuel-gauge/src/transaction-summary.test.ts @@ -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'; @@ -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() }] }, + ], + }); + }); }); });