use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec::Vec;
use keetanetwork_account::KeyPairType;
use keetanetwork_block::{
AccountRef, AdjustMethod, Amount, Block, IdentifierCreateArguments, ManageCertificate, ModifyPermissions,
Operation, Receive, Send, SetInfo,
};
use keetanetwork_vote::{VoteQuote, VoteStaple};
use crate::builder::TransactionBuilder;
use crate::client::{is_ledger_code, KeetaClient};
use crate::error::ClientError;
use crate::model::{
AccountState, Acl, Certificate, ChainQuery, HistoryEntry, HistoryQuery, TokenBalance, TransmitOptions,
};
use crate::swap::{AcceptSwapRequest, CreateSwapRequest, SwapTokenAmount};
use crate::transport::LedgerSide;
#[cfg(feature = "http")]
use {crate::config::ClientConfig, crate::network::Network, crate::rep::RepEndpoint, num_bigint::BigInt};
#[cfg(feature = "std")]
use crate::genesis::{generate_initial_vote_staple, InitializeNetwork};
pub struct UserClient {
client: KeetaClient,
account: Option<AccountRef>,
signer: Option<AccountRef>,
}
impl UserClient {
const MAX_REBUILD_RETRIES: u32 = 2;
pub fn from_parts(client: KeetaClient, signer: Option<AccountRef>) -> Self {
Self { client, account: None, signer }
}
#[must_use]
pub fn with_account(mut self, account: AccountRef) -> Self {
self.account = Some(account);
self
}
#[cfg(feature = "http")]
pub fn from_network(network: Network, signer: Option<AccountRef>) -> Result<Self, ClientError> {
let client = KeetaClient::try_from(network)?;
Ok(Self::from_parts(client, signer))
}
#[cfg(feature = "http")]
pub fn from_single_rep(
hostname: impl AsRef<str>,
ssl: bool,
rep_key: &AccountRef,
network_id: impl Into<BigInt>,
signer: Option<AccountRef>,
) -> Self {
let scheme = match ssl {
true => "https",
false => "http",
};
let api_url = alloc::format!("{scheme}://{}/api", hostname.as_ref());
let rep = RepEndpoint::new(api_url, Arc::clone(rep_key), 1u8);
let client = KeetaClient::with_representatives([rep], ClientConfig::default()).with_network(network_id);
Self::from_parts(client, signer)
}
pub fn client(&self) -> &KeetaClient {
&self.client
}
pub fn account(&self) -> Result<AccountRef, ClientError> {
self.account_or(None)
}
pub fn signer_account(&self) -> Option<&AccountRef> {
self.signer.as_ref()
}
pub fn is_read_only(&self) -> bool {
self.signer.is_none()
}
fn account_or(&self, account: Option<&AccountRef>) -> Result<AccountRef, ClientError> {
if let Some(account) = account {
return Ok(Arc::clone(account));
}
if let Some(account) = &self.account {
return Ok(Arc::clone(account));
}
if let Some(signer) = &self.signer {
return Ok(Arc::clone(signer));
}
Err(ClientError::SignerRequired)
}
fn signer(&self) -> Result<AccountRef, ClientError> {
match &self.signer {
Some(signer) => Ok(Arc::clone(signer)),
None => Err(ClientError::SignerRequired),
}
}
fn signed_builder(&self) -> Result<TransactionBuilder, ClientError> {
let signer = self.signer()?;
let account = self.account_or(None)?;
let mut builder = self.client.builder(&account);
if account.to_string() != signer.to_string() {
builder.for_account_with_signer(&account, &signer);
}
Ok(builder)
}
pub async fn balance(&self, token: impl AsRef<str>) -> Result<Amount, ClientError> {
let account = self.account_or(None)?;
self.client.balance(account.to_string(), token).await
}
pub async fn all_balances(&self) -> Result<Vec<TokenBalance>, ClientError> {
let account = self.account_or(None)?;
self.client.balances(account.to_string()).await
}
pub async fn state(&self) -> Result<AccountState, ClientError> {
let account = self.account_or(None)?;
self.client.state(account.to_string()).await
}
pub async fn head(&self) -> Result<Option<Block>, ClientError> {
let account = self.account_or(None)?;
self.client.head_block(account.to_string()).await
}
pub async fn chain(&self) -> Result<Vec<Block>, ClientError> {
let account = self.account_or(None)?;
self.client.chain(account.to_string()).await
}
pub async fn chain_page(&self, query: ChainQuery) -> Result<Vec<Block>, ClientError> {
let account = self.account_or(None)?;
self.client.chain_page(account.to_string(), query).await
}
pub async fn chain_all(&self, page_limit: u32) -> Result<Vec<Block>, ClientError> {
let account = self.account_or(None)?;
self.client.chain_all(account.to_string(), page_limit).await
}
pub async fn history(&self) -> Result<Vec<HistoryEntry>, ClientError> {
let account = self.account_or(None)?;
self.client.history(account.to_string()).await
}
pub async fn history_page(&self, query: HistoryQuery) -> Result<Vec<HistoryEntry>, ClientError> {
let account = self.account_or(None)?;
self.client.history_page(account.to_string(), query).await
}
pub async fn pending_block(&self) -> Result<Option<Block>, ClientError> {
let account = self.account_or(None)?;
self.client.pending_block(account.to_string()).await
}
pub async fn recover(&self, publish: bool) -> Result<Option<VoteStaple>, ClientError> {
let account = self.account_or(None)?;
let options = TransmitOptions { fee_signer: self.signer.clone(), ..Default::default() };
self.client
.recover_account(&account, publish, options)
.await
}
pub async fn sync(&self, publish: bool) -> Result<Option<VoteStaple>, ClientError> {
let account = self.account_or(None)?;
self.client.sync_account(&account, publish).await
}
pub async fn acls(&self) -> Result<Vec<Acl>, ClientError> {
let account = self.account_or(None)?;
self.client.acls_by_principal(account.to_string()).await
}
pub async fn acls_by_entity(&self) -> Result<Vec<Acl>, ClientError> {
let account = self.account_or(None)?;
self.client.acls_by_entity(account.to_string()).await
}
#[cfg(feature = "std")]
pub async fn acls_with_info(&self) -> Result<serde_json::Value, ClientError> {
let account = self.account_or(None)?;
self.client
.acls_by_principal_with_info(account.to_string())
.await
}
pub async fn block(
&self,
blockhash: impl AsRef<str>,
side: Option<LedgerSide>,
) -> Result<Option<Block>, ClientError> {
self.client.block(blockhash, side).await
}
pub async fn block_from_idempotent(&self, key: impl AsRef<str>) -> Result<Option<Block>, ClientError> {
let account = self.account_or(None)?;
self.client
.block_by_idempotent(account.to_string(), key)
.await
}
pub async fn quotes(&self, blocks: &[Block]) -> Result<Vec<VoteQuote>, ClientError> {
self.client.quotes(blocks).await
}
pub async fn certificates(&self) -> Result<Vec<Certificate>, ClientError> {
let account = self.account_or(None)?;
self.client.certificates(account.to_string()).await
}
pub async fn certificate(&self, hash: impl AsRef<str>) -> Result<Option<Certificate>, ClientError> {
let account = self.account_or(None)?;
self.client.certificate(account.to_string(), hash).await
}
pub fn init_builder(&self) -> Result<TransactionBuilder, ClientError> {
self.signed_builder()
}
pub async fn publish(&self, block: Block, options: TransmitOptions) -> Result<bool, ClientError> {
self.client
.publish(block, self.with_fee_signer(options)?)
.await
}
pub async fn transmit(&self, blocks: &[Block], options: TransmitOptions) -> Result<bool, ClientError> {
self.client
.transmit(blocks, self.with_fee_signer(options)?)
.await
}
pub async fn send(&self, to: &AccountRef, token: &AccountRef, amount: Amount) -> Result<bool, ClientError> {
self.build_and_publish(move |builder| {
builder.send(to, token, amount.clone());
})
.await
}
pub async fn send_external(
&self,
to: &AccountRef,
token: &AccountRef,
amount: Amount,
external: impl Into<String>,
) -> Result<bool, ClientError> {
let external = external.into();
self.build_and_publish(move |builder| {
builder.send_external(to, token, amount.clone(), external.clone());
})
.await
}
pub async fn set_rep(&self, rep: &AccountRef) -> Result<bool, ClientError> {
let rep = Arc::clone(rep);
self.build_and_publish(move |builder| {
builder.set_rep(&rep);
})
.await
}
pub async fn set_info(&self, info: SetInfo) -> Result<bool, ClientError> {
self.build_and_publish(move |builder| {
builder.set_info(info.clone());
})
.await
}
pub async fn update_permissions(&self, permissions: ModifyPermissions) -> Result<bool, ClientError> {
self.build_and_publish(move |builder| {
builder.modify_permissions(permissions.clone());
})
.await
}
pub async fn modify_certificate(&self, certificate: ManageCertificate) -> Result<bool, ClientError> {
self.build_and_publish(move |builder| {
builder.manage_certificate(certificate.clone());
})
.await
}
pub async fn modify_token_supply_and_balance(
&self,
token: &AccountRef,
holder: Option<&AccountRef>,
amount: Amount,
method: AdjustMethod,
) -> Result<bool, ClientError> {
let signer = self.signer()?;
let token = Arc::clone(token);
let holder = match holder {
Some(holder) => Arc::clone(holder),
None => self.account_or(None)?,
};
let distinct_holder = holder.to_string() != token.to_string();
let burn = matches!(method, AdjustMethod::Subtract);
self.build_and_publish(move |builder| {
if burn {
builder.for_account_with_signer(&holder, &signer);
builder.modify_token_balance(&token, amount.clone(), method);
if distinct_holder {
builder.for_account_with_signer(&token, &signer);
}
builder.modify_token_supply(amount.clone(), method);
} else {
builder.for_account_with_signer(&token, &signer);
builder.modify_token_supply(amount.clone(), method);
if distinct_holder {
builder.for_account_with_signer(&holder, &signer);
}
builder.modify_token_balance(&token, amount.clone(), method);
}
})
.await
}
pub async fn generate_identifier(
&self,
key_type: KeyPairType,
create_arguments: Option<IdentifierCreateArguments>,
) -> Result<AccountRef, ClientError> {
let mut builder = self.signed_builder()?;
let pending = builder.generate_identifier(key_type, create_arguments);
let blocks = builder.build().await?;
self.originate(blocks).await?;
pending.get()
}
#[cfg(feature = "std")]
pub async fn initialize_network(&self, options: InitializeNetwork) -> Result<bool, ClientError> {
let trusted = self.signer()?;
let recipient = self.account_or(None)?;
let delegate_to = match &options.delegate_to {
Some(delegate) => Arc::clone(delegate),
None => self
.client
.first_rep_account()?
.ok_or(ClientError::NoRepresentatives)?,
};
let staple = generate_initial_vote_staple(&self.client, &trusted, &recipient, &delegate_to, &options)?;
self.client.transmit_staple(&staple).await
}
pub async fn create_swap_request(&self, request: CreateSwapRequest) -> Result<Block, ClientError> {
let mut builder = self.signed_builder()?;
builder.send(&request.counterparty, &request.send_token, request.send_amount);
builder.receive_with(
&request.counterparty,
&request.receive_token,
request.receive_amount,
request.receive_exact,
None,
);
let mut blocks = builder.build().await?;
match blocks.len() {
1 => Ok(blocks.remove(0)),
_ => Err(ClientError::SwapMultiBlock),
}
}
pub async fn accept_swap_request(&self, request: AcceptSwapRequest) -> Result<Vec<Block>, ClientError> {
let account = self.account_or(None)?;
let (send, receive) = swap_legs(&request.block)?;
if send.to.to_string() != receive.from.to_string() {
return Err(ClientError::SwapAccountMismatch);
}
if send.to.to_string() != account.to_string() {
return Err(ClientError::SwapAccountMismatch);
}
let send_amount: Amount = resolve_swap_amount(send, receive, request.expected.as_ref())?;
let maker = request.block.data().account();
let mut builder = self.signed_builder()?;
builder.send(maker, &receive.token, send_amount);
let mut blocks = builder.build().await?;
blocks.push(request.block);
Ok(blocks)
}
fn with_fee_signer(&self, mut options: TransmitOptions) -> Result<TransmitOptions, ClientError> {
if options.fee_signer.is_none() {
options.fee_signer = Some(self.signer()?);
}
Ok(options)
}
async fn build_and_publish(&self, assemble: impl Fn(&mut TransactionBuilder)) -> Result<bool, ClientError> {
let mut attempt = 0u32;
loop {
let mut builder = self.signed_builder()?;
assemble(&mut builder);
let blocks = builder.build().await?;
match self.originate(blocks).await {
Ok(accepted) => return Ok(accepted),
Err(error) => {
let conflict = is_ledger_code(&error, "LEDGER_SUCCESSOR_VOTE_EXISTS");
if !conflict || attempt >= Self::MAX_REBUILD_RETRIES {
return Err(error);
}
match self.recover(true).await? {
Some(_) => attempt += 1,
None => return Err(error),
}
}
}
}
}
async fn originate(&self, blocks: Vec<Block>) -> Result<bool, ClientError> {
let signer = self.signer()?;
let options = TransmitOptions { fee_signer: Some(signer), ..Default::default() };
let mut accepted = true;
for block in blocks {
accepted &= self.client.publish(block, options.clone()).await?;
if !accepted {
break;
}
}
Ok(accepted)
}
}
fn swap_legs(block: &Block) -> Result<(&Send, &Receive), ClientError> {
let mut send = None;
let mut receive = None;
for operation in block.data().operations() {
match operation {
Operation::Send(value) => send = Some(value),
Operation::Receive(value) => receive = Some(value),
_ => {}
}
}
let send = send.ok_or(ClientError::SwapMissingSend)?;
let receive = receive.ok_or(ClientError::SwapMissingReceive)?;
Ok((send, receive))
}
fn resolve_swap_amount(
send: &Send,
receive: &Receive,
expected: Option<&crate::swap::SwapExpectation>,
) -> Result<Amount, ClientError> {
let mut send_amount = receive.amount.clone();
let Some(expected) = expected else {
return Ok(send_amount);
};
if let Some(expected_receive) = &expected.receive {
assert_swap_token(&send.token, expected_receive)?;
assert_swap_amount(&send.amount, expected_receive)?;
}
if let Some(expected_send) = &expected.send {
assert_swap_token(&receive.token, expected_send)?;
if let Some(amount) = &expected_send.amount {
if *amount < receive.amount {
return Err(ClientError::SwapAmountTooLow);
}
if receive.exact && receive.amount != *amount {
return Err(ClientError::SwapExactMismatch);
}
send_amount = amount.clone();
}
}
Ok(send_amount)
}
fn assert_swap_token(token: &AccountRef, expected: &SwapTokenAmount) -> Result<(), ClientError> {
if let Some(wanted) = &expected.token {
if token.to_string() != wanted.to_string() {
return Err(ClientError::SwapTokenMismatch);
}
}
Ok(())
}
fn assert_swap_amount(amount: &Amount, expected: &SwapTokenAmount) -> Result<(), ClientError> {
if let Some(wanted) = &expected.amount {
if amount != wanted {
return Err(ClientError::SwapAmountMismatch);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use keetanetwork_block::testing::generate_ed25519_ref;
use core::mem::discriminant;
use super::*;
use crate::swap::SwapExpectation;
fn send_op(amount: u64) -> Send {
Send {
to: generate_ed25519_ref(1),
amount: Amount::from(amount),
token: generate_ed25519_ref(3),
external: None,
}
}
fn receive_op(amount: u64, exact: bool) -> Receive {
Receive {
amount: Amount::from(amount),
token: generate_ed25519_ref(4),
from: generate_ed25519_ref(1),
exact,
forward: None,
}
}
fn send_expectation(amount: u64) -> SwapExpectation {
SwapExpectation {
receive: None,
send: Some(SwapTokenAmount { token: None, amount: Some(Amount::from(amount)) }),
}
}
fn assert_resolves_to(expectation: Option<SwapExpectation>, exact: bool, expected: u64) {
let resolved = resolve_swap_amount(&send_op(100), &receive_op(50, exact), expectation.as_ref());
assert_eq!(resolved.ok(), Some(Amount::from(expected)));
}
fn assert_rejects(expectation: SwapExpectation, exact: bool, expected: ClientError) {
let resolved = resolve_swap_amount(&send_op(100), &receive_op(50, exact), Some(&expectation));
assert_eq!(resolved.err().map(|error| discriminant(&error)), Some(discriminant(&expected)));
}
#[test]
fn swap_amount_defaults_to_requested_receive() {
assert_resolves_to(None, false, 50);
}
#[test]
fn swap_raises_send_amount_when_permitted() {
assert_resolves_to(Some(send_expectation(70)), false, 70);
}
#[test]
fn swap_rejects_send_amount_below_requested() {
assert_rejects(send_expectation(49), false, ClientError::SwapAmountTooLow);
}
#[test]
fn swap_rejects_inexact_override_of_exact_receive() {
assert_rejects(send_expectation(60), true, ClientError::SwapExactMismatch);
}
#[test]
fn swap_rejects_mismatched_receive_token() {
let expectation = SwapExpectation {
receive: Some(SwapTokenAmount { token: Some(generate_ed25519_ref(9)), amount: None }),
send: None,
};
assert_rejects(expectation, false, ClientError::SwapTokenMismatch);
}
#[test]
fn swap_rejects_mismatched_receive_amount() {
let expectation = SwapExpectation {
receive: Some(SwapTokenAmount { token: None, amount: Some(Amount::from(99u64)) }),
send: None,
};
assert_rejects(expectation, false, ClientError::SwapAmountMismatch);
}
}