Skip to content

Commit

Permalink
update test txn generator flags
Browse files Browse the repository at this point in the history
  • Loading branch information
yuunlimm committed Feb 7, 2025
1 parent 677bfd1 commit ef98235
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 132 deletions.
1 change: 1 addition & 0 deletions .github/workflows/indexer-processor-testing.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ jobs:
cat ./imported_transactions/imported_transactions.yaml # Print the updated file for verification
cargo run -- --testing-folder ./imported_transactions --output-folder ../indexer-test-transactions/src/new_json_transactions
cargo run -- --testing-folder ./imported_transactions --output-folder ../indexer-test-transactions/src/new_json_transactions --mode=script
- name: Install jq
run: sudo apt-get install jq # Ensure jq is installed for JSON processing
Expand Down
79 changes: 41 additions & 38 deletions ecosystem/indexer-grpc/indexer-transaction-generator/README.md
Original file line number Diff line number Diff line change
@@ -1,69 +1,72 @@
# Indexer Transaction Generator

This tool is to generate transactions for testing purpose.
This tool is to generate transactions for testing purposes.

## Usage

Under root folder, i.e., `aptos-core`, run
Under the root folder, i.e., `aptos-core`, run

```bash
cargo run -p aptos-indexer-transaction-generator -- \
--testing-folder ecosystem/indexer-grpc/indexer-transaction-generator/imported_transactions \
--output-folder ecosystem/indexer-grpc/indexer-test-transactions/src
```

**You can also use absolute path, run(using binary as an example)**
**You can also use absolute path, run (using binary as an example)**

```bash
./aptos-indexer-transaction-generator \
--testing-folder /your/aptos-core/ecosystem/indexer-grpc/indexer-transaction-generator/imported_transactions \
--output-folder /tmp/ttt
```

### Config overview

* Your testing folder should contain:
* One file called `testing_accounts.yaml`, which contains testing accounts used.
```yaml
accounts:
a531b7fdd7917f73ca216d89a8d9ce0cf7e7cfb9086ca6f6cbf9521532748d16:
private_key: "0x99978d48e7b2d50d0a7a3273db0929447ae59635e71118fa256af654c0ce56c9"
public_key: "0x39b4acc85e026dc056464a5ea00b98f858260eaad2b74dd30b86ae0d4d94ddf5"
account: a531b7fdd7917f73ca216d89a8d9ce0cf7e7cfb9086ca6f6cbf9521532748d16
```
* One file called `imported_transactions.yaml`, which is used for importing transactions.
### Command-Line Options

- `mode`: (Optional) Specify the mode of operation for the indexer. Possible values are `import` and `script`. Default is `import`.

### Config Overview

Your testing folder should contain:
- One file called `testing_accounts.yaml`, which contains testing accounts used.
```yaml
accounts:
a531b7fdd7917f73ca216d89a8d9ce0cf7e7cfb9086ca6f6cbf9521532748d16:
private_key: "0x99978d48e7b2d50d0a7a3273db0929447ae59635e71118fa256af654c0ce56c9"
public_key: "0x39b4acc85e026dc056464a5ea00b98f858260eaad2b74dd30b86ae0d4d94ddf5"
account: a531b7fdd7917f73ca216d89a8d9ce0cf7e7cfb9086ca6f6cbf9521532748d16
```
- One file called `imported_transactions.yaml`, which is used for importing transactions.

```yaml
testnet:
# Transaction Stream endpoint addresss.
transaction_stream_endpoint: https://grpc.testnet.aptoslabs.com:443
# (Optional) The key to use with developers.aptoslabs.com
api_key: YOUR_KEY_HERE
# A map from versions to dump and their output names.
versions_to_import:
123: testnet_v1.json
mainnet:
...
```
* One folder called `move_fixtures`, which contains move scripts and configs.
```yaml
testnet:
# Transaction Stream endpoint address.
transaction_stream_endpoint: https://grpc.testnet.aptoslabs.com:443
# (Optional) The key to use with developers.aptoslabs.com
api_key: YOUR_KEY_HERE
# A map from versions to dump and their output names.
versions_to_import:
123: testnet_v1.json
mainnet:
...
```
- One folder called `move_fixtures`, which contains move scripts and configs.
* An example script transaction config looks like:
```yaml
transactions:
- output_name: fa_mint_transfer_burn
script_path: fa_mint_transfer_burn
sender_address: REPLACE_WITH_ACCOUNT_ADDRESS
transactions:
- output_name: fa_mint_transfer_burn
script_path: fa_mint_transfer_burn
sender_address: REPLACE_WITH_ACCOUNT_ADDRESS
```


You can check the example [here](imported_transactions).


### Account Management
Each sender_address specified in script transaction config is a place holder string;
the actual account address will be allocated by account manager.

The accounts in `testing_accounts.yaml` will be used to run scripted transaction.
They are persisted in config so each scripted transaction's generated output stays consistent between
Each `sender_address` specified in the script transaction config is a placeholder string;
the actual account address will be allocated by the account manager.

