Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add function for splitting UTXOs and docs on maxOutputs #3435

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c105246
docs: add max outputs documentation to combining UTXO docs
maschad Nov 30, 2024
35ac54c
docs: add changeset
maschad Nov 30, 2024
db1d8cd
docs: updated spelling
maschad Nov 30, 2024
799cedb
docs: linting fixes
maschad Dec 1, 2024
69f721c
Merge branch 'master' into mc/docs/add-docs-around-utxo-splitting
maschad Dec 2, 2024
bf6a2da
docs: add max inputs and outputs snippet
maschad Dec 2, 2024
08a08ff
Merge branch 'master' into mc/docs/add-docs-around-utxo-splitting
maschad Dec 3, 2024
81588ab
feat: add function for splitting UTXOs and docs on maxOutputs
maschad Dec 3, 2024
1340ca5
docs: update changeset
maschad Dec 3, 2024
34bfb91
build: update lockfile
maschad Dec 3, 2024
922796e
build: update spellcheck
maschad Dec 3, 2024
5e1049c
test: add environment to tests
maschad Dec 4, 2024
8d616c7
fix: update return type from splitting utxos function
maschad Dec 4, 2024
a04142b
Merge branch 'master' into mc/docs/add-docs-around-utxo-splitting
maschad Dec 4, 2024
f1b8088
docs: add vitepress config for docs
maschad Dec 4, 2024
5aa78b8
test: cover fees
maschad Dec 4, 2024
1d4e5fb
docs: update title
maschad Dec 4, 2024
04fc429
docs: update grammatical issues
maschad Dec 4, 2024
2f01c06
docs: update grammar
maschad Dec 4, 2024
5d71683
docs: remove unnecessary fullstop
maschad Dec 4, 2024
4600252
docs: update snippets comment
maschad Dec 4, 2024
ebf5d27
docs: update snippet formatting
maschad Dec 4, 2024
0568b11
fix: adjust tests and documentation errors
maschad Dec 4, 2024
ecefb57
docs: update JS docs
maschad Dec 4, 2024
b7e6fb4
docs: adjust grammatical issue in max inputs/outputs
maschad Dec 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/rotten-chefs-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/utils": patch
---

feat: add function for splitting UTXOs and docs on `maxOutputs`
4 changes: 4 additions & 0 deletions apps/docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,10 @@ export default defineConfig({
text: 'Combining UTXOs',
link: '/guide/cookbook/combining-utxos',
},
{
text: 'Splitting UTXOs',
link: '/guide/cookbook/splitting-utxos',
},
],
},
{
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/spell-check-custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ tsconfig
TTL
tuple's
turbofish
TxParameters
TypeChain
typeclass
typedoc
Expand Down Expand Up @@ -321,6 +322,7 @@ Utils
Utils
UTXO
UTXOs
utxos
validator
validators
vercel
Expand Down
6 changes: 6 additions & 0 deletions apps/docs/src/guide/cookbook/combining-utxos.md
petertonysmith94 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ One way to avoid these errors is to combine your UTXOs. This can be done by perf
> **Note:** You will not be able to have a single UTXO for the base asset after combining, as one output will be for the transfer, and you will have another for the fees.
<<< @./snippets/combining-utxos.ts#combining-utxos{ts:line-numbers}

## Max Inputs and Outputs

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit

Suggested change
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
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.


<<< @./snippets/max-outputs.ts#max-outputs{ts:line-numbers}
13 changes: 13 additions & 0 deletions apps/docs/src/guide/cookbook/snippets/max-outputs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// #region max-outputs
import { Provider } from 'fuels';

import { LOCAL_NETWORK_URL } from '../../../env';

const provider = await Provider.create(LOCAL_NETWORK_URL);

const { maxInputs, maxOutputs } =
provider.getChain().consensusParameters.txParameters;

// #endregion max-outputs
console.log('Max Inputs', maxInputs);
console.log('Max Outputs', maxOutputs);
55 changes: 55 additions & 0 deletions apps/docs/src/guide/cookbook/snippets/splitting-utxos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// #region splitting-utxos
import { BN, Provider, Wallet, splitUTXOs } from 'fuels';

import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../env';

const provider = await Provider.create(LOCAL_NETWORK_URL);
const fundingWallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);

