Skip to content

Commit

Permalink
feat: implement batch transfer to contracts (#3335)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel Bate <[email protected]>
Co-authored-by: Chad Nehemiah <[email protected]>
  • Loading branch information
3 people authored Oct 21, 2024
1 parent 955d8cc commit eede61c
Show file tree
Hide file tree
Showing 7 changed files with 307 additions and 114 deletions.
5 changes: 5 additions & 0 deletions .changeset/popular-seals-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/account": patch
---

feat: implement batch transfer to contracts
68 changes: 58 additions & 10 deletions apps/docs-snippets/src/guide/cookbook/transferring-assets.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Address, BN, Wallet } from 'fuels';
import { launchTestNode } from 'fuels/test-utils';
import type { ContractTransferParams, ReceiptTransfer } from 'fuels';
import { BN, ReceiptType, Wallet } from 'fuels';
import { launchTestNode, TestAssetId } from 'fuels/test-utils';

import { CounterFactory } from '../../../test/typegen';
import { CounterFactory, TokenFactory } from '../../../test/typegen';

/**
* @group node
Expand Down Expand Up @@ -111,7 +112,7 @@ describe('Transferring Assets', () => {
// #endregion transferring-assets-3
});

it('should successfully prepare transfer transaction request', async () => {
it('should successfully transfer to contract', async () => {
using launched = await launchTestNode({
contractsConfigs: [
{
Expand All @@ -124,25 +125,72 @@ describe('Transferring Assets', () => {
wallets: [sender],
contracts: [deployedContract],
} = launched;
const contractId = Address.fromAddressOrString(deployedContract.id);
// #region transferring-assets-4
// #import { Wallet, BN };

// #context const senderWallet = Wallet.fromPrivateKey('...');
// #context const sender = Wallet.fromPrivateKey('...');

const amountToTransfer = 400;
const assetId = provider.getBaseAssetId();
// #context const contractId = Address.fromAddressOrString('0x123...');
const contractId = deployedContract.id;

const contractBalance = await deployedContract.getBalance(assetId);

const tx = await sender.transferToContract(contractId, amountToTransfer, assetId);
await tx.waitForResult();
expect(new BN(contractBalance).toNumber()).toBe(0);

await tx.waitForResult();

expect(new BN(contractBalance).toNumber()).toBe(0);
expect(new BN(await deployedContract.getBalance(assetId)).toNumber()).toBe(amountToTransfer);
// #endregion transferring-assets-4
});

it('should successfully batch transfer to contracts', async () => {
using launched = await launchTestNode({
contractsConfigs: [
{
factory: CounterFactory,
},
{
factory: TokenFactory,
},
],
});
const {
provider,
wallets: [sender],
contracts: [contract1, contract2],
} = launched;

// #region transferring-assets-5
const baseAssetId = provider.getBaseAssetId();
const assetA = TestAssetId.A.value;

const contractTransferParams: ContractTransferParams[] = [
{
contractId: contract1.id,
amount: 999,
assetId: baseAssetId,
},
{
contractId: contract1.id,
amount: 550,
assetId: assetA,
},
{
contractId: contract2.id,
amount: 200,
assetId: assetA,
},
];

const submit = await sender.batchTransferToContracts(contractTransferParams);
const txResult = await submit.waitForResult();
// #endregion transferring-assets-5

const transferReceipts = txResult.receipts.filter(
(receipt) => receipt.type === ReceiptType.Transfer
) as ReceiptTransfer[];

expect(transferReceipts.length).toBe(contractTransferParams.length);
});
});
18 changes: 13 additions & 5 deletions apps/docs/src/guide/wallets/wallet-transferring.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ This method also creates a `ScriptTransactionRequest` and populates it with the
<<< @/../../docs-snippets/src/guide/cookbook/transferring-assets.test.ts#transferring-assets-3{ts:line-numbers}

## Transferring Assets To Multiple Wallets

To transfer assets to multiple wallets, use the `Account.batchTransfer` method:

<<< @/../../docs-snippets/src/guide/wallets/wallet-transferring.test.ts#wallet-transferring-6{ts:line-numbers}

## Transferring Assets To Contracts

When transferring assets to a deployed contract, we use the `transferToContract` method, which shares a similar parameter structure with the `transfer` method.
Expand All @@ -42,15 +48,17 @@ Here's an example demonstrating how to use `transferToContract`:

<<< @/../../docs-snippets/src/guide/cookbook/transferring-assets.test.ts#transferring-assets-4{ts:line-numbers}

Always remember to call the `waitForResult()` function on the transaction response. That ensures the transaction has been mined successfully before proceeding.
_Note: Use `transferToContract` exclusively for transfers to a contract. For transfers to an account address, use `transfer` instead._

## Transferring Assets To Multiple Wallets
## Transferring Assets To Multiple Contracts

To transfer assets to multiple wallets, use the `Account.batchTransfer` method:
Similar to the `Account.batchTransfer` method, you can transfer multiple assets to multiple contracts using the `Account.batchTransferToContracts` method. Here's how it works:

<<< @/../../docs-snippets/src/guide/wallets/wallet-transferring.test.ts#wallet-transferring-6{ts:line-numbers}
<<< @/../../docs-snippets/src/guide/cookbook/transferring-assets.test.ts#transferring-assets-5{ts:line-numbers}

Always remember to call the `waitForResult()` function on the transaction response. That ensures the transaction has been mined successfully before proceeding.

This section demonstrates additional examples of transferring assets between wallets and to contracts.
_Note: Use `batchTransferToContracts` solely for transferring assets to contracts. Do not use account addresses with this method. For multiple account transfers, use `batchTransfer` instead._

## Checking Balances

Expand Down
72 changes: 44 additions & 28 deletions packages/account/src/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ export type TransferParams = {
assetId?: BytesLike;
};

export type ContractTransferParams = {
contractId: string | AbstractAddress;
amount: BigNumberish;
assetId?: BytesLike;
};

export type EstimatedTxParams = Pick<
TransactionCost,
'estimatedPredicates' | 'addedSignatures' | 'requiredQuantities' | 'updateMaxFee' | 'gasPrice'
Expand Down Expand Up @@ -433,41 +439,50 @@ export class Account extends AbstractAccount {
assetId?: BytesLike,
txParams: TxParamsType = {}
): Promise<TransactionResponse> {
if (bn(amount).lte(0)) {
throw new FuelError(
ErrorCode.INVALID_TRANSFER_AMOUNT,
'Transfer amount must be a positive number.'
);
}

const contractAddress = Address.fromAddressOrString(contractId);
const assetIdToTransfer = assetId ?? this.provider.getBaseAssetId();
const { script, scriptData } = await assembleTransferToContractScript({
hexlifiedContractId: contractAddress.toB256(),
amountToTransfer: bn(amount),
assetId: assetIdToTransfer,
});
return this.batchTransferToContracts([{ amount, assetId, contractId }], txParams);
}

async batchTransferToContracts(
contractTransferParams: ContractTransferParams[],
txParams: TxParamsType = {}
): Promise<TransactionResponse> {
let request = new ScriptTransactionRequest({
...txParams,
script,
scriptData,
});

request.addContractInputAndOutput(contractAddress);
const quantities: CoinQuantity[] = [];

const txCost = await this.getTransactionCost(request, {
quantities: [{ amount: bn(amount), assetId: String(assetIdToTransfer) }],
});
const transferParams = contractTransferParams.map((transferParam) => {
const amount = bn(transferParam.amount);
const contractAddress = Address.fromAddressOrString(transferParam.contractId);

request = this.validateGasLimitAndMaxFee({
transactionRequest: request,
gasUsed: txCost.gasUsed,
maxFee: txCost.maxFee,
txParams,
const assetId = transferParam.assetId
? hexlify(transferParam.assetId)
: this.provider.getBaseAssetId();

if (amount.lte(0)) {
throw new FuelError(
ErrorCode.INVALID_TRANSFER_AMOUNT,
'Transfer amount must be a positive number.'
);
}

request.addContractInputAndOutput(contractAddress);
quantities.push({ amount, assetId });

return {
amount,
contractId: contractAddress.toB256(),
assetId,
};
});

await this.fund(request, txCost);
const { script, scriptData } = await assembleTransferToContractScript(transferParams);

request.script = script;
request.scriptData = scriptData;

request = await this.estimateAndFundTransaction(request, txParams, { quantities });

return this.sendTransaction(request);
}
Expand Down Expand Up @@ -693,10 +708,11 @@ export class Account extends AbstractAccount {
/** @hidden * */
private async estimateAndFundTransaction(
transactionRequest: ScriptTransactionRequest,
txParams: TxParamsType
txParams: TxParamsType,
costParams?: TransactionCostParams
) {
let request = transactionRequest;
const txCost = await this.getTransactionCost(request);
const txCost = await this.getTransactionCost(request, costParams);
request = this.validateGasLimitAndMaxFee({
transactionRequest: request,
gasUsed: txCost.gasUsed,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BigNumberCoder } from '@fuel-ts/abi-coder';
import { ZeroBytes32 } from '@fuel-ts/address/configs';
import { getRandomB256 } from '@fuel-ts/address';
import type { BytesLike } from '@fuel-ts/interfaces';
import type { BigNumberish } from '@fuel-ts/math';
import { bn, type BigNumberish } from '@fuel-ts/math';
import * as arrayifyMod from '@fuel-ts/utils';

import {
Expand All @@ -18,26 +18,30 @@ describe('util', () => {
});

it('should ensure "composeScriptForTransferringToContract" returns script just fine', async () => {
const hexlifiedContractId = '0x1234567890123456789012345678901234567890';
const amountToTransfer: BigNumberish = 0;
const assetId: BytesLike = ZeroBytes32;
const contractId = '0xf3eb53ed00347d305fc6f6e3a57e91ea6c3182a9efc253488db29494f63c9610';
const assetId: BytesLike = '0x0f622143ec845f9095bdf02d80273ac48556efcf9f95c1fdadb9351fd8ffcd24';
const amount: BigNumberish = bn(0);

const { script, scriptData } = await assembleTransferToContractScript({
hexlifiedContractId,
amountToTransfer,
assetId,
});
const { script, scriptData } = await assembleTransferToContractScript([
{
contractId,
amount,
assetId,
},
]);

expect(script).toStrictEqual(
new Uint8Array([
97, 64, 0, 10, 80, 69, 0, 32, 93, 73, 16, 0, 80, 77, 16, 8, 60, 65, 36, 192, 36, 4, 0, 0,
97, 64, 0, 10, 80, 69, 0, 0, 80, 73, 16, 32, 93, 77, 32, 0, 80, 81, 32, 8, 60, 69, 53, 0,
36, 4, 0, 0,
])
);
expect(scriptData).toStrictEqual(
new Uint8Array([
18, 52, 86, 120, 144, 18, 52, 86, 120, 144, 18, 52, 86, 120, 144, 18, 52, 86, 120, 144, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
243, 235, 83, 237, 0, 52, 125, 48, 95, 198, 246, 227, 165, 126, 145, 234, 108, 49, 130, 169,
239, 194, 83, 72, 141, 178, 148, 148, 246, 60, 150, 16, 0, 0, 0, 0, 0, 0, 0, 0, 15, 98, 33,
67, 236, 132, 95, 144, 149, 189, 240, 45, 128, 39, 58, 196, 133, 86, 239, 207, 159, 149,
193, 253, 173, 185, 53, 31, 216, 255, 205, 36,
])
);
});
Expand All @@ -51,15 +55,17 @@ describe('util', () => {

const arrayify = vi.spyOn(arrayifyMod, 'arrayify').mockReturnValue(Uint8Array.from(byte));

const hexlifiedContractId = '0x1234567890123456789012345678901234567890';
const amountToTransfer: BigNumberish = 0;
const assetId: BytesLike = ZeroBytes32;
const contractId = getRandomB256();
const amount: BigNumberish = bn(0);
const assetId: BytesLike = getRandomB256();

const scriptData = formatTransferToContractScriptData({
hexlifiedContractId,
amountToTransfer,
assetId,
});
const scriptData = formatTransferToContractScriptData([
{
contractId,
amount,
assetId,
},
]);

expect(scriptData).toStrictEqual(Uint8Array.from([].concat(...Array(3).fill(byte))));

Expand Down
Loading

0 comments on commit eede61c

Please sign in to comment.