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

docs: add docs for tx deps prefetching optimisation #3499

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .changeset/six-waves-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
4 changes: 4 additions & 0 deletions apps/docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ export default defineConfig({
text: 'Methods',
link: '/guide/contracts/methods',
},
{
text: 'Optimising Contract Calls',
link: '/guide/contracts/optimising-contract-calls',
},
Comment on lines +198 to +201
Copy link
Member

Choose a reason for hiding this comment

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

It seems confusing to mix React tips with core contract documentation.

A simple React recipe demoing the example hook may suffice.

Copy link
Member Author

Choose a reason for hiding this comment

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

I disagree that this is a React tip. Like I mentioned here: #3499 (comment)

{
text: 'Call Parameters',
link: '/guide/contracts/call-parameters',
Expand Down
5 changes: 4 additions & 1 deletion apps/docs/spell-check-custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ Macbook
macOS
MacOS
mainnet
memoize
memoized
mempool
merkle
Merkle
Expand Down Expand Up @@ -229,6 +231,7 @@ relayer
relayers
repo
repos
reprepare
reuploaded
rhs
RPC
Expand Down Expand Up @@ -345,4 +348,4 @@ Workspaces
WSL
XOR
XORs
YAML
YAML
106 changes: 106 additions & 0 deletions apps/docs/src/guide/contracts/optimising-contract-calls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Optimizing Contract Calls
Copy link
Member

Choose a reason for hiding this comment

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

We are not optimizing contract calls.

Suggested change
# Optimizing Contract Calls
# Optimizing Contract Estimation in React Apps

Copy link
Member Author

Choose a reason for hiding this comment

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

I disagree, the estimation itself isn't optimized - it's just done in advance which leads to faster/optimised contract calls.


When you call a contract function via the SDK, it makes two essential network requests:

1. **Preparing the transaction**: This involves creating the transaction request, fetching dependencies for it and funding it properly.

2. **Sending the transaction**: This involves sending the transaction to the network and waiting for it to be confirmed.

The below flowchart shows this entire process:

![Transaction Lifecycle in the SDK without prefetching](../../public/txdep1.png)
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, I'm hesitant about adding images to the docs.

Copy link
Member Author

Choose a reason for hiding this comment

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

Why?


The SDK will prepare the contract call _after_ the user has submitted the transaction at the application level. This is so the transaction is funded in it's finalised state. However this makes the chain feel slower than it actually is as we are making two network requests.

This can be mitigated by preparing the contract call _before_ the user submits the transaction. If the transaction is prepared beforehand, the SDK only has to send the transaction to the network and wait for it to be confirmed. This reflects the actual speed of the chain to the user.

You can experience this yourself by trying out this [demo](https://fuel-wallet-prefetch-experiment-75ug.vercel.app/).
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm should this be brought inside the SDK? Is it more beneficial for this to live inside a snippet?

Copy link
Member Author

Choose a reason for hiding this comment

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

This = demo or the hook?

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor

@danielbate danielbate Dec 24, 2024

Choose a reason for hiding this comment

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

The demo, usually we don't have any external deps for the docs, the links is from an external projects on Dhai's github


![Transaction Lifecycle in the SDK with prefetching](../../public/txdep2.png)

Because of the massive performance gains, we recommend this strategy for all contract calls.

## Recommended Implementation

> [!Note] This is an example for React, but the same logic can be applied to any framework.

We recommend creating a `usePrepareContractCall` hook:

```tsx
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this all be in a snippet for forwards compatibility?

Copy link
Member Author

Choose a reason for hiding this comment

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

It could be; but that would mean us having to create a new project for snippets since this is a React snippet. In the interest of speed and considering how important this info is for our ecosystem projects, I think it is fine if we create the snippets in a separate PR. Nick wants us to announce this asap on Twitter

Copy link
Member

Choose a reason for hiding this comment

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

As much as we love teaching devs how to fetch data in React, we still have two more weeks of code freeze. 🙂

Copy link
Member Author

Choose a reason for hiding this comment

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

I disagree, this is not about teaching people how to fetch data in React. It's about the overall idea of preparing contract calls in advance, leading to a faster transaction experience.

About the code freeze, this is not adding any new code. Can we not get this in?

import { FunctionInvocationScope, ScriptTransactionRequest } from "fuels";
import { useEffect, useState, useCallback, useRef } from "react";

export const usePrepareContractCall = (fn?: FunctionInvocationScope) => {
const [preparedTxReq, setPreparedTxReq] =
useState<ScriptTransactionRequest>();
const fnRef = useRef(fn);

useEffect(() => {
if (fn && fn !== fnRef.current) {
fnRef.current = fn;
(async () => {
const txReq = await fn.fundWithRequiredCoins();
setPreparedTxReq(txReq);
})();
}
}, [fn]);

const reprepareTxReq = useCallback(async () => {
if (!fnRef.current) {
return;
}
const txReq = await fnRef.current.fundWithRequiredCoins();
setPreparedTxReq(txReq);
}, []);

return {
preparedTxReq,
reprepareTxReq,
};
};
```

This hook will prepare the transaction request for your contract call beforehand, and you can use the `reprepareTxReq` function to reprepare the transaction request if needed.

You can use this hook in your UI logic like this:

```tsx
function YourPage() {
// It is important to memoize the function call object.
const incrementFunction = useMemo(() => {
if (!contract || !wallet) return undefined;
return contract.functions.increment_counter(amount);
}, [contract, wallet, amount]);

const { preparedTxReq, reprepareTxReq } =
usePrepareContractCall(incrementFunction);

const onIncrementPressed = async () => {
if (preparedTxReq) {
// Use the prepared transaction request if it is available. Much faster.
await wallet.sendTransaction(preparedTxReq);
} else {
// Fallback to the regular call if the transaction request is not available.
await contract.functions.increment_counter(1).call();
}

/*
* Reprepare the transaction request since the user may want to increment again.
* This is important, since the old `preparedTxReq` will not be valid anymore
* because it contains UTXOs that have been used among other things.
*
* You *must* re-prepare any prepared transaction requests whenever
* the user does any transaction.
*/
await reprepareTxReq();
};
}
```

You can view the full code for this example [here](https://github.com/Dhaiwat10/fuel-wallet-prefetch-experiment/blob/main/src/components/Contract.tsx).

## Important Things to Note

- You _must_ re-prepare any prepared transaction requests whenever the user does any transaction. This is because the prepared transaction request will contain UTXOs that have been used among other things, and will therefore not be valid anymore.

- You _must_ memoize the function call object. This is because the function call object is used to prepare the transaction request, and if it is not memoized, the function call object will be recreated on every render, and the transaction request will not be prepared correctly.
Binary file added apps/docs/src/public/txdep1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/docs/src/public/txdep2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading