fdev 0.3.258

Freenet development tool
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
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
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>,
    /// Maximum seconds to wait for the node's response before giving up.
    /// Takes precedence over the `FDEV_RESPONSE_TIMEOUT` env var; when neither
    /// is set the default of 300s applies. See issue #4102.
    #[arg(long)]
    pub(crate) timeout: Option<u64>,
}

/// 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>,
    /// Maximum seconds to wait for the node's initial subscribe confirmation
    /// before giving up. Takes precedence over the `FDEV_RESPONSE_TIMEOUT` env
    /// var; when neither is set the default of 300s applies. See issue #4102.
    #[arg(long)]
    pub(crate) timeout: Option<u64>,
}

/// 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,
    /// Maximum seconds to wait for the node's response before giving up.
    /// Takes precedence over the `FDEV_RESPONSE_TIMEOUT` env var; when neither
    /// is set the default of 300s applies. See issue #4102.
    #[arg(long)]
    pub(crate) timeout: Option<u64>,
}

/// 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,

    /// Maximum seconds to wait for the node's response before giving up.
    /// Takes precedence over the `FDEV_RESPONSE_TIMEOUT` env var; when neither
    /// is set the default of 300s applies. See issue #4102.
    #[arg(long)]
    pub(crate) timeout: Option<u64>,
}

/// 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"
        );
    }

    // -----------------------------------------------------------------
    // #4102: `--timeout <seconds>` on publish / execute {put,get,update}.
    // These parse assertions fail to COMPILE on pre-fix code because the
    // `timeout` field does not exist on the config structs yet.
    // -----------------------------------------------------------------

    use super::{NodeCommand, SubCommand};
    use crate::commands::PutType;

    #[test]
    fn publish_accepts_timeout_flag() {
        let config = Config::try_parse_from([
            "fdev",
            "publish",
            "--code",
            "contract.wasm",
            "--timeout",
            "600",
            "contract",
        ])
        .expect("publish --timeout 600 should parse");
        match config.sub_command {
            SubCommand::Publish(put) => {
                assert_eq!(put.timeout, Some(600));
                assert!(matches!(put.package_type, PutType::Contract(_)));
            }
            _ => panic!("expected Publish subcommand"),
        }
    }

    #[test]
    fn publish_timeout_defaults_to_none_when_omitted() {
        let config =
            Config::try_parse_from(["fdev", "publish", "--code", "contract.wasm", "contract"])
                .expect("publish without --timeout should parse");
        match config.sub_command {
            SubCommand::Publish(put) => assert_eq!(put.timeout, None),
            _ => panic!("expected Publish subcommand"),
        }
    }

    #[test]
    fn execute_put_accepts_timeout_flag() {
        let config = Config::try_parse_from([
            "fdev",
            "execute",
            "put",
            "--code",
            "contract.wasm",
            "--timeout",
            "45",
            "contract",
        ])
        .expect("execute put --timeout 45 should parse");
        match config.sub_command {
            SubCommand::Execute(run) => match run.command {
                NodeCommand::Put(put) => assert_eq!(put.timeout, Some(45)),
                _ => panic!("expected Put command"),
            },
            _ => panic!("expected Execute subcommand"),
        }
    }

    #[test]
    fn execute_get_accepts_timeout_flag() {
        let config = Config::try_parse_from([
            "fdev",
            "execute",
            "get",
            "SomeBase58ContractKey",
            "--timeout",
            "42",
        ])
        .expect("execute get --timeout 42 should parse");
        match config.sub_command {
            SubCommand::Execute(run) => match run.command {
                NodeCommand::Get(get) => assert_eq!(get.timeout, Some(42)),
                _ => panic!("expected Get command"),
            },
            _ => panic!("expected Execute subcommand"),
        }
    }

    #[test]
    fn execute_update_accepts_timeout_flag() {
        let config = Config::try_parse_from([
            "fdev",
            "execute",
            "update",
            "SomeBase58ContractKey",
            "/tmp/delta.bin",
            "--timeout",
            "42",
        ])
        .expect("execute update --timeout 42 should parse");
        match config.sub_command {
            SubCommand::Execute(run) => match run.command {
                NodeCommand::Update(update) => assert_eq!(update.timeout, Some(42)),
                _ => panic!("expected Update command"),
            },
            _ => panic!("expected Execute subcommand"),
        }
    }

    #[test]
    fn execute_subscribe_accepts_timeout_flag() {
        let config = Config::try_parse_from([
            "fdev",
            "execute",
            "subscribe",
            "SomeBase58ContractKey",
            "--timeout",
            "42",
        ])
        .expect("execute subscribe --timeout 42 should parse");
        match config.sub_command {
            SubCommand::Execute(run) => match run.command {
                NodeCommand::Subscribe(subscribe) => assert_eq!(subscribe.timeout, Some(42)),
                _ => panic!("expected Subscribe command"),
            },
            _ => panic!("expected Execute subcommand"),
        }
    }

    #[test]
    fn website_publish_accepts_timeout_flag() {
        use crate::website::WebsiteCommand;
        let config = Config::try_parse_from([
            "fdev",
            "website",
            "publish",
            "/tmp/site",
            "--key",
            "my-site",
            "--timeout",
            "120",
        ])
        .expect("website publish --timeout 120 should parse");
        match config.sub_command {
            SubCommand::Website { command } => match command {
                WebsiteCommand::Publish { timeout, .. } => assert_eq!(timeout, Some(120)),
                _ => panic!("expected Publish website command"),
            },
            _ => panic!("expected Website subcommand"),
        }
    }

    #[test]
    fn website_update_accepts_timeout_flag() {
        use crate::website::WebsiteCommand;
        let config = Config::try_parse_from([
            "fdev",
            "website",
            "update",
            "/tmp/site",
            "--key",
            "my-site",
            "--timeout",
            "120",
        ])
        .expect("website update --timeout 120 should parse");
        match config.sub_command {
            SubCommand::Website { command } => match command {
                WebsiteCommand::Update { timeout, .. } => assert_eq!(timeout, Some(120)),
                _ => panic!("expected Update website command"),
            },
            _ => panic!("expected Website subcommand"),
        }
    }
}