use std::path::Path;
use thruput_json_rpc_types::runtime_manifest::{
BOOTSTRAP_ACCOUNT_ROLE_HOSTED_DEV_ACCOUNT, RuntimeManifest, RuntimeManifestParseError,
parse_runtime_manifest,
};
use thruput_signing::{
DEFAULT_PUBKEY_ACCOUNT_NONCE, LocalSigner, PubkeyAccount, Signer, derive_dev_account_signer,
};
use thruput_xdr::types::{ContractAddress, NetworkHash};
use crate::error::{Result, TestkitError};
const SUPPORTED_DERIVATION_SCHEME: &str = "thruput-bootstrap-account-v1";
const MAX_RUNTIME_MANIFEST_BYTES: usize = 1024 * 1024;
#[derive(Debug)]
pub(crate) struct TestAccount {
pub(crate) index: u32,
pub(crate) signer: LocalSigner,
pub(crate) wallet_address: ContractAddress,
pub(crate) network_hash: Option<NetworkHash>,
}
#[derive(Debug)]
pub(crate) enum AccountSource {
Manifest(Box<RuntimeManifest>),
Native {
count: usize,
seed: String,
network_hash: Option<NetworkHash>,
},
}
impl AccountSource {
pub(crate) fn accounts(&self) -> Result<Vec<TestAccount>> {
match self {
Self::Manifest(manifest) => derive_manifest_accounts(manifest),
Self::Native {
count,
seed,
network_hash,
} => Ok(derive_native_accounts(*count, seed, network_hash.as_ref())),
}
}
}
pub(crate) fn load_runtime_manifest(path: &Path) -> Result<RuntimeManifest> {
let display = path.display().to_string();
let bytes = std::fs::read(path).map_err(|source| TestkitError::ManifestLoad {
path: display.clone(),
detail: source.to_string(),
})?;
if bytes.len() > MAX_RUNTIME_MANIFEST_BYTES {
return Err(TestkitError::ManifestLoad {
path: display,
detail: format!("manifest exceeds {MAX_RUNTIME_MANIFEST_BYTES} bytes"),
});
}
let manifest = parse_runtime_manifest(&bytes).map_err(|error| match error {
RuntimeManifestParseError::Json(source) => TestkitError::ManifestLoad {
path: display,
detail: source.to_string(),
},
RuntimeManifestParseError::UnsupportedSchema { actual, .. } => {
TestkitError::ManifestSchema { actual }
}
})?;
Ok(manifest)
}
fn derive_manifest_accounts(manifest: &RuntimeManifest) -> Result<Vec<TestAccount>> {
let derivation = hosted_derivation(manifest)?;
if !manifest
.bootstrap_accounts
.iter()
.any(|account| account.role == BOOTSTRAP_ACCOUNT_ROLE_HOSTED_DEV_ACCOUNT)
{
return Err(TestkitError::NoBootstrapAccounts);
}
let seed = &derivation.seed;
manifest
.bootstrap_accounts
.iter()
.filter(|account| account.role == BOOTSTRAP_ACCOUNT_ROLE_HOSTED_DEV_ACCOUNT)
.map(|account| {
let account_index = hosted_index(account)?;
let index = account_index as usize;
let signer = derive_dev_account_signer(seed, index);
if account.public_key.as_ref() != signer.verifying_key_bytes().as_slice() {
return Err(TestkitError::BootstrapPublicKeyMismatch {
index: account_index,
});
}
if PubkeyAccount::from_public_key_bytes(signer.verifying_key_bytes(), 0).address()
!= account.wallet_address.0
{
return Err(TestkitError::Manifest {
field: "bootstrapAccounts.walletAddress",
detail: "address is not its nonce-zero native account".into(),
});
}
Ok(TestAccount {
index: account_index,
signer,
wallet_address: account.wallet_address.0,
network_hash: Some(manifest.network_hash.to_network_hash()),
})
})
.collect()
}
pub(crate) fn derive_native_accounts(
count: usize,
seed: &str,
network_hash: Option<&NetworkHash>,
) -> Vec<TestAccount> {
(0..count)
.map(|index| {
let signer = derive_dev_account_signer(&format!("thruput-testkit:{seed}"), index);
let account = PubkeyAccount::new(signer.public_key(), DEFAULT_PUBKEY_ACCOUNT_NONCE);
TestAccount {
index: u32::try_from(index).unwrap_or(u32::MAX),
wallet_address: account.address(),
signer,
network_hash: network_hash.cloned(),
}
})
.collect()
}
pub(crate) fn derive_faucet(manifest: &RuntimeManifest) -> Result<Option<TestAccount>> {
if manifest.kind
!= thruput_json_rpc_types::runtime_manifest::HOSTED_DEVNET_RUNTIME_MANIFEST_KIND
{
return Ok(None);
}
let Some(account) = manifest.bootstrap_accounts.iter().find(|account| {
account.role == thruput_json_rpc_types::runtime_manifest::BOOTSTRAP_ACCOUNT_ROLE_FAUCET
}) else {
return Ok(None);
};
let derivation = hosted_derivation(manifest)?;
let account_index = hosted_index(account)?;
let signer = derive_dev_account_signer(&derivation.seed, account_index as usize);
if account.public_key.as_ref() != signer.verifying_key_bytes().as_slice() {
return Err(TestkitError::BootstrapPublicKeyMismatch {
index: account_index,
});
}
if PubkeyAccount::from_public_key_bytes(signer.verifying_key_bytes(), 0).address()
!= account.wallet_address.0
{
return Err(TestkitError::Manifest {
field: "bootstrapAccounts.walletAddress",
detail: "address is not its nonce-zero native account".into(),
});
}
Ok(Some(TestAccount {
index: account_index,
signer,
wallet_address: account.wallet_address.0,
network_hash: Some(manifest.network_hash.to_network_hash()),
}))
}
fn hosted_derivation(
manifest: &RuntimeManifest,
) -> Result<&thruput_json_rpc_types::runtime_manifest::BootstrapAccountDerivation, TestkitError> {
if manifest.kind
!= thruput_json_rpc_types::runtime_manifest::HOSTED_DEVNET_RUNTIME_MANIFEST_KIND
{
return Err(TestkitError::Manifest { field: "bootstrapAccounts", detail: "Local manifests contain public allocations only. Use scenario-owned accounts instead of bootstrap_accounts().".into() });
}
let derivation = manifest
.bootstrap_account_derivation
.as_ref()
.ok_or_else(|| TestkitError::Manifest {
field: "bootstrapAccountDerivation",
detail: "hosted bootstrap derivation is missing".into(),
})?;
if derivation.scheme != SUPPORTED_DERIVATION_SCHEME {
return Err(TestkitError::UnsupportedDerivationScheme {
scheme: derivation.scheme.clone(),
});
}
Ok(derivation)
}
fn hosted_index(
account: &thruput_json_rpc_types::runtime_manifest::BootstrapAccount,
) -> Result<u32> {
account.index.ok_or_else(|| TestkitError::Manifest {
field: "bootstrapAccounts.index",
detail: "hosted bootstrap index is missing".into(),
})
}
#[cfg(test)]
mod tests {
use super::*;
const HOGD_HOSTED_RUNTIME: &str = include_str!("../tests/fixtures/hogd-hosted-runtime.json");
#[test]
fn derives_every_account_of_a_hogd_hosted_runtime() {
let manifest = parse_runtime_manifest(HOGD_HOSTED_RUNTIME.as_bytes()).unwrap();
let accounts = derive_manifest_accounts(&manifest).unwrap();
assert_eq!(
accounts
.iter()
.map(|account| account.index)
.collect::<Vec<_>>(),
[0, 1, 2]
);
for (account, published) in accounts.iter().zip(&manifest.bootstrap_accounts) {
assert_eq!(account.wallet_address, published.wallet_address.0);
}
assert!(derive_faucet(&manifest).unwrap().is_none());
}
}