mirror of
https://github.com/logos-co/nomos-node.git
synced 2026-08-27 09:31:10 +00:00
feat(api): tx fund api endpoint (#3100)
This commit is contained in:
@@ -309,6 +309,33 @@ mod tests {
|
||||
sdp::{DeclarationId, Locator, ProviderId, ServiceType},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn serde_round_trip() {
|
||||
// The builder crosses the HTTP boundary (e.g. the wallet fund
|
||||
// endpoint), so a serialized builder must deserialize back to the
|
||||
// same transaction.
|
||||
let builder = MantleTxBuilder::new()
|
||||
.push_op(Op::ChannelInscribe(InscriptionOp {
|
||||
channel_id: [0; 32].into(),
|
||||
inscription: b"hello".into(),
|
||||
parent: [1; 32].into(),
|
||||
signer: Ed25519Key::from_bytes(&[0; 32]).public_key(),
|
||||
}))
|
||||
.unwrap()
|
||||
.add_ledger_input(Utxo::new([0u8; 32], 0, Note::new(50, ZkPublicKey::zero())))
|
||||
.unwrap()
|
||||
.add_ledger_output(Note::new(40, ZkPublicKey::zero()))
|
||||
.unwrap();
|
||||
|
||||
let json = serde_json::to_string(&builder).expect("builder should serialize");
|
||||
let restored: MantleTxBuilder =
|
||||
serde_json::from_str(&json).expect("builder should deserialize");
|
||||
|
||||
assert_eq!(restored.net_balance(), builder.net_balance());
|
||||
assert_eq!(restored.ledger_inputs(), builder.ledger_inputs());
|
||||
assert_eq!(restored.build().unwrap(), builder.build().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inscription_op() {
|
||||
// Build an operation
|
||||
|
||||
@@ -143,6 +143,41 @@ pub mod transfer_funds {
|
||||
}
|
||||
}
|
||||
|
||||
pub mod fund {
|
||||
use lb_core::{
|
||||
header::HeaderId,
|
||||
mantle::{
|
||||
OpProof,
|
||||
gas::GasCost,
|
||||
transactions::{MantleTx, builder::MantleTxBuilder},
|
||||
},
|
||||
};
|
||||
use lb_key_management_system_keys::keys::ZkPublicKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct WalletFundRequestBody {
|
||||
pub tip: Option<HeaderId>,
|
||||
pub tx_builder: MantleTxBuilder,
|
||||
pub change_public_key: ZkPublicKey,
|
||||
pub funding_public_keys: Vec<ZkPublicKey>,
|
||||
pub max_tx_fee: GasCost,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct WalletFundResponseBody {
|
||||
/// Tip the transaction was funded against.
|
||||
pub tip: HeaderId,
|
||||
/// The funded transaction, with the fee transfer appended as the last
|
||||
/// op. All ops are still unsigned.
|
||||
pub funded_tx: MantleTx,
|
||||
/// Proof for the appended fee transfer, signed over the funded
|
||||
/// transaction hash. `None` if funding required no transfer (zero
|
||||
/// fee and no inputs pulled in).
|
||||
pub transfer_proof: Option<OpProof>,
|
||||
}
|
||||
}
|
||||
|
||||
pub mod sign {
|
||||
use lb_core::mantle::TxHash;
|
||||
use lb_key_management_system_keys::keys::{
|
||||
|
||||
@@ -34,6 +34,7 @@ pub mod wallet {
|
||||
pub const TRANSACTIONS_TRANSFER_FUNDS: &str = "/wallet/transactions/transfer-funds";
|
||||
pub const SIGN_TX_ED25519: &str = "/wallet/sign/ed25519";
|
||||
pub const SIGN_TX_ZK: &str = "/wallet/sign/zk";
|
||||
pub const FUND: &str = "/wallet/fund";
|
||||
}
|
||||
|
||||
pub mod admin {
|
||||
|
||||
@@ -345,6 +345,10 @@ where
|
||||
paths::wallet::SIGN_TX_ZK,
|
||||
routing::post(wallet::sign_tx_zk::<WalletService, MempoolStorageAdapter, _>),
|
||||
)
|
||||
.route(
|
||||
paths::wallet::FUND,
|
||||
routing::post(wallet::fund::<WalletService, MempoolStorageAdapter, _>),
|
||||
)
|
||||
.route(
|
||||
paths::admin::TRACING_FILTER,
|
||||
routing::put(reload_tracing_filter::<RuntimeServiceId>),
|
||||
|
||||
@@ -26,7 +26,7 @@ use lb_core::{
|
||||
events::Events,
|
||||
header::HeaderId,
|
||||
mantle::{
|
||||
Op, SignedMantleTx, Transaction, TxHash, ops::channel::ChannelId,
|
||||
Op, OpProof, SignedMantleTx, Transaction, TxHash, ops::channel::ChannelId,
|
||||
transactions::MantleTxBuilder,
|
||||
},
|
||||
};
|
||||
@@ -1568,9 +1568,12 @@ where
|
||||
}
|
||||
|
||||
pub mod wallet {
|
||||
use lb_http_api_common::bodies::wallet::sign::{
|
||||
WalletSignTxEd25519RequestBody, WalletSignTxEd25519ResponseBody, WalletSignTxZkRequestBody,
|
||||
WalletSignTxZkResponseBody,
|
||||
use lb_http_api_common::bodies::wallet::{
|
||||
fund::{WalletFundRequestBody, WalletFundResponseBody},
|
||||
sign::{
|
||||
WalletSignTxEd25519RequestBody, WalletSignTxEd25519ResponseBody,
|
||||
WalletSignTxZkRequestBody, WalletSignTxZkResponseBody,
|
||||
},
|
||||
};
|
||||
use lb_key_management_system_service::keys::ZkPublicKey;
|
||||
|
||||
@@ -1875,6 +1878,105 @@ pub mod wallet {
|
||||
Ok::<_, DynError>(WalletSignTxZkResponseBody { sig })
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = paths::wallet::FUND,
|
||||
responses(
|
||||
(status = 200, description = "Funded transaction with fee transfer proof"),
|
||||
(status = 500, description = "Internal server error", body = String),
|
||||
)
|
||||
)]
|
||||
pub async fn fund<WalletService, StorageAdapter, RuntimeServiceId>(
|
||||
State(handle): State<OverwatchHandle<RuntimeServiceId>>,
|
||||
Json(req): Json<WalletFundRequestBody>,
|
||||
) -> Response
|
||||
where
|
||||
WalletService: WalletServiceData,
|
||||
StorageAdapter: lb_tx_service::storage::MempoolStorageAdapter<
|
||||
RuntimeServiceId,
|
||||
Item = SignedMantleTx,
|
||||
Key = <SignedMantleTx as Transaction>::Hash,
|
||||
> + Send
|
||||
+ Sync
|
||||
+ Clone
|
||||
+ 'static,
|
||||
StorageAdapter::Error: Debug,
|
||||
RuntimeServiceId: Debug
|
||||
+ Display
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static
|
||||
+ AsServiceId<WalletService>
|
||||
+ AsServiceId<
|
||||
TxMempoolService<
|
||||
MempoolNetworkAdapter<
|
||||
SignedMantleTx,
|
||||
<SignedMantleTx as Transaction>::Hash,
|
||||
RuntimeServiceId,
|
||||
>,
|
||||
Mempool<
|
||||
HeaderId,
|
||||
SignedMantleTx,
|
||||
<SignedMantleTx as Transaction>::Hash,
|
||||
StorageAdapter,
|
||||
RuntimeServiceId,
|
||||
>,
|
||||
StorageAdapter,
|
||||
RuntimeServiceId,
|
||||
>,
|
||||
>,
|
||||
{
|
||||
make_request_and_return_response!(async {
|
||||
let wallet = WalletApi::<WalletService, RuntimeServiceId>::new(
|
||||
handle.relay::<WalletService>().await?,
|
||||
);
|
||||
|
||||
let lb_wallet_service::TipResponse {
|
||||
tip,
|
||||
response: funded_tx_builder,
|
||||
} = wallet
|
||||
.fund_tx(
|
||||
req.tip,
|
||||
req.tx_builder,
|
||||
req.change_public_key,
|
||||
req.funding_public_keys,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let tx_fee = funded_tx_builder.tx_fee()?;
|
||||
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
|
||||
)));
|
||||
}
|
||||
|
||||
// Owners of the funding inputs, in input order — the ledger
|
||||
// verifies the transfer proof against this exact list.
|
||||
let funding_note_pks: Vec<ZkPublicKey> = funded_tx_builder
|
||||
.ledger_inputs()
|
||||
.iter()
|
||||
.map(|utxo| utxo.note.pk)
|
||||
.collect();
|
||||
|
||||
let funded_tx = funded_tx_builder.build()?;
|
||||
let transfer_proof = if funding_note_pks.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let tx_hash = funded_tx.hash();
|
||||
Some(OpProof::ZkSig(
|
||||
wallet.sign_tx_with_zk(tx_hash, funding_note_pks).await?,
|
||||
))
|
||||
};
|
||||
|
||||
Ok::<_, DynError>(WalletFundResponseBody {
|
||||
tip,
|
||||
funded_tx,
|
||||
transfer_proof,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -31,6 +31,7 @@ use utoipa::OpenApi;
|
||||
crate::api::handlers::wallet::get_claimable_vouchers,
|
||||
crate::api::handlers::get_gas_prices,
|
||||
crate::api::handlers::wallet::post_transactions_transfer_funds,
|
||||
crate::api::handlers::wallet::fund,
|
||||
crate::api::tracing::reload_tracing_filter,
|
||||
),
|
||||
components(schemas(schema::Status, schema::MempoolMetrics)),
|
||||
|
||||
@@ -21,6 +21,7 @@ use lb_http_api_common::{
|
||||
wallet::{
|
||||
balance::WalletBalanceResponseBody,
|
||||
claimable_vouchers::WalletClaimableVouchersResponseBody,
|
||||
fund::{WalletFundRequestBody, WalletFundResponseBody},
|
||||
transfer_funds::{WalletTransferFundsRequestBody, WalletTransferFundsResponseBody},
|
||||
},
|
||||
},
|
||||
@@ -28,7 +29,7 @@ use lb_http_api_common::{
|
||||
BLEND_JOIN_NETWORK, BLOCK_EVENTS, BLOCKS, BLOCKS_DETAIL, BLOCKS_RANGE_STREAM,
|
||||
BLOCKS_STREAM, CHANNEL, CRYPTARCHIA_INFO, CRYPTARCHIA_LIB_STREAM, LEADER_CLAIM_VOUCHERS,
|
||||
MANTLE_GAS_PRICES, MEMPOOL_ADD_TX, SDP_POST_DECLARATION, TIME_INFO,
|
||||
wallet::{BALANCE, TRANSACTIONS_TRANSFER_FUNDS},
|
||||
wallet::{BALANCE, FUND, TRANSACTIONS_TRANSFER_FUNDS},
|
||||
},
|
||||
queries::BlocksStreamQuery,
|
||||
settings::default_max_body_size,
|
||||
@@ -582,6 +583,23 @@ impl CommonHttpClient {
|
||||
self.post(request_url, &body).await
|
||||
}
|
||||
|
||||
/// Post a request to fund a transaction from the node's wallet.
|
||||
///
|
||||
/// The node adds fee inputs and change from its own wallet, signs only
|
||||
/// the appended fee transfer, and returns the funded (still unsigned)
|
||||
/// transaction together with the transfer proof.
|
||||
pub async fn fund_tx(
|
||||
&self,
|
||||
base_url: Url,
|
||||
body: WalletFundRequestBody,
|
||||
) -> Result<WalletFundResponseBody, Error> {
|
||||
let request_url = base_url
|
||||
.join(FUND.trim_start_matches('/'))
|
||||
.map_err(Error::Url)?;
|
||||
|
||||
self.post(request_url, &body).await
|
||||
}
|
||||
|
||||
/// Post a request via an SDP declaration to join the blend network and
|
||||
/// returns its declaration ID if successful.
|
||||
pub async fn join_blend_network(
|
||||
|
||||
@@ -12,3 +12,31 @@ Feature: Fees
|
||||
When node "NODE_1" is at height 1 in 180 seconds
|
||||
Then gas prices on node "NODE_1" equal the genesis gas prices
|
||||
Then I stop all nodes
|
||||
|
||||
@transactions_ci
|
||||
Scenario: Wallet fund endpoint funds a payment and returns a transfer proof
|
||||
Given the genesis block has the following wallet resources:
|
||||
| account_index | token_count | token_amount |
|
||||
| 1 | 1 | 1000 |
|
||||
And I have a cluster with capacity of 1 nodes
|
||||
And I start nodes with wallet resources:
|
||||
| node_name | account_index | wallet_name | connected_to |
|
||||
| NODE_1 | 1 | WALLET_1A | |
|
||||
When node "NODE_1" is at height 2 in 240 seconds
|
||||
And I fund a transaction paying 10 LGO from node "NODE_1" wallet to wallet "WALLET_1A" as "FUNDED_PAYMENT"
|
||||
Then transaction "FUNDED_PAYMENT" is included on node "NODE_1" in 120 seconds
|
||||
Then I stop all nodes
|
||||
|
||||
@transactions_ci
|
||||
Scenario: Wallet fund endpoint leaves a feeless transaction unchanged
|
||||
Given the genesis block has the following wallet resources:
|
||||
| account_index | token_count | token_amount |
|
||||
| 1 | 1 | 1000 |
|
||||
And I have a cluster with capacity of 1 nodes
|
||||
And I start nodes with wallet resources:
|
||||
| node_name | account_index | wallet_name | connected_to |
|
||||
| NODE_1 | 1 | WALLET_1A | |
|
||||
When node "NODE_1" is at height 2 in 240 seconds
|
||||
And I fund an inscription transaction on node "NODE_1" as "FUNDED_INSCRIPTION"
|
||||
Then transaction "FUNDED_INSCRIPTION" is included on node "NODE_1" in 120 seconds
|
||||
Then I stop all nodes
|
||||
|
||||
@@ -9,5 +9,6 @@ pub mod manual_mempool;
|
||||
pub mod manual_nodes;
|
||||
pub mod manual_transactions;
|
||||
pub mod manual_zone;
|
||||
pub mod wallet_fund;
|
||||
|
||||
const TARGET: &str = "cucumber_steps";
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
//! Steps exercising the node's `/wallet/fund` HTTP endpoint: fund a
|
||||
//! transaction from the node's wallet, assemble the returned proofs and
|
||||
//! submit the result to the mempool.
|
||||
|
||||
use cucumber::{gherkin::Step, when};
|
||||
use lb_core::mantle::{
|
||||
Note, Op, OpProof, SignedMantleTx, Transaction as _,
|
||||
gas::GasCost,
|
||||
ops::channel::{
|
||||
ChannelId, MsgId,
|
||||
inscribe::{Inscription, InscriptionOp},
|
||||
},
|
||||
transactions::builder::MantleTxBuilder,
|
||||
};
|
||||
use lb_http_api_common::bodies::wallet::fund::{WalletFundRequestBody, WalletFundResponseBody};
|
||||
use lb_key_management_system_service::keys::{Ed25519Key, ZkPublicKey};
|
||||
use lb_testing_framework::NodeHttpClient;
|
||||
use tracing::info;
|
||||
|
||||
use crate::cucumber::{
|
||||
error::{StepError, StepResult},
|
||||
steps::TARGET,
|
||||
world::CucumberWorld,
|
||||
};
|
||||
|
||||
/// Fund a payment transaction from the node's wallet: the fund endpoint must
|
||||
/// pull wallet inputs to cover the output, append the fee transfer and return
|
||||
/// a transfer proof that the ledger accepts.
|
||||
#[when(
|
||||
expr = "I fund a transaction paying {int} LGO from node {string} wallet to wallet {string} as {string}"
|
||||
)]
|
||||
async fn step_fund_payment_transaction(
|
||||
world: &mut CucumberWorld,
|
||||
step: &Step,
|
||||
amount: u64,
|
||||
node_name: String,
|
||||
receiver_wallet_name: String,
|
||||
transaction_alias: String,
|
||||
) -> StepResult {
|
||||
let receiver_pk = world.resolve_wallet(&receiver_wallet_name)?.public_key()?;
|
||||
let funding_wallet = world.resolve_wallet(&format!("{node_name}_WALLET"))?;
|
||||
let funding_pk = funding_wallet.public_key()?;
|
||||
let client = world.resolve_node_http_client(&node_name)?;
|
||||
|
||||
let tx_builder = MantleTxBuilder::new()
|
||||
.add_ledger_output(Note::new(amount, receiver_pk))
|
||||
.map_err(|source| StepError::LogicalError {
|
||||
message: format!(
|
||||
"Step `{}` error: failed to add output: {source}",
|
||||
step.value
|
||||
),
|
||||
})?;
|
||||
|
||||
let response = fund_via_node(&client, step, tx_builder, funding_pk).await?;
|
||||
|
||||
let Some(transfer_proof) = response.transfer_proof else {
|
||||
return Err(StepError::LogicalError {
|
||||
message: format!(
|
||||
"Step `{}` error: funding a payment must return a transfer proof",
|
||||
step.value
|
||||
),
|
||||
});
|
||||
};
|
||||
let has_transfer = response
|
||||
.funded_tx
|
||||
.ops()
|
||||
.iter()
|
||||
.any(|op| matches!(op, Op::Transfer(_)));
|
||||
if !has_transfer {
|
||||
return Err(StepError::LogicalError {
|
||||
message: format!(
|
||||
"Step `{}` error: funded payment must contain a transfer op",
|
||||
step.value
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
let signed_tx =
|
||||
SignedMantleTx::new(response.funded_tx, vec![transfer_proof]).map_err(|source| {
|
||||
StepError::LogicalError {
|
||||
message: format!(
|
||||
"Step `{}` error: assembling the funded transaction failed: {source:?}",
|
||||
step.value
|
||||
),
|
||||
}
|
||||
})?;
|
||||
let tx_hash = signed_tx.hash();
|
||||
|
||||
world
|
||||
.submit_transaction(&funding_wallet, &signed_tx, &client)
|
||||
.await?;
|
||||
world.remember_submitted_transaction(transaction_alias.clone(), tx_hash);
|
||||
|
||||
info!(
|
||||
target: TARGET,
|
||||
"Submitted funded payment `{transaction_alias}` of {amount} LGO from node `{node_name}` wallet"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fund an inscription-only transaction: with zero gas prices the transaction
|
||||
/// owes no fee, so funding must be a no-op — no transfer appended and no
|
||||
/// proof returned — and the caller signs the channel op over the funded hash.
|
||||
#[when(expr = "I fund an inscription transaction on node {string} as {string}")]
|
||||
async fn step_fund_inscription_transaction(
|
||||
world: &mut CucumberWorld,
|
||||
step: &Step,
|
||||
node_name: String,
|
||||
transaction_alias: String,
|
||||
) -> StepResult {
|
||||
let funding_wallet = world.resolve_wallet(&format!("{node_name}_WALLET"))?;
|
||||
let funding_pk = funding_wallet.public_key()?;
|
||||
let client = world.resolve_node_http_client(&node_name)?;
|
||||
|
||||
// Deterministic sequencer key claiming a fresh channel; every scenario
|
||||
// runs on a fresh chain, so the channel cannot pre-exist.
|
||||
let signing_key = Ed25519Key::from_bytes(&[7u8; 32]);
|
||||
let channel_id = ChannelId::from(signing_key.public_key().to_bytes());
|
||||
let inscription =
|
||||
Inscription::try_from(b"wallet fund endpoint".to_vec()).map_err(|source| {
|
||||
StepError::LogicalError {
|
||||
message: format!(
|
||||
"Step `{}` error: failed to build inscription: {source}",
|
||||
step.value
|
||||
),
|
||||
}
|
||||
})?;
|
||||
let inscription_op = InscriptionOp {
|
||||
channel_id,
|
||||
inscription,
|
||||
parent: MsgId::root(),
|
||||
signer: signing_key.public_key(),
|
||||
};
|
||||
|
||||
let tx_builder = MantleTxBuilder::new()
|
||||
.push_op(Op::ChannelInscribe(inscription_op))
|
||||
.map_err(|source| StepError::LogicalError {
|
||||
message: format!(
|
||||
"Step `{}` error: failed to push inscription op: {source}",
|
||||
step.value
|
||||
),
|
||||
})?;
|
||||
|
||||
let response = fund_via_node(&client, step, tx_builder, funding_pk).await?;
|
||||
|
||||
if response.transfer_proof.is_some() {
|
||||
return Err(StepError::LogicalError {
|
||||
message: format!(
|
||||
"Step `{}` error: feeless transaction must not get a transfer proof",
|
||||
step.value
|
||||
),
|
||||
});
|
||||
}
|
||||
if response.funded_tx.ops().len() != 1 {
|
||||
return Err(StepError::LogicalError {
|
||||
message: format!(
|
||||
"Step `{}` error: feeless transaction must keep its single op, got {}",
|
||||
step.value,
|
||||
response.funded_tx.ops().len()
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
let tx_hash = response.funded_tx.hash();
|
||||
let signature = signing_key.sign_payload(tx_hash.as_signing_bytes().as_ref());
|
||||
let signed_tx = SignedMantleTx::new(response.funded_tx, vec![OpProof::Ed25519Sig(signature)])
|
||||
.map_err(|source| StepError::LogicalError {
|
||||
message: format!(
|
||||
"Step `{}` error: assembling the funded transaction failed: {source:?}",
|
||||
step.value
|
||||
),
|
||||
})?;
|
||||
|
||||
world
|
||||
.submit_transaction(&funding_wallet, &signed_tx, &client)
|
||||
.await?;
|
||||
world.remember_submitted_transaction(transaction_alias.clone(), tx_hash);
|
||||
|
||||
info!(
|
||||
target: TARGET,
|
||||
"Submitted funded inscription `{transaction_alias}` via node `{node_name}` fund endpoint"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fund_via_node(
|
||||
client: &NodeHttpClient,
|
||||
step: &Step,
|
||||
tx_builder: MantleTxBuilder,
|
||||
funding_pk: ZkPublicKey,
|
||||
) -> Result<WalletFundResponseBody, StepError> {
|
||||
client
|
||||
.fund_tx(WalletFundRequestBody {
|
||||
tip: None,
|
||||
tx_builder,
|
||||
change_public_key: funding_pk,
|
||||
funding_public_keys: vec![funding_pk],
|
||||
max_tx_fee: GasCost::new(u64::MAX),
|
||||
})
|
||||
.await
|
||||
.map_err(|source| StepError::StepFail {
|
||||
message: format!("Step `{}` error: fund request failed: {source}", step.value),
|
||||
})
|
||||
}
|
||||
@@ -17,6 +17,7 @@ use lb_http_api_common::{
|
||||
mantle::GasPricesResponseBody,
|
||||
wallet::{
|
||||
balance::WalletBalanceResponseBody,
|
||||
fund::{WalletFundRequestBody, WalletFundResponseBody},
|
||||
transfer_funds::{WalletTransferFundsRequestBody, WalletTransferFundsResponseBody},
|
||||
},
|
||||
},
|
||||
@@ -182,6 +183,17 @@ impl NodeHttpClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn fund_tx(
|
||||
&self,
|
||||
body: WalletFundRequestBody,
|
||||
) -> Result<WalletFundResponseBody, Error> {
|
||||
self.with_timeout(
|
||||
"Fund transaction request",
|
||||
self.http_client.fund_tx(self.base_url.clone(), body),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_sdp_declarations(&self) -> Result<Vec<Declaration>, Error> {
|
||||
self.get_sdp_declarations_at(self.base_url.clone()).await
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user