diff --git a/.gitignore b/.gitignore
index 6c66251467..45495bf88a 100755
--- a/.gitignore
+++ b/.gitignore
@@ -54,7 +54,7 @@ packages/frontend/src/generated.ts
# Typedoc generated api reference.
# Ignored to discourage manual editing
-packages/docs/docs/pages/*/api
+packages/docs/docs/pages/**/api
# Cloned Repositories
packages/bundler/alto
diff --git a/packages/docs/Makefile b/packages/docs/Makefile
index d56209aded..dbec3b95ac 100644
--- a/packages/docs/Makefile
+++ b/packages/docs/Makefile
@@ -14,6 +14,7 @@ dev: ## Serves the docs in watch mode
--names "doc-js,doc-react,vocs" \
'make typedoc-js.watch'\
'make typedoc-react.watch'\
+ 'make typedoc-txm.watch'\
'make vocs.watch';
.PHONY: dev
@@ -21,9 +22,8 @@ dev: ## Serves the docs in watch mode
build: typedoc vocs ## Generates the latest docs from source code
.PHONY: build
-clean: ## Removes build artifacts
+clean: typedoc.clean ## Removes build artifacts
rm -rf docs/dist
- rm -rf docs/pages/{js,react}/api
rm -rf vocs.config.ts.timestamp-*
rm -rf node_modules/.tmp
.PHONY: clean
@@ -45,13 +45,15 @@ vocs.watch:
# generate all markdown API reference
typedoc: ## Generates the markdown files from package source code
- concurrently --names "js,react"\
+ concurrently --names "js,react,txm"\
'make typedoc-js'\
- 'make typedoc-react';
+ 'make typedoc-react'\
+ 'make typedoc-txm';
.PHONY: typedoc
typedoc.clean:
- rm -rf docs/pages/{js,react}/api
+ rm -rf docs/pages/sdk/{js,react}/api
+ rm -rf docs/pages/transaction-manager/api
.PHONY: typedoc
# generate JS markdown API reference
@@ -78,3 +80,14 @@ typedoc-react.watch:
--options ./typedoc.react.js;
.PHONY: typedoc-react.watch
+# generate Transaction Manager markdown API reference
+typedoc-txm:
+ typedoc --options ./typedoc.txm.js;
+.PHONY: typedoc-txm
+
+# generate Transaction Manager markdown API reference in watch mode
+typedoc-txm.watch:
+ typedoc\
+ --watch\
+ --options ./typedoc.txm.js;
+.PHONY: typedoc-txm.watch
diff --git a/packages/docs/docs/pages/index.mdx b/packages/docs/docs/pages/index.mdx
index feecd90d73..dcca7e664a 100644
--- a/packages/docs/docs/pages/index.mdx
+++ b/packages/docs/docs/pages/index.mdx
@@ -2,72 +2,5 @@
layout: doc
---
-import { HomePage } from 'vocs/components'
-
-# HappyChain SDK
-
-## JavaScript & TypeScript
-
-
-
- Integrate the Happy Wallet without a web framework or with an unsupported framework.
-
-
-
-:::code-group
-
-```bash [npm]
-npm install @happychain/js
-```
-
-```bash [pnpm]
-pnpm add @happychain/js
-```
-
-```bash [yarn]
-yarn add @happychain/js
-```
-
-```bash [bun]
-bun add @happychain/js
-```
-
-:::
-
-
- Get Started
- Full API Reference
-
-
-## React
-
-
- Integrate the Happy Wallet with React
-
-
-:::code-group
-
-```bash [npm]
-npm install @happychain/react
-```
-
-```bash [pnpm]
-pnpm add @happychain/react
-```
-
-```bash [yarn]
-yarn add @happychain/react
-```
-
-```bash [bun]
-bun add @happychain/react
-```
-
-:::
-
-
-Get Started
- Full API Reference
-
-
+# HappyChain Docs
diff --git a/packages/docs/docs/pages/sdk/index.mdx b/packages/docs/docs/pages/sdk/index.mdx
new file mode 100644
index 0000000000..c5f7863d9e
--- /dev/null
+++ b/packages/docs/docs/pages/sdk/index.mdx
@@ -0,0 +1,73 @@
+---
+layout: doc
+---
+
+import { HomePage } from 'vocs/components'
+
+# HappyChain SDK
+
+## JavaScript & TypeScript
+
+
+
+ Integrate the Happy Wallet without a web framework or with an unsupported framework.
+
+
+
+:::code-group
+
+```bash [npm]
+npm install @happychain/js
+```
+
+```bash [pnpm]
+pnpm add @happychain/js
+```
+
+```bash [yarn]
+yarn add @happychain/js
+```
+
+```bash [bun]
+bun add @happychain/js
+```
+
+:::
+
+
+ Get Started
+ Full API Reference
+
+
+## React
+
+
+ Integrate the Happy Wallet with React
+
+
+:::code-group
+
+```bash [npm]
+npm install @happychain/react
+```
+
+```bash [pnpm]
+pnpm add @happychain/react
+```
+
+```bash [yarn]
+yarn add @happychain/react
+```
+
+```bash [bun]
+bun add @happychain/react
+```
+
+:::
+
+
+ Get Started
+ Full API Reference
+
+
+
diff --git a/packages/docs/docs/pages/js/getting-started.mdx b/packages/docs/docs/pages/sdk/js/getting-started.mdx
similarity index 95%
rename from packages/docs/docs/pages/js/getting-started.mdx
rename to packages/docs/docs/pages/sdk/js/getting-started.mdx
index 3dc20fd633..763387a8dd 100644
--- a/packages/docs/docs/pages/js/getting-started.mdx
+++ b/packages/docs/docs/pages/sdk/js/getting-started.mdx
@@ -84,7 +84,7 @@ import { happyProvider } from '@happychain/js'
User changes, such as when a user logs in or out, can be subscribed to the `onUserUpdate` listener.
If the `user` is undefined, then they are not currently logged in have or have logged out.
-If the `user` is a [HappyUser](/js/api/type-aliases/HappyUser) then it will be populated with all their
+If the `user` is a [HappyUser](/sdk/js/api/type-aliases/HappyUser) then it will be populated with all their
shared info, such as wallet address and name.
```ts twoslash
diff --git a/packages/docs/docs/pages/react/getting-started.mdx b/packages/docs/docs/pages/sdk/react/getting-started.mdx
similarity index 94%
rename from packages/docs/docs/pages/react/getting-started.mdx
rename to packages/docs/docs/pages/sdk/react/getting-started.mdx
index 900810e847..538e42b115 100644
--- a/packages/docs/docs/pages/react/getting-started.mdx
+++ b/packages/docs/docs/pages/sdk/react/getting-started.mdx
@@ -68,7 +68,7 @@ import { happyProvider } from '@happychain/react'
### Getting the Active User
-The `useHappyChain` hook returns the current user as a [HappyUser](/react/api/type-aliases/HappyUser)
+The `useHappyChain` hook returns the current user as a [HappyUser](/sdk/react/api/type-aliases/HappyUser)
if the user is connected, otherwise it returns undefined.
```tsx twoslash
@@ -90,7 +90,7 @@ function UserAddressComponent() {
User changes, such as when a user logs in or out, can be subscribed to the `onUserUpdate` listener.
If the `user` is undefined, then they are not currently logged in have or have logged out.
-If the `user` is a [HappyUser](/react/api/type-aliases/HappyUser) then it will be populated with all their
+If the `user` is a [HappyUser](/sdk/react/api/type-aliases/HappyUser) then it will be populated with all their
shared info, such as wallet address and name.
```ts twoslash
diff --git a/packages/docs/docs/pages/transaction-manager/getting-started.mdx b/packages/docs/docs/pages/transaction-manager/getting-started.mdx
new file mode 100644
index 0000000000..b217f6f122
--- /dev/null
+++ b/packages/docs/docs/pages/transaction-manager/getting-started.mdx
@@ -0,0 +1,173 @@
+---
+layout: doc
+---
+
+# Getting Started With the Transaction Manager
+
+The transaction manager is a package that allows you to easily send transactions to the blockchain without having to handle all the potential errors that may occur until the transaction is included in a block and successfully executed.
+
+## Install
+:::code-group
+
+```bash [npm]
+npm install @happychain/transaction-manager
+```
+
+```bash [pnpm]
+pnpm add @happychain/transaction-manager
+```
+
+```bash [yarn]
+yarn add @happychain/transaction-manager
+```
+
+```bash [bun]
+bun add @happychain/transaction-manager
+```
+
+:::
+
+## Setup
+
+After installing, the first thing you will need to do is import the `TransactionManager` class and then create an instance of it.
+
+```ts
+import { TransactionManager } from '@happychain/transaction-manager'
+import { anvil } from "viem/chains"
+
+const abis = {
+ "erc20": {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "_to",
+ "type": "address"
+ },
+ {
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "transfer",
+ "outputs": [
+ {
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "function"
+ }
+}
+
+const txm = new TransactionManager({
+ account: privateKeyToAccount("0x..."),
+ transport: webSocket(),
+ chain: anvil,
+ id: "txm-demo",
+ abis
+})
+```
+
+After creating the transaction manager, you will need to start it.
+
+```tsx
+txm.start()
+```
+
+
+## Usage
+
+### Creating Transactions
+
+We use our custom `Transaction` class to represent transactions. Along with the standard parameters such as source, destination, and gas fees, this class incorporates custom
+fields specifically tailored for the transaction manager.
+
+You can see all the fields that our `Transaction` class has [here](/transaction-manager/api/classes/Transaction#properties).
+
+To create a transaction, you will have to use its constructor. You can see all the fields that you can provide to the constructor [here](/transaction-manager/api/interfaces/TransactionConstructorConfig).
+
+
+```tsx
+import { Transaction } from '@happychain/transaction-manager'
+
+const tx = new Transaction({
+ chainId: 216,
+ address: "0x...",
+ functionName: "transfer",
+ contractName: "erc20",
+ args: ["0x...", "1000000000000000000"],
+ deadline: Math.floor(Date.now() / 1000) + 60,
+})
+```
+
+A very important field to understand is the `intentId`. This is a unique identifier for your transaction within the transaction manager.
+
+The `intentId` is a field automatically generated by the transaction manager, so you don't need to provide it when creating a transaction.
+
+It is not the same as the transaction hash, because a transaction can be resent multiple times with different nonces, gas prices, and gas limits, which alter the hash.
+
+It is important to note that if you want to monitor the status of the transaction, you will need to use the `intentId`.
+
+
+### Sending Transactions
+
+To send a transaction, you will need to add a [TransactionOriginator](/transaction-manager/api/type-aliases/TransactionOriginator)
+
+A `TransactionOriginator` is a function that will be called every time the transaction manager detects a new block.
+It will receive the latest block as an argument and will need to return an array of [Transactions](/transaction-manager/api/classes/Transaction) to be sent.
+
+```tsx
+import { LatestBlock } from '@happychain/transaction-manager'
+
+const transactions: Transaction[] = []
+
+const demoOriginator = async (block: LatestBlock) => {
+ return transactions
+}
+
+txm.addTransactionOriginator(demoOriginator)
+```
+
+After adding the transaction originator, the transaction manager will automatically send the transactions returned by the originator and will handle all the errors that may occur during the inclusion of the transactions in a block for you.
+
+### Monitoring Transactions
+
+There are two ways to monitor the status of a transaction that you have sent through the transaction manager.
+
+#### Active monitoring
+
+You can call the method [getTransaction](/transaction-manager/api/classes/TransactionManager#gettransaction) of the transaction manager. This method will return the transaction with the latest changes.
+
+*Example*
+
+```tsx
+import { TransactionStatus } from "@happychain/transaction-manager"
+
+const tx = await txm.getTransaction(intentId)
+
+if (tx.status === TransactionStatus.Success) {
+ console.log("Transaction executed successfully")
+} else if (tx.status === TransactionStatus.Failed) {
+ console.log("Transaction failed")
+}
+```
+
+You can check all the possible statuses of a transaction [here](/transaction-manager/api/enumerations/TransactionStatus).
+
+#### Passive monitoring
+
+Use hooks to be notified when the status of a transaction changes. To subscribe to a hook you will have to use the [addHook](/transaction-manager/api/classes/TransactionManager#addhook) method.
+
+*Example*
+
+```tsx
+import { TxmHookType, TxmHookHandler } from "@happychain/transaction-manager"
+
+const hookHandler: TxmHookHandler = (tx: Transaction) => {
+ console.log("Transaction status changed to", tx.status)
+}
+
+txm.addHook({ type: TxmHookType.OnStatusChange, handler: hookHandler })
+```
\ No newline at end of file
diff --git a/packages/docs/docs/pages/transaction-manager/index.mdx b/packages/docs/docs/pages/transaction-manager/index.mdx
new file mode 100644
index 0000000000..ceabdbead4
--- /dev/null
+++ b/packages/docs/docs/pages/transaction-manager/index.mdx
@@ -0,0 +1,47 @@
+---
+layout: doc
+---
+
+# HappyChain Transaction Manager
+
+import { HomePage } from 'vocs/components'
+
+The HappyChain Transaction Manager is a robust library designed to streamline and simplify the management of blockchain transactions.
+
+Managing transactions on the blockchain can be a complex endeavor, as it requires careful consideration of various factors, including:
+
+- Reverted transactions
+- Insufficient gas price
+- Incorrect nonce values
+- Inaccurate gas prices
+- Improper gas limits
+- Transactions lost in the mempool
+- And more
+
+To address these challenges, we developed the HappyChain Transaction Manager. This library enables you to effortlessly send transactions to the blockchain without worrying about the aforementioned issues.
+
+:::code-group
+
+```bash [npm]
+npm install @happychain/transaction-manager
+```
+
+```bash [pnpm]
+pnpm add @happychain/transaction-manager
+```
+
+```bash [yarn]
+yarn add @happychain/transaction-manager
+```
+
+```bash [bun]
+bun add @happychain/transaction-manager
+```
+
+:::
+
+
+ Get Started
+ Full API Reference
+
+
diff --git a/packages/docs/tsconfig.typedoc.txm.json b/packages/docs/tsconfig.typedoc.txm.json
new file mode 100644
index 0000000000..cefcc322e8
--- /dev/null
+++ b/packages/docs/tsconfig.typedoc.txm.json
@@ -0,0 +1,6 @@
+{
+ "extends": ["../transaction-manager/tsconfig.json"],
+ "compilerOptions": {
+ "rootDir": ".."
+ }
+}
diff --git a/packages/docs/typedoc.react.js b/packages/docs/typedoc.react.js
index 1e8a92a460..3cd237a882 100644
--- a/packages/docs/typedoc.react.js
+++ b/packages/docs/typedoc.react.js
@@ -10,8 +10,8 @@ export default {
entryPoints: ["../sdk-react/lib/index.ts"],
// https://typedoc.org/options/output
- out: "docs/pages/react/api",
+ out: "docs/pages/sdk/react/api",
// https://typedoc-plugin-markdown.org/docs/options/utility-options
- publicPath: "/react/api/",
+ publicPath: "/sdk/react/api/",
}
diff --git a/packages/docs/typedoc.txm.js b/packages/docs/typedoc.txm.js
new file mode 100644
index 0000000000..d72bde307f
--- /dev/null
+++ b/packages/docs/typedoc.txm.js
@@ -0,0 +1,17 @@
+import base from "./typedoc.base.js"
+
+/** @type {import('typedoc').TypeDocOptions} */
+export default {
+ ...base,
+ // https://typedoc.org/options/configuration/
+ tsconfig: "./tsconfig.typedoc.txm.json",
+ // https://typedoc.org/options/input/
+ name: "@happychain/transaction-manager",
+ entryPoints: ["../transaction-manager/lib/index.ts"],
+
+ // https://typedoc.org/options/output
+ out: "docs/pages/transaction-manager/api",
+
+ // https://typedoc-plugin-markdown.org/docs/options/utility-options
+ publicPath: "/transaction-manager/api/",
+}
diff --git a/packages/docs/typedoc.vanilla.js b/packages/docs/typedoc.vanilla.js
index a07229e786..81f213d249 100644
--- a/packages/docs/typedoc.vanilla.js
+++ b/packages/docs/typedoc.vanilla.js
@@ -10,8 +10,8 @@ export default {
entryPoints: ["../sdk-vanillajs/lib/index.ts"],
// https://typedoc.org/options/output
- out: "docs/pages/js/api",
+ out: "docs/pages/sdk/js/api",
// https://typedoc-plugin-markdown.org/docs/options/utility-options
- publicPath: "/js/api/",
+ publicPath: "/sdk/js/api/",
}
diff --git a/packages/docs/vocs.config.ts b/packages/docs/vocs.config.ts
index 26218ce2e4..8a352e4314 100644
--- a/packages/docs/vocs.config.ts
+++ b/packages/docs/vocs.config.ts
@@ -2,7 +2,7 @@ import { defineConfig } from "vocs"
export default defineConfig({
rootDir: "docs",
- title: "HappyChain SDK",
+ title: "HappyChain Docs",
socials: [
{
link: "https://x.com/HappyChainDevs",
@@ -15,30 +15,50 @@ export default defineConfig({
],
sidebar: [
{
- text: "JavaScript & TypeScript",
+ text: "SDK",
+ link: "/sdk",
items: [
{
- text: "Getting Started",
- link: "/js/getting-started",
+ text: "JavaScript & TypeScript",
+ items: [
+ {
+ text: "Getting Started",
+ link: "/sdk/js/getting-started",
+ },
+ {
+ text: "API Reference",
+ link: "/sdk/js/api",
+ },
+ ],
},
{
- text: "API Reference",
- link: "/js/api",
- },
- ],
+ text: "React",
+ items: [
+ {
+ text: "Getting Started",
+ link: "/sdk/react/getting-started",
+ },
+ {
+ text: "API Reference",
+ link: "/sdk/react/api",
+ },
+ ],
+ }
+ ]
},
{
- text: "React",
+ text: "Transaction Manager",
+ link: "/transaction-manager",
items: [
{
text: "Getting Started",
- link: "/react/getting-started",
+ link: "/transaction-manager/getting-started",
},
{
text: "API Reference",
- link: "/react/api",
+ link: "/transaction-manager/api",
},
- ],
- },
+ ]
+ }
],
})
diff --git a/packages/randomness-service/src/CustomGasEstimator.ts b/packages/randomness-service/src/CustomGasEstimator.ts
index 735d952262..6587b4aa9d 100644
--- a/packages/randomness-service/src/CustomGasEstimator.ts
+++ b/packages/randomness-service/src/CustomGasEstimator.ts
@@ -1,12 +1,12 @@
import {
+ DefaultGasLimitEstimator,
type EstimateGasErrorCause,
- GasEstimator,
type Transaction,
type TransactionManager,
} from "@happychain/transaction-manager"
import { type Result, ok } from "neverthrow"
-export class CustomGasEstimator extends GasEstimator {
+export class CustomGasEstimator extends DefaultGasLimitEstimator {
override async estimateGas(
transactionManager: TransactionManager,
transaction: Transaction,
diff --git a/packages/randomness-service/src/index.ts b/packages/randomness-service/src/index.ts
index 8454fde8d2..af0d950d02 100644
--- a/packages/randomness-service/src/index.ts
+++ b/packages/randomness-service/src/index.ts
@@ -33,7 +33,7 @@ class RandomnessService {
})
this.txm.start()
- this.txm.addTransactionCollector(this.onCollectTransactions.bind(this))
+ this.txm.addTransactionOriginator(this.onCollectTransactions.bind(this))
}
private async onCollectTransactions(block: LatestBlock): Promise {
diff --git a/packages/transaction-manager/lib/AbiManager.ts b/packages/transaction-manager/lib/AbiManager.ts
index 2430220999..7405fce1c3 100644
--- a/packages/transaction-manager/lib/AbiManager.ts
+++ b/packages/transaction-manager/lib/AbiManager.ts
@@ -1,5 +1,9 @@
import type { Abi } from "viem"
+/**
+ * This is an internal module responsible for storing contracts' ABIs and retrieving them using contract aliases.
+ * The user can't modify the alias-to-ABI bindings: those are provided in the `abis` field of the object passed to the {@link TransactionManager} constructor.
+ */
export class ABIManager {
public store: Record
diff --git a/packages/transaction-manager/lib/BlockMonitor.ts b/packages/transaction-manager/lib/BlockMonitor.ts
index 20b85d2276..6aeace90de 100644
--- a/packages/transaction-manager/lib/BlockMonitor.ts
+++ b/packages/transaction-manager/lib/BlockMonitor.ts
@@ -7,6 +7,11 @@ import type { TransactionManager } from "./TransactionManager.js"
*/
export type LatestBlock = Block
+/**
+ * This module is responsible for emitting the {@link Topics.NewBlock} event when a new block is received,
+ * notifying other parts of the code that a new block has just occurred. Along with the notification,
+ * it sends the latest block information with the event, allowing other parts of the application to use this data for their own purposes.
+ */
export class BlockMonitor {
private txmgr: TransactionManager
diff --git a/packages/transaction-manager/lib/GasEstimator.ts b/packages/transaction-manager/lib/GasEstimator.ts
index 51508e2d08..9a77464e99 100644
--- a/packages/transaction-manager/lib/GasEstimator.ts
+++ b/packages/transaction-manager/lib/GasEstimator.ts
@@ -8,7 +8,42 @@ export enum EstimateGasErrorCause {
ClientError = "ClientError",
}
-export class GasEstimator {
+/**
+ * This is the interface that you have to implement if you want to provide a custom gas estimator.
+ * The default implementation is {@link DefaultGasLimitEstimator}.
+ *
+ * You have to implement this interface if the standard gas estimation method does not work. That's because the standard method
+ * simulates the transaction against the current blockchain state, which can change when the transaction is actually executed onchain.
+ * This can lead to inaccurate gas estimations that cause transactions to revert onchain. It can also lead happen that some transactions fail
+ * at the estimation stage when they would have succeeded on chain. For instance, this can happen to transactions
+ * who have a strict requirement on the block number or timestamp on or after which they should land.
+ *
+ * By avoiding transaction simulation, you can also improve the performance of the transaction manager.
+ * You eliminate an RPC call, increasing the likelihood that your transaction will be included in the next block.
+ * Furthermore, if you're always executing the same type of transaction, it typically consumes a similar amount of gas.
+ * In those cases, an upper bound of the gas limit can be computed in advance and injected by a custom gas estimator.
+ *
+ * In some cases, you may want to hardcode the gas limit anyway (if at all possible). Doing so avoids an extra round-trip to the RPC server,
+ * which might help your transaction land in an earlier block and will save on RPC costs.
+ *
+ * Because the extra gas gets refunded, this is both safe and doesn't incur extra cost,
+ * though it increases your gas capital requirement if you have a lot of transactions in flight.
+ */
+export interface GasEstimator {
+ estimateGas(
+ transactionManager: TransactionManager,
+ transaction: Transaction,
+ ): Promise>
+}
+
+/**
+ * This is the default module used to estimate the gas for a transaction. When creating a new transaction manager,
+ * you can instead pass a custom gas estimator implementing {@link GasEstimator}.
+ * Whenever applicable, you should extend this class to reuse the protected {@link simulateTransactionForGas} method.
+ *
+ * This class estimates the gas by simulating the transaction against the current blockchain state.
+ */
+export class DefaultGasLimitEstimator implements GasEstimator {
public async estimateGas(
transactionManager: TransactionManager,
transaction: Transaction,
diff --git a/packages/transaction-manager/lib/GasPriceOracle.ts b/packages/transaction-manager/lib/GasPriceOracle.ts
index 3727adb53b..6a631ef960 100644
--- a/packages/transaction-manager/lib/GasPriceOracle.ts
+++ b/packages/transaction-manager/lib/GasPriceOracle.ts
@@ -3,6 +3,24 @@ import type { LatestBlock } from "./BlockMonitor.js"
import { Topics, eventBus } from "./EventBus.js"
import type { TransactionManager } from "./TransactionManager.js"
+/**
+ * This module estimates the gas price for transaction execution. It updates with each new block, basing its calculations on EIP-1559.
+ *
+ * The estimation considers the expected gas usage target per block, adjusting the next block's base fee
+ * by incrementing or decrementing it by a certain percentage.
+ *
+ * Other parts of the application use the `suggestGasForNextBlock` function to get a gas recommendation at a given moment.
+ *
+ * This module adds a safety margin to the base gas, called `baseFeeMargin`, to prevent transactions from getting stuck in the mempool.
+ * It's configurable in the `TransactionManager` constructor. The default value is 20%. The parameter is called `baseFeePercentageMargin`.
+ *
+ * This margin is necessary because a transaction must meet at least the base fee to be valid.
+ *
+ * If you only use the expected base fee for the next block and your transaction isn't included immediately,
+ * it might become invalid if the base fee increases in subsequent blocks.
+ *
+ * Including a safety margin is prudent — if the base fee doesn't increase, the excess fee is refunded, so there's no loss.
+ */
export class GasPriceOracle {
private txmgr: TransactionManager
private expectedNextBaseFeePerGas!: bigint
diff --git a/packages/transaction-manager/lib/HookManager.ts b/packages/transaction-manager/lib/HookManager.ts
index 6846062df2..da69836b7e 100644
--- a/packages/transaction-manager/lib/HookManager.ts
+++ b/packages/transaction-manager/lib/HookManager.ts
@@ -13,6 +13,16 @@ export type TxmHookPayload = {
export type TxmHookHandler = (event: TxmHookPayload) => void
+/**
+ * This module manages the hooks system. A hook in the transaction manager is a callback function that
+ * executes when specific events occur, such as when a transaction's status changes.
+ * This allows the library consumer to receive notifications about events and respond accordingly.
+ * To add a hook, call the {@link TransactionManager.addHook} function.
+ *
+ * This method accepts two parameters:
+ * - The hook type you want to subscribe to (optional)
+ * - The callback function that executes when the event occurs.
+ */
export class HookManager {
private hooks: Record
diff --git a/packages/transaction-manager/lib/NonceManager.ts b/packages/transaction-manager/lib/NonceManager.ts
index 0404363442..7fe9f1d125 100644
--- a/packages/transaction-manager/lib/NonceManager.ts
+++ b/packages/transaction-manager/lib/NonceManager.ts
@@ -1,5 +1,30 @@
import type { TransactionManager } from "./TransactionManager"
+/*
+ * This class manages the nonce of the account that the transaction manager is using.
+ *
+ * This is critical because a gap in the nonces would mean that transactions sent by the manager are never included onchain.
+ *
+ * Assumes that it's the only user of the account from the moment it is initialized.
+ *
+ * It's designed to be fault-tolerant, meaning that if a failure occurs, it will recover the latest state from the database and the RPC.
+ *
+ * The module must be initialized first. During initialization, it retrieves the transaction count from the RPC,
+ * which represents the expected next nonce without considering potential pending transactions in the mempool.
+ *
+ * To handle this scenario, we check for pending transactions in our database and identify the last nonce of these transactions.
+ *
+ * We also check for gaps in the pending transactions, aiming to use these gaps before assigning new nonces, thus eliminating nonce gaps.
+ *
+ * Once started, this module exposes two public methods. The first is `public requestNonce(): number`.
+ *
+ * This is an atomic method (as it doesn't await any promises, and Node.js is single-threaded)
+ * that provides and reserves a nonce when you need to emit a new transaction.
+ *
+ * The second method is `public returnNonce(nonce: number)`. It's used when you've reserved a nonce, but the transaction hasn't reached the mempool.
+ *
+ * In this case, the nonce isn't actually used, and if we don't return it, it would cause a nonce gap.
+ */
export class NonceManager {
private txmgr: TransactionManager
private nonce!: number
diff --git a/packages/transaction-manager/lib/Transaction.ts b/packages/transaction-manager/lib/Transaction.ts
index 6775a2d689..3203bbc0e3 100644
--- a/packages/transaction-manager/lib/Transaction.ts
+++ b/packages/transaction-manager/lib/Transaction.ts
@@ -6,11 +6,29 @@ import { Topics, eventBus } from "./EventBus.js"
import type { TransactionTable } from "./db/types.js"
export enum TransactionStatus {
+ /**
+ * Default state for new transaction: the transaction is awaiting processing by TXM or has been submitted in the mempool and is waiting to be included in a block.
+ */
Pending = "Pending",
+ /**
+ * The transaction has been included in a block but its execution reverted.
+ */
Failed = "Failed",
+ /**
+ * The transaction has expired. This indicates that the deadline has passed without the transaction being included in a block.
+ */
Expired = "Expired",
+ /**
+ * The transaction has expired and we are trying to cancel it to save gas
+ */
Cancelling = "Cancelling",
+ /**
+ * The transaction has expired, and we are attempting to cancel it to save gas, preventing it from being included on-chain and potentially reverting or executing actions that are no longer relevant.
+ */
Cancelled = "Cancelled",
+ /**
+ * The transaction has been included onchain and its execution was successful.
+ */
Success = "Success",
}
@@ -30,6 +48,39 @@ export interface Attempt {
export const NotFinalizedStatuses = [TransactionStatus.Pending, TransactionStatus.Cancelling]
+export interface TransactionConstructorConfig {
+ /**
+ * The chain ID where the transaction will be sent
+ */
+ chainId: number
+ /**
+ * The address of the contract that will be called
+ */
+ address: Address
+ /**
+ * The function name of the contract that will be called
+ */
+ functionName: string
+ /**
+ * This doesn't need to match the Solidity contract name but must match the contract alias of one of the contracts
+ * that you have provided when initializing the transaction manager with the ABI Manager
+ */
+ contractName: string
+ /**
+ * The arguments of the function that will be called
+ */
+ args: ContractFunctionArgs
+ /**
+ * The deadline of the transaction in seconds (optional)
+ * This is used to try to cancel the transaction if it is not included in a block after the deadline to save gas
+ */
+ deadline?: number
+ /**
+ * Additional metadata for the transaction that can be used by your custom GasEstimator
+ */
+ metadata?: Record
+}
+
export class Transaction {
readonly intentId: UUID
@@ -41,7 +92,7 @@ export class Transaction {
readonly args: ContractFunctionArgs
- // This doesn't need to match the Solidity contract name but must match the alias specified in the ABI Manager
+ // This doesn't need to match the Solidity contract name but must match the contract alias of one of the contracts that you have provided when initializing the transaction manager with the ABI Manager
readonly contractName: string
readonly deadline: number | undefined
@@ -58,7 +109,7 @@ export class Transaction {
* Stores additional information for the transaction.
* Enables originators to provide extra details, such as gas limits, which can be leveraged by customizable services.
*/
- metadata: Record | undefined
+ readonly metadata: Record
constructor({
intentId,
@@ -73,19 +124,12 @@ export class Transaction {
createdAt,
updatedAt,
metadata,
- }: {
+ }: TransactionConstructorConfig & {
intentId?: UUID
- chainId: number
- address: Address
- functionName: string
- contractName: string
- args: ContractFunctionArgs
- deadline?: number
status?: TransactionStatus
attempts?: Attempt[]
createdAt?: Date
updatedAt?: Date
- metadata?: Record
}) {
this.intentId = intentId ?? createUUID()
this.chainId = chainId
@@ -98,7 +142,7 @@ export class Transaction {
this.attempts = attempts ?? []
this.createdAt = createdAt ?? new Date()
this.updatedAt = updatedAt ?? new Date()
- this.metadata = metadata
+ this.metadata = metadata ?? {}
}
addAttempt(attempt: Attempt): void {
diff --git a/packages/transaction-manager/lib/TransactionCollector.ts b/packages/transaction-manager/lib/TransactionCollector.ts
index d36c038a95..42297a8c4a 100644
--- a/packages/transaction-manager/lib/TransactionCollector.ts
+++ b/packages/transaction-manager/lib/TransactionCollector.ts
@@ -3,6 +3,13 @@ import { Topics, eventBus } from "./EventBus.js"
import { AttemptType } from "./Transaction.js"
import type { TransactionManager } from "./TransactionManager.js"
+/**
+ * This module is responsible for retrieving transactions from the originators when a new block is received.
+ * It also sorts the transactions by deadline, prioritizing those that will expire sooner.
+ * Additionally, this module handles submitting the first attempt of every transaction and saves
+ * the initial version of the Transaction object to the database (including its first attempt).
+ */
+
export class TransactionCollector {
private readonly txmgr: TransactionManager
diff --git a/packages/transaction-manager/lib/TransactionManager.ts b/packages/transaction-manager/lib/TransactionManager.ts
index f6ef369326..af5887e717 100644
--- a/packages/transaction-manager/lib/TransactionManager.ts
+++ b/packages/transaction-manager/lib/TransactionManager.ts
@@ -8,7 +8,7 @@ import {
import { type Abi, type Account, type Chain, type Transport, createPublicClient, createWalletClient } from "viem"
import { ABIManager } from "./AbiManager.js"
import { BlockMonitor, type LatestBlock } from "./BlockMonitor.js"
-import { GasEstimator } from "./GasEstimator.js"
+import { DefaultGasLimitEstimator, type GasEstimator } from "./GasEstimator.js"
import { GasPriceOracle } from "./GasPriceOracle.js"
import { HookManager, type TxmHookHandler, type TxmHookType } from "./HookManager.js"
import { NonceManager } from "./NonceManager.js"
@@ -78,13 +78,20 @@ export type TransactionManagerConfig = {
/**
* The gas estimator to use for estimating the gas limit of a transaction.
* You can provide your own implementation to override the default one.
- * Default: {@link GasEstimator}
+ * Default: {@link DefaultGasLimitEstimator}
*/
gasEstimator?: GasEstimator
}
export type TransactionOriginator = (block: LatestBlock) => Promise
+/**
+ * The TransactionManager is the core module of the transaction manager.
+ * To use the transaction manager, you must instantiate this class.
+ * Before using the transaction manager, call the {@link TransactionManager.start} method to start it.
+ * Once started, use the {@link TransactionManager.addTransactionOriginator} method
+ * to add a transaction originator and begin sending transactions to the blockchain.
+ */
export class TransactionManager {
public readonly collectors: TransactionOriginator[]
public readonly blockMonitor: BlockMonitor
@@ -127,7 +134,7 @@ export class TransactionManager {
this.nonceManager = new NonceManager(this)
this.gasPriceOracle = new GasPriceOracle(this)
- this.gasEstimator = _config.gasEstimator || new GasEstimator()
+ this.gasEstimator = _config.gasEstimator || new DefaultGasLimitEstimator()
this.blockMonitor = new BlockMonitor(this)
this.pendingTxReporter = new TxMonitor(this)
this.transactionRepository = new TransactionRepository(this)
@@ -148,13 +155,13 @@ export class TransactionManager {
}
/**
- * Adds a collector to the transaction manager.
- * A collector is a function that returns a list of transactions to be sent in the next block.
- * It is important that the collector function is as fast as possible to avoid delays when sending transactions to the blockchain
- * @param collector - The collector to add.
+ * Adds an originator to the transaction manager.
+ * An originator is a function that returns a list of transactions to be sent in the next block.
+ * It is important that the originator function is as fast as possible to avoid delays when sending transactions to the blockchain
+ * @param originator - The originator to add.
*/
- public addTransactionCollector(collector: TransactionOriginator): void {
- this.collectors.push(collector)
+ public addTransactionOriginator(originator: TransactionOriginator): void {
+ this.collectors.push(originator)
}
/**
diff --git a/packages/transaction-manager/lib/TransactionRepository.ts b/packages/transaction-manager/lib/TransactionRepository.ts
index 9f1f2ee3ce..334dc5b0b7 100644
--- a/packages/transaction-manager/lib/TransactionRepository.ts
+++ b/packages/transaction-manager/lib/TransactionRepository.ts
@@ -6,6 +6,13 @@ import { NotFinalizedStatuses, Transaction } from "./Transaction.js"
import type { TransactionManager } from "./TransactionManager.js"
import { db } from "./db/driver.js"
+/**
+ * This module act as intermediate layer between the library and the database.
+ * Its maintains an in memory copy of all the not finalized transaction to avoid to access to database and
+ * improving the latency of get transactions. In addition it contains other methods helpful methods like getHighestNonce
+ * that returns the biggest nonce of all the not finalized transactions that exist in the database or the flush function
+ * that is used to commit all the changes that have occurred in tracked entities by the ORM in the database
+ */
export class TransactionRepository {
private readonly transactionManager: TransactionManager
private notFinalizedTransactions: Transaction[]
diff --git a/packages/transaction-manager/lib/TransactionSubmitter.ts b/packages/transaction-manager/lib/TransactionSubmitter.ts
index e3d23f3e3d..9950741e4b 100644
--- a/packages/transaction-manager/lib/TransactionSubmitter.ts
+++ b/packages/transaction-manager/lib/TransactionSubmitter.ts
@@ -27,6 +27,20 @@ export type AttemptSubmissionError = {
export type AttemptSubmissionResult = Result
+/**
+ * This module is responsible for submitting a new attempt to the blockchain.
+ * It coordinates the process using a transaction and an {@link AttemptSubmissionParameters}.
+ *
+ * This module given that information is in charge of:
+ * - Requesting a nonce
+ * - Estimating the gas limit and pulling the gas price
+ * - Signing the transaction
+ * - Adding the attempt to the transaction attempts list.
+ * - Flushing the transaction. We do this **before** sending the transaction, to avoid the case where the transaction is
+ * sent successfully but not saved to the DB (because of an app/server/… crash),
+ * which would cause us to lose information (there would be an untracked attempt in flight).
+ * - Sending the transaction to the RPC
+ */
export class TransactionSubmitter {
private readonly txmgr: TransactionManager
diff --git a/packages/transaction-manager/lib/TxMonitor.ts b/packages/transaction-manager/lib/TxMonitor.ts
index 3de02f2e52..c6de658312 100644
--- a/packages/transaction-manager/lib/TxMonitor.ts
+++ b/packages/transaction-manager/lib/TxMonitor.ts
@@ -8,6 +8,25 @@ import type { TransactionManager } from "./TransactionManager.js"
type AttemptWithReceipt = { attempt: Attempt; receipt: TransactionReceipt }
+/**
+ * This module is responsible for monitoring in-flight transactions. It handles scenarios where a transaction completes successfully, fails, or becomes stuck.
+ * This module requests all receipts for all attempts made on a transaction.
+ * All the checked attempts share the same nonce, so only one of them could have been executed, and the expected response for the rest will always be "no receipt found."
+ * For this reason, we consider different scenarios based on the responses received from the requests:
+ *
+ * - The RPC responds to all requests, but all are in the "no receipt found" state. In this case, we act as if the transaction is stuck
+ * due to insufficient gas price and initiate a new attempt with gas priced at the current market rate and sufficient
+ * gas to overwrite the last attempt.
+ *
+ * - The RPC responds that one of the attempts has been executed. In this case, we act accordingly depending on whether
+ * the execution was successful or failed. If the successfully executed attempt was a transaction execution attempt,
+ * the transaction transitions to the {@link TransactionStatus.Success} state. If the successfully executed attempt was a cancellation attempt,
+ * the transaction moves to the {@link TransactionStatus.Cancelled} state. If the executed attempt failed, the transaction is moved to the {@link TransactionStatus.Failed} state
+ * unless the retry policy class indicates that the transaction should be retried. In that case, the transaction is retried with a new nonce and continues to be in {@link TransactionStatus.Pending} state.
+ *
+ * - If the execution receipt is not received and at least one of the RPC queries does not respond. In that case, we do nothing,
+ * as it is possible that the unanswered RPC query corresponds to a transaction that has been executed, which could lead to potential issues.
+ */
export class TxMonitor {
private readonly transactionManager: TransactionManager
private locked = false
diff --git a/packages/transaction-manager/lib/index.ts b/packages/transaction-manager/lib/index.ts
index 48451d3dc9..8bb89e41dd 100644
--- a/packages/transaction-manager/lib/index.ts
+++ b/packages/transaction-manager/lib/index.ts
@@ -1,5 +1,6 @@
-export { Transaction, TransactionStatus } from "./Transaction.js"
-export { TransactionManager } from "./TransactionManager.js"
-export { GasEstimator, EstimateGasErrorCause } from "./GasEstimator.js"
+export { Transaction, TransactionStatus, type TransactionConstructorConfig } from "./Transaction.js"
+export { TransactionManager, type TransactionManagerConfig, type TransactionOriginator } from "./TransactionManager.js"
+export type { Abi } from "viem"
+export { DefaultGasLimitEstimator, EstimateGasErrorCause, type GasEstimator } from "./GasEstimator.js"
export type { LatestBlock } from "./BlockMonitor.js"
export { TxmHookType, type TxmHookHandler } from "./HookManager.js"