The accounts in `testing_accounts.yaml` will be used to run scripted transactions.
They are persisted in the config so each scripted transaction's generated output stays consistent between
`aptos-indexer-transaction-generator` runs. You can generate more testing accounts using
Aptos CLI by running `aptos init --profile local`.

Expand Down
226 changes: 132 additions & 94 deletions ecosystem/indexer-grpc/indexer-transaction-generator/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,47 @@ use crate::{
},
};
use anyhow::Context;
use clap::Parser;
use serde::{Deserialize, Serialize};
use std::{
collections::{HashMap, HashSet},
fs,
path::{Path, PathBuf},
};
use url::Url;
use clap::{Parser, ValueEnum};
use std::fmt;
use std::str::FromStr;

const SCRIPTED_TRANSACTIONS_FOLDER: &str = "scripted_transactions";
const MOVE_SCRIPTS_FOLDER: &str = "move_fixtures";
const IMPORTED_TRANSACTION_CONFIG_FILE: &str = "imported_transactions.yaml";
const ACCOUNT_MANAGER_FILE_NAME: &str = "testing_accounts.yaml";

#[derive(Clone, Debug, PartialEq)]
pub enum Mode {
Import,
Script,
}

// Implement Display for Mode
impl fmt::Display for Mode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self) // Use the debug representation
}
}

impl FromStr for Mode {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"import" => Ok(Mode::Import),
"script" => Ok(Mode::Script),
_ => Err(anyhow::anyhow!("Invalid mode: {}", s)),
}
}
}

