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
18 changes: 17 additions & 1 deletion .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ This project uses Snafu for error handling.
- Every new *primary erroring action* gets its own error variant. There is no `MyError::Io { source: io::Error }`, instead (hypothetically) `OpenSocket` and `WriteSocket` should be separate. `snafu(context(false))` is not permitted. `snafu(transparent)` should *only* be used for source error types defined elsewhere in this repo, *not* for foreign error types.
- Every error regarding a file in some way (processing, creating, etc.) should contain the file path of the error. It is okay to add 'dummy' file path parameters only used in error handling routes. For 'basic' file ops and JSON/YML loading use the functions in `icp::fs`, whose errors include the file path and can be made `snafu(transparent)`.

## Examples
## Examples and Templates

The `examples/` directory contains working project templates demonstrating:

Expand Down Expand Up @@ -293,3 +293,19 @@ Before finalizing recipe-related changes, ask the user:
- What branch or version to compare with

Then verify documentation matches the recipe templates from the specified source.

### Dependency Versions in Examples/Templates

When creating or updating examples and templates:

1. **Always use the latest versions** of dependencies like `candid`, `ic-cdk`, `ic-cdk-macros`, etc. Use `cargo search <crate-name> --limit 1` to check the current latest version on crates.io before specifying dependency versions.

2. **When updating a single example**, ask the developer whether other examples should also be updated to maintain consistency across all examples.

3. **Schema references** in YAML files (`$schema=`) should point to the latest stable release tag (e.g., `v0.1.0`), not beta versions.

4. **Related repository**: The `icp-cli-templates` repository (https://github.com/dfinity/icp-cli-templates) contains cargo-generate templates that should follow the same versioning guidelines.

5. **Motoko imports**: When writing Motoko code, prefer `mo:core` over `mo:base`. See https://docs.internetcomputer.org/motoko/core for documentation.

6. **Gitignore for ICP projects**: Only add `.icp/cache/` to `.gitignore`, NOT the entire `.icp/` directory. The `.icp/data/` directory contains mainnet canister ID mappings that should be version controlled to preserve deployment information.
8 changes: 8 additions & 0 deletions crates/icp/src/context/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,8 @@ async fn test_get_agent_defaults_inside_project_with_default_local() {
ii: false,
nns: false,
subnets: None,
bitcoind_addr: None,
dogecoind_addr: None,
})),
},
},
Expand Down Expand Up @@ -669,6 +671,8 @@ async fn test_get_agent_defaults_with_overridden_local_network() {
ii: false,
nns: false,
subnets: None,
bitcoind_addr: None,
dogecoind_addr: None,
})),
},
},
Expand Down Expand Up @@ -736,6 +740,8 @@ async fn test_get_agent_defaults_with_overridden_local_environment() {
ii: false,
nns: false,
subnets: None,
bitcoind_addr: None,
dogecoind_addr: None,
})),
},
},
Expand All @@ -754,6 +760,8 @@ async fn test_get_agent_defaults_with_overridden_local_environment() {
ii: false,
nns: false,
subnets: None,
bitcoind_addr: None,
dogecoind_addr: None,
})),
},
},
Expand Down
4 changes: 4 additions & 0 deletions crates/icp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,8 @@ impl MockProjectLoader {
ii: false,
nns: false,
subnets: None,
bitcoind_addr: None,
dogecoind_addr: None,
})),
},
},
Expand All @@ -384,6 +386,8 @@ impl MockProjectLoader {
ii: false,
nns: false,
subnets: None,
bitcoind_addr: None,
dogecoind_addr: None,
})),
},
},
Expand Down
66 changes: 65 additions & 1 deletion crates/icp/src/manifest/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ pub enum ManagedMode {
nns: Option<bool>,
/// Configure the list of subnets (one application subnet by default)
subnets: Option<Vec<SubnetKind>>,
/// Bitcoin P2P node addresses to connect to (e.g. "127.0.0.1:18444")
bitcoind_addr: Option<Vec<String>>,
/// Dogecoin P2P node addresses to connect to
dogecoind_addr: Option<Vec<String>>,
},
}

Expand All @@ -78,6 +82,8 @@ impl Default for ManagedMode {
ii: None,
nns: None,
subnets: None,
bitcoind_addr: None,
dogecoind_addr: None,
}
}
}
Expand Down Expand Up @@ -231,7 +237,9 @@ mod tests {
artificial_delay_ms: None,
ii: None,
nns: None,
subnets: None
subnets: None,
bitcoind_addr: None,
dogecoind_addr: None,
})
})
},
Expand Down Expand Up @@ -259,6 +267,8 @@ mod tests {
ii: None,
nns: None,
subnets: None,
bitcoind_addr: None,
dogecoind_addr: None,
})
})
},
Expand Down Expand Up @@ -287,6 +297,60 @@ mod tests {
ii: None,
nns: None,
subnets: None,
bitcoind_addr: None,
dogecoind_addr: None,
})
})
},
);
}

