Skip to content

Commit 71dba47

Browse files
authored
Merge pull request #31 from hyperledger/tx-submission-ergonomics
feat: return and monitor TXs from the same method
2 parents b5393fc + f1615a4 commit 71dba47

File tree

15 files changed

+170
-56
lines changed

15 files changed

+170
-56
lines changed

Cargo.lock

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

firefly-balius/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "firefly-balius"
3-
version = "0.4.0"
3+
version = "0.4.1"
44
description = "Helpers to write contracts for the FireFly Cardano connector"
55
license-file.workspace = true
66
publish = false

firefly-balius/src/monitor.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::collections::HashMap;
22

3-
use balius_sdk::WorkerResult;
3+
use balius_sdk::{wit, Json, WorkerResult};
44
use serde::{Deserialize, Serialize};
55

66
use crate::kv;
@@ -27,3 +27,34 @@ pub enum FinalizationCondition {
2727
// Treat the transaction as finalized after the given number of blocks have reached the chain.
2828
AfterBlocks(u64),
2929
}
30+
31+
/// A new transaction which FireFly will build, submit, and monitor for you.
32+
pub struct NewMonitoredTx(
33+
pub Box<dyn balius_sdk::txbuilder::TxExpr>,
34+
pub FinalizationCondition,
35+
);
36+
37+
impl TryInto<wit::Response> for NewMonitoredTx {
38+
type Error = balius_sdk::Error;
39+
40+
fn try_into(self) -> Result<wit::Response, Self::Error> {
41+
let balius_sdk::wit::Response::PartialTx(tx) = balius_sdk::NewTx(self.0).try_into()? else {
42+
return Err(balius_sdk::Error::Internal("Unexpected response".into()));
43+
};
44+
45+
let new_tx = SerializedNewTx::FireFlyCardanoNewTx {
46+
bytes: hex::encode(tx),
47+
condition: self.1,
48+
};
49+
Json(new_tx).try_into()
50+
}
51+
}
52+
53+
#[derive(Serialize)]
54+
#[serde(tag = "type")]
55+
enum SerializedNewTx {
56+
FireFlyCardanoNewTx {
57+
bytes: String,
58+
condition: FinalizationCondition,
59+
},
60+
}

firefly-cardanoconnect/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "firefly-cardanoconnect"
3-
version = "0.4.0"
3+
version = "0.4.1"
44
description = "An implementation of the FireFly Connector API for Cardano"
55
license-file.workspace = true
66
publish = false

firefly-cardanoconnect/src/contracts.rs

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ use std::{
44
};
55

66
use anyhow::{bail, Result};
7-
use balius_runtime::{ledgers::Ledger, Response};
7+
use balius_runtime::ledgers::Ledger;
88
use dashmap::{DashMap, Entry};
9-
pub use runtime::ContractEvent;
10-
use runtime::ContractRuntime;
9+
pub use runtime::{ContractEvent, NewTx};
10+
use runtime::{ContractRuntime, InvokeResponse};
1111
use serde::Deserialize;
12-
use serde_json::{json, Value};
12+
use serde_json::Value;
1313
use tokio::fs;
1414
use tracing::{error, warn};
1515

@@ -81,13 +81,13 @@ impl ContractManager {
8181
contract: &str,
8282
method: &str,
8383
params: Value,
84-
) -> Result<Option<Vec<u8>>> {
84+
) -> Result<Option<NewTx>> {
8585
let Some(runtime) = self.runtimes.get(contract) else {
8686
bail!("unrecognized contract {contract}");
8787
};
8888
let response = runtime.invoke(method, params).await?;
8989
match response {
90-
Response::PartialTx(bytes) => Ok(Some(bytes)),
90+
InvokeResponse::NewTx(tx) => Ok(Some(tx)),
9191
_ => Ok(None),
9292
}
9393
}
@@ -98,20 +98,14 @@ impl ContractManager {
9898
};
9999
let response = runtime.invoke(method, params).await?;
100100
match response {
101-
Response::Acknowledge => Ok(json!({})),
102-
Response::Cbor(bytes) => Ok(json!({ "cbor": hex::encode(bytes) })),
103-
Response::Json(bytes) => Ok(serde_json::from_slice(&bytes)?),
104-
Response::PartialTx(_) => bail!("Cannot build transactions from query"),
101+
InvokeResponse::Json(value) => Ok(value),
102+
InvokeResponse::NewTx(_) => bail!("Cannot build transactions from query"),
105103
}
106104
}
107105