#[derive(Parser)]
pub struct IndexerCliArgs {
/// Path to the testing folder, which includes:
Expand All @@ -36,6 +63,10 @@ pub struct IndexerCliArgs {
/// Path to the output folder where the generated transactions will be saved.
#[clap(long)]
pub output_folder: PathBuf,

/// Mode of operation for the indexer.
#[clap(long, default_value_t = Mode::Import, value_parser = Mode::from_str)]
pub mode: Mode,
}

impl IndexerCliArgs {
Expand All @@ -47,113 +78,119 @@ impl IndexerCliArgs {
}
let testing_folder = convert_relative_path_to_absolute_path(&self.testing_folder);

// Run the transaction importer.
let imported_transactions_config_path =
testing_folder.join(IMPORTED_TRANSACTION_CONFIG_FILE);
// Determine the behavior based on the flags
match self.mode {
Mode::Import => {
// Run the transaction importer.
println!("Running the transaction importer.");

// TODO: refactor this further to reduce the nesting.
// if the imported transactions config file exists, run the transaction importer.
if imported_transactions_config_path.exists() {
let imported_transactions_config_raw: String =
tokio::fs::read_to_string(&imported_transactions_config_path).await?;
let imported_transactions_config: TransactionImporterConfig =
serde_yaml::from_str(&imported_transactions_config_raw)?;
let imported_transactions_config_path =
testing_folder.join(IMPORTED_TRANSACTION_CONFIG_FILE);

imported_transactions_config
.validate_and_run(&output_folder)
.await
.context("Importing transactions failed.")?;
}
if imported_transactions_config_path.exists() {
let imported_transactions_config_raw: String =
tokio::fs::read_to_string(&imported_transactions_config_path).await?;
let imported_transactions_config: TransactionImporterConfig =
serde_yaml::from_str(&imported_transactions_config_raw)?;

// Run the script transaction generator.
let script_transactions_output_folder = output_folder.join(SCRIPTED_TRANSACTIONS_FOLDER);
let move_folder_path = testing_folder.join(MOVE_SCRIPTS_FOLDER);
// If the move fixtures folder does not exist, skip the script transaction generator.
if !move_folder_path.exists() {
return Ok(());
}
if !script_transactions_output_folder.exists() {
tokio::fs::create_dir_all(&script_transactions_output_folder).await?;
}
// 1. Validate.
// Scan all yaml files in the move folder path.
let mut script_transactions_vec: Vec<(String, ScriptTransactions)> = vec![];
let move_files = std::fs::read_dir(&move_folder_path)?;
let mut used_sender_addresses: HashSet<String> = HashSet::new();
for entry in move_files {
let entry = entry?;
// entry has to be a file.
if !entry.file_type()?.is_file() {
continue;
}
let path = entry.path();
if path.extension().unwrap_or_default() == "yaml" {
let file_name = path.file_name().unwrap().to_str().unwrap();
let script_transactions_raw: String = tokio::fs::read_to_string(&path).await?;
let script_transactions: ScriptTransactions =
serde_yaml::from_str(&script_transactions_raw)?;

let new_senders: HashSet<String> = script_transactions
.transactions
.iter()
.map(|txn| txn.sender_address.clone())
.collect();
// Check if any new sender is already used
if new_senders
.iter()
.any(|sender| used_sender_addresses.contains(sender))
{
return Err(anyhow::anyhow!(
"[Script Transaction Generator] Sender address in file `{}` is already being used",
file_name
));
imported_transactions_config
.validate_and_run(&output_folder)
.await
.context("Importing transactions failed.")?;
}
used_sender_addresses.extend(new_senders);
script_transactions_vec.push((file_name.to_string(), script_transactions));
},
Mode::Script => {
// Run the script transaction generator.
let script_transactions_output_folder =
output_folder.join(SCRIPTED_TRANSACTIONS_FOLDER);
let move_folder_path = testing_folder.join(MOVE_SCRIPTS_FOLDER);
// If the move fixtures folder does not exist, skip the script transaction generator.
if !move_folder_path.exists() {
return Ok(());
}
}
// Validate the configuration.
let mut output_script_transactions_set = HashSet::new();
for (file_name, script_transactions) in script_transactions_vec.iter() {
if script_transactions.transactions.is_empty() {
return Err(anyhow::anyhow!(
"[Script Transaction Generator] No transactions found in file `{}`",
file_name
));
if !script_transactions_output_folder.exists() {
tokio::fs::create_dir_all(&script_transactions_output_folder).await?;
}
for script_transaction in script_transactions.transactions.iter() {
if let Some(output_name) = &script_transaction.output_name {
if !output_script_transactions_set.insert(output_name.clone()) {
// 1. Validate.
// Scan all yaml files in the move folder path.
let mut script_transactions_vec: Vec<(String, ScriptTransactions)> = vec![];
let move_files = std::fs::read_dir(&move_folder_path)?;
let mut used_sender_addresses: HashSet<String> = HashSet::new();
for entry in move_files {
let entry = entry?;
// entry has to be a file.
if !entry.file_type()?.is_file() {
continue;
}
let path = entry.path();
if path.extension().unwrap_or_default() == "yaml" {
let file_name = path.file_name().unwrap().to_str().unwrap();
let script_transactions_raw: String = tokio::fs::read_to_string(&path).await?;
let script_transactions: ScriptTransactions =
serde_yaml::from_str(&script_transactions_raw)?;

let new_senders: HashSet<String> = script_transactions
.transactions
.iter()
.map(|txn| txn.sender_address.clone())
.collect();
// Check if any new sender is already used
if new_senders
.iter()
.any(|sender| used_sender_addresses.contains(sender))
{
return Err(anyhow::anyhow!(
"[Script Transaction Generator] Output file name `{}` is duplicated in file `{}`",
output_name.clone(),
"[Script Transaction Generator] Sender address in file `{}` is already being used",
file_name
));
}
used_sender_addresses.extend(new_senders);
script_transactions_vec.push((file_name.to_string(), script_transactions));
}
}
// Validate the configuration.
let mut output_script_transactions_set = HashSet::new();
for (file_name, script_transactions) in script_transactions_vec.iter() {
if script_transactions.transactions.is_empty() {
return Err(anyhow::anyhow!(
"[Script Transaction Generator] No transactions found in file `{}`",
file_name
));
}
for script_transaction in script_transactions.transactions.iter() {
if let Some(output_name) = &script_transaction.output_name {
if !output_script_transactions_set.insert(output_name.clone()) {
return Err(anyhow::anyhow!(
"[Script Transaction Generator] Output file name `{}` is duplicated in file `{}`",
output_name.clone(),
file_name
));
}
}
}
}
// Run each config.
let account_manager_file_path = testing_folder.join(ACCOUNT_MANAGER_FILE_NAME);
let mut account_manager = AccountManager::load(&account_manager_file_path).await?;
let mut managed_node = ManagedNode::start(None, None).await?;
for (file_name, script_transactions) in script_transactions_vec {
script_transactions
.run(
&move_folder_path,
&script_transactions_output_folder,
&mut account_manager,
)
.await
.context(format!(
"Failed to generate script transaction for file `{}`",
file_name
))?;
}
// Stop the localnet.
managed_node.stop().await?;
},
}

// Run each config.
let account_manager_file_path = testing_folder.join(ACCOUNT_MANAGER_FILE_NAME);
let mut account_manager = AccountManager::load(&account_manager_file_path).await?;
let mut managed_node = ManagedNode::start(None, None).await?;
for (file_name, script_transactions) in script_transactions_vec {
script_transactions
.run(
&move_folder_path,
&script_transactions_output_folder,
&mut account_manager,
)
.await
.context(format!(
"Failed to generate script transaction for file `{}`",
file_name
))?;
}
// Stop the localnet.
managed_node.stop().await?;

// Using the builder pattern to construct the code
let code = TransactionCodeBuilder::new()
.add_license_in_comments()
Expand Down Expand Up @@ -364,6 +401,7 @@ mod tests {
let indexer_cli_args = IndexerCliArgs {
testing_folder: tempfile.path().to_path_buf(),
output_folder,
mode: Mode::Script,
};
let result = indexer_cli_args.run().await;
assert!(result.is_err());
Expand Down

0 comments on commit ef98235

Please sign in to comment.