#[test]
fn managed_network_with_dogecoind_addr() {
assert_eq!(
validate_network_yaml(indoc! {r#"
name: my-network
mode: managed
dogecoind-addr:
- "127.0.0.1:22556"
"#}),
NetworkManifest {
name: "my-network".to_string(),
configuration: Mode::Managed(Managed {
mode: Box::new(ManagedMode::Launcher {
gateway: None,
artificial_delay_ms: None,
ii: None,
nns: None,
subnets: None,
bitcoind_addr: None,
dogecoind_addr: Some(vec!["127.0.0.1:22556".to_string()]),
})
})
},
);
}

#[test]
fn managed_network_with_bitcoind_addr() {
assert_eq!(
validate_network_yaml(indoc! {r#"
name: my-network
mode: managed
bitcoind-addr:
- "127.0.0.1:18444"
"#}),
NetworkManifest {
name: "my-network".to_string(),
configuration: Mode::Managed(Managed {
mode: Box::new(ManagedMode::Launcher {
gateway: None,
artificial_delay_ms: None,
ii: None,
nns: None,
subnets: None,
bitcoind_addr: Some(vec!["127.0.0.1:18444".to_string()]),
dogecoind_addr: None,
})
})
},
Expand Down
4 changes: 3 additions & 1 deletion crates/icp/src/manifest/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,9 @@ mod tests {
artificial_delay_ms: None,
ii: None,
nns: None,
subnets: None
subnets: None,
bitcoind_addr: None,
dogecoind_addr: None,
}),
}),
})],
Expand Down
55 changes: 55 additions & 0 deletions crates/icp/src/network/managed/docker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ pub struct ManagedImageOptions {
pub status_dir: String,
/// Parsed mounts (excluding the status directory mount, which is added at runtime).
pub mounts: Vec<Mount>,
/// Extra hosts entries for Docker networking (e.g. "host.docker.internal:host-gateway").
pub extra_hosts: Vec<String>,
}

impl ManagedImageOptions {
Expand Down Expand Up @@ -160,6 +162,7 @@ impl TryFrom<&ManagedImageConfig> for ManagedImageOptions {
shm_size: config.shm_size,
status_dir: config.status_dir.clone(),
mounts,
extra_hosts: vec![],
})
}
}
Expand Down Expand Up @@ -187,6 +190,52 @@ pub enum ManagedImageConversionError {
WslPathConvert { source: WslPathConversionError },
}

/// Translates a host:port address for use inside a Docker container.
/// Replaces `127.0.0.1`, `localhost`, and `::1` with `host.docker.internal`
/// so the container can reach services running on the host machine.
pub(super) fn translate_addr_for_docker(addr: &str) -> String {
if let Some((host, port)) = addr.rsplit_once(':') {
let translated = match host {
"127.0.0.1" | "localhost" | "::1" => "host.docker.internal",
_ => host,
};
format!("{translated}:{port}")
} else {
addr.to_string()
}
}

/// Returns extra_hosts entries needed for Docker to resolve `host.docker.internal`.
/// On Linux, Docker Engine does not provide `host.docker.internal` by default,
/// so we add `host.docker.internal:host-gateway` when any addresses reference localhost.
pub(super) fn docker_extra_hosts_for_addrs(addrs: &[String]) -> Vec<String> {
let needs_host_gateway = addrs.iter().any(|addr| {
addr.rsplit_once(':')
.map(|(host, _)| matches!(host, "127.0.0.1" | "localhost" | "::1"))
.unwrap_or(false)
});
if needs_host_gateway {
vec!["host.docker.internal:host-gateway".to_string()]
} else {
vec![]
}
}

/// Translates localhost addresses in `--bitcoind-addr=` and `--dogecoind-addr=` flags
/// for use inside a Docker container.
pub(super) fn translate_launcher_args_for_docker(args: Vec<String>) -> Vec<String> {
args.into_iter()
.map(|arg| {
for prefix in ["--bitcoind-addr=", "--dogecoind-addr="] {
if let Some(addr) = arg.strip_prefix(prefix) {
return format!("{prefix}{}", translate_addr_for_docker(addr));
}
}
arg
})
.collect()
}

pub async fn spawn_docker_launcher(
options: &ManagedImageOptions,
) -> Result<
Expand All @@ -211,6 +260,7 @@ pub async fn spawn_docker_launcher(
shm_size,
status_dir,
mounts,
extra_hosts,
} = options;

// Create status tmpdir and convert path for WSL2 if needed
Expand Down Expand Up @@ -320,6 +370,11 @@ pub async fn spawn_docker_launcher(
mounts: Some(all_mounts),
binds: Some(volumes.clone()),
shm_size: *shm_size,
extra_hosts: if extra_hosts.is_empty() {
None
} else {
Some(extra_hosts.clone())
},
..<_>::default()
}),
..<_>::default()
Expand Down
12 changes: 12 additions & 0 deletions crates/icp/src/network/managed/launcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ pub fn launcher_settings_flags(config: &ManagedLauncherConfig) -> Vec<String> {
ii,
nns,
subnets,
bitcoind_addr,
dogecoind_addr,
} = config;
let mut flags = vec![];
if *ii {
Expand All @@ -190,6 +192,16 @@ pub fn launcher_settings_flags(config: &ManagedLauncherConfig) -> Vec<String> {
flags.push(format!("--subnet={subnet}"));
}
}
if let Some(addrs) = &bitcoind_addr {
for addr in addrs {
flags.push(format!("--bitcoind-addr={addr}"));
}
}
if let Some(addrs) = &dogecoind_addr {
for addr in addrs {
flags.push(format!("--dogecoind-addr={addr}"));
}
}
flags
}

Expand Down
Loading
Loading