Skip to content

Commit

Permalink
fix!: assemble of Transfer operation (FuelLabs#1787)
Browse files Browse the repository at this point in the history
  • Loading branch information
Torres-ssf authored Feb 26, 2024
1 parent c57860a commit 086dbba
Show file tree
Hide file tree
Showing 16 changed files with 580 additions and 320 deletions.
2 changes: 2 additions & 0 deletions .changeset/sweet-rings-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
5 changes: 5 additions & 0 deletions .changeset/thick-otters-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/account": minor
---

fix assemble of transfer operations
1 change: 1 addition & 0 deletions apps/docs-snippets/test/fixtures/forc-projects/Forc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ members = [
"echo-asset-id",
"script-transfer-to-contract",
"echo-bytes",
"token",
"echo-raw-slice",
"echo-std-string",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
authors = ["Fuel Labs <[email protected]>"]
entry = "main.sw"
license = "Apache-2.0"
name = "token"

[dependencies]
30 changes: 30 additions & 0 deletions apps/docs-snippets/test/fixtures/forc-projects/token/src/main.sw
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
contract;

use std::asset::{burn, force_transfer_to_contract, mint, transfer_to_address,};

abi Token {
fn transfer_to_address(target: Address, asset_id: AssetId, coins: u64);
fn transfer_to_contract(recipient: ContractId, asset_id: AssetId, coins: u64);
fn mint_coins(sub_id: b256, mint_amount: u64);
fn burn_coins(sub_id: b256, burn_amount: u64);
}

impl Token for Contract {
// #region variable-outputs-1
fn transfer_to_address(recipient: Address, asset_id: AssetId, amount: u64) {
transfer_to_address(recipient, asset_id, amount);
}

fn transfer_to_contract(target: ContractId, asset_id: AssetId, amount: u64) {
force_transfer_to_contract(target, asset_id, amount);
}

fn mint_coins(sub_id: b256, mint_amount: u64) {
mint(sub_id, mint_amount);
}

fn burn_coins(sub_id: b256, burn_amount: u64) {
burn(sub_id, burn_amount);
}
// #endregion variable-outputs-1
}
30 changes: 17 additions & 13 deletions apps/docs/src/guide/contracts/variable-outputs.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
<!-- NOTE: Review the relevance of this documentation page. The TypeScript SDK manages Output variables automatically, which may make the current content lack sufficient context. Consider providing a detailed explanation of how transactions work in a UTXO-based blockchain before discussing Output variables. This approach will ensure users have a better understanding of the topic and its importance. -->

# Variable Outputs

You may need to send funds to the transaction output in certain scenarios. Sway provides a method called `transfer_to_address(coins, asset_id, recipient)` that we can use for this purpose, which allows you to transfer a specific number of coins for a given asset to a recipient address.
Sway includes robust functions for transferring assets to wallets and contracts.

When using these transfer functions within your Sway projects, it is important to be aware that each call will require an [Output Variable](https://specs.fuel.network/master/tx-format/output.html#outputvariable) within the [Outputs](https://specs.fuel.network/master/tx-format/output.html) of the transaction.

For instance, if a contract function calls a Sway transfer function 3 times, it will require 3 Output Variables present within the list of outputs in your transaction.

## Example: Using `transfer_to_address` in a Contract
## Example: Sway's built-in functions that requires `Output Variable`

Here's an example of a contract function that utilizes the `transfer_to_address` method:
<<< @/../../docs-snippets/test/fixtures/forc-projects/token/src/main.sw#variable-outputs-1{ts:line-numbers}

```rust:line-numbers
fn transfer_coins_to_output(coins: u64, asset_id: ContractId, recipient: Address) {
transfer_to_address(coins, asset_id, recipient);
}
```
> **Note:** Functions like `mint` and `burn` also requires an Output Variable for each call, as they internally execute the transfer function.
## Using the SDK to Call the `transfer_coins_to_output` Function
## Adding Variable Outputs to the contract call

With the SDK, you can call `transfer_coins_to_output` by chaining the `txParams` and adding the property `variableOutputs: amount` to your contract call. Like this:
When your contract invokes any of these functions, or if it calls a function that leads to another contract invoking these functions, you need to add the appropriate number of Output Variables.

This can be done as shown in the following example:

<<< @/../../docs-snippets/src/guide/contracts/transaction-parameters.test.ts#variable-outputs-1{ts:line-numbers}

In the TypeScript SDK, the output variables are automatically added to the transaction's list of outputs. The output's amount and owner may vary based on the transaction execution.
In the TypeScript SDK, the Output Variables are automatically added to the transaction's list of outputs.

This process is done by a brute-force strategy, performing sequential dry runs until no errors are returned. This method identifies the number of Output Variables required to process the transaction.

However, this can significantly delay the transaction processing. Therefore, it is **highly recommended** to manually add the correct number of Output Variables before submitting the transaction.
206 changes: 17 additions & 189 deletions packages/account/src/providers/transaction-summary/operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
MOCK_RECEIPT_RETURN_DATA_1,
MOCK_RECEIPT_RETURN_DATA_2,
MOCK_RECEIPT_SCRIPT_RESULT,
MOCK_RECEIPT_TRANSFER,
MOCK_RECEIPT_TRANSFER_OUT,
MOCK_TRANSACTION_RAWPAYLOAD,
} from '../../../test/fixtures/transaction-summary';
Expand All @@ -33,14 +32,12 @@ import {
addOperation,
getContractCallOperations,
getContractCreatedOperations,
getContractTransferOperations,
getOperations,
getPayProducerOperations,
getReceiptsCall,
getReceiptsMessageOut,
getReceiptsTransferOut,
getTransactionTypeName,
getTransferOperations,
getWithdrawFromFuelOperations,
isType,
isTypeCreate,
Expand Down Expand Up @@ -243,134 +240,6 @@ describe('operations', () => {
});
});

describe('getTransferOperations', () => {
it('should ensure getTransferOperations return transfer operations from coin inputs', () => {
const expected: Operation = {
assetsSent: [
{
amount: bn('0x1'),
assetId: '0x0000000000000000000000000000000000000000000000000000000000000000',
},
],
from: {
address: '0x3e7ddda4d0d3f8307ae5f1aed87623992c1c4decefec684936960775181b2302',
type: 1,
},
name: OperationName.transfer,
to: {
address: '0xf65d6448a273b531ee942c133bb91a6f904c7d7f3104cdaf6b9f7f50d3518871',
type: 1,
},
};

const operations = getTransferOperations({
inputs: [MOCK_INPUT_COIN, MOCK_INPUT_COIN],
outputs: [MOCK_OUTPUT_COIN, MOCK_OUTPUT_CHANGE],
receipts: [],
});
expect(operations.length).toEqual(1);

expect(operations[0]).toStrictEqual(expected);
});

it('should ensure getTransferOperations return transfer operations from coin inputs (CONTRACT_TRANSFER)', () => {
const expected: Operation = {
assetsSent: [
{
amount: bn('0x3dc'),
assetId: '0x0000000000000000000000000000000000000000000000000000000000000000',
},
],
from: {
address: '0x3e7ddda4d0d3f8307ae5f1aed87623992c1c4decefec684936960775181b2302',
type: AddressType.account,
},
name: OperationName.transfer,
to: {
address: '0x0a98320d39c03337401a4e46263972a9af6ce69ec2f35a5420b1bd35784c74b1',
type: AddressType.contract,
},
};

const operations = getTransferOperations({
inputs: [MOCK_INPUT_CONTRACT, MOCK_INPUT_COIN, MOCK_INPUT_COIN],
outputs: [MOCK_OUTPUT_CONTRACT, MOCK_OUTPUT_CHANGE],
receipts: [MOCK_RECEIPT_TRANSFER],
});
expect(operations.length).toEqual(1);

expect(operations[0]).toStrictEqual(expected);
});

it('should ensure getTransferOperations return transfer operations from message inputs', () => {
const expected: Operation = {
assetsSent: [
{
amount: bn('0x1'),
assetId: '0x0000000000000000000000000000000000000000000000000000000000000000',
},
],
from: {
address: '0x06300e686a5511c7ba0399fc68dcbe0ca2d8f54f7e6afea73c505dd3bcacf33b',
type: 1,
},
name: OperationName.transfer,
to: {
address: '0xf65d6448a273b531ee942c133bb91a6f904c7d7f3104cdaf6b9f7f50d3518871',
type: 1,
},
};

const operations = getTransferOperations({
inputs: [MOCK_INPUT_MESSAGE, MOCK_INPUT_MESSAGE],
outputs: [MOCK_OUTPUT_COIN, MOCK_OUTPUT_CHANGE],
receipts: [],
});

expect(operations.length).toEqual(1);
expect(operations[0]).toStrictEqual(expected);
});

it('should ensure getTransferOperations return transfer operations from message inputs (CONTRACT_TRANSFER)', () => {
const expected: Operation = {
assetsSent: [
{
amount: bn('0x3dc'),
assetId: '0x0000000000000000000000000000000000000000000000000000000000000000',
},
],
from: {
address: '0x06300e686a5511c7ba0399fc68dcbe0ca2d8f54f7e6afea73c505dd3bcacf33b',
type: AddressType.account,
},
name: OperationName.transfer,
to: {
address: '0x0a98320d39c03337401a4e46263972a9af6ce69ec2f35a5420b1bd35784c74b1',
type: AddressType.contract,
},
};

const operations = getTransferOperations({
inputs: [MOCK_INPUT_CONTRACT, MOCK_INPUT_MESSAGE, MOCK_INPUT_MESSAGE],
outputs: [MOCK_OUTPUT_CONTRACT, MOCK_OUTPUT_CHANGE],
receipts: [MOCK_RECEIPT_TRANSFER],
});
expect(operations.length).toEqual(1);

expect(operations[0]).toStrictEqual(expected);
});

it('should ensure getTransferOperations return empty', () => {
const operations = getTransferOperations({
inputs: [MOCK_INPUT_CONTRACT, MOCK_INPUT_COIN],
outputs: [MOCK_OUTPUT_CONTRACT, MOCK_OUTPUT_VARIABLE, MOCK_OUTPUT_CHANGE],
receipts: [],
});

expect(operations.length).toEqual(0);
});
});

describe('getWithdrawFromFuelOperations', () => {
it('should ensure getWithdrawFromFuelOperations return withdraw from fuel operations', () => {
const expected: Operation = {
Expand Down Expand Up @@ -426,64 +295,19 @@ describe('operations', () => {
});
});

describe('getContractTransferOperations', () => {
it('should ensure getContractTransferOperations return contract transfer operations', () => {
const expected: Operation = {
assetsSent: [
{
amount: bn('0x5f5e100'),
assetId: '0x0000000000000000000000000000000000000000000000000000000000000000',
},
],
from: {
address: '0x0a98320d39c03337401a4e46263972a9af6ce69ec2f35a5420b1bd35784c74b1',
type: 0,
},
name: OperationName.contractTransfer,
to: {
address: '0x3e7ddda4d0d3f8307ae5f1aed87623992c1c4decefec684936960775181b2302',
type: 1,
},
};

const operations = getContractTransferOperations({
receipts: [
MOCK_RECEIPT_CALL,
MOCK_RECEIPT_TRANSFER_OUT,
MOCK_RECEIPT_RETURN_DATA_1,
MOCK_RECEIPT_RETURN_DATA_2,
MOCK_RECEIPT_SCRIPT_RESULT,
],
});

expect(operations.length).toEqual(1);

expect(operations[0]).toStrictEqual(expected);
});

it('should ensure getContractTransferOperations return empty', () => {
const operations = getContractTransferOperations({
receipts: [MOCK_RECEIPT_RETURN, MOCK_RECEIPT_SCRIPT_RESULT],
});

expect(operations.length).toEqual(0);
});
});

describe('getOperation', () => {
it('should getOperations return contract call and contract transfer operations', () => {
const expected: Operation[] = [
{
name: OperationName.contractCall,
calls: [],
name: OperationName.transfer,
from: {
type: AddressType.account,
address: '0x3e7ddda4d0d3f8307ae5f1aed87623992c1c4decefec684936960775181b2302',
},
to: {
type: AddressType.contract,
address: '0x0a98320d39c03337401a4e46263972a9af6ce69ec2f35a5420b1bd35784c74b1',
},
to: {
type: AddressType.account,
address: '0x3e7ddda4d0d3f8307ae5f1aed87623992c1c4decefec684936960775181b2302',
},
assetsSent: [
{
amount: bn(100000000),
Expand All @@ -492,15 +316,16 @@ describe('operations', () => {
],
},
{
name: OperationName.contractTransfer,
name: OperationName.contractCall,
calls: [],
from: {
type: AddressType.contract,
address: '0x0a98320d39c03337401a4e46263972a9af6ce69ec2f35a5420b1bd35784c74b1',
},
to: {
type: AddressType.account,
address: '0x3e7ddda4d0d3f8307ae5f1aed87623992c1c4decefec684936960775181b2302',
},
to: {
type: AddressType.contract,
address: '0x0a98320d39c03337401a4e46263972a9af6ce69ec2f35a5420b1bd35784c74b1',
},
assetsSent: [
{
amount: bn(100000000),
Expand Down Expand Up @@ -573,8 +398,9 @@ describe('operations', () => {
receipts: receiptsCallNoAmount,
maxInputs: bn(255),
});

expect(operations.length).toEqual(2);
expect(operations[0]).toStrictEqual(operationsCallNoAmount); // contract call
expect(operations[1]).toStrictEqual(operationsCallNoAmount); // contract call
});

it('should getOperations return transfer operations from coin input', () => {
Expand Down Expand Up @@ -609,6 +435,8 @@ describe('operations', () => {
});

it('should getOperations return transfer operations from message input', () => {
const sender = '0x06300e686a5511c7ba0399fc68dcbe0ca2d8f54f7e6afea73c505dd3bcacf33b';

const expected: Operation = {
assetsSent: [
{
Expand All @@ -617,7 +445,7 @@ describe('operations', () => {
},
],
from: {
address: '0x06300e686a5511c7ba0399fc68dcbe0ca2d8f54f7e6afea73c505dd3bcacf33b',
address: sender,
type: 1,
},
name: OperationName.transfer,
Expand All @@ -630,7 +458,7 @@ describe('operations', () => {
const operations = getOperations({
transactionType: TransactionType.Script,
inputs: [MOCK_INPUT_MESSAGE, MOCK_INPUT_MESSAGE],
outputs: [MOCK_OUTPUT_COIN, MOCK_OUTPUT_CHANGE],
outputs: [MOCK_OUTPUT_COIN, { ...MOCK_OUTPUT_CHANGE, to: sender }],
receipts: [MOCK_RECEIPT_RETURN, MOCK_RECEIPT_SCRIPT_RESULT],
maxInputs: bn(255),
});
Expand Down
Loading

0 comments on commit 086dbba

Please sign in to comment.