-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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: transaction optimization #3513
Changes from all commits
6c3460a
b5acb11
7e26880
2da9c34
f157187
43fbfc7
e2682a0
702ff62
d68e10b
23047d8
32b3145
48f00eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
--- | ||
--- | ||
|
||
docs: transaction optimization |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,26 @@ | ||||||
# Optimizing Transactions | ||||||
|
||||||
When submitting transactions using the SDK, the following actions are being performed: | ||||||
|
||||||
- Fetching chain information to compute transaction data | ||||||
- Retrieving the gas price for cost estimation | ||||||
- Simulating the transaction to obtain missing or estimating transaction data | ||||||
- Fetching funds for the transaction | ||||||
|
||||||
Depending on how you are performing the transaction, all of the above may have been abstracted away underneath a single function call that is performing multiple calls to the network to retrieve necessary information. Which gives the appearance of slowness for users interacting with your application. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
This process can be illustrated by the following diagram: | ||||||
|
||||||
![Pre-optimization Tx Process](../../public/tx-optimization-before.png) | ||||||
|
||||||
This can be mitigated by optimistically building the transaction before your user submits the transaction. Pre-preparation of the transaction can improve the perceived speed of transactions by **~2x**. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Do we need to provide a figure? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, numbers can be tricky. I still think the whole message around this may have been framed incorrectly from the idea's inception. I will review this in-depth to provide better and more actionable feedback. |
||||||
|
||||||
The process now looks like the following: | ||||||
|
||||||
![Post-optimization Tx Process](../../public/tx-optimization-after.png) | ||||||
|
||||||
Check out the following guides on implementing optimistic transaction building: | ||||||
|
||||||
- [Optimistic Transactions](./optimistic-transactions.md) | ||||||
- [Optimistic Contract Calls](./optimistic-contract-calls.md) | ||||||
- [Optimistic Predicates](./optimistic-predicates.md) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# Optimistic Contract Calls | ||
|
||
Imagine we have an application that allows a user to interact with a counter contract. | ||
|
||
On the frontend, we'd have a button that would allow the user to submit a transaction that increments a counter. | ||
|
||
```tsx | ||
<Button onClick={onIncrementPressed}>Increment</Button> | ||
``` | ||
|
||
This would likely have the following handler function: | ||
|
||
<<< @./snippets/optimistic-contract-calls-before.ts#main{ts:line-numbers} | ||
|
||
Under the hood, `call` is making multiple calls to the network to both simulate and fund the transaction, then submitting it. This may give the appearance of slowness for users interacting with your application. | ||
|
||
This process can be optimized by optimistically building the contract transaction on page load, like so: | ||
|
||
<<< @./snippets/optimistic-contract-calls-after.ts#main{ts:line-numbers} | ||
|
||
> [!NOTE] | ||
> Any change to the underlying transaction will require re-estimation and re-funding of the transaction. Otherwise the transaction could increase in size and therefore cost, causing the transaction to fail. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# Optimistic Predicates | ||
|
||
Imagine we have an application that allows a user to transfer funds to another user given a validated predicate condition, here we'll use a pin number. | ||
|
||
```tsx | ||
<Input | ||
placeholder="Enter PIN number" | ||
value={pin} | ||
onChange={(e) => setPin(e.target.value)} | ||
/> | ||
<Input | ||
placeholder="Enter recipient address" | ||
value={recipientAddress} | ||
onChange={(e) => setRecipientAddress(e.target.value)} | ||
/> | ||
<Button onClick={() => onTransferPressed(pin, recipientAddress)}>Transfer</Button> | ||
``` | ||
|
||
This would likely have the following handler function: | ||
|
||
<<< @./snippets/optimistic-predicates-before.ts#main{ts:line-numbers} | ||
|
||
Under the hood, the `transfer` call is making multiple calls to the network to both simulate and fund the transaction, then submitting it. This may give the appearance of slowness for users interacting with your application. | ||
|
||
This process can be optimized by optimistically building the transaction on page load, like so: | ||
|
||
<<< @./snippets/optimistic-predicates-after.ts#main{ts:line-numbers} | ||
|
||
> [!NOTE] | ||
> Any change to the underlying transaction will require re-estimation and re-funding of the transaction. Otherwise the transaction could increase in size and therefore cost, causing the transaction to fail. | ||
> | ||
> For predicates, the data passed to validate it could potentially alter the amount of gas the predicate consumes. This may mean we need to re-estimate and re-fund the transaction. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# Optimistic Transactions | ||
|
||
Imagine we have an application that allows users to transfer funds between accounts. | ||
|
||
On the frontend, we'd have a button that would allow the user to submit a transfer to a specified address. | ||
|
||
```tsx | ||
<Input | ||
placeholder="Enter recipient address" | ||
value={recipientAddress} | ||
onChange={(e) => setRecipientAddress(e.target.value)} | ||
/> | ||
<Button onClick={() => onTransferPressed(recipientAddress)}>Transfer</Button> | ||
``` | ||
|
||
This would likely have the following handler function: | ||
|
||
<<< @./snippets/optimistic-transactions-before.ts#main{ts:line-numbers} | ||
|
||
Under the hood, the `transfer` call is making multiple calls to the network to both simulate and fund the transaction, then submitting it. This may give the appearance of slowness for users interacting with your application. | ||
|
||
This process can be optimized by optimistically building the transaction on page load, like so: | ||
|
||
<<< @./snippets/optimistic-transactions-after.ts#main{ts:line-numbers} | ||
|
||
> [!NOTE] | ||
> Any change to the underlying transaction will require re-estimation and re-funding of the transaction. Otherwise the transaction could increase in size and therefore cost, causing the transaction to fail. |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,46 @@ | ||||||||||
// #region main | ||||||||||
import type { Account, ScriptTransactionRequest } from 'fuels'; | ||||||||||
import { Provider, Wallet } from 'fuels'; | ||||||||||
|
||||||||||
import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../env'; | ||||||||||
import { CounterFactory } from '../../../typegend/contracts'; | ||||||||||
|
||||||||||
const { info } = console; | ||||||||||
|
||||||||||
let provider: Provider; | ||||||||||
let wallet: Account; | ||||||||||
let request: ScriptTransactionRequest; | ||||||||||
|
||||||||||
// This is a generic page load function which should be called | ||||||||||
// as soon as the user lands on the page | ||||||||||
async function onPageLoad() { | ||||||||||
// Initialize the provider, sender and the contract | ||||||||||
provider = await Provider.create(LOCAL_NETWORK_URL); | ||||||||||
wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider); | ||||||||||
const { waitForResult } = await CounterFactory.deploy(wallet); | ||||||||||
const { contract } = await waitForResult(); | ||||||||||
|
||||||||||
// Get the transaction request for the increment_count function | ||||||||||
request = await contract.functions.increment_count(1).getTransactionRequest(); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
|
||||||||||
// Estimate and fund the transaction | ||||||||||
const txCost = await wallet.getTransactionCost(request); | ||||||||||
request.gasLimit = txCost.gasUsed; | ||||||||||
request.maxFee = txCost.maxFee; | ||||||||||
await wallet.fund(request, txCost); | ||||||||||
} | ||||||||||
|
||||||||||
async function onIncrementPressed() { | ||||||||||
// When the user presses the increment button, we submit the transaction | ||||||||||
// ensuring that the dependencies are not re-estimated and making redundant calls to the network | ||||||||||
Comment on lines
+34
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
const transaction = await wallet.sendTransaction(request, { | ||||||||||
estimateTxDependencies: false, | ||||||||||
}); | ||||||||||
info(`Transaction ID Submitted: ${transaction.id}`); | ||||||||||
const result = await transaction.waitForResult(); | ||||||||||
info(`Transaction ID Successful: ${result.id}`); | ||||||||||
} | ||||||||||
// #endregion main | ||||||||||
|
||||||||||
await onPageLoad(); | ||||||||||
await onIncrementPressed(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
// #region main | ||
import { Provider, Wallet } from 'fuels'; | ||
|
||
import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../env'; | ||
import { CounterFactory } from '../../../typegend/contracts'; | ||
|
||
const { info } = console; | ||
|
||
async function onIncrementPressed() { | ||
// Initialize the provider, sender and the contract | ||
const provider = await Provider.create(LOCAL_NETWORK_URL); | ||
const wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider); | ||
const { waitForResult } = await CounterFactory.deploy(wallet); | ||
const { contract } = await waitForResult(); | ||
|
||
// Calling the call function for a contract method will create a | ||
// transaction request using the contract call, estimate and fund it | ||
// and then submit it | ||
const transaction = await contract.functions.increment_count(1).call(); | ||
info(`Transaction ID Submitted: ${transaction.transactionId}`); | ||
const result = await transaction.waitForResult(); | ||
info(`Transaction ID Successful: ${result.transactionId}`); | ||
} | ||
// #endregion main | ||
|
||
await onIncrementPressed(); |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,70 @@ | ||||||||||||||||||||||||||||||
// #region main | ||||||||||||||||||||||||||||||
import type { Account } from 'fuels'; | ||||||||||||||||||||||||||||||
import { Provider, Wallet, ScriptTransactionRequest, Address } from 'fuels'; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
import { LOCAL_NETWORK_URL, WALLET_PVT_KEY_2 } from '../../../env'; | ||||||||||||||||||||||||||||||
import { ConfigurablePin } from '../../../typegend'; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
const { info } = console; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
let provider: Provider; | ||||||||||||||||||||||||||||||
let sender: Account; | ||||||||||||||||||||||||||||||
let request: ScriptTransactionRequest; | ||||||||||||||||||||||||||||||
let predicate: ConfigurablePin; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// This is a generic page load function which should be called | ||||||||||||||||||||||||||||||
// as soon as the user lands on the page | ||||||||||||||||||||||||||||||
async function onPageLoad() { | ||||||||||||||||||||||||||||||
// Initialize the provider and wallet | ||||||||||||||||||||||||||||||
provider = await Provider.create(LOCAL_NETWORK_URL); | ||||||||||||||||||||||||||||||
sender = Wallet.fromPrivateKey(WALLET_PVT_KEY_2, provider); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// Instantiate the predicate and fund it | ||||||||||||||||||||||||||||||
predicate = new ConfigurablePin({ provider }); | ||||||||||||||||||||||||||||||
const fundTx = await sender.transfer( | ||||||||||||||||||||||||||||||
predicate.address, | ||||||||||||||||||||||||||||||
500_000, | ||||||||||||||||||||||||||||||
provider.getBaseAssetId() | ||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||
await fundTx.waitForResult(); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// Create a new transaction request and add the predicate resources | ||||||||||||||||||||||||||||||
request = new ScriptTransactionRequest(); | ||||||||||||||||||||||||||||||
const resources = await predicate.getResourcesToSpend([ | ||||||||||||||||||||||||||||||
[100_000, provider.getBaseAssetId()], | ||||||||||||||||||||||||||||||
]); | ||||||||||||||||||||||||||||||
request.addResources(resources); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// Estimate and fund the transaction, including the predicate gas used | ||||||||||||||||||||||||||||||
const txCost = await predicate.getTransactionCost(request); | ||||||||||||||||||||||||||||||
request.updatePredicateGasUsed(txCost.estimatedPredicates); | ||||||||||||||||||||||||||||||
request.gasLimit = txCost.gasUsed; | ||||||||||||||||||||||||||||||
request.maxFee = txCost.maxFee; | ||||||||||||||||||||||||||||||
await predicate.fund(request, txCost); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
async function onTransferPressed(pin: number, recipientAddress: string) { | ||||||||||||||||||||||||||||||
// When the user presses the transfer button, we add the output | ||||||||||||||||||||||||||||||
// to the transaction request | ||||||||||||||||||||||||||||||
request.addCoinOutput( | ||||||||||||||||||||||||||||||
Address.fromString(recipientAddress), | ||||||||||||||||||||||||||||||
10_000, | ||||||||||||||||||||||||||||||
provider.getBaseAssetId() | ||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||
// Then we must alter any existing predicate data that may have changed | ||||||||||||||||||||||||||||||
const predicateWithData = new ConfigurablePin({ provider, data: [pin] }); | ||||||||||||||||||||||||||||||
const requestWithData = | ||||||||||||||||||||||||||||||
predicateWithData.populateTransactionPredicateData(request); | ||||||||||||||||||||||||||||||
// And submit the transaction, ensuring that the dependencies are | ||||||||||||||||||||||||||||||
Comment on lines
+53
to
+58
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||
// not re-estimated and making redundant calls to the network | ||||||||||||||||||||||||||||||
const transaction = await sender.sendTransaction(requestWithData, { | ||||||||||||||||||||||||||||||
estimateTxDependencies: false, | ||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
info(`Transaction ID Submitted: ${transaction.id}`); | ||||||||||||||||||||||||||||||
const result = await transaction.waitForResult(); | ||||||||||||||||||||||||||||||
info(`Transaction ID Successful: ${result.id}`); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
// #endregion main | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
await onPageLoad(); | ||||||||||||||||||||||||||||||
await onTransferPressed(1337, Wallet.generate().address.toString()); |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,32 @@ | ||||||
// #region main | ||||||
import { Provider, Wallet } from 'fuels'; | ||||||
|
||||||
import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../env'; | ||||||
import { ConfigurablePin } from '../../../typegend'; | ||||||
|
||||||
const { info } = console; | ||||||
|
||||||
async function onTransferPressed(pin: number, recipientAddress: string) { | ||||||
// Initialize the provider and sender | ||||||
const provider = await Provider.create(LOCAL_NETWORK_URL); | ||||||
const sender = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider); | ||||||
|
||||||
// Instantiate the predicate and fund it | ||||||
const predicate = new ConfigurablePin({ provider, data: [pin] }); | ||||||
const fundTx = await sender.transfer( | ||||||
predicate.address, | ||||||
500_000, | ||||||
provider.getBaseAssetId() | ||||||
); | ||||||
await fundTx.waitForResult(); | ||||||
|
||||||
// Calling the transfer function will create the transaction, | ||||||
// and then perform multiple network requests to fund, simulate and submit | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
const transaction = await predicate.transfer(recipientAddress, 10_000); | ||||||
info(`Transaction ID Submitted: ${transaction.id}`); | ||||||
const result = await transaction.waitForResult(); | ||||||
info(`Transaction ID Successful: ${result.id}`); | ||||||
} | ||||||
// #endregion main | ||||||
|
||||||
await onTransferPressed(1337, Wallet.generate().address.toString()); |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,59 @@ | ||||||
// #region main | ||||||
import type { Account } from 'fuels'; | ||||||
import { ScriptTransactionRequest, Address, Provider, Wallet, bn } from 'fuels'; | ||||||
|
||||||
import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../env'; | ||||||
|
||||||
const { info } = console; | ||||||
|
||||||
let provider: Provider; | ||||||
let sender: Account; | ||||||
let request: ScriptTransactionRequest; | ||||||
|
||||||
// This is a generic page load function which should be called | ||||||
// as soon as the user lands on the page | ||||||
async function onPageLoad() { | ||||||
// Initialize the provider and sender | ||||||
provider = await Provider.create(LOCAL_NETWORK_URL); | ||||||
sender = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider); | ||||||
|
||||||
// Create and prepare the transaction request | ||||||
request = new ScriptTransactionRequest(); | ||||||
|
||||||
// Estimate and fund the transaction with enough resources to cover | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
// both the transfer and the gas | ||||||
const txCost = await sender.getTransactionCost(request, { | ||||||
quantities: [ | ||||||
{ | ||||||
assetId: provider.getBaseAssetId(), | ||||||
amount: bn(1_000_000), | ||||||
}, | ||||||
], | ||||||
}); | ||||||
request.gasLimit = txCost.gasUsed; | ||||||
request.maxFee = txCost.maxFee; | ||||||
await sender.fund(request, txCost); | ||||||
} | ||||||
|
||||||
async function onTransferPressed(recipientAddress: string) { | ||||||
// When the user presses the transfer button, we add the output | ||||||
// to the transaction request | ||||||
request.addCoinOutput( | ||||||
Address.fromString(recipientAddress), | ||||||
1_000_000, | ||||||
provider.getBaseAssetId() | ||||||
); | ||||||
Comment on lines
+41
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the user clicks transfer without resetting the request, we could add another coin output. |
||||||
|
||||||
// And submit the transaction, ensuring that the dependencies are | ||||||
// not re-estimated and making redundant calls to the network | ||||||
const transaction = await sender.sendTransaction(request, { | ||||||
estimateTxDependencies: false, | ||||||
}); | ||||||
info(`Transaction ID Submitted: ${transaction.id}`); | ||||||
const result = await transaction.waitForResult(); | ||||||
info(`Transaction ID Successful: ${result.id}`); | ||||||
} | ||||||
// #endregion main | ||||||
|
||||||
await onPageLoad(); | ||||||
await onTransferPressed(Wallet.generate().address.toString()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.