diff --git a/CLI.md b/CLI.md index 6925e5e77179..387aaeec54fa 100644 --- a/CLI.md +++ b/CLI.md @@ -40,6 +40,8 @@ This document contains the help content for the `linera` command-line program. * [`linera wallet show`↴](#linera-wallet-show) * [`linera wallet set-default`↴](#linera-wallet-set-default) * [`linera wallet init`↴](#linera-wallet-init) +* [`linera wallet request-chain`↴](#linera-wallet-request-chain) +* [`linera wallet follow-chain`↴](#linera-wallet-follow-chain) * [`linera wallet forget-keys`↴](#linera-wallet-forget-keys) * [`linera wallet forget-chain`↴](#linera-wallet-forget-chain) * [`linera project`↴](#linera-project) @@ -744,7 +746,9 @@ Show the contents of the wallet * `show` — Show the contents of the wallet * `set-default` — Change the wallet default chain * `init` — Initialize a wallet from the genesis configuration -* `forget-keys` — Forgets the specified chain's keys +* `request-chain` — Request a new chain from a faucet and add it to the wallet +* `follow-chain` — Add a new followed chain (i.e. a chain without keypair) to the wallet +* `forget-keys` — Forgets the specified chain's keys. The chain will still be followed by the wallet * `forget-chain` — Forgets the specified chain, including the associated key pair @@ -794,9 +798,34 @@ Initialize a wallet from the genesis configuration +## `linera wallet request-chain` + +Request a new chain from a faucet and add it to the wallet + +**Usage:** `linera wallet request-chain [OPTIONS] --faucet ` + +###### **Options:** + +* `--faucet ` — The address of a faucet +* `--set-default` — Whether this chain should become the default chain + + + +## `linera wallet follow-chain` + +Add a new followed chain (i.e. a chain without keypair) to the wallet + +**Usage:** `linera wallet follow-chain ` + +###### **Arguments:** + +* `` — The chain ID + + + ## `linera wallet forget-keys` -Forgets the specified chain's keys +Forgets the specified chain's keys. The chain will still be followed by the wallet **Usage:** `linera wallet forget-keys ` diff --git a/README.md b/README.md index b261818d0d82..ebf74288fdf2 100644 --- a/README.md +++ b/README.md @@ -67,32 +67,44 @@ microchains owned by a single wallet. ```bash # Make sure to compile the Linera binaries and add them in the $PATH. -# cargo build -p linera-storage-service -p linera-service --bins --features storage-service +# cargo build -p linera-storage-service -p linera-service --bins export PATH="$PWD/target/debug:$PATH" -# Import the optional helper function `linera_spawn_and_read_wallet_variables`. +# Import the optional helper function `linera_spawn`. source /dev/stdin <<<"$(linera net helper 2>/dev/null)" # Run a local test network with the default parameters and a number of microchains -# owned by the default wallet. (The helper function `linera_spawn_and_read_wallet_variables` -# is used to set the two environment variables LINERA_{WALLET,STORAGE}.) -linera_spawn_and_read_wallet_variables \ -linera net up +# owned by the default wallet. This also defines `LINERA_TMP_DIR`. +linera_spawn \ +linera net up --with-faucet --faucet-port 8080 -# Print the set of validators. -linera query-validators +# Remember the URL of the faucet. +FAUCET_URL=http://localhost:8080 + +# If you're using a testnet, start here and run this instead: +# LINERA_TMP_DIR=$(mktemp -d) +# FAUCET_URL=https://faucet.testnet-XXX.linera.net # for some value XXX + +# Set the path of the future wallet. +export LINERA_WALLET="$LINERA_TMP_DIR/wallet.json" +export LINERA_STORAGE="rocksdb:$LINERA_TMP_DIR/client.db" + +# Initialize a new user wallet. +linera wallet init --faucet $FAUCET_URL + +# Request chains. +CHAIN1=$(linera wallet request-chain --faucet $FAUCET_URL | head -n 1) +CHAIN2=$(linera wallet request-chain --faucet $FAUCET_URL | head -n 1) # Query the chain balance of some of the chains. -CHAIN1="aee928d4bf3880353b4a3cd9b6f88e6cc6e5ed050860abae439e7782e9b2dfe8" -CHAIN2="a3edc33d8e951a1139333be8a4b56646b5598a8f51216e86592d881808972b07" linera query-balance "$CHAIN1" linera query-balance "$CHAIN2" -# Transfer 10 units then 5 back +# Transfer 10 units then 5 back. linera transfer 10 --from "$CHAIN1" --to "$CHAIN2" linera transfer 5 --from "$CHAIN2" --to "$CHAIN1" -# Query balances again +# Query balances again. linera query-balance "$CHAIN1" linera query-balance "$CHAIN2" ``` diff --git a/linera-client/src/client_options.rs b/linera-client/src/client_options.rs index 7bf5f65c8c7b..1e6a8987572d 100644 --- a/linera-client/src/client_options.rs +++ b/linera-client/src/client_options.rs @@ -1199,7 +1199,25 @@ pub enum WalletCommand { testing_prng_seed: Option, }, - /// Forgets the specified chain's keys. + /// Request a new chain from a faucet and add it to the wallet. + RequestChain { + /// The address of a faucet. + #[arg(long)] + faucet: String, + + /// Whether this chain should become the default chain. + #[arg(long)] + set_default: bool, + }, + + /// Add a new followed chain (i.e. a chain without keypair) to the wallet. + FollowChain { + /// The chain ID. + chain_id: ChainId, + }, + + /// Forgets the specified chain's keys. The chain will still be followed by the + /// wallet. ForgetKeys { chain_id: ChainId }, /// Forgets the specified chain, including the associated key pair. diff --git a/linera-service/src/cli_wrappers/wallet.rs b/linera-service/src/cli_wrappers/wallet.rs index d309661c4a39..dff4462653e3 100644 --- a/linera-service/src/cli_wrappers/wallet.rs +++ b/linera-service/src/cli_wrappers/wallet.rs @@ -356,6 +356,28 @@ impl ClientWrapper { } } + /// Runs `linera wallet request-chain`. + pub async fn request_chain(&self, faucet: &Faucet, set_default: bool) -> Result { + let mut command = self.command().await?; + command.args(["wallet", "request-chain", "--faucet", faucet.url()]); + if set_default { + command.arg("--set-default"); + } + let stdout = command.spawn_and_wait_for_stdout().await?; + let mut lines = stdout.split_whitespace(); + let chain_id_str = lines.next().context("missing chain ID")?; + let message_id_str = lines.next().context("missing message ID")?; + let certificate_hash_str = lines.next().context("missing certificate hash")?; + let outcome = ClaimOutcome { + chain_id: chain_id_str.parse().context("invalid chain ID")?, + message_id: message_id_str.parse().context("invalid message ID")?, + certificate_hash: certificate_hash_str + .parse() + .context("invalid certificate hash")?, + }; + Ok(outcome) + } + /// Runs `linera wallet publish-and-create`. pub async fn publish_and_create< A: ContractAbi, @@ -801,6 +823,16 @@ impl ClientWrapper { Ok(()) } + /// Runs `linera wallet follow-chain CHAIN_ID`. + pub async fn follow_chain(&self, chain_id: ChainId) -> Result<()> { + let mut command = self.command().await?; + command + .args(["wallet", "follow-chain"]) + .arg(chain_id.to_string()); + command.spawn_and_wait_for_stdout().await?; + Ok(()) + } + /// Runs `linera wallet forget-chain CHAIN_ID`. pub async fn forget_chain(&self, chain_id: ChainId) -> Result<()> { let mut command = self.command().await?; diff --git a/linera-service/src/linera/main.rs b/linera-service/src/linera/main.rs index 1184d04f6500..baf6f96b2e6d 100644 --- a/linera-service/src/linera/main.rs +++ b/linera-service/src/linera/main.rs @@ -1196,6 +1196,47 @@ impl Runnable for Job { ); } + Wallet(WalletCommand::RequestChain { + faucet: faucet_url, + set_default, + }) => { + let start_time = Instant::now(); + let key_pair = context.wallet.generate_key_pair(); + let owner = key_pair.public().into(); + info!( + "Requesting a new chain for owner {owner} using the faucet at address \ + {faucet_url}", + ); + context + .wallet_mut() + .mutate(|w| w.add_unassigned_key_pair(key_pair)) + .await?; + let faucet = cli_wrappers::Faucet::new(faucet_url); + let outcome = faucet.claim(&owner).await?; + let validators = faucet.current_validators().await?; + println!("{}", outcome.chain_id); + println!("{}", outcome.message_id); + println!("{}", outcome.certificate_hash); + Self::assign_new_chain_to_key( + outcome.chain_id, + outcome.message_id, + owner, + Some(validators), + &mut context, + ) + .await?; + if set_default { + context + .wallet_mut() + .mutate(|w| w.set_default_chain(outcome.chain_id)) + .await??; + } + info!( + "New chain requested and added in {} ms", + start_time.elapsed().as_millis() + ); + } + CreateGenesisConfig { .. } | Keygen | Net(_) @@ -1829,6 +1870,22 @@ async fn run(options: &ClientOptions) -> Result { Ok(0) } + WalletCommand::FollowChain { chain_id } => { + let start_time = Instant::now(); + options + .wallet() + .await? + .mutate(|wallet| { + wallet.extend([UserChain::make_other(*chain_id, Timestamp::now())]) + }) + .await?; + info!( + "Chain followed and added in {} ms", + start_time.elapsed().as_millis() + ); + Ok(0) + } + WalletCommand::ForgetChain { chain_id } => { let start_time = Instant::now(); options @@ -1840,6 +1897,11 @@ async fn run(options: &ClientOptions) -> Result { Ok(0) } + WalletCommand::RequestChain { .. } => { + options.run_with_storage(Job(options.clone())).await??; + Ok(0) + } + WalletCommand::Init { genesis_config_path, faucet, diff --git a/linera-service/tests/linera_net_tests.rs b/linera-service/tests/linera_net_tests.rs index d7288e6beb7d..3b9b653ad63d 100644 --- a/linera-service/tests/linera_net_tests.rs +++ b/linera-service/tests/linera_net_tests.rs @@ -2814,11 +2814,24 @@ async fn test_end_to_end_faucet(config: impl LineraNetConfig) -> Result<()> { // Use the faucet directly to initialize client 3. let client3 = net.make_client().await; - let outcome = client3 - .wallet_init(&[], FaucetOption::NewChain(&faucet)) - .await?; - let chain3 = outcome.unwrap().chain_id; - assert_eq!(chain3, client3.load_wallet()?.default_chain().unwrap()); + client3.wallet_init(&[], FaucetOption::None).await?; + let outcome = client3.request_chain(&faucet, false).await?; + assert_eq!( + outcome.chain_id, + client3.load_wallet()?.default_chain().unwrap() + ); + + let outcome = client3.request_chain(&faucet, false).await?; + assert!(outcome.chain_id != client3.load_wallet()?.default_chain().unwrap()); + client3.forget_chain(outcome.chain_id).await?; + client3.follow_chain(outcome.chain_id).await?; + + let outcome = client3.request_chain(&faucet, true).await?; + assert_eq!( + outcome.chain_id, + client3.load_wallet()?.default_chain().unwrap() + ); + let chain3 = outcome.chain_id; faucet_service.ensure_is_running()?; faucet_service.terminate().await?; @@ -2826,8 +2839,8 @@ async fn test_end_to_end_faucet(config: impl LineraNetConfig) -> Result<()> { // Chain 1 should have transferred four tokens, two to each child. client1.sync(chain1).await?; let faucet_balance = client1.query_balance(Account::chain(chain1)).await?; - assert!(faucet_balance <= balance1 - Amount::from_tokens(4)); - assert!(faucet_balance > balance1 - Amount::from_tokens(5)); + assert!(faucet_balance <= balance1 - Amount::from_tokens(8)); + assert!(faucet_balance > balance1 - Amount::from_tokens(9)); // Assign chain2 to client2_key. assert_eq!(chain2, client2.assign(owner2, message_id).await?); diff --git a/scripts/linera_net_helper.sh b/scripts/linera_net_helper.sh index f8408c9bcec1..b55a50a7ca28 100644 --- a/scripts/linera_net_helper.sh +++ b/scripts/linera_net_helper.sh @@ -1,4 +1,5 @@ # Runs a command such as `linera net up` in the background. +# - Export a temporary directory `$LINERA_TMP_DIR`. # - Records stdout # - Waits for the background process to print READY! on stderr # - Then executes the bash command recorded from stdout @@ -29,3 +30,25 @@ function linera_spawn_and_read_wallet_variables() { # Continue to output the command's stderr onto stdout. cat "$LINERA_TMP_ERR" & } + +# Runs a command such as `linera net up` in the background. +# - Export a temporary directory `$LINERA_TMP_DIR`. +# - Waits for the background process to print READY! on stderr +# - Returns without killing the process +function linera_spawn() { + LINERA_TMP_DIR=$(mktemp -d) || exit 1 + + trap 'jobs -p | xargs -r kill; rm -rf "$LINERA_TMP_DIR"' EXIT + + LINERA_TMP_OUT="$LINERA_TMP_DIR/out" + LINERA_TMP_ERR="$LINERA_TMP_DIR/err" + mkfifo "$LINERA_TMP_ERR" || exit 1 + + "$@" 2>"$LINERA_TMP_ERR" & + + # Read from LINERA_TMP_ERR until the string "READY!" is found. + sed '/^READY!/q' <"$LINERA_TMP_ERR" || exit 1 + + # Continue to output the command's stderr onto stdout. + cat "$LINERA_TMP_ERR" & +}