const wallet = Wallet.generate({ provider });

// Let's fund the wallet with 1000 of the base asset
const fundingTx = await fundingWallet.transfer(
wallet.address,
1000,
provider.getBaseAssetId()
);
await fundingTx.waitForResult();

// We can fetch the coins to see how many UTXOs we have and confirm it is 1
const { coins: initialCoins } = await wallet.getCoins(
provider.getBaseAssetId()
);
console.log('Initial Coins Length', initialCoins.length);
// 1

// Now we can split the UTXO into 5 UTXOs of 200 each
const splitTx = splitUTXOs(
new BN(1000),
new BN(200),
provider.getBaseAssetId(),
wallet.address,
5
);

// We will also add some funds to the wallet to cover the fee
const fundTx = await fundingWallet.transfer(
wallet.address,
500,
provider.getBaseAssetId()
);
await fundTx.waitForResult();
Comment on lines +35 to +41
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this part of the documentation. Why is the wallet being funded a second time?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the wallet has a balance of 1000 comprised of 5 UTXOs each worth 200. Therefore there is an insufficient amount to cover the fee.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we better clarify this in the comment where we fund the wallet the second time?


console.log('Split UTXOs', splitTx);
// [
// { amount: 200, assetId: '0x0', destination : '0x...' },
// { amount: 200, assetId: '0x0', destination: '0x...' },
// { amount: 200, assetId: '0x0', destination: '0x...' },
// { amount: 200, assetId: '0x0', destination: '0x...' },
// { amount: 200, assetId: '0x0', destination: '0x...' }
// ]

// Then we can send the transactions using the batchTransfer function
const batchTx = await wallet.batchTransfer(splitTx);
await batchTx.waitForResult();
// #endregion splitting-utxos
7 changes: 7 additions & 0 deletions apps/docs/src/guide/cookbook/splitting-utxos.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Splitting UTXOs

There may be times when you want to split one large UTXO into multiple smaller UTXOs. This can be useful if you want to send multiple concurrent transactions without having to wait for them to be processed sequentially.

To split a UTXO, you can use the `splitUTXOs` function. This will return an array of the number of UTXOs each with the specified amount, as long as the balance is greater than the amount.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
To split a UTXO, you can use the `splitUTXOs` function. This will return an array of the number of UTXOs each with the specified amount, as long as the balance is greater than the amount.
To split a UTXO, you can use the `splitUTXOs` function. This will return an array of the number of UTXOs each with the specified amount, as long as the balance is greater than the combined amount of the split UTXOs.

Maybe more clear?


<<< @./snippets/splitting-utxos.ts#splitting-utxos{ts:line-numbers}
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from './utils/dataSlice';
export * from './utils/toUtf8Bytes';
export * from './utils/toUtf8String';
export * from './utils/bytecode';
export * from './utils/split-utxos';

