Skip to content

Commit

Permalink
feat(Transactions Module): sendTransaction & waitForTransaction (#322)
Browse files Browse the repository at this point in the history
* feat: transactions module in thor client

* docs: refactor thor client tsdocs

* feat: transactions module with sendTx & waitForTx

* feat: tx module types

* feat: tx module assertions helper

* test: transactions module

* chore: indexing & minors

* fix: miss some export

---------

Co-authored-by: rodolfopietro97 <[email protected]>
  • Loading branch information
pierobassa and rodolfopietro97 authored Nov 29, 2023
1 parent e2ae986 commit cb161b9
Show file tree
Hide file tree
Showing 10 changed files with 373 additions and 1 deletion.
1 change: 1 addition & 0 deletions packages/network/src/clients/thor-client/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Single clients
export * from './nodes';
export * from './transactions';

// Main client
export * from './thor-client';
10 changes: 9 additions & 1 deletion packages/network/src/clients/thor-client/thor-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type HttpClient } from '../../utils';
import { NodesModule } from './nodes';
import { TransactionsModule } from './transactions';

/**
* The `ThorClient` class serves as an interface to interact with the Vechain Thor blockchain.
Expand All @@ -8,16 +9,23 @@ import { NodesModule } from './nodes';
*/
class ThorClient {
/**
* The `NodeClient` instance
* The `NodesModule` instance
*/
public readonly nodes: NodesModule;

/**
* The `TransactionsModule` instance
*/
public readonly transactions: TransactionsModule;

/**
* Constructs a new `ThorClient` instance with a given HTTP client.
*
* @param httpClient - The HTTP client instance used for making network requests.
*/
constructor(protected readonly httpClient: HttpClient) {
this.nodes = new NodesModule(httpClient);
this.transactions = new TransactionsModule(httpClient);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { type Transaction } from '@vechainfoundation/vechain-sdk-core';
import { TRANSACTION, assert } from '@vechainfoundation/vechain-sdk-errors';

/**
* Asserts that the given transaction is signed.
* @param tx - The transaction to check.
*
* @throws {InvalidTransactionError} if the transaction is not signed.
*/
const assertIsSignedTx = (tx: Transaction): void => {
assert(tx.isSigned, TRANSACTION.NOT_SIGNED, 'Transaction must be signed.', {
tx
});
};

export { assertIsSignedTx };
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './assertions';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './types.d';
export * from './transactions-module';
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { type Transaction } from '@vechainfoundation/vechain-sdk-core';
import { Poll, type HttpClient } from '../../../utils';
import {
type TransactionReceipt,
TransactionsClient
} from '../../thorest-client';
import { type WaitForTransactionOptions } from './types';
import { assertIsSignedTx } from './helpers';

/**
* The `TransactionsModule` handles transaction related operations and provides
* convenient methods for sending transactions and waiting for transaction confirmation.
*/
class TransactionsModule {
/**
* Reference to the `TransactionsClient` instance.
*/
private readonly transactionsClient: TransactionsClient;

/**
* Initializes a new instance of the `TransactionsModule` class.
* @param httpClient - The HTTP client instance used for making HTTP requests.
*/
constructor(readonly httpClient: HttpClient) {
this.transactionsClient = new TransactionsClient(httpClient);
}

/**
* Sends a signed transaction to the network.
*
* @param signedTx - the transaction to send. It must be signed.
*
* @returns A promise that resolves to the transaction ID of the sent transaction.
*
* @throws an error if the transaction is not signed.
*/
public async sendTransaction(signedTx: Transaction): Promise<string> {
assertIsSignedTx(signedTx);

const rawTx = `0x${signedTx.encoded.toString('hex')}`;

const txID = (await this.transactionsClient.sendTransaction(rawTx)).id;

return txID;
}

/**
* Waits for a transaction to be included in a block.
*
* @param txID - The transaction ID of the transaction to wait for.
* @param options - Optional parameters for the request. Includes the timeout and interval between requests.
* Both parameters are in milliseconds. If the timeout is not specified, the request will not timeout!
*
* @returns A promise that resolves to the transaction receipt of the transaction. If the transaction is not included in a block before the timeout,
* the promise will resolve to `null`.
*/
public async waitForTransaction(
txID: string,
options?: WaitForTransactionOptions
): Promise<TransactionReceipt | null> {
const result = await Poll.SyncPoll(
async () =>
await this.transactionsClient.getTransactionReceipt(txID),
{
requestIntervalInMilliseconds: options?.intervalMs,
maximumWaitingTimeInMilliseconds: options?.timeoutMs
}
).waitUntil((result) => {
return result !== null;
});

return result;
}
}

export { TransactionsModule };
21 changes: 21 additions & 0 deletions packages/network/src/clients/thor-client/transactions/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* --- Input options start --- */

/**
* Options for `waitForTransaction` method.
*/
interface WaitForTransactionOptions {
/**
* Timeout in milliseconds.
* After this time, the method will throw an error.
*/
timeoutMs?: number;
/**
* Interval in milliseconds.
* The method will check the transaction status every `intervalMs` milliseconds.
*/
intervalMs?: number;
}

/* --- Input options end --- */

export type { WaitForTransactionOptions };
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class TransactionsClient {
*
* @param id - Transaction ID of the transaction to retrieve.
* @param options - (Optional) Other optional parameters for the request.
* If `head` is not specified, the receipt of the transaction at the best block is returned.
* @returns A promise that resolves to the receipt of the transaction.
*/
public async getTransactionReceipt(
Expand Down
124 changes: 124 additions & 0 deletions packages/network/tests/clients/thor-client/transactions/fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import {
type TransactionBody,
TransactionUtils,
contract,
unitsUtils,
networkInfo
} from '@vechainfoundation/vechain-sdk-core';
import { BUILT_IN_CONTRACTS } from '../../../built-in-fixture';
import { TEST_ACCOUNTS } from '../../../fixture';

/**
* Clause to transfer 1 VTHO to TEST_ACCOUNTS.TRANSACTION.TRANSACTION_RECEIVER
*/
const transfer1VTHOClause = {
to: BUILT_IN_CONTRACTS.ENERGY_ADDRESS,
value: '0',
data: contract.encodeFunctionInput(
BUILT_IN_CONTRACTS.ENERGY_ABI,
'transfer',
[
TEST_ACCOUNTS.TRANSACTION.TRANSACTION_RECEIVER.address,
unitsUtils.parseVET('1')
]
)
};

/**
* transaction body that transfers 1 VTHO to TEST_ACCOUNTS.TRANSACTION.TRANSACTION_RECEIVER
*/
const transferTransactionBody: Omit<TransactionBody, 'nonce'> = {
gas: 5000 + TransactionUtils.intrinsicGas([transfer1VTHOClause]) * 5, // @NOTE it is a temporary gas offered solution. This part will be replaced with estimateGas
clauses: [transfer1VTHOClause],
chainTag: networkInfo.solo.chainTag,
blockRef: networkInfo.solo.genesisBlock.id.slice(0, 18),
expiration: 1000,
gasPriceCoef: 128,
dependsOn: null
};

/**
* Expected transaction receipt values.
* Note that this object is not a valid `TransactionReceipt` object.
*/
const expectedReceipt = {
events: [],
gasPayer: '0x2669514f9fe96bc7301177ba774d3da8a06cace4',
gasUsed: 36518,
outputs: [
{
contractAddress: null,
events: [
{
address: '0x0000000000000000000000000000456e65726779',
data: '0x0000000000000000000000000000000000000000000000000de0b6b3a7640000',
topics: [
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
'0x0000000000000000000000002669514f9fe96bc7301177ba774d3da8a06cace4',
'0x0000000000000000000000009e7911de289c3c856ce7f421034f66b6cde49c39'
]
}
],
transfers: []
}
],
reverted: false
};

/**
* waitForTransaction test cases that should return a transaction receipt
*/
const waitForTransactionTestCases = [
{
description:
'Should wait for transaction without timeout and return TransactionReceipt',
options: {
timeoutMs: undefined,
intervalMs: undefined
}
},
{
description:
'Should wait for transaction with timeout and return TransactionReceipt',
options: {
timeoutMs: 5000,
intervalMs: undefined
}
},
{
description:
'Should wait for transaction with intervalMs TransactionReceipt',
options: {
timeoutMs: undefined,
intervalMs: 100
}
},
{
description:
'Should wait for transaction with intervalMs & timeoutMs and return TransactionReceipt',
options: {
timeoutMs: 5000,
intervalMs: 100
}
}
];

/**
* waitForTransaction test cases that should not return a transaction receipt. Instead, should return null.
*/
const invalidWaitForTransactionTestCases = [
{
description: 'Should throw error when timeoutMs is too low',
options: {
timeoutMs: 1,
intervalMs: undefined
}
}
];

export {
waitForTransactionTestCases,
invalidWaitForTransactionTestCases,
transferTransactionBody,
expectedReceipt
};
Loading

1 comment on commit cb161b9

@github-actions
Copy link

Choose a reason for hiding this comment

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

Test Coverage

Summary

Lines Statements Branches Functions
Coverage: 100%
100% (1226/1226) 100% (267/267) 100% (258/258)
Title Tests Skipped Failures Errors Time
core 319 0 💤 0 ❌ 0 🔥 1m 7s ⏱️
network 86 0 💤 0 ❌ 0 🔥 57.565s ⏱️
errors 30 0 💤 0 ❌ 0 🔥 8.466s ⏱️

Please sign in to comment.