Skip to content
Draft
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 .github/workflows/reusable-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,8 @@ jobs:
path: tests/lazy-account
- cmd: cd tests/test-instruction-validation && ./test.sh
path: tests/test-instruction-validation
- cmd: cd tests/account-generation-test && anchor test
path: tests/account-generation-test

steps:
- uses: actions/checkout@v3
Expand Down
180 changes: 180 additions & 0 deletions cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1332,6 +1332,45 @@ pub struct AccountDirEntry {
pub directory: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FundedAccount {
// Base58 pubkey string of the account to fund
pub address: String,
// Amount of lamports to fund the account with (default: 1 SOL = 1_000_000_000 lamports)
#[serde(skip_serializing_if = "Option::is_none")]
pub lamports: Option<u64>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenMint {
// Base58 pubkey string of the mint account, or "new" to generate a random keypair
pub address: String,
// Number of base 10 digits to the right of the decimal place (required)
pub decimals: u8,
// Initial supply of tokens (default: 0)
#[serde(skip_serializing_if = "Option::is_none")]
pub supply: Option<u64>,
// Optional mint authority (default: None = fixed supply)
#[serde(skip_serializing_if = "Option::is_none")]
pub mint_authority: Option<String>,
// Optional freeze authority (default: None = no freeze authority)
#[serde(skip_serializing_if = "Option::is_none")]
pub freeze_authority: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenAccount {
// Reference to mint (pubkey string or "new" to use the most recently created mint)
pub mint: String,
// Owner of the token account ("new" to generate random keypair, or specific pubkey)
pub owner: String,
// Amount of tokens to fund the account with
pub amount: u64,
// Optional: specific token account address (default: generate new)
#[serde(skip_serializing_if = "Option::is_none")]
pub address: Option<String>,
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct _Validator {
// Load an account from the provided JSON file
Expand All @@ -1340,6 +1379,15 @@ pub struct _Validator {
// Load all the accounts from the JSON files found in the specified DIRECTORY
#[serde(skip_serializing_if = "Option::is_none")]
pub account_dir: Option<Vec<AccountDirEntry>>,
// Generate and fund accounts with lamports
#[serde(skip_serializing_if = "Option::is_none")]
pub fund_accounts: Option<Vec<FundedAccount>>,
// Create SPL token mints
#[serde(skip_serializing_if = "Option::is_none")]
pub mints: Option<Vec<TokenMint>>,
// Create and fund SPL token accounts
#[serde(skip_serializing_if = "Option::is_none")]
pub token_accounts: Option<Vec<TokenAccount>>,
// IP address to bind the validator ports. [default: 127.0.0.1]
#[serde(skip_serializing_if = "Option::is_none")]
pub bind_address: Option<String>,
Expand Down Expand Up @@ -1396,6 +1444,12 @@ pub struct Validator {
pub account: Option<Vec<AccountEntry>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub account_dir: Option<Vec<AccountDirEntry>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fund_accounts: Option<Vec<FundedAccount>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mints: Option<Vec<TokenMint>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_accounts: Option<Vec<TokenAccount>>,
pub bind_address: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub clone: Option<Vec<CloneEntry>>,
Expand Down Expand Up @@ -1432,6 +1486,9 @@ impl From<_Validator> for Validator {
Self {
account: _validator.account,
account_dir: _validator.account_dir,
fund_accounts: _validator.fund_accounts,
mints: _validator.mints,
token_accounts: _validator.token_accounts,
bind_address: _validator
.bind_address
.unwrap_or_else(|| DEFAULT_BIND_ADDRESS.to_string()),
Expand Down Expand Up @@ -1461,6 +1518,9 @@ impl From<Validator> for _Validator {
Self {
account: validator.account,
account_dir: validator.account_dir,
fund_accounts: validator.fund_accounts,
mints: validator.mints,
token_accounts: validator.token_accounts,
bind_address: Some(validator.bind_address),
clone: validator.clone,
dynamic_port_range: validator.dynamic_port_range,
Expand Down Expand Up @@ -1529,6 +1589,62 @@ impl Merge for _Validator {
}
},
},
fund_accounts: match self.fund_accounts.take() {
None => other.fund_accounts,
Some(mut entries) => match other.fund_accounts {
None => Some(entries),
Some(other_entries) => {
for other_entry in other_entries {
match entries
.iter()
.position(|my_entry| *my_entry.address == other_entry.address)
{
None => entries.push(other_entry),
Some(i) => entries[i] = other_entry,
};
}
Some(entries)
}
},
},
mints: match self.mints.take() {
None => other.mints,
Some(mut entries) => match other.mints {
None => Some(entries),
Some(other_entries) => {
for other_entry in other_entries {
match entries
.iter()
.position(|my_entry| *my_entry.address == other_entry.address)
{
None => entries.push(other_entry),
Some(i) => entries[i] = other_entry,
};
}
Some(entries)
}
},
},
token_accounts: match self.token_accounts.take() {
None => other.token_accounts,
Some(mut entries) => match other.token_accounts {
None => Some(entries),
Some(other_entries) => {
// For token accounts, we merge by mint+owner combination
for other_entry in other_entries {
match entries.iter().position(|my_entry| {
*my_entry.mint == other_entry.mint
&& *my_entry.owner == other_entry.owner
&& my_entry.address == other_entry.address
}) {
None => entries.push(other_entry),
Some(i) => entries[i] = other_entry,
};
}
Some(entries)
}
},
},
bind_address: other.bind_address.or_else(|| self.bind_address.take()),
clone: match self.clone.take() {
None => other.clone,
Expand Down Expand Up @@ -1797,6 +1913,70 @@ mod tests {
assert!(!config.features.skip_lint);
}

#[test]
fn parse_fund_accounts_config() {
let config_str = r#"
[provider]
cluster = "localnet"
wallet = "id.json"

[test.validator]
[[test.validator.fund_accounts]]
address = "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM"
lamports = 2000000000

[[test.validator.fund_accounts]]
address = "GjJyeC1rB1hL8ZkLqKqJzJzJzJzJzJzJzJzJzJzJzJzJz"
"#;

let config = Config::from_str(config_str).unwrap();
assert!(config.test_validator.is_some());
let test_validator = config.test_validator.as_ref().unwrap();
assert!(test_validator.validator.is_some());
let validator = test_validator.validator.as_ref().unwrap();
assert!(validator.fund_accounts.is_some());

let fund_accounts = validator.fund_accounts.as_ref().unwrap();
assert_eq!(fund_accounts.len(), 2);
assert_eq!(
fund_accounts[0].address,
"9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM"
);
assert_eq!(fund_accounts[0].lamports, Some(2000000000));
assert_eq!(
fund_accounts[1].address,
"GjJyeC1rB1hL8ZkLqKqJzJzJzJzJzJzJzJzJzJzJzJzJz"
);
assert_eq!(fund_accounts[1].lamports, None); // Should default to 1 SOL
}

#[test]
fn parse_fund_accounts_without_lamports() {
let config_str = r#"
[provider]
cluster = "localnet"
wallet = "id.json"

[test.validator]
[[test.validator.fund_accounts]]
address = "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM"
"#;

let config = Config::from_str(config_str).unwrap();
let fund_accounts = config
.test_validator
.as_ref()
.unwrap()
.validator
.as_ref()
.unwrap()
.fund_accounts
.as_ref()
.unwrap();
assert_eq!(fund_accounts.len(), 1);
assert_eq!(fund_accounts[0].lamports, None);
}

#[test]
fn parse_skip_lint_no_value() {
let string = BASE_CONFIG.to_owned() + "[features]";
Expand Down
Loading
Loading