Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ clap = { version = "4.6.1", default-features = false }
coset = { version = "0.3", default-features = false }
cranelift-codegen = { version = "0.136", default-features = false }
ed25519-dalek = { version = "2", default-features = false }
form_urlencoded = { version = "1", default-features = false }
futures = { version = "0.3", default-features = false }
headers-accept = { version = "0.3", default-features = false }
headers-accept = { git = "https://github.com/maxcountryman/headers-accept", rev = "e5a875f2504267f2fa4913e595e4299a76365db8", default-features = false }
headers-core = { version = "0.3", default-features = false }
hex = { version = "0.4.3", default-features = false }
http = { version = "1", default-features = false }
Expand Down
117 changes: 54 additions & 63 deletions starstream-ledger-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use starstream_ledger::client::http::ClientBuilder;
use starstream_ledger::client::runtime::UtxoCtx;
use starstream_ledger::client::runtime::{Client, Ctx, call_coordination_script, compile_contract};
use starstream_ledger::{
CardanoCtx, Transaction, TransactionInput, TransactionOutput, encode_digest,
APPLICATION_WASM, CardanoCtx, Transaction, TransactionInput, TransactionOutput, encode_digest,
};
use starstream_runtime::Utxo;
use tokio::fs;
Expand Down Expand Up @@ -226,32 +226,44 @@ enum TransactionCommand {
},
}

#[derive(Debug, clap::Args)]
struct UtxoArgs {
/// Digest of the transaction that produced the UTXO, either as multibase multihash
/// or `sha256:HEX`, defaults to the genesis block.
#[arg(long, value_name = "DIGEST", value_parser = parse_digest)]
transaction: Option<[u8; 32]>,

/// Index of the UTXO in the transaction outputs.
index: u32,
}

impl From<UtxoArgs> for TransactionInput {
fn from(UtxoArgs { transaction, index }: UtxoArgs) -> Self {
match transaction {
Some(transaction) => Self::new(transaction, index),
None => Self::genesis(index),
}
}
}

#[derive(Debug, Subcommand)]
enum UtxoCommand {
/// Call a method of a UTXO on the ledger, discarding the resulting state.
Call {
/// Digest of the transaction that produced the UTXO, either as multibase multihash
/// or `sha256:HEX`, defaults to the genesis block.
#[arg(long, value_name = "DIGEST", value_parser = parse_digest)]
transaction: Option<[u8; 32]>,

/// Index of the UTXO in the transaction outputs.
index: u32,
#[command(flatten)]
utxo: UtxoArgs,

/// Method to call.
method: Box<str>,

/// WAVE-encoded method arguments.
args: Vec<Box<str>>,
},
}

fn decode_component(wasm: &[u8]) -> anyhow::Result<(wit_parser::Resolve, wit_parser::WorldId)> {
let wasm = wit_parser::decoding::decode(wasm).context("failed to decode Wasm")?;
let wit_parser::decoding::DecodedWasm::Component(resolve, world) = wasm else {
bail!("Wasm is not a component")
};
Ok((resolve, world))
/// Print the WIT of a UTXO's methods, in the shape `utxo call` invokes them.
Wit {
#[command(flatten)]
utxo: UtxoArgs,
},
}

/// Encode WAVE-encoded `args` as parameters of type `ty`.
Expand Down Expand Up @@ -319,14 +331,12 @@ fn parse_input(s: &str) -> anyhow::Result<TransactionInput> {
let (transaction, index) = s
.rsplit_once(':')
.context("input must be formatted as `[TRANSACTION]:INDEX`")?;
let transaction = if transaction.is_empty() {
Box::default()
} else {
let digest = parse_digest(transaction).context("transaction digest is not valid")?;
encode_digest(&digest).into()
};
let index = index.parse().context("input index is not valid")?;
Ok(TransactionInput { transaction, index })
if transaction.is_empty() {
return Ok(TransactionInput::genesis(index));
}
let transaction = parse_digest(transaction).context("transaction digest is not valid")?;
Ok(TransactionInput::new(transaction, index))
}