/**
* Used to verify that a switch statement exhausts all variants.
Expand Down
53 changes: 53 additions & 0 deletions packages/utils/src/utils/split-utxos.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { FuelError, ErrorCode } from '@fuel-ts/errors';
import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
import type { AbstractAddress } from '@fuel-ts/interfaces';
import { BN } from '@fuel-ts/math';

import { splitUTXOs } from './split-utxos';

/**
* @group node
* @group browser
*/
describe('splitUTXOs', () => {
it('should generate two UTXOs by default', () => {
// using unknown as AbstractAddress to avoid importing @fuel-ts/address as that's a circular dependency
const destination = '0x...' as unknown as AbstractAddress;
Comment on lines +13 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This indicates that we should define this helper somewhere else. We can put it within the account package. The result will be the same since users will import it from fuels.

Copy link
Member Author

@maschad maschad Dec 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree, this is a utility specific to UTXOs and had I chosen to accept the destination param as a string this wouldn't be necessary.
I could move it to the account package to avoid this cast but I don't think that this functionality is structurally apart of what the account package should offer


expect(splitUTXOs(new BN(100), new BN(10), '0x0', destination)).toEqual([
{ amount: new BN(10), assetId: '0x0', destination },
{ amount: new BN(10), assetId: '0x0', destination },
]);
});

it('should throw an error if the balance is zero', async () => {
const destination = '0x...' as unknown as AbstractAddress;

await expectToThrowFuelError(
() => splitUTXOs(new BN(0), new BN(10), '0x0', destination),
new FuelError(ErrorCode.INVALID_DATA, 'Balance must be greater than zero')
);
});

it('should generate an exact number of UTXOs if specified', () => {
const destination = '0x...' as unknown as AbstractAddress;

expect(splitUTXOs(new BN(100), new BN(10), '0x0', destination, 3)).toEqual([
{ amount: new BN(10), assetId: '0x0', destination },
{ amount: new BN(10), assetId: '0x0', destination },
{ amount: new BN(10), assetId: '0x0', destination },
]);
});

it('should throw an error if the number of coins to split into is greater than the balance', async () => {
const destination = '0x...' as unknown as AbstractAddress;

await expectToThrowFuelError(
() => splitUTXOs(new BN(40), new BN(10), '0x0', destination, 5),
new FuelError(
ErrorCode.INVALID_DATA,
'The number of coins to split into is greater than the balance'
)
);
});
});
36 changes: 36 additions & 0 deletions packages/utils/src/utils/split-utxos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ErrorCode, FuelError } from '@fuel-ts/errors';
import type { AbstractAddress } from '@fuel-ts/interfaces';
import type { BN } from '@fuel-ts/math';

/**
* This function allows a consumer to split a UTXO into multiple smaller UTXOs
* @param balance - The total balance that the user wants to split, this is equivalent of the amount you would like to send to the recipient
* @param amount - The amount of each UTXO
* @param assetId - The asset ID of the UTXOs
* @param destination - The destination address for the UTXOs
* @param number_of_coins - The number of UTXOs to split into, it defaults to 2
* @returns An array of the desired UTXOs with the amount, assetId, and destination address
*/
export function splitUTXOs(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The naming splitUTXOs implies that this function would be accepting actual UTXOs and creating transaction outputs that would be added to a transaction's outputs in a fashion like this:

const txRequest = new ScriptTransactionRequest();

txRequest.addCoinInput(coin1);
txRequest.addCoinInput(coin2);

const outputs = splitUTXOs({
  utxos: txRequest.getCoinInputs(),
  destination: '0x...',
  amount: bn(10),
});

txRequest.addCoinOutputs(outputs);

With the current implementation of this function, a more appropriate name would be generateTransactionOutputs. One could still use it in a similar fashion as in the code block above by summing the amounts of the coins, but because it's not taking in any UTXO its name is misaligned.

balance: BN,
amount: BN,
assetId: string,
destination: AbstractAddress,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe destination could just take a string as it is returning a string? And then we don't need the unknown cast in the above test?

Copy link
Member Author

@maschad maschad Dec 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion the benefits of enforcing the address type outweigh the cast in the test. The primary goal here is to validate user input not to have "clean" tests per say.

maschad marked this conversation as resolved.
Show resolved Hide resolved
number_of_coins: number = 2
): { amount: BN; assetId: string; destination: string }[] {
if (balance.lte(0)) {
throw new FuelError(ErrorCode.INVALID_DATA, 'Balance must be greater than zero');
}
if (balance.divRound(amount).lt(number_of_coins)) {
throw new FuelError(
ErrorCode.INVALID_DATA,
'The number of coins to split into is greater than the balance'
);
}

return new Array(number_of_coins).fill({
amount,
assetId,
destination,
});
}
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading