From 6dde6161b666fc6504e97d2f6b9d24c93ad79b63 Mon Sep 17 00:00:00 2001 From: Youngjoon Lee <5462944+youngjoon-lee@users.noreply.github.com> Date: Thu, 2 Apr 2026 03:04:31 +0100 Subject: [PATCH] feat(api): add POST `/channel/deposit` (#2443) Co-authored-by: Alejandro Cabeza Romero --- core/src/mantle/tx_builder.rs | 17 +++- nodes/api-common/src/bodies/channel.rs | 24 ++++++ nodes/api-common/src/bodies/mod.rs | 1 + nodes/api-common/src/paths.rs | 1 + nodes/node/binary/src/api/backend.rs | 10 ++- nodes/node/binary/src/api/handlers.rs | 110 ++++++++++++++++++++++++- wallet/src/lib.rs | 11 ++- 7 files changed, 166 insertions(+), 8 deletions(-) create mode 100644 nodes/api-common/src/bodies/channel.rs diff --git a/core/src/mantle/tx_builder.rs b/core/src/mantle/tx_builder.rs index 6896085e6..e6430c209 100644 --- a/core/src/mantle/tx_builder.rs +++ b/core/src/mantle/tx_builder.rs @@ -3,7 +3,7 @@ use std::cmp::Ordering; use lb_key_management_system_keys::keys::ZkPublicKey; use super::{GasConstants, GasCost as _, MantleTx, Note, Op, Utxo}; -use crate::mantle::ops::transfer::TransferOp; +use crate::mantle::{NoteId, ops::transfer::TransferOp}; #[derive(Debug, Clone)] pub struct MantleTxBuilder { @@ -147,6 +147,21 @@ impl MantleTxBuilder { self.net_balance() - i128::from(self.gas_cost::()) } + /// Returns all note IDs used as inputs in the transaction, including + /// - Transfer operations already in the transaction + /// - Additional transfer operations that will be added to the transaction + pub fn input_notes(&self) -> impl Iterator { + self.mantle_tx + .ops + .iter() + .filter_map(|op| match op { + Op::Transfer(transfer) => Some(transfer.inputs.iter().copied()), + _ => None, + }) + .flatten() + .chain(self.ledger_inputs().iter().map(Utxo::id)) + } + #[must_use] pub fn ledger_inputs(&self) -> &[Utxo] { &self.ledger_inputs diff --git a/nodes/api-common/src/bodies/channel.rs b/nodes/api-common/src/bodies/channel.rs new file mode 100644 index 000000000..5076f8744 --- /dev/null +++ b/nodes/api-common/src/bodies/channel.rs @@ -0,0 +1,24 @@ +use lb_core::{ + header::HeaderId, + mantle::{ + TxHash, Value, + ops::{channel::deposit::DepositOp, transfer::TransferOp}, + }, +}; +use lb_key_management_system_keys::keys::ZkPublicKey; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct ChannelDepositRequestBody { + pub tip: Option, + pub deposit: DepositOp, + pub burn: TransferOp, + pub change_public_key: ZkPublicKey, + pub funding_public_keys: Vec, + pub max_tx_fee: Value, +} + +#[derive(Serialize, Deserialize)] +pub struct ChannelDepositResponseBody { + pub hash: TxHash, +} diff --git a/nodes/api-common/src/bodies/mod.rs b/nodes/api-common/src/bodies/mod.rs index b6fa1b4ed..2a5ea7713 100644 --- a/nodes/api-common/src/bodies/mod.rs +++ b/nodes/api-common/src/bodies/mod.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; +pub mod channel; pub mod wallet; /// A no-operation body for endpoints that do not require a request or response diff --git a/nodes/api-common/src/paths.rs b/nodes/api-common/src/paths.rs index 885e7f945..24e59a321 100644 --- a/nodes/api-common/src/paths.rs +++ b/nodes/api-common/src/paths.rs @@ -7,6 +7,7 @@ pub const CRYPTARCHIA_LIB_STREAM: &str = "/cryptarchia/lib-stream"; pub const NETWORK_INFO: &str = "/network/info"; pub const STORAGE_BLOCK: &str = "/storage/block"; pub const MEMPOOL_ADD_TX: &str = "/mempool/add/tx"; +pub const CHANNEL_DEPOSIT: &str = "/channel/deposit"; pub const SDP_POST_DECLARATION: &str = "/sdp/declaration"; pub const SDP_POST_ACTIVITY: &str = "/sdp/activity"; pub const SDP_POST_WITHDRAWAL: &str = "/sdp/withdrawal"; diff --git a/nodes/node/binary/src/api/backend.rs b/nodes/node/binary/src/api/backend.rs index c27813803..6695bb7ff 100644 --- a/nodes/node/binary/src/api/backend.rs +++ b/nodes/node/binary/src/api/backend.rs @@ -46,7 +46,9 @@ use super::handlers::{ use crate::{ WalletService, api::{ - handlers::{leader_claim, post_activity, post_declaration, post_withdrawal}, + handlers::{ + channel_deposit, leader_claim, post_activity, post_declaration, post_withdrawal, + }, openapi::ApiDoc, }, }; @@ -215,6 +217,12 @@ where paths::MEMPOOL_ADD_TX, routing::post(add_tx::), ) + .route( + paths::CHANNEL_DEPOSIT, + routing::post( + channel_deposit::, + ), + ) .route( paths::SDP_POST_DECLARATION, routing::post( diff --git a/nodes/node/binary/src/api/handlers.rs b/nodes/node/binary/src/api/handlers.rs index 986dddd80..b54ce561c 100644 --- a/nodes/node/binary/src/api/handlers.rs +++ b/nodes/node/binary/src/api/handlers.rs @@ -19,12 +19,17 @@ use lb_chain_service::ConsensusMsg; use lb_core::{ block::Block, header::HeaderId, - mantle::{SignedMantleTx, Transaction}, + mantle::{ + Op, SignedMantleTx, Transaction, gas::MainnetGasConstants, tx_builder::MantleTxBuilder, + }, }; use lb_http_api_common::{ - bodies::wallet::{ - balance::WalletBalanceResponseBody, - transfer_funds::{WalletTransferFundsRequestBody, WalletTransferFundsResponseBody}, + bodies::{ + channel::{ChannelDepositRequestBody, ChannelDepositResponseBody}, + wallet::{ + balance::WalletBalanceResponseBody, + transfer_funds::{WalletTransferFundsRequestBody, WalletTransferFundsResponseBody}, + }, }, paths, }; @@ -360,6 +365,103 @@ where >(&handle, tx, Transaction::hash)) } +#[utoipa::path( + post, + path = paths::CHANNEL_DEPOSIT, + responses( + (status = 200, description = "Submit a channel deposit"), + (status = 500, description = "Internal server error", body = String), + ) +)] +pub async fn channel_deposit( + State(handle): State>, + Json(req): Json, +) -> Response +where + WalletService: WalletServiceData, + StorageAdapter: lb_tx_service::storage::MempoolStorageAdapter< + RuntimeServiceId, + Item = SignedMantleTx, + Key = ::Hash, + > + Send + + Sync + + Clone + + 'static, + StorageAdapter::Error: Debug, + RuntimeServiceId: Debug + + Display + + Send + + Sync + + 'static + + AsServiceId + + AsServiceId< + TxMempoolService< + MempoolNetworkAdapter< + SignedMantleTx, + ::Hash, + RuntimeServiceId, + >, + Mempool< + HeaderId, + SignedMantleTx, + ::Hash, + StorageAdapter, + RuntimeServiceId, + >, + StorageAdapter, + RuntimeServiceId, + >, + >, +{ + make_request_and_return_response!(async { + let wallet = WalletApi::::new( + handle.relay::().await?, + ); + + let tx_builder = MantleTxBuilder::new() + .push_op(Op::ChannelDeposit(req.deposit)) + .push_op(Op::Transfer(req.burn)); + let lb_wallet_service::TipResponse { + tip, + response: funded_tx_builder, + } = wallet + .fund_tx( + None, + tx_builder, + req.change_public_key, + req.funding_public_keys, + ) + .await?; + + let tx_fee = funded_tx_builder.gas_cost::(); + if tx_fee > req.max_tx_fee { + return Err(overwatch::DynError::from(format!( + "tx_fee({tx_fee}) exceeds max_tx_fee({})", + req.max_tx_fee + ))); + } + + let signed_tx = wallet.sign_tx(Some(tip), funded_tx_builder).await?.response; + let tx_hash = signed_tx.hash(); + + mempool::add_tx::< + Libp2pNetworkBackend, + MempoolNetworkAdapter< + SignedMantleTx, + ::Hash, + RuntimeServiceId, + >, + StorageAdapter, + SignedMantleTx, + ::Hash, + RuntimeServiceId, + >(&handle, signed_tx, Transaction::hash) + .await?; + + Ok(ChannelDepositResponseBody { hash: tx_hash }) + }) +} + #[utoipa::path( post, path = paths::SDP_POST_DECLARATION, diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 86fadfa4a..60c237801 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -4,7 +4,7 @@ mod voucher; use std::{ borrow::Borrow, cmp::Ordering, - collections::{BTreeMap, HashMap}, + collections::{BTreeMap, HashMap, HashSet}, fmt::Debug, }; @@ -94,7 +94,14 @@ impl WalletState { change_pk: ZkPublicKey, pks: impl IntoIterator>, ) -> Result { - let mut utxos = self.utxos_owned_by_pks(pks); + // Get all UTXOs owned by the provided PKs, excluding any that are already being + // used as inputs in the tx builder. + let inputs = tx_builder.input_notes().collect::>(); + let mut utxos = self + .utxos_owned_by_pks(pks) + .into_iter() + .filter(|utxo| !inputs.contains(&utxo.id())) + .collect::>(); // Consume large valued notes first to ensure we converge. utxos.sort_by_key(|utxo| -i128::from(utxo.note.value));