Skip to content

Commit 55ad003

Browse files
feat: consolidate non-base asset (#3923)
* Implemented basic functionality for consolidation of non base assets * Finalized the consolidation process * Lintfix * Updated documentation * Changeset * Finalised non-base asset consolidation
1 parent 9810db5 commit 55ad003

File tree

4 files changed

+531
-127
lines changed

4 files changed

+531
-127
lines changed

.changeset/wild-dryers-retire.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+
feat: consolidate non-base asset

apps/docs/src/guide/cookbook/combining-utxos.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,13 @@ The SDK provides a built-in method to consolidate your base asset UTXOs:
1414

1515
The `consolidateCoins` method accepts the following parameters:
1616

17-
- `assetId`: The ID of the asset to consolidate (currently supports only the base asset)
17+
- `assetId`: The ID of the asset to consolidate
18+
1819
- `mode` (optional): How to submit consolidation transactions
1920
- `'parallel'` (default): Submit all transactions simultaneously for faster processing
2021
- `'sequential'`: Submit transactions one after another, waiting for each to complete
2122
- `outputNum` (optional): Number of output UTXOs to create (default is 1)
2223

23-
### Limitations
24-
25-
- Currently only supports consolidating the base asset
26-
2724
## Max Inputs and Outputs
2825

2926
It's also important to note that depending on the chain configuration, you may be limited on the number of inputs and/or outputs that you can have in a transaction. These amounts can be queried via the [TxParameters](https://docs.fuel.network/docs/graphql/reference/objects/#txparameters) GraphQL query.

packages/account/src/account.ts

Lines changed: 105 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export type SubmitAllCallbackResponse = {
102102
export type SubmitAllCallback = () => Promise<SubmitAllCallbackResponse>;
103103

104104
export type AssembleConsolidationTxsParams = {
105+
assetId: string;
105106
coins: Coin[];
106107
mode?: SubmitAllMode;
107108
outputNum?: number;
@@ -640,6 +641,7 @@ export class Account extends AbstractAccount implements WithAddress {
640641

641642
let submitAll: SubmitAllCallback;
642643
const consolidationParams: AssembleConsolidationTxsParams = {
644+
assetId,
643645
coins,
644646
mode: params.mode,
645647
outputNum: params.outputNum,
@@ -648,10 +650,7 @@ export class Account extends AbstractAccount implements WithAddress {
648650
if (isBaseAsset) {
649651
({ submitAll } = await this.assembleBaseAssetConsolidationTxs(consolidationParams));
650652
} else {
651-
throw new FuelError(
652-
ErrorCode.UNSUPPORTED_FEATURE,
653-
'Consolidation for non-base assets is not supported yet.'
654-
);
653+
({ submitAll } = await this.assembleNonBaseAssetConsolidationTxs(consolidationParams));
655654
}
656655

657656
return submitAll();
@@ -668,11 +667,10 @@ export class Account extends AbstractAccount implements WithAddress {
668667
*
669668
* @returns An object containing the assembled transactions, the total fee cost, and a callback to submit all transactions.
670669
*/
671-
async assembleBaseAssetConsolidationTxs(params: AssembleConsolidationTxsParams) {
670+
async assembleBaseAssetConsolidationTxs(params: Omit<AssembleConsolidationTxsParams, 'assetId'>) {
672671
const { coins, mode = 'parallel', outputNum = 1 } = params;
673672

674673
const baseAssetId = await this.provider.getBaseAssetId();
675-
676674
this.validateConsolidationTxsCoins(coins, baseAssetId);
677675

678676
const chainInfo = await this.provider.getChain();
@@ -738,6 +736,107 @@ export class Account extends AbstractAccount implements WithAddress {
738736
return { txs, totalFeeCost, submitAll };
739737
}
740738

739+
async assembleNonBaseAssetConsolidationTxs(
740+
params: AssembleConsolidationTxsParams & { assetId: string }
741+
) {
742+
const { assetId, coins, mode = 'parallel', outputNum = 1 } = params;
743+
744+
this.validateConsolidationTxsCoins(coins, assetId);
745+
746+
const chainInfo = await this.provider.getChain();
747+
const maxInputsNumber = chainInfo.consensusParameters.txParameters.maxInputs.toNumber();
748+
749+
// Collate the base asset for funding purposes
750+
const baseAssetId = chainInfo.consensusParameters.baseAssetId;
751+
const { coins: baseAssetCoins } = await this.provider.getCoins(this.address, baseAssetId);
752+
753+
let totalFeeCost = bn(0);
754+
const txs: ScriptTransactionRequest[] = [];
755+
const gasPrice = await this.provider.estimateGasPrice(10);
756+
const consolidateMoreThanOneCoin = outputNum > 1;
757+
const assetCoinBatches = splitCoinsIntoBatches(coins, maxInputsNumber);
758+
759+
assetCoinBatches
760+
// Skip batches with just one Coin to avoid consolidate just one coin
761+
.filter((batch) => batch.length > 1)
762+
.forEach((coinBatch) => {
763+
const request = new ScriptTransactionRequest({
764+
script: '0x',
765+
});
766+
767+
request.addResources(coinBatch);
768+
769+
if (consolidateMoreThanOneCoin) {
770+
// We decrease one because the change output will also create one UTXO
771+
Array.from({ length: outputNum - 1 }).forEach(() => {
772+
// Real value will be added later after having fee calculated
773+
request.addCoinOutput(this.address, 0, assetId);
774+
});
775+
}
776+
777+
const minGas = request.calculateMinGas(chainInfo);
778+
779+
const fee = calculateGasFee({
780+
gasPrice,
781+
gas: minGas,
782+
priceFactor: chainInfo.consensusParameters.feeParameters.gasPriceFactor,
783+
tip: request.tip,
784+
});
785+
786+
request.maxFee = fee;
787+
788+
if (consolidateMoreThanOneCoin) {
789+
const total = request.inputs
790+
.filter(isRequestInputCoin)
791+
.reduce((acc, input) => acc.add(input.amount), bn(0));
792+
793+
// We add a +1 as the change output will also include one part of the total amount
794+
const amountPerNewUtxo = total.div(outputNum + 1);
795+
796+
request.outputs.forEach((output) => {
797+
if (output.type === OutputType.Coin) {
798+
output.amount = amountPerNewUtxo;
799+
}
800+
});
801+
}
802+
803+
totalFeeCost = totalFeeCost.add(fee);
804+
805+
const baseAssetResources: Coin[] = [];
806+
let fundingFeeTotal: BN = bn(0);
807+
808+
while (fundingFeeTotal.lt(fee)) {
809+
const baseAssetCoin = baseAssetCoins.pop();
810+
if (!baseAssetCoin) {
811+
break;
812+
}
813+
814+
baseAssetResources.push(baseAssetCoin);
815+
fundingFeeTotal = fundingFeeTotal.add(baseAssetCoin.amount);
816+
}
817+
818+
// Need to remove the extra assets from the request input
819+
const { inputs } = request;
820+
request.inputs = inputs.slice(0, maxInputsNumber - baseAssetResources.length);
821+
const removedCoins = coinBatch.slice(maxInputsNumber - baseAssetResources.length);
822+
823+
// Add our base assets
824+
request.addResources(baseAssetResources);
825+
826+
const lastCoinBatch = assetCoinBatches[assetCoinBatches.length - 1];
827+
lastCoinBatch.push(...removedCoins);
828+
if (lastCoinBatch.length > maxInputsNumber) {
829+
assetCoinBatches.push(lastCoinBatch.slice(maxInputsNumber));
830+
}
831+
832+
txs.push(request);
833+
});
834+
835+
const submitAll = this.prepareSubmitAll({ txs, mode });
836+
837+
return { txs, totalFeeCost, submitAll };
838+
}
839+
741840
/**
742841
* Prepares a function to submit all transactions either sequentially or in parallel.
743842
*

0 commit comments

Comments
 (0)