fn parse_verifying_key(s: &str) -> anyhow::Result<VerifyingKey> {
Expand Down Expand Up @@ -571,50 +581,23 @@ async fn exec(args: Args) -> anyhow::Result<()> {
.await
.context("failed to write transaction to stdout")
}
Command::Utxo(UtxoCommand::Call {
transaction,
index,
method,
args,
}) => {
let transaction = if let Some(transaction) = transaction {
encode_digest(&transaction).into()
} else {
Box::default()
Command::Utxo(UtxoCommand::Call { utxo, method, args }) => {
let input = TransactionInput::from(utxo);
let wasm = client.get_utxo_wit(&input, Some(APPLICATION_WASM)).await?;
let wasm = wit_parser::decoding::decode(&wasm).context("failed to decode UTXO WIT")?;
let wit_parser::decoding::DecodedWasm::WitPackage(resolve, pkg) = wasm else {
bail!("UTXO WIT is not a WIT package")
};
let input = TransactionInput { transaction, index };
let TransactionOutput {
contract, instance, ..
} = client.get_input_utxo(&input).await?;
let digest = starstream_ledger::parse_digest(&contract)
.with_context(|| format!("failed to parse `{contract}` as multibase multihash"))?;
let wasm = client.get_contract(digest).await?;
let wasm = wasm.context("contract not found")?;
let (resolve, world) = decode_component(&wasm)?;
let world = &resolve.worlds[world];
let ty = world
.exports
.iter()
.find_map(|(name, item)| {
let wit_parser::WorldKey::Name(name) = name else {
return None;
};
let wit_parser::WorldItem::Interface { id, .. } = item else {
return None;
};
if name == instance.as_ref() {
Some(id)
} else {
None
}
})
.with_context(|| format!("UTXO instance `{instance}` not found"))?;
let ty = resolve.interfaces[*ty]
let utxo = resolve.packages[pkg]
.interfaces
.get("utxo")
.context("`utxo` interface not found")?;
let ty = resolve.interfaces[*utxo]
.functions
.get(&format!("[method]utxo.{method}"))
.get(&*method)
.with_context(|| format!("method `{method}` not found"))?;
let ty = wit_parser::Function {
params: ty.params.iter().skip(1).cloned().collect(),
params: ty.params.iter().skip(2).cloned().collect(),
..ty.clone()
};
let ty = wasm_wave::value::resolve_wit_func_type(&resolve, &ty)
Expand All @@ -623,5 +606,13 @@ async fn exec(args: Args) -> anyhow::Result<()> {
let rx = client.call_utxo_method(&input, &method, &args).await?;
write_results(&ty, rx).await
}
Command::Utxo(UtxoCommand::Wit { utxo }) => {
let input = TransactionInput::from(utxo);
let wit = client.get_utxo_wit(&input, None).await?;
stdout()
.write_all(&wit)
.await
.context("failed to write WIT to stdout")
}
}
}
72 changes: 53 additions & 19 deletions starstream-ledger-cli/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,23 +71,6 @@ async fn cli() {
starstream_ledger::server::configure_wasmtime(&mut engine_config);
let engine = wasmtime::Engine::new(&engine_config).unwrap();

let ledger = Ledger::new(
engine,
128,
u64::MAX,
NETWORK,
ADMIN.verifying_key(),
[],
CardanoCtx::default(),
);
let ledger = Arc::new(ledger);
let addr = free_tcp_addr().await.unwrap();
let (ledger, shutdown) = ledger
.handle_http(addr, Duration::from_secs(30))
.await
.expect("failed to handle HTTP");
let ledger = tokio::spawn(ledger);

let score_wasm = NamedTempFile::new().unwrap();
fs::write(&score_wasm, &*SCORE_WASM)
.await
Expand All @@ -103,8 +86,6 @@ async fn cli() {

let tx_file = NamedTempFile::new().unwrap();
let stdout = run_cli([
"--url",
&format!("http://{addr}"),
"contract",
"script",
"call",
Expand All @@ -130,6 +111,23 @@ async fn cli() {
let shown: toml::Value = toml::from_slice(&stdout).expect("failed to decode transaction TOML");
assert_eq!(shown, toml::Value::try_from(&tx).unwrap());

let ledger = Ledger::new(
engine,
128,
u64::MAX,
NETWORK,
ADMIN.verifying_key(),
tx.outputs,
CardanoCtx::default(),
);
let ledger = Arc::new(ledger);
let addr = free_tcp_addr().await.unwrap();
let (ledger, shutdown) = ledger
.handle_http(addr, Duration::from_secs(30))
.await
.expect("failed to handle HTTP");
let ledger = tokio::spawn(ledger);

let publish_envelope = build_publish_envelope(
account.clone(),
NETWORK,
Expand Down Expand Up @@ -207,6 +205,42 @@ async fn cli() {
let tx = fs::read(&tx_file).await.unwrap();
assert_score_transaction(&tx, score_digest);

let stdout = run_cli(["--url", &format!("http://{addr}"), "utxo", "wit", "0"])
.await
.unwrap();
assert_eq!(
str::from_utf8(&stdout).unwrap(),
r#"package starstream:ledger;

interface utxo {
get-chips: func(transaction: option<string>, index: u32) -> u64;

get-mult: func(transaction: option<string>, index: u32) -> u64;

plus-chips: func(transaction: option<string>, index: u32, arg-chips2: u64);

plus-mult: func(transaction: option<string>, index: u32, arg-mult2: u64);

mult-mult: func(transaction: option<string>, index: u32, arg-mult-pct: u64);

finish: func(transaction: option<string>, index: u32);
}

"#
);

let stdout = run_cli([
"--url",
&format!("http://{addr}"),
"utxo",
"call",
"0",
"get-chips",
])
.await
.unwrap();
assert_eq!(stdout, b"(42)\n");

let require_hash_preimage_wasm = NamedTempFile::new().unwrap();
fs::write(&require_hash_preimage_wasm, &*REQUIRE_HASH_PREIMAGE_WASM)
.await
Expand Down
11 changes: 11 additions & 0 deletions starstream-ledger/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ client = [
"wrpc-http/client-legacy",
]
server = [
"dep:anyhow",
"dep:headers-accept",
"dep:headers-core",
"dep:http-body",
"dep:hyper",
"dep:tokio-io-timeout",
"dep:tower-http",
"dep:wit-component",
"dep:wit-parser",
"dep:wrpc-pack",
"hyper-util/server",
"hyper-util/server-graceful",
Expand All @@ -34,6 +37,7 @@ anyhow = { workspace = true }
bytes = { workspace = true }
coset = { workspace = true, features = ["std"] }
ed25519-dalek = { workspace = true, features = ["fast", "std", "zeroize"] }
form_urlencoded = { workspace = true, features = ["std"] }
futures = { workspace = true }
headers-accept = { workspace = true, optional = true }
headers-core = { workspace = true, optional = true }
Expand Down Expand Up @@ -72,10 +76,17 @@ wasmtime-wizer = { workspace = true, optional = true, features = [
"wasmtime",
] }
wit-bindgen-wrpc = { workspace = true }
wit-component = { workspace = true, optional = true }
wit-parser = { workspace = true, optional = true }
wrpc-http = { workspace = true, optional = true }
wrpc-pack = { workspace = true, optional = true }
wrpc-transport = { workspace = true }

[build-dependencies]
anyhow = { workspace = true, optional = true }
wit-component = { workspace = true, optional = true }
wit-parser = { workspace = true, optional = true }

[dev-dependencies]
starstream-compiler = { workspace = true }
starstream-to-wasm = { workspace = true }
Expand Down
21 changes: 21 additions & 0 deletions starstream-ledger/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#[cfg(feature = "server")]
fn main() -> anyhow::Result<()> {
use std::env;
use std::fs;
use std::path::PathBuf;

use anyhow::Context as _;

println!("cargo::rerun-if-changed=wit/ledger.wit");
let mut resolve = wit_parser::Resolve::default();
let pkg = resolve.push_file("wit/ledger.wit")?;
let wasm = wit_component::encode(&resolve, pkg).context("failed to encode ledger WIT")?;
let out_dir = env::var_os("OUT_DIR").context("`OUT_DIR` not set")?;
let path = PathBuf::from(out_dir).join("ledger.wasm");
fs::write(&path, wasm).with_context(|| format!("failed to write `{}`", path.display()))
}

#[cfg(not(feature = "server"))]
fn main() {
println!("cargo::rerun-if-changed=build.rs");
}
Loading
Loading