108-
pub async fn handle_submit(&self, contract: &str, method: &str, tx_id: &str) {
109-
let params = serde_json::json!({
110-
"method": method,
111-
"hash": tx_id,
112-
});
106+
pub async fn handle_submit(&self, contract: &str, tx_id: &str, tx: NewTx) -> Result<()> {
113107
let runtime = self.get_contract_runtime(contract).await;
114-
let _: Result<_, _> = runtime.invoke("__tx_submitted", params.clone()).await;
108+
runtime.handle_submit(tx_id, tx).await
115109
}
116110

117111
pub async fn listen(&self, listener: &Listener) -> ContractListener {

firefly-cardanoconnect/src/contracts/runtime.rs

Lines changed: 95 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ impl ContractRuntime {
7676
rx.await?
7777
}
7878

79-
pub async fn invoke(&self, method: &str, params: Value) -> Result<Response> {
79+
pub async fn invoke(&self, method: &str, params: Value) -> Result<InvokeResponse> {
8080
let (tx, rx) = oneshot::channel();
8181
if self
8282
.tx
@@ -92,6 +92,22 @@ impl ContractRuntime {
9292
rx.await?
9393
}
9494

95+
pub async fn handle_submit(&self, tx_id: &str, new_tx: NewTx) -> Result<()> {
96+
let (tx, rx) = oneshot::channel();
97+
if self
98+
.tx
99+
.send(ContractRuntimeCommand::HandleSubmit {
100+
tx_id: tx_id.to_string(),
101+
new_tx,
102+
done: tx,
103+
})
104+
.is_err()
105+
{
106+
bail!("runtime for contract {} has stopped", self.contract);
107+
}
108+
rx.await?
109+
}
110+
95111
pub async fn apply(&self, rollbacks: &[BlockInfo], block: &BlockInfo) -> Result<()> {
96112
let (tx, rx) = oneshot::channel();
97113
if self
@@ -139,7 +155,12 @@ enum ContractRuntimeCommand {
139155
Invoke {
140156
method: String,
141157
params: Value,
142-
done: oneshot::Sender<Result<Response>>,
158+
done: oneshot::Sender<Result<InvokeResponse>>,
159+
},
160+
HandleSubmit {
161+
tx_id: String,
162+
new_tx: NewTx,
163+
done: oneshot::Sender<Result<()>>,
143164
},
144165
Apply {
145166
rollbacks: Vec<BlockInfo>,
@@ -194,6 +215,13 @@ impl ContractRuntimeWorker {
194215
} => {
195216
let _ = done.send(self.invoke(&method, params).await);
196217
}
218+
ContractRuntimeCommand::HandleSubmit {
219+
tx_id,
220+
new_tx,
221+
done,
222+
} => {
223+
let _ = done.send(self.handle_submit(&tx_id, new_tx).await);
224+
}
197225
ContractRuntimeCommand::Apply {
198226
rollbacks,
199227
block,
@@ -241,14 +269,53 @@ impl ContractRuntimeWorker {
241269
Ok(())
242270
}
243271

244-
async fn invoke(&mut self, method: &str, params: Value) -> Result<Response> {
272+
async fn invoke(&mut self, method: &str, params: Value) -> Result<InvokeResponse> {
245273
let params = serde_json::to_vec(&params)?;
246274
let Some(runtime) = self.runtime.as_mut() else {
247275
bail!("Contract {} failed to initialize", self.contract);
248276
};
249-
Ok(runtime
277+
let response = runtime
250278
.handle_request(&self.contract, method, params)
251-
.await?)
279+
.await?;
280+
match response {
281+
Response::Acknowledge => Ok(InvokeResponse::Json(json!({}))),
282+
Response::Cbor(bytes) => Ok(InvokeResponse::Json(json!({ "cbor": bytes }))),
283+
Response::PartialTx(bytes) => Ok(InvokeResponse::NewTx(NewTx {
284+
bytes,
285+
method: method.to_string(),
286+
condition: None,
287+
})),
288+
Response::Json(bytes) => {
289+
let value: Value = serde_json::from_slice(&bytes)?;
290+
if let Ok(RawNewTx::FireFlyCardanoNewTx { bytes, condition }) =
291+
serde_json::from_value(value.clone())
292+
{
293+
Ok(InvokeResponse::NewTx(NewTx {
294+
bytes: hex::decode(bytes)?,
295+
method: method.to_string(),
296+
condition: Some(condition),
297+
}))
298+
} else {
299+
Ok(InvokeResponse::Json(value))
300+
}
301+
}
302+
}
303+
}
304+
305+
async fn handle_submit(&mut self, tx_id: &str, new_tx: NewTx) -> Result<()> {
306+
if let Some(condition) = new_tx.condition {
307+
let mut monitored_txs: HashMap<String, FinalizationCondition> =
308+
self.get_value("__monitored_txs").await?.unwrap_or_default();
309+
monitored_txs.insert(tx_id.to_string(), condition);
310+
self.set_value("__monitored_txs", monitored_txs).await?;
311+
}
312+
313+
let params = json!({
314+
"method": new_tx.method,
315+
"hash": tx_id,
316+
});
317+
let _: Result<_, _> = self.invoke("__tx_submitted", params).await;
318+
Ok(())
252319
}
253320

254321
async fn apply(&mut self, rollbacks: &[BlockInfo], block: &BlockInfo) -> Result<()> {
@@ -419,14 +486,34 @@ pub struct ContractEvent {
419486
pub data: serde_json::Value,
420487
}
421488

489+
pub enum InvokeResponse {
490+
Json(Value),
491+
NewTx(NewTx),
492+
}
493+
494+
pub struct NewTx {
495+
pub bytes: Vec<u8>,
496+
method: String,
497+
condition: Option<FinalizationCondition>,
498+
}
499+
500+
#[derive(Debug, Serialize, Deserialize)]
501+
enum FinalizationCondition {
502+
AfterBlocks(u64),
503+
}
504+
422505
#[derive(Debug, Clone, Serialize, Deserialize)]
423506
struct RawEvent {
424507
pub tx_hash: Vec<u8>,
425508
pub signature: String,
426509
pub data: serde_json::Value,
427510
}
428511

429-
#[derive(Debug, Serialize, Deserialize)]
430-
enum FinalizationCondition {
431-
AfterBlocks(u64),
512+
#[derive(Deserialize)]
513+
#[serde(tag = "type")]
514+
enum RawNewTx {
515+
FireFlyCardanoNewTx {
516+
bytes: String,
517+
condition: FinalizationCondition,
518+
},
432519
}

firefly-cardanoconnect/src/operations/manager.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,9 @@ impl OperationsManager {
8585
}
8686
};
8787
if let Some(tx) = value {
88-
let tx_id = self.submit_transaction(from, tx).await?;
88+
let tx_id = self.submit_transaction(from, &tx.bytes).await?;
8989
op.tx_id = Some(tx_id.clone());
90-
self.contracts.handle_submit(contract, method, &tx_id).await;
90+
self.contracts.handle_submit(contract, &tx_id, tx).await?;
9191
}
9292

9393
op.status = OperationStatus::Succeeded;
@@ -114,8 +114,8 @@ impl OperationsManager {
114114
Ok(())
115115
}
116116

117-
async fn submit_transaction(&self, address: &str, tx: Vec<u8>) -> ApiResult<String> {
118-
let mut transaction: Tx = minicbor::decode(&tx)?;
117+
async fn submit_transaction(&self, address: &str, tx: &[u8]) -> ApiResult<String> {
118+
let mut transaction: Tx = minicbor::decode(tx)?;
119119
self.signer
120120
.sign(address.to_string(), &mut transaction)
121121
.await?;

firefly-cardanosigner/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "firefly-cardanosigner"
3-
version = "0.4.0"
3+
version = "0.4.1"
44
description = "A service managing keys and signing for the FireFly Cardano connector"
55
license-file.workspace = true
66
publish = false

firefly-server/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "firefly-server"
3-
version = "0.4.0"
3+
version = "0.4.1"
44
description = "Internal library with shared code for services"
55
license-file.workspace = true
66
publish = false

scripts/demo/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "firefly-cardano-demo"
3-
version = "0.4.0"
3+
version = "0.4.1"
44
description = "A demo of the firefly-cardanoconnect API"
55
license-file.workspace = true
66
publish = false

0 commit comments

Comments
 (0)