Skip to content

Commit

Permalink
Create command request-chain to allow using testnets in bash (#3385)
Browse files Browse the repository at this point in the history
## Motivation

* Allow README files to run on the testnet
* Do not hardcode chain IDs

## Proposal

* Create a command `request-chain`
* Use it in the main README file
* Also create a comment `follow-chain` while we're at it

## Test Plan

Tested new CLI commands manually (together with `forget-keys` and
`forget-chain`) + CI
  • Loading branch information
ma2bd authored Feb 22, 2025
1 parent cfefa52 commit 8f74e54
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 22 deletions.
33 changes: 31 additions & 2 deletions CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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 <FAUCET>`

###### **Options:**

* `--faucet <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 <CHAIN_ID>`

###### **Arguments:**

* `<CHAIN_ID>` — 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 <CHAIN_ID>`

Expand Down
36 changes: 24 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
```
Expand Down
20 changes: 19 additions & 1 deletion linera-client/src/client_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1199,7 +1199,25 @@ pub enum WalletCommand {
testing_prng_seed: Option<u64>,
},

/// 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.
Expand Down
32 changes: 32 additions & 0 deletions linera-service/src/cli_wrappers/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,28 @@ impl ClientWrapper {
}
}

/// Runs `linera wallet request-chain`.
pub async fn request_chain(&self, faucet: &Faucet, set_default: bool) -> Result<ClaimOutcome> {
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,
Expand Down Expand Up @@ -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?;
Expand Down
62 changes: 62 additions & 0 deletions linera-service/src/linera/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1141,6 +1141,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(_)
Expand Down Expand Up @@ -1774,6 +1815,22 @@ async fn run(options: &ClientOptions) -> Result<i32, anyhow::Error> {
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
Expand All @@ -1785,6 +1842,11 @@ async fn run(options: &ClientOptions) -> Result<i32, anyhow::Error> {
Ok(0)
}

WalletCommand::RequestChain { .. } => {
options.run_with_storage(Job(options.clone())).await??;
Ok(0)
}

WalletCommand::Init {
genesis_config_path,
faucet,
Expand Down
27 changes: 20 additions & 7 deletions linera-service/tests/linera_net_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2814,20 +2814,33 @@ 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?;

// 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?);
Expand Down
23 changes: 23 additions & 0 deletions scripts/linera_net_helper.sh
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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" &
}

0 comments on commit 8f74e54

Please sign in to comment.