From a51d6e8d65a9f28de5280814bb1ca35a04fff98d Mon Sep 17 00:00:00 2001 From: Armani Ferrante Date: Sun, 27 Dec 2020 12:29:03 -0800 Subject: [PATCH] Add account creation to node setup tutorial (#78) --- Cargo.lock | 1 + context/src/lib.rs | 23 +++++ dex/crank/src/lib.rs | 6 +- docs/node-setup.md | 144 +++++++++++++++++++---------- registry/cli/src/lib.rs | 52 ++++++----- registry/rewards/cli/Cargo.toml | 3 +- registry/rewards/cli/src/lib.rs | 5 +- registry/src/accounts/registrar.rs | 1 - scripts/deploy-staking.sh | 62 +++++++++---- 9 files changed, 202 insertions(+), 95 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bdf485c..8d7c62b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3123,6 +3123,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap 3.0.0-beta.2", + "serde_json", "serum-context", "serum-registry-rewards-client", "solana-client-gen", diff --git a/context/src/lib.rs b/context/src/lib.rs index 61989dd..47aa6af 100644 --- a/context/src/lib.rs +++ b/context/src/lib.rs @@ -27,6 +27,9 @@ pub struct Context { pub lockup_pid: Pubkey, pub dex_pid: Pubkey, pub faucet_pid: Option, + pub registrar: Pubkey, + pub safe: Pubkey, + pub rewards_instance: Pubkey, } impl Context { @@ -66,6 +69,7 @@ struct Config { pub network: Network, pub mints: Mints, pub programs: Programs, + pub accounts: Option, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -89,6 +93,13 @@ struct Programs { pub faucet_pid: Option, } +#[derive(Clone, Debug, Serialize, Deserialize)] +struct Accounts { + pub registrar: String, + pub safe: String, + pub rewards_instance: String, +} + impl Config { fn from(path: &str) -> Result { let rdr = fs::File::open(path)?; @@ -126,6 +137,18 @@ impl TryFrom for Context { meta_entity_pid: cfg.programs.meta_entity_pid.parse()?, dex_pid: cfg.programs.dex_pid.parse()?, faucet_pid, + registrar: cfg + .accounts + .as_ref() + .map_or(Ok(Default::default()), |a| a.registrar.parse::())?, + safe: cfg + .accounts + .as_ref() + .map_or(Ok(Default::default()), |a| a.safe.parse())?, + rewards_instance: cfg + .accounts + .as_ref() + .map_or(Ok(Default::default()), |a| a.rewards_instance.parse())?, }) } } diff --git a/dex/crank/src/lib.rs b/dex/crank/src/lib.rs index 4e0aa20..defee5a 100644 --- a/dex/crank/src/lib.rs +++ b/dex/crank/src/lib.rs @@ -210,9 +210,6 @@ pub enum Command { #[derive(Debug, Clap, Clone)] pub struct RewardsContext { - /// Account controlling the rewards vault for crankers. - #[clap(long = "rewards.instance")] - pub instance: Pubkey, /// Token account rewards are sent to when cranking. #[clap(long = "rewards.receiver")] pub receiver: Pubkey, @@ -220,6 +217,8 @@ pub struct RewardsContext { #[clap(long = "rewards.registry-entity")] pub registry_entity: Pubkey, #[clap(skip)] + pub instance: Pubkey, + #[clap(skip)] pub url: String, #[clap(skip)] pub program_id: Pubkey, @@ -337,6 +336,7 @@ pub fn start(ctx: Option, opts: Opts) -> Result<()> { let ctx = ctx.expect("context must exist when consuming rewards"); r_ctx.url = opts.cluster.url().to_string(); r_ctx.program_id = ctx.rewards_pid; + r_ctx.instance = ctx.rewards_instance; init_logger(log_directory); // Unused by the DEX. Nevertheless required to be passed in. diff --git a/docs/node-setup.md b/docs/node-setup.md index bfe884a..d508086 100644 --- a/docs/node-setup.md +++ b/docs/node-setup.md @@ -9,17 +9,13 @@ when their node has at least 1 MSRM staked. These "cranking rewards" are effectively transaction fees earned for operating the DEX. For an introduction to the DEX and the idea of cranking, see -[A technical introduction to the Serum DEX](https://docs.google.com/document/d/1isGJES4jzQutI0GtQGuqtrBUqeHxl_xJNXdtOv4SdII/edit). +[A technical introduction to the Serum DEX](https://docs.google.com/document/d/1isGJES4jzQutI0GtQGuqtrBUqeHxl_xJNXdtOv4SdII/edit). For an introduction to staking, see [Serum Staking](./staking.md). The way cranking rewards work is simple, instead of sending transactions directly to the DEX, a cranker sends transactions to a cranking rewards vendor, which is an on-chain Solana program that proxies all requests to the DEX, recording the amount of events cranked, and then sends a reward to the cranker's wallet as a function of the number -of events processed and the reward vendor's fee rate. - -(Note that, although similar in spirit, the cranking rewards vendor is an entirely different -program and account from the **Registry**'s reward vendors. Only node leaders are eligible -to crank.) +of events processed and the reward vendor's fee rate. This proxy program can be found [here](../registry/rewards/program). If the rewards vendor's vault becomes empty or if the node leader's Entity stake balance ever transitions to **inactive**, then the vendor will refuse to pay @@ -39,7 +35,6 @@ sudo apt-get install -y pkg-config build-essential python3-pip jq ## Install the CLI -The CLI is a work in progress, so there's not yet a proper installer. For now, we can use Cargo. ```bash @@ -50,30 +45,7 @@ To verify the installation worked, run `serum -h`. ## Setup your CLI Config -Add your YAML config for Mainnet Beta at `~/.config/serum/cli/config.yaml`. - -For Mainnet Beta - -```yaml ---- -network: - cluster: mainnet - -mints: - srm: SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt - msrm: MSRMcoVyrFxnSgo5uXwone5SKcGhT1KEJMFEkMEWf9L - -programs: - rewards_pid: 8xYo1X6uw7SBngXgPzib8jghWb8BhiiVxv5yV799Tw3G - registry_pid: Gw1XNGbSnx7PJcHTTuxxhWfkjjPmq29Qkv1hWbVFnrDp - meta_entity_pid: 9etE5ZjHZTrZ2wQfyfTSp5WBxjpvaakNJa5fSVToZn17 - lockup_pid: 6GSn1woRF541HaiEWqNofYn8quzJuRBPi1nwoho8zNnh - dex_pid: EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o -``` - -When operating over multiple networks, you can specify your config file with the -`serum --config ` option. For example, one might want to test -against Devnet with the following config +Add your YAML config for Devnet at `~/.config/serum/cli/config.yaml`. ```yaml --- @@ -81,35 +53,83 @@ network: cluster: devnet mints: - srm: 5ya5rnzm5MkvCXhLDCJqAUzT16A37ks2DekkfoNnwNMn - msrm: 3fHm9sEBS3CukUX366mwzn3YwEc5zWNzR4JcGK6EVQad + srm: Bap9SwT53SjGPeKq4LAC6i86fCzEzUGGHsYfRiCFSGyF + msrm: CmtL8e86367ZLiAuJELx4WqmDz7dRnD1oyaiq4TQDdEU programs: - rewards_pid: EXzpf5GBfUQkwLeLEJXLmVKxGpxyMQWxpudYxogW4ad8 - registry_pid: 3ofaHrxu7RdqH8m1wXfVrsTqgwctmx2NsHPv6m7oh1dy - meta_entity_pid: 8v8hwdeyBhmV4y235F9XQ7g5Vz2EYvJTkGqTfrh3Hz5f - lockup_pid: Az4dD6YeA4akzz4Qx3RuQqaCtLEaDiBT8u7mDL24sbAu - dex_pid: DiDDva9iDSXTtJ4CeWXbKdDvQ3M6g5G87PZGUGvxi3eV + rewards_pid: 7sXyzeu6GJqkXZz8VhjdsXvDg1xR1PEkXbbDaxMc186C + registry_pid: CKKz2WYvneiLb2mzouWc4iPpKisuXs5XKYn7ZUrRjkeK + meta_entity_pid: BsVgsh8mqi3qn8eKRiye1a4eF8Qwqot8p2n3ZMNdL2UY + lockup_pid: 8wreDpv5nuY1gee1X4wkqtRkzoGypVYzWBrMmzipAJKN + dex_pid: 9MVDeYQnJmN2Dt7H44Z8cob4bET2ysdNu2uFJcatDJno + +accounts: + registrar: 7Tzf4D4BU1tzwitXbHeUf7bwMNSSVQXfzPsgnbM5RY7d + safe: CxiGCt8kVm5BuzmWycJdZwXsYt52iLB8Huty5r1xRvRZ + rewards_instance: 32qFc9QWX4wBU6EFZ9FzyxGthSHNwCUKaN7APn3GAH2X ``` -## Cranking a market +If not specified, the `wallet` key will be searched for in the standard location: +`~/.config/solana/id.json` and used as the **payer** for all transactions initiated +by the CLI. + +## Create an Entity + +An **Entity** is the on-chain Solana account representing a node and +it's collective **Member** accounts. + +To create an **Entity** with the **Registrar**, run + +```bash +serum registry create-entity --name --about +``` + +Entering your node's `name` and `about` info, which can be displayed in UIs. Note that, by default, +the wallet creating the entity will be tagged as the node leader, which is the address eligible for +earning crank rewards. + +## Create a Member + +After creating a node entity, use your new **Entity** address to create a member account, which will +allow you to stake. + +```bash +serum registry create-member --entity
+``` + +## Activate your Node + +Once created, one must "activate" a node by staking MSRM before being able to earn rewards. Any **Member** +associated with the **Entity** can stake this MSRM. + +For now, it's recommended to do this through the UI at https://project-serum.github.io/serum-ts/stake-ui, +where you can + +* Select a network. +* Connect your wallet. +* Deposit 1 MSRM into your **Member** account via the **Deposit** button. If you're on Devnet, + [airdrop](https://www.spl-token-ui.com/#/token-faucets) yourself tokens. The faucet address can be found in the **Environment** tab of the UI. +* Stake the newly deposited 1 MSRM via the **Stake** tab into the **Mega Stake Pool**. + +You should see that your Entity is now in the `Active` state making it eligible for rewards. + +## Cranking a Market Finally you can run your crank. Pick a market and run ```bash - serum crank consume-event-rewards \ - --market
\ - --log-directory \ - --rewards.receiver
\ - --rewards.registry-entity
\ - --rewards.instance
+serum crank consume-event-rewards \ + --market
\ + --log-directory \ + --rewards.receiver
\ + --rewards.registry-entity
``` -If the given `--rewards.registry-entity` is properly staked, and if the given -`--rewards.instance` is funded, then you should see your token account +If the given `--rewards.registry-entity` is properly staked, and if the configured +rewards `instance` is funded, then you should see your SPL token account `--rewards.receiver` start to receive rewards with each event consumed. -## Finding a market to crank +## Finding a Market to Crank You can crank any market of your choosing. To find all market addresses one can use the `getProgramAccounts` API exposed by the Solana JSON RPC. In python, @@ -133,3 +153,31 @@ def find_market_addresses(program_id: str): }).json() return [info['pubkey'] for info in resp['result']] ``` + +## Switching to Mainnet Beta + +When operating over multiple networks, you can specify your config file with the +`serum --config ` option. For example, when switching to Mainnet Beta, +one can use the following config. + +```yaml +--- +network: + cluster: mainnet + +mints: + srm: SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt + msrm: MSRMcoVyrFxnSgo5uXwone5SKcGhT1KEJMFEkMEWf9L + +programs: + rewards_pid: 4bcHoAgLP9NBje1oVo9WKRDYkvSxcqtJeTSXMRFX5AdZ + registry_pid: 6J7ZoSxtKJUjVLpGRcBrEtvE2T3YVf9mfKUaicndzpCc + meta_entity_pid: 68gpi9be8NNVTDViQxSYtbM1788uebczX2Vz7obSnQRz + lockup_pid: 4nvqpaMz7H12VgHSABjEDFmH62MoWP3BxfMG3BAFQiBo + dex_pid: EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o + +accounts: + registrar: 8ZS85GGfa92JH6vrmpy8fgQQEBLWwYUwWmpBhcA94fDH + safe: Dp5zzdTLnYNq9E6H81YQu1ucNLK3FzH3Ah3KedydiNgE + rewards_instance: BX1aNRFES78bz9G8GJWmrYSkRVWcV5wdnvSWfXJhTEkE +``` diff --git a/registry/cli/src/lib.rs b/registry/cli/src/lib.rs index 54eb500..0bb3fb8 100644 --- a/registry/cli/src/lib.rs +++ b/registry/cli/src/lib.rs @@ -30,9 +30,6 @@ pub enum Command { }, /// Creates a node entity, setting the active wallet as leader. CreateEntity { - /// Registrar account address. - #[clap(short, long)] - registrar: Pubkey, #[clap(short, long)] name: String, #[clap(short, long)] @@ -40,6 +37,11 @@ pub enum Command { #[clap(short, long)] image_url: Option, }, + /// Creates a member account with the given entity. + CreateMember { + #[clap(short, long)] + entity: Pubkey, + }, /// Updates an entity. Active wallet must be the node leader. UpdateEntity { #[clap(short, long)] @@ -59,8 +61,6 @@ pub enum Command { token: Pubkey, #[clap(long)] vendor: Pubkey, - #[clap(short, long)] - registrar: Pubkey, }, /// Sends all unclaimed funds from an expired locked reward vendor to a given /// account. @@ -70,8 +70,6 @@ pub enum Command { token: Pubkey, #[clap(long)] vendor: Pubkey, - #[clap(short, long)] - registrar: Pubkey, }, } @@ -126,41 +124,33 @@ pub fn run(ctx: Context, cmd: Command) -> Result<()> { stake_rate_mega, ), Command::CreateEntity { - registrar, name, about, image_url, - } => create_entity_cmd(&ctx, registry_pid, registrar, name, about, image_url), + } => create_entity_cmd(&ctx, registry_pid, ctx.registrar, name, about, image_url), + Command::CreateMember { entity } => create_member_cmd(&ctx, entity), Command::UpdateEntity { name, about, image_url, entity, } => update_entity_cmd(&ctx, name, about, image_url, entity), - Command::ExpireUnlockedReward { - token, - vendor, - registrar, - } => { + Command::ExpireUnlockedReward { token, vendor } => { let client = ctx.connect::(registry_pid)?; let resp = client.expire_unlocked_reward(ExpireUnlockedRewardRequest { token, vendor, - registrar, + registrar: ctx.registrar, })?; println!("Transaction executed: {:?}", resp.tx); Ok(()) } - Command::ExpireLockedReward { - token, - vendor, - registrar, - } => { + Command::ExpireLockedReward { token, vendor } => { let client = ctx.connect::(registry_pid)?; let resp = client.expire_locked_reward(ExpireLockedRewardRequest { token, vendor, - registrar, + registrar: ctx.registrar, })?; println!("Transaction executed: {:?}", resp.tx); Ok(()) @@ -195,6 +185,26 @@ fn create_entity_cmd( Ok(()) } +fn create_member_cmd(ctx: &Context, entity: Pubkey) -> Result<()> { + let beneficiary = ctx.wallet()?; + let client = ctx.connect::(ctx.registry_pid)?; + // Vesting vault authority. + let (delegate, _nonce) = Pubkey::find_program_address( + &[ctx.safe.as_ref(), beneficiary.pubkey().as_ref()], + &ctx.lockup_pid, + ); + let CreateMemberResponse { member, .. } = client.create_member(CreateMemberRequest { + entity, + beneficiary: &beneficiary, + delegate, + registrar: ctx.registrar, + })?; + + println!("Member created: {}", member.to_string()); + + Ok(()) +} + fn update_entity_cmd( ctx: &Context, name: Option, diff --git a/registry/rewards/cli/Cargo.toml b/registry/rewards/cli/Cargo.toml index 889e78b..f792cbb 100644 --- a/registry/rewards/cli/Cargo.toml +++ b/registry/rewards/cli/Cargo.toml @@ -10,4 +10,5 @@ clap = "3.0.0-beta.1" serum-context = { path = "../../../context" } solana-client-gen = { path = "../../../solana-client-gen" } anyhow = "1.0.32" -serum-registry-rewards-client = { path = "../client" } \ No newline at end of file +serum-registry-rewards-client = { path = "../client" } +serde_json = "1.0.59" \ No newline at end of file diff --git a/registry/rewards/cli/src/lib.rs b/registry/rewards/cli/src/lib.rs index 44fede3..3bda9b3 100644 --- a/registry/rewards/cli/src/lib.rs +++ b/registry/rewards/cli/src/lib.rs @@ -106,7 +106,10 @@ fn gov_cmd(ctx: &Context, cmd: GovCommand) -> Result<()> { fee_rate, authority: wallet.pubkey(), })?; - println!("Rewards instance created: {:?}", resp.instance); + println!( + "{}", + serde_json::json!({"instance": resp.instance.to_string()}) + ); } GovCommand::SetAuthority { new_authority, diff --git a/registry/src/accounts/registrar.rs b/registry/src/accounts/registrar.rs index 8a34a33..83078ed 100644 --- a/registry/src/accounts/registrar.rs +++ b/registry/src/accounts/registrar.rs @@ -9,7 +9,6 @@ lazy_static::lazy_static! { .expect("Registrar has a fixed size"); } -/// Registry defines the account representing an instance of the program. #[derive(Clone, Debug, Default, PartialEq, BorshSerialize, BorshDeserialize, BorshSchema)] pub struct Registrar { /// Set by the program on initialization. diff --git a/scripts/deploy-staking.sh b/scripts/deploy-staking.sh index b059a72..1939e1b 100755 --- a/scripts/deploy-staking.sh +++ b/scripts/deploy-staking.sh @@ -61,6 +61,10 @@ STAKE_RATE=10000000 # STAKE_RATE_MEGA=1 # +# Unit of srm to pay crankers. +# +CRANK_FEE_RATE=1 +# # Must be built with the `dev` feature on. # serum=$(pwd)/target/debug/serum @@ -158,7 +162,7 @@ EOM # Now intialize all the accounts. # echo "Initializing registrar..." - local rInit=$($serum --config $CONFIG_FILE \ + local r_init=$($serum --config $CONFIG_FILE \ registry init \ --deactivation-timelock $DEACTIVATION_TIMELOCK \ --withdrawal-timelock $WITHDRAWAL_TIMELOCK \ @@ -166,29 +170,15 @@ EOM --stake-rate $STAKE_RATE \ --stake-rate-mega $STAKE_RATE_MEGA) - local registrar=$(echo $rInit | jq .registrar -r) - local registrar_nonce=$(echo $rInit | jq .nonce -r) - local reward_q=$(echo $rInit | jq .rewardEventQueue -r) + local registrar=$(echo $r_init | jq .registrar -r) + local registrar_nonce=$(echo $r_init | jq .nonce -r) + local reward_q=$(echo $r_init | jq .rewardEventQueue -r) echo "Initializing lockup..." - local lInit=$($serum --config $CONFIG_FILE \ + local l_init=$($serum --config $CONFIG_FILE \ lockup initialize) - local safe=$(echo $lInit | jq .safe -r) - - # - # Initialize a node entity. Hack until we separate joining entities - # from creating member accounts. - # - echo "Creating the default node entity..." - local createEntity=$($serum --config $CONFIG_FILE \ - registry create-entity \ - --registrar $registrar \ - --about "This the default entity all new members join." \ - --image-url " " \ - --name "Default" ) - - local entity=$(echo $createEntity | jq .entity -r) + local safe=$(echo $l_init | jq .safe -r) # # Add the registry to the lockup program whitelist. @@ -202,6 +192,38 @@ EOM --nonce $registrar_nonce \ --program-id $registry_pid + # + # Create a crank rewards instance. + # + local crank_rewards=$($serum --config $CONFIG_FILE rewards gov init \ + --registrar $registrar \ + --reward-mint $srm_mint \ + --fee-rate $CRANK_FEE_RATE) + local rewards_instance=$(echo $crank_rewards | jq .instance -r) + + # + # Append to the previously generated config file. + # + cat << EOM >> $CONFIG_FILE +accounts: + registrar: $registrar + safe: $safe + rewards_instance: $rewards_instance +EOM + + + # + # Initialize the default node entity. + # + echo "Creating the default node entity..." + local create_entity=$($serum --config $CONFIG_FILE \ + registry create-entity \ + --about "This the default entity all new members join." \ + --image-url " " \ + --name "Default" ) + + local entity=$(echo $create_entity | jq .entity -r) + # # Log the generated TypeScript. #