|
1 | 1 | //! Publish command for publishing subgraphs to The Graph's decentralized network. |
2 | 2 | //! |
3 | | -//! This command publishes a subgraph to The Graph's decentralized network, |
4 | | -//! which requires GRT tokens for indexer rewards and query fees. |
| 3 | +//! This command builds a subgraph, uploads it to IPFS, and opens a browser |
| 4 | +//! to complete the publishing process on The Graph Network. |
5 | 5 |
|
6 | 6 | use std::path::PathBuf; |
7 | 7 |
|
8 | 8 | use anyhow::{anyhow, Result}; |
9 | 9 | use clap::Parser; |
| 10 | +use inquire::Confirm; |
10 | 11 |
|
| 12 | +use crate::commands::build::{run_build, BuildOpt}; |
11 | 13 | use crate::output::{step, Step}; |
12 | 14 |
|
| 15 | +/// Default IPFS URL for The Graph's hosted IPFS node. |
| 16 | +const DEFAULT_IPFS_URL: &str = "https://api.thegraph.com/ipfs/api/v0"; |
| 17 | + |
| 18 | +/// Default webapp URL for publishing. |
| 19 | +const DEFAULT_WEBAPP_URL: &str = "https://cli.thegraph.com/publish"; |
| 20 | + |
13 | 21 | #[derive(Clone, Debug, Parser)] |
14 | 22 | #[clap(about = "Publish a subgraph to The Graph's decentralized network")] |
15 | 23 | pub struct PublishOpt { |
16 | | - /// Subgraph deployment ID (IPFS hash) |
17 | | - #[clap()] |
18 | | - pub deployment_id: Option<String>, |
19 | | - |
20 | 24 | /// Path to the subgraph manifest |
21 | | - #[clap(short = 'm', long, default_value = "subgraph.yaml")] |
| 25 | + #[clap(default_value = "subgraph.yaml")] |
22 | 26 | pub manifest: PathBuf, |
23 | 27 |
|
24 | | - /// IPFS node URL |
25 | | - #[clap(short = 'i', long)] |
26 | | - pub ipfs: Option<String>, |
| 28 | + /// IPFS node URL to upload build results to |
| 29 | + #[clap(short = 'i', long, default_value = DEFAULT_IPFS_URL)] |
| 30 | + pub ipfs: String, |
27 | 31 |
|
28 | | - /// Subgraph Studio deploy key |
| 32 | + /// IPFS hash of the subgraph manifest to deploy (skips build) |
29 | 33 | #[clap(long)] |
30 | | - pub deploy_key: Option<String>, |
| 34 | + pub ipfs_hash: Option<String>, |
31 | 35 |
|
32 | | - /// Network to publish to (mainnet, arbitrum-one, etc.) |
| 36 | + /// Subgraph ID to publish to (for updating existing subgraphs) |
33 | 37 | #[clap(long)] |
34 | | - pub network: Option<String>, |
| 38 | + pub subgraph_id: Option<String>, |
| 39 | + |
| 40 | + /// The network to use for the subgraph deployment |
| 41 | + #[clap(long, default_value = "arbitrum-one")] |
| 42 | + pub protocol_network: String, |
35 | 43 |
|
36 | | - /// Version label for the deployment |
37 | | - #[clap(short = 'l', long)] |
38 | | - pub version_label: Option<String>, |
| 44 | + /// The API key for Subgraph queries (required when updating existing subgraph) |
| 45 | + #[clap(long)] |
| 46 | + pub api_key: Option<String>, |
| 47 | + |
| 48 | + /// URL of the web UI to use for publishing |
| 49 | + #[clap(long, default_value = DEFAULT_WEBAPP_URL)] |
| 50 | + pub webapp_url: String, |
| 51 | + |
| 52 | + /// Output directory for build results |
| 53 | + #[clap(short = 'o', long, default_value = "build/")] |
| 54 | + pub output_dir: PathBuf, |
| 55 | + |
| 56 | + /// Skip subgraph migrations |
| 57 | + #[clap(long)] |
| 58 | + pub skip_migrations: bool, |
39 | 59 |
|
40 | | - /// Skip confirmation prompt |
| 60 | + /// Network configuration to use from the networks config file |
41 | 61 | #[clap(long)] |
42 | | - pub skip_confirmation: bool, |
| 62 | + pub network: Option<String>, |
| 63 | + |
| 64 | + /// Networks config file path |
| 65 | + #[clap(long, default_value = "networks.json")] |
| 66 | + pub network_file: PathBuf, |
43 | 67 | } |
44 | 68 |
|
45 | 69 | /// Run the publish command. |
46 | | -pub fn run_publish(opt: PublishOpt) -> Result<()> { |
47 | | - step( |
48 | | - Step::Generate, |
49 | | - "Publishing to The Graph's decentralized network", |
50 | | - ); |
| 70 | +pub async fn run_publish(opt: PublishOpt) -> Result<()> { |
| 71 | + // Validate: if subgraph_id is provided, api_key is required |
| 72 | + if opt.subgraph_id.is_some() && opt.api_key.is_none() { |
| 73 | + return Err(anyhow!( |
| 74 | + "API key is required to publish to an existing subgraph (--api-key).\n\ |
| 75 | + See https://thegraph.com/docs/en/deploying/subgraph-studio-faqs/#2-how-do-i-create-an-api-key" |
| 76 | + )); |
| 77 | + } |
51 | 78 |
|
52 | | - // Validate we have a deployment ID or manifest |
53 | | - if opt.deployment_id.is_none() && !opt.manifest.exists() { |
| 79 | + // Validate protocol network |
| 80 | + if opt.protocol_network != "arbitrum-one" && opt.protocol_network != "arbitrum-sepolia" { |
54 | 81 | return Err(anyhow!( |
55 | | - "No deployment ID provided and manifest '{}' not found.\n\ |
56 | | - Please provide either:\n\ |
57 | | - - A deployment ID (IPFS hash): gnd publish <deployment-id>\n\ |
58 | | - - A valid manifest path: gnd publish -m <path>", |
59 | | - opt.manifest.display() |
| 82 | + "Invalid protocol network '{}'. Must be 'arbitrum-one' or 'arbitrum-sepolia'", |
| 83 | + opt.protocol_network |
60 | 84 | )); |
61 | 85 | } |
62 | 86 |
|
63 | | - Err(anyhow!( |
64 | | - "Publish command is not yet implemented.\n\ |
65 | | - This feature requires:\n\ |
66 | | - - Integration with The Graph Network subgraph\n\ |
67 | | - - Wallet connection for signing transactions\n\ |
68 | | - - GRT token handling for indexer rewards\n\n\ |
69 | | - To publish your subgraph:\n\ |
70 | | - 1. Visit https://thegraph.com/studio\n\ |
71 | | - 2. Create your subgraph in the Studio\n\ |
72 | | - 3. Deploy using: gnd deploy --studio <subgraph-name>\n\ |
73 | | - 4. Use the Studio UI to publish to the network\n\n\ |
74 | | - Or use the TypeScript graph-cli:\n\ |
75 | | - graph publish" |
76 | | - )) |
| 87 | + // Extract URL-related fields before potentially moving opt fields into BuildOpt |
| 88 | + let webapp_url = opt.webapp_url.clone(); |
| 89 | + let subgraph_id = opt.subgraph_id.clone(); |
| 90 | + let protocol_network = opt.protocol_network.clone(); |
| 91 | + let api_key = opt.api_key.clone(); |
| 92 | + |
| 93 | + // Get the IPFS hash - either from flag or by building |
| 94 | + let ipfs_hash = if let Some(hash) = opt.ipfs_hash { |
| 95 | + step(Step::Skip, "Using provided IPFS hash"); |
| 96 | + hash |
| 97 | + } else { |
| 98 | + // Build the subgraph and upload to IPFS |
| 99 | + step(Step::Load, "Building subgraph for publishing"); |
| 100 | + |
| 101 | + // Validate manifest exists |
| 102 | + if !opt.manifest.exists() { |
| 103 | + return Err(anyhow!( |
| 104 | + "Manifest '{}' not found.\n\ |
| 105 | + Please provide either:\n\ |
| 106 | + - A valid manifest path: gnd publish <manifest>\n\ |
| 107 | + - An IPFS hash: gnd publish --ipfs-hash <hash>", |
| 108 | + opt.manifest.display() |
| 109 | + )); |
| 110 | + } |
| 111 | + |
| 112 | + let build_opt = BuildOpt { |
| 113 | + manifest: opt.manifest, |
| 114 | + output_dir: opt.output_dir, |
| 115 | + output_format: "wasm".to_string(), |
| 116 | + skip_migrations: opt.skip_migrations, |
| 117 | + watch: false, |
| 118 | + ipfs: Some(opt.ipfs), |
| 119 | + network: opt.network, |
| 120 | + network_file: opt.network_file, |
| 121 | + }; |
| 122 | + |
| 123 | + match run_build(build_opt).await? { |
| 124 | + Some(hash) => hash, |
| 125 | + None => { |
| 126 | + return Err(anyhow!( |
| 127 | + "Build completed but IPFS upload failed. Cannot proceed with publish." |
| 128 | + )); |
| 129 | + } |
| 130 | + } |
| 131 | + }; |
| 132 | + |
| 133 | + // Prompt user to open browser |
| 134 | + step(Step::Generate, "Ready to publish to The Graph Network"); |
| 135 | + |
| 136 | + let open_browser = Confirm::new("Open browser to continue publishing?") |
| 137 | + .with_default(true) |
| 138 | + .prompt()?; |
| 139 | + |
| 140 | + if !open_browser { |
| 141 | + println!("\nTo publish manually, visit:"); |
| 142 | + println!( |
| 143 | + " {}", |
| 144 | + build_publish_url( |
| 145 | + &webapp_url, |
| 146 | + &ipfs_hash, |
| 147 | + &subgraph_id, |
| 148 | + &protocol_network, |
| 149 | + &api_key |
| 150 | + ) |
| 151 | + ); |
| 152 | + return Ok(()); |
| 153 | + } |
| 154 | + |
| 155 | + // Build the URL with query parameters |
| 156 | + let url = build_publish_url( |
| 157 | + &webapp_url, |
| 158 | + &ipfs_hash, |
| 159 | + &subgraph_id, |
| 160 | + &protocol_network, |
| 161 | + &api_key, |
| 162 | + ); |
| 163 | + |
| 164 | + step(Step::Done, "Opening browser to complete publishing"); |
| 165 | + println!("\n {}\n", url); |
| 166 | + |
| 167 | + // Open the browser |
| 168 | + open::that(&url).map_err(|e| anyhow!("Failed to open browser: {}", e))?; |
| 169 | + |
| 170 | + Ok(()) |
| 171 | +} |
| 172 | + |
| 173 | +/// Build the publish URL with query parameters. |
| 174 | +fn build_publish_url( |
| 175 | + base_url: &str, |
| 176 | + ipfs_hash: &str, |
| 177 | + subgraph_id: &Option<String>, |
| 178 | + protocol_network: &str, |
| 179 | + api_key: &Option<String>, |
| 180 | +) -> String { |
| 181 | + let mut params = vec![format!("id={}", ipfs_hash)]; |
| 182 | + |
| 183 | + if let Some(id) = subgraph_id { |
| 184 | + params.push(format!("subgraphId={}", id)); |
| 185 | + } |
| 186 | + |
| 187 | + params.push(format!("network={}", protocol_network)); |
| 188 | + |
| 189 | + if let Some(key) = api_key { |
| 190 | + params.push(format!("apiKey={}", key)); |
| 191 | + } |
| 192 | + |
| 193 | + format!("{}?{}", base_url, params.join("&")) |
77 | 194 | } |
78 | 195 |
|
79 | 196 | #[cfg(test)] |
80 | 197 | mod tests { |
81 | 198 | use super::*; |
82 | 199 |
|
83 | 200 | #[test] |
84 | | - fn test_publish_not_implemented() { |
85 | | - let opt = PublishOpt { |
86 | | - deployment_id: Some("QmTest".to_string()), |
87 | | - manifest: PathBuf::from("subgraph.yaml"), |
88 | | - ipfs: None, |
89 | | - deploy_key: None, |
90 | | - network: None, |
91 | | - version_label: None, |
92 | | - skip_confirmation: false, |
93 | | - }; |
| 201 | + fn test_build_publish_url_basic() { |
| 202 | + let url = build_publish_url( |
| 203 | + DEFAULT_WEBAPP_URL, |
| 204 | + "QmTest123", |
| 205 | + &None, |
| 206 | + "arbitrum-one", |
| 207 | + &None, |
| 208 | + ); |
| 209 | + assert_eq!( |
| 210 | + url, |
| 211 | + "https://cli.thegraph.com/publish?id=QmTest123&network=arbitrum-one" |
| 212 | + ); |
| 213 | + } |
94 | 214 |
|
95 | | - let result = run_publish(opt); |
96 | | - assert!(result.is_err()); |
97 | | - assert!(result |
98 | | - .unwrap_err() |
99 | | - .to_string() |
100 | | - .contains("not yet implemented")); |
| 215 | + #[test] |
| 216 | + fn test_build_publish_url_with_subgraph_id() { |
| 217 | + let url = build_publish_url( |
| 218 | + DEFAULT_WEBAPP_URL, |
| 219 | + "QmTest456", |
| 220 | + &Some("my-subgraph".to_string()), |
| 221 | + "arbitrum-sepolia", |
| 222 | + &Some("abc123".to_string()), |
| 223 | + ); |
| 224 | + assert_eq!( |
| 225 | + url, |
| 226 | + "https://cli.thegraph.com/publish?id=QmTest456&subgraphId=my-subgraph&network=arbitrum-sepolia&apiKey=abc123" |
| 227 | + ); |
| 228 | + } |
| 229 | + |
| 230 | + #[test] |
| 231 | + fn test_build_publish_url_custom_webapp() { |
| 232 | + let url = build_publish_url( |
| 233 | + "https://custom.example.com/publish", |
| 234 | + "QmCustom", |
| 235 | + &None, |
| 236 | + "arbitrum-one", |
| 237 | + &None, |
| 238 | + ); |
| 239 | + assert_eq!( |
| 240 | + url, |
| 241 | + "https://custom.example.com/publish?id=QmCustom&network=arbitrum-one" |
| 242 | + ); |
101 | 243 | } |
102 | 244 | } |
0 commit comments