Skip to content
Open
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
2 changes: 2 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions ddk-node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ bitcoin = { version = "0.32.6", features = ["rand", "serde"] }
anyhow = "1.0.86"
clap = { version = "4.5.9", features = ["derive"] }
hex = "0.4.3"
hmac = "0.12"
sha2 = "0.10"
homedir = "0.3.3"
inquire = "0.7.5"
prost = "0.12.1"
Expand Down
5 changes: 4 additions & 1 deletion ddk-node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,17 @@ Options:
-n, --network <NETWORK> Set the Bitcoin network [default: signet]
-s, --storage-dir <STORAGE_DIR> Data storage path [default: ~/.ddk]
-p, --port <PORT> Transport listening port [default: 1776]
--grpc <GRPC_HOST> gRPC server host:port [default: 0.0.0.0:3030]
--grpc <GRPC_HOST> gRPC server host:port [default: 127.0.0.1:3030]
--api-secret <API_SECRET> HMAC secret for gRPC authentication
--esplora <ESPLORA_HOST> Esplora server URL [default: https://mutinynet.com/api]
--oracle <ORACLE_HOST> Kormir oracle URL [default: https://kormir.dlcdevkit.com]
--seed <SEED> Seed strategy: 'file' or 'bytes' [default: file]
--postgres-url <URL> PostgreSQL connection URL
-h, --help Print help
```

When binding to non-localhost addresses (e.g., `--grpc 0.0.0.0:3030`), an API secret is required via `--api-secret`.

## CLI Usage

```
Expand Down
43 changes: 41 additions & 2 deletions ddk-node/src/bin/cli.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
use clap::Parser;
use ddk_node::cli_opts::CliCommand;
use ddk_node::ddkrpc::ddk_rpc_client::DdkRpcClient;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use tonic::metadata::MetadataValue;
use tonic::transport::Channel;

type HmacSha256 = Hmac<Sha256>;

fn compute_signature(timestamp: &str, secret: &[u8]) -> String {
let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC can take key of any size");
mac.update(timestamp.as_bytes());
hex::encode(mac.finalize().into_bytes())
}

#[derive(Debug, Clone, Parser)]
#[clap(name = "ddk-cli")]
Expand All @@ -14,6 +26,9 @@ struct DdkCliArgs {
#[arg(help = "ddk-node gRPC server to connect to.")]
#[arg(default_value = "http://127.0.0.1:3030")]
pub server: String,
#[arg(long)]
#[arg(help = "HMAC secret for authentication")]
pub api_secret: Option<String>,
#[clap(subcommand)]
pub command: CliCommand,
}
Expand All @@ -22,9 +37,33 @@ struct DdkCliArgs {
async fn main() -> anyhow::Result<()> {
let opts = DdkCliArgs::parse();

let mut client = DdkRpcClient::connect(opts.server).await?;
if let Some(secret) = opts.api_secret {
let channel = Channel::from_shared(opts.server)?.connect().await?;
let secret_bytes = secret.into_bytes();

let mut client =
DdkRpcClient::with_interceptor(channel, move |mut req: tonic::Request<()>| {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("Time went backwards")
.as_secs()
.to_string();

let signature = compute_signature(&timestamp, &secret_bytes);

let ts_value: MetadataValue<_> = timestamp.parse().unwrap();
let sig_value: MetadataValue<_> = signature.parse().unwrap();

req.metadata_mut().insert("x-timestamp", ts_value);
req.metadata_mut().insert("x-signature", sig_value);
Ok(req)
});

ddk_node::command::cli_command(opts.command, &mut client).await?;
ddk_node::command::cli_command(opts.command, &mut client).await?;
} else {
let mut client = DdkRpcClient::connect(opts.server).await?;
ddk_node::command::cli_command(opts.command, &mut client).await?;
}

Ok(())
}
26 changes: 18 additions & 8 deletions ddk-node/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,15 @@ use ddk_messages::oracle_msgs::{EventDescriptor, OracleAnnouncement};
use ddk_messages::{AcceptDlc, OfferDlc};
use inquire::{Select, Text};
use serde_json::Value;
use tonic::transport::Channel;

pub async fn cli_command(
arg: CliCommand,
client: &mut DdkRpcClient<Channel>,
) -> anyhow::Result<()> {
pub async fn cli_command<T>(arg: CliCommand, client: &mut DdkRpcClient<T>) -> anyhow::Result<()>
where
T: tonic::client::GrpcService<tonic::body::BoxBody> + Send + 'static,
T::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
T::ResponseBody: tonic::codegen::Body<Data = tonic::codegen::Bytes> + Send + 'static,
<T::ResponseBody as tonic::codegen::Body>::Error:
Into<Box<dyn std::error::Error + Send + Sync>> + Send,
{
match arg {
CliCommand::Info => {
let info = client.info(InfoRequest::default()).await?.into_inner();
Expand Down Expand Up @@ -306,9 +309,16 @@ async fn generate_contract_input() -> anyhow::Result<ContractInput> {
})
}

async fn interactive_contract_input(
client: &mut DdkRpcClient<Channel>,
) -> anyhow::Result<ContractInput> {
async fn interactive_contract_input<T>(
client: &mut DdkRpcClient<T>,
) -> anyhow::Result<ContractInput>
where
T: tonic::client::GrpcService<tonic::body::BoxBody> + Send + 'static,
T::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
T::ResponseBody: tonic::codegen::Body<Data = tonic::codegen::Bytes> + Send + 'static,
<T::ResponseBody as tonic::codegen::Body>::Error:
Into<Box<dyn std::error::Error + Send + Sync>> + Send,
{
let contract_type =
Select::new("Select type of contract.", vec!["enum", "numerical"]).prompt()?;

Expand Down
Loading
Loading