1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
use std::{
fmt::Display,
net::{IpAddr, Ipv4Addr},
path::PathBuf,
};
use crate::{commands::PutType, wasm_runtime::ExecutorConfig};
use clap::ValueEnum;
use freenet::{config::ConfigPathsArgs, dev_tool::OperationMode};
use semver::Version;
#[derive(clap::Parser, Clone)]
#[clap(name = "Freenet Development Tool")]
#[clap(author = "The Freenet Project Inc.")]
#[clap(version = env!("CARGO_PKG_VERSION"))]
pub struct Config {
#[clap(subcommand)]
pub sub_command: SubCommand,
#[clap(flatten)]
pub additional: BaseConfig,
}
#[derive(clap::Parser, Clone)]
pub struct BaseConfig {
#[clap(flatten)]
pub(crate) paths: ConfigPathsArgs,
/// Node operation mode.
#[arg(value_enum, default_value_t=OperationMode::Local, env = "MODE")]
pub mode: OperationMode,
/// The port of the running local freenet node websocket API.
#[arg(short, long, default_value = "7509", env = "WS_API_PORT")]
pub(crate) port: u16,
/// The ip address of freenet node to publish the contract to. If the node is running in local mode,
/// The default value is `127.0.0.1`.
#[arg(short, long, default_value_t = IpAddr::V4(Ipv4Addr::LOCALHOST))]
pub(crate) address: IpAddr,
/// Full WebSocket URL to connect to (e.g. ws://host:port/secret/v1/contract/command?encodingProtocol=native).
/// When provided, --address and --port must not be specified.
#[arg(long, env = "FREENET_NODE_URL", conflicts_with_all = ["address", "port"])]
pub(crate) node_url: Option<String>,
}
#[derive(clap::Subcommand, Clone)]
pub enum SubCommand {
Init(InitPackageConfig),
New(NewPackageConfig),
Build(BuildToolConfig),
Inspect(crate::inspect::InspectConfig),
Publish(PutConfig),
/// Query the local node for information. Currently only shows open connections.
Query {},
/// Get detailed node diagnostics including network state, subscriptions, and metrics.
Diagnostics {
/// Contract keys to include in diagnostics (Base58 encoded)
#[arg(long = "contract")]
contract_keys: Vec<String>,
},
WasmRuntime(ExecutorConfig),
Execute(RunCliConfig),
Test(crate::testing::TestConfig),
NetworkMetricsServer(crate::network_metrics_server::ServerConfig),
/// Get the contract ID without publishing
GetContractId(crate::commands::GetContractIdConfig),
/// Verify network state consistency from telemetry event logs.
///
/// Reads event log (AOF) files from one or more nodes, linearizes the
/// state transitions, and detects anomalies that indicate consistency
/// failures (missing broadcasts, unapplied updates, state divergence).
VerifyState(crate::verify_state::VerifyStateConfig),
/// Publish and manage static websites on Freenet.
///
/// Use `init` to generate a signing keypair, `publish` to deploy a website,
/// and `update` to push new content to an existing website.
Website {
#[clap(subcommand)]
command: crate::website::WebsiteCommand,
},
}
impl SubCommand {
pub fn is_child(&self) -> bool {
if let SubCommand::Test(config) = self {
if let crate::testing::TestMode::Network(config) = &config.command {
return matches!(config.mode, crate::testing::network::Process::Peer);
}
}
false
}
}
/// Core CLI tool for interacting with the Freenet local node.
///
/// This tool allows the execution of commands against the local node
/// and is intended to be used for development and automated workflows.
#[derive(clap::Parser, Clone)]
pub struct RunCliConfig {
/// Command to execute.
#[clap(subcommand)]
pub command: NodeCommand,
}
#[derive(clap::Subcommand, Clone)]
pub enum NodeCommand {
Put(PutConfig),
/// Get the current state of a contract.
Get(GetConfig),
Update(UpdateConfig),
/// Subscribe to a contract and stream update notifications.
Subscribe(SubscribeConfig),
GetContractId(crate::commands::GetContractIdConfig),
}
/// Retrieves the current state of a contract from the network.
#[derive(clap::Parser, Clone)]
pub struct GetConfig {
/// Contract key in Base58 format.
pub(crate) key: String,
/// Also return the contract code in the response.
#[arg(long)]
pub(crate) return_code: bool,
/// Write the state to a file instead of stdout.
#[arg(short, long)]
pub(crate) output: Option<PathBuf>,
}
/// Subscribes to a contract and streams update notifications until interrupted.
#[derive(clap::Parser, Clone)]
pub struct SubscribeConfig {
/// Contract key in Base58 format.
pub(crate) key: String,
/// Write each update to a file (overwritten on each update) instead of stdout.
#[arg(short, long)]
pub(crate) output: Option<PathBuf>,
}
/// Updates a contract in the network.
#[derive(clap::Parser, Clone)]
pub struct UpdateConfig {
/// Contract id of the contract being updated in Base58 format.
pub(crate) key: String,
/// The ip address of freenet node to update the contract to. If the node is running in local mode,
/// The default value is `127.0.0.1`
#[arg(short, long, default_value_t = IpAddr::V4(Ipv4Addr::LOCALHOST))]
pub(crate) address: IpAddr,
/// Path to the file being pushed to the contract. Interpreted as a
/// `UpdateData::Delta` by default, or as a full-state replacement
/// (`UpdateData::State`) when `--as-state` is set.
pub(crate) delta: PathBuf,
/// Send the file contents as a full state replacement (`UpdateData::State`)
/// instead of the default delta (`UpdateData::Delta`). Use this when the
/// contract's `update_state` only accepts full-state replacements.
#[arg(long)]
pub(crate) as_state: bool,
}
/// Publishes a new contract or delegate to the network.
// todo: make some of this options exclusive depending on the value of `package_type`
#[derive(clap::Parser, Clone)]
pub struct PutConfig {
/// A path to the compiled WASM code file. This must be a valid packaged contract or component,
/// (built using the `fdev` tool). Not an arbitrary WASM file.
#[arg(long)]
pub(crate) code: PathBuf,
/// A path to the file parameters for the contract/delegate. If not specified, will be published
/// with empty parameters.
#[arg(long)]
pub(crate) parameters: Option<PathBuf>,
/// Type of put to perform.
#[clap(subcommand)]
pub(crate) package_type: PutType,
/// Flag that indicates if the node should subscribe to the contract.
#[arg(long)]
pub(crate) subscribe: bool,
}
/// Builds and packages a contract or delegate.
///
/// This tool will build the WASM contract or delegate and publish it to the network.
#[derive(clap::Parser, Clone, Debug)]
pub struct BuildToolConfig {
/// Compile the contract or delegate with specific features.
#[arg(long)]
pub(crate) features: Option<String>,
/// Compile the contract or delegate with a specific API version.
#[arg(long, value_parser = parse_version, default_value_t=Version::new(0, 0, 1))]
pub(crate) version: Version,
/// Output object type.
#[arg(long, value_enum, default_value_t=PackageType::default())]
pub(crate) package_type: PackageType,
/// Compile in debug mode instead of release. Warning: Debug mode produces WASM files that are
/// typically 40-50x larger than release mode (e.g., 10MB vs 200KB), which may exceed WebSocket
/// message size limits and cause deployment failures. Use only for development and debugging.
#[arg(long)]
pub(crate) debug: bool,
}
#[derive(Default, Debug, Clone, Copy, ValueEnum)]
pub(crate) enum PackageType {
#[default]
Contract,
Delegate,
}
impl PackageType {
pub fn feature(&self) -> &'static str {
match self {
PackageType::Contract => "freenet-main-contract",
PackageType::Delegate => "freenet-main-delegate",
}
}
}
impl Display for PackageType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PackageType::Contract => write!(f, "contract"),
PackageType::Delegate => write!(f, "delegate"),
}
}
}
impl Default for BuildToolConfig {
fn default() -> Self {
Self {
features: None,
version: Version::new(0, 0, 1),
package_type: PackageType::default(),
debug: false,
}
}
}
fn parse_version(src: &str) -> Result<Version, String> {
Version::parse(src).map_err(|e| e.to_string())
}
/// Initialize a new Freenet contract and/or app in the CWD by default.
#[derive(clap::Parser, Clone)]
pub struct InitPackageConfig {
#[arg(id = "type", value_enum)]
pub(crate) kind: ContractKind,
#[arg(short, long, value_name = "PATH", value_hint = clap::ValueHint::DirPath)]
pub(crate) path: Option<PathBuf>,
}
impl From<NewPackageConfig> for InitPackageConfig {
fn from(NewPackageConfig { kind, path }: NewPackageConfig) -> Self {
Self {
kind,
path: path.into(),
}
}
}
/// Create a new Freenet contract and/or app.
#[derive(clap::Parser, Clone)]
pub struct NewPackageConfig {
#[arg(id = "type", value_enum)]
pub(crate) kind: ContractKind,
#[arg(value_name = "PATH", value_hint = clap::ValueHint::DirPath)]
pub(crate) path: PathBuf,
}
#[derive(clap::ValueEnum, Clone)]
pub(crate) enum ContractKind {
/// A web app container contract.
WebApp,
/// An standard contract.
Contract,
}
#[cfg(test)]
mod tests {
use clap::Parser as _;
use super::Config;
/// Regression test for #4088: `fdev publish --release` used to bail
/// unconditionally with "Cannot publish contracts in the network yet".
/// After the fix the `--release` flag no longer exists on `publish`;
/// passing it should be a CLI parse error, not a silent rejection at
/// runtime.
#[test]
fn publish_does_not_accept_release_flag() {
let result = Config::try_parse_from([
"fdev",
"publish",
"--code",
"contract.wasm",
"--release", // must not exist
"contract",
]);
let err_msg = result
.as_ref()
.map(|_| String::new())
.unwrap_or_else(|e| e.to_string());
assert!(
result.is_err(),
"fdev publish --release should be a parse error after #4088 removal, but parsed OK"
);
assert!(
err_msg.contains("--release") || err_msg.contains("release"),
"error message should mention the unknown flag, got: {err_msg}"
);
}
/// Regression test for #4088: `fdev execute update <KEY> <DELTA> <RELEASE>`
/// used to accept a positional `release: bool` argument that bailed
/// unconditionally when true. After the fix the positional arg is gone.
#[test]
fn execute_update_does_not_accept_positional_release_bool() {
// Passing "true" as a third positional arg should now be rejected
// because UpdateConfig only accepts key + delta.
let result = Config::try_parse_from([
"fdev",
"execute",
"update",
"SomeBase58ContractKey",
"/tmp/delta.bin",
"true", // was the `release: bool` positional; must not exist
]);
assert!(
result.is_err(),
"fdev execute update with a third positional arg should fail after #4088 removal"
);
}
}