use std::path::Path;
use std::sync::Arc;
use base64::Engine as _;
use guardian_client::{
AccountState, DeltaObject as ProtoDeltaObject, DeltaStatus, GetStateResponse, PendingStatus,
delta_status,
};
use guardian_shared::SignatureScheme;
use miden_client::Serializable;
use miden_client::builder::ClientBuilder;
use miden_client::keystore::FilesystemKeyStore;
use miden_client::note_transport::NoteTransportClient;
use miden_client::rpc::Endpoint;
use miden_client::testing::mock::MockRpcApi;
use miden_client::testing::note_transport::{MockNoteTransportApi, MockNoteTransportNode};
use miden_client_sqlite_store::SqliteStore;
use miden_confidential_contracts::multisig_guardian::{
MultisigGuardianBuilder, MultisigGuardianConfig,
};
use miden_protocol::Word;
use miden_protocol::account::Account;
use miden_protocol::account::auth::AuthSecretKey;
use miden_protocol::asset::FungibleAsset;
use miden_protocol::crypto::dsa::falcon512_poseidon2::SecretKey;
use miden_protocol::crypto::rand::RandomCoin;
use miden_protocol::note::{Note, NoteDetails, NoteType};
use miden_standards::note::P2idNote;
use miden_tx::utils::sync::RwLock;
use super::MultisigClient;
use crate::keystore::{GuardianKeyStore, KeyManager};
use crate::prover::ProverConfig;
use crate::rpc::RpcConfig;
use crate::transaction::word_to_hex;
const BASE64: base64::engine::general_purpose::GeneralPurpose =
base64::engine::general_purpose::STANDARD;
pub(crate) async fn offline_client_parts(
dir: &Path,
node: Arc<MockRpcApi>,
transport: Option<Arc<dyn NoteTransportClient>>,
) -> (MultisigClient, Arc<SqliteStore>) {
offline_client_parts_with_keystore(dir, node, transport, Arc::new(GuardianKeyStore::generate()))
.await
}
pub(crate) async fn offline_client_parts_with_keystore(
dir: &Path,
node: Arc<MockRpcApi>,
transport: Option<Arc<dyn NoteTransportClient>>,
keystore: Arc<dyn KeyManager>,
) -> (MultisigClient, Arc<SqliteStore>) {
let store = Arc::new(
SqliteStore::new(dir.join("store.sqlite3"))
.await
.expect("sqlite store opens"),
);
let keystore_dir = dir.join("keys");
std::fs::create_dir_all(&keystore_dir).expect("keystore dir");
let protocol_config = node.protocol_config();
let mut builder = ClientBuilder::<FilesystemKeyStore>::new()
.rpc(node)
.store(store.clone())
.filesystem_keystore(keystore_dir)
.expect("keystore opens");
if let Some(transport) = transport {
builder = builder.note_transport(transport);
}
let miden_client = builder.build().await.expect("miden client builds");
miden_client
.seed_protocol_config(protocol_config)
.await
.expect("mock protocol config is stored");
let client = MultisigClient::new(
miden_client,
keystore,
"http://localhost:1".to_string(),
dir.to_path_buf(),
Endpoint::localhost(),
None,
ProverConfig::new(),
RpcConfig::new(),
);
(client, store)
}
pub(crate) async fn offline_client(
dir: &Path,
transport: Option<Arc<dyn NoteTransportClient>>,
) -> MultisigClient {
offline_client_parts(dir, Arc::new(MockRpcApi::default()), transport)
.await
.0
}
pub(crate) async fn offline_client_with_node(dir: &Path, node: Arc<MockRpcApi>) -> MultisigClient {
offline_client_with_node_parts(dir, node).await.0
}
pub(crate) async fn offline_client_with_node_parts(
dir: &Path,
node: Arc<MockRpcApi>,
) -> (MultisigClient, Arc<SqliteStore>) {
let (mut client, store) = offline_client_parts(dir, node.clone(), None).await;
client.set_node_rpc_client(node);
(client, store)
}
pub(crate) fn mock_transport() -> (
Arc<RwLock<MockNoteTransportNode>>,
Arc<dyn NoteTransportClient>,
) {
let node = Arc::new(RwLock::new(MockNoteTransportNode::new()));
let api: Arc<dyn NoteTransportClient> = Arc::new(MockNoteTransportApi::new(node.clone()));
(node, api)
}
pub(crate) fn add_to_transport(node: &Arc<RwLock<MockNoteTransportNode>>, note: Note) {
let header = *note.header();
let details_bytes = NoteDetails::from(note).to_bytes();
node.write().add_note(header, details_bytes);
}
pub(crate) fn test_wallet(seed: u8) -> Account {
use miden_client::account::component::BasicWallet;
use miden_client::account::{AccountBuilder, AccountBuilderSchemaCommitmentExt, AccountType};
use miden_protocol::account::auth::AuthScheme;
use miden_standards::account::auth::{Approver, AuthSingleSig};
let key_pair = AuthSecretKey::new_falcon512_poseidon2();
let auth_component = AuthSingleSig::new(Approver::new(
key_pair.public_key().to_commitment(),
AuthScheme::Falcon512Poseidon2,
));
AccountBuilder::new([seed; 32])
.account_type(AccountType::Private)
.with_component(auth_component)
.with_component(BasicWallet)
.build_with_schema_commitment()
.expect("test wallet builds")
}
pub(crate) fn chain_with_notes(
notes: Vec<miden_protocol::transaction::RawOutputNote>,
) -> Arc<MockRpcApi> {
let mut builder = miden_client::testing::MockChain::builder();
for note in notes {
builder.add_output_note(note);
}
let api = Arc::new(MockRpcApi::new(builder.build().expect("mock chain builds")));
api.advance_blocks(4);
api
}
pub(crate) fn guarded_multisig_account() -> Account {
let signer_commitment = SecretKey::new().public_key().to_commitment();
MultisigGuardianBuilder::new(MultisigGuardianConfig::new(
1,
vec![signer_commitment],
Word::from([9u32, 8, 7, 6]),
))
.build()
.expect("guarded multisig account builds")
}
pub(crate) fn p2id_note_for(target: &Account, seed: u32, note_type: NoteType) -> Note {
let mut rng = RandomCoin::new(Word::from(&[seed, 0, 0, 0]));
P2idNote::builder()
.sender(test_wallet(200).id())
.target(target.id())
.asset(FungibleAsset::mock(1))
.note_type(note_type)
.generate_serial_number(&mut rng)
.build()
.expect("p2id note builds")
.into()
}
pub(crate) fn multisig_account(signer: Word, guardian_commitment: Word, seed: u8) -> Account {
multisig_account_with_scheme(signer, guardian_commitment, seed, SignatureScheme::Falcon)
}
pub(crate) fn multisig_account_with_scheme(
signer: Word,
guardian_commitment: Word,
seed: u8,
scheme: SignatureScheme,
) -> Account {
let config = MultisigGuardianConfig::new(1, vec![signer], guardian_commitment)
.with_signature_scheme(scheme);
MultisigGuardianBuilder::new(config)
.with_seed([seed; 32])
.build()
.expect("multisig account builds")
}
pub(crate) fn registered_state(account: &Account) -> GetStateResponse {
GetStateResponse {
success: true,
message: String::new(),
state: Some(AccountState {
account_id: account.id().to_string(),
state_json: serde_json::json!({
"data": BASE64.encode(account.to_bytes()),
})
.to_string(),
commitment: word_to_hex(&account.to_commitment()),
created_at: String::new(),
updated_at: String::new(),
}),
}
}
pub(crate) fn pending_proto_delta(
account: &Account,
nonce: u64,
delta_payload: String,
proposer_hex: &str,
) -> ProtoDeltaObject {
ProtoDeltaObject {
account_id: account.id().to_string(),
nonce,
prev_commitment: word_to_hex(&account.to_commitment()),
delta_payload,
new_commitment: String::new(),
ack_sig: String::new(),
candidate_at: String::new(),
canonical_at: None,
discarded_at: None,
status: Some(DeltaStatus {
status: Some(delta_status::Status::Pending(PendingStatus {
timestamp: "2026-01-01T00:00:00Z".to_string(),
proposer_id: proposer_hex.to_string(),
cosigner_sigs: vec![],
})),
discard_reason: String::new(),
retain_reason: String::new(),
}),
ack_pubkey: None,
ack_scheme: None,
}
}