diff --git a/.changelog/unreleased/improvements/2359-validator-avatar.md b/.changelog/unreleased/improvements/2359-validator-avatar.md new file mode 100644 index 0000000000..765f61fd0f --- /dev/null +++ b/.changelog/unreleased/improvements/2359-validator-avatar.md @@ -0,0 +1,2 @@ +- Include validator avatar url in their medatada + ([\#2359](https://github.com/anoma/namada/pull/2359)) \ No newline at end of file diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index fca330ec48..ace4a025e3 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2839,6 +2839,7 @@ pub mod args { pub const ALLOW_DUPLICATE_IP: ArgFlag = flag("allow-duplicate-ip"); pub const AMOUNT: Arg = arg("amount"); pub const ARCHIVE_DIR: ArgOpt = arg_opt("archive-dir"); + pub const AVATAR_OPT: ArgOpt = arg_opt("avatar"); pub const BALANCE_OWNER: ArgOpt = arg_opt("owner"); pub const BASE_DIR: ArgDefault = arg_default( "base-dir", @@ -4042,6 +4043,7 @@ pub mod args { description: self.description, website: self.website, discord_handle: self.discord_handle, + avatar: self.avatar, unsafe_dont_encrypt: self.unsafe_dont_encrypt, tx_code_path: self.tx_code_path.to_path_buf(), } @@ -4064,6 +4066,7 @@ pub mod args { let description = DESCRIPTION_OPT.parse(matches); let website = WEBSITE_OPT.parse(matches); let discord_handle = DISCORD_OPT.parse(matches); + let avatar = AVATAR_OPT.parse(matches); let unsafe_dont_encrypt = UNSAFE_DONT_ENCRYPT.parse(matches); let tx_code_path = PathBuf::from(TX_BECOME_VALIDATOR_WASM); Self { @@ -4080,6 +4083,7 @@ pub mod args { description, website, discord_handle, + avatar, unsafe_dont_encrypt, tx_code_path, } @@ -4128,6 +4132,7 @@ pub mod args { .arg(DESCRIPTION_OPT.def().help("The validator's description.")) .arg(WEBSITE_OPT.def().help("The validator's website.")) .arg(DISCORD_OPT.def().help("The validator's discord handle.")) + .arg(AVATAR_OPT.def().help("The validator's avatar.")) .arg(VALIDATOR_CODE_PATH.def().help( "The path to the validity predicate WASM code to be used \ for the validator account. Uses the default validator VP \ @@ -4163,6 +4168,7 @@ pub mod args { description: self.description, website: self.website, discord_handle: self.discord_handle, + avatar: self.avatar, validator_vp_code_path: self .validator_vp_code_path .to_path_buf(), @@ -4193,6 +4199,7 @@ pub mod args { let description = DESCRIPTION_OPT.parse(matches); let website = WEBSITE_OPT.parse(matches); let discord_handle = DISCORD_OPT.parse(matches); + let avatar = AVATAR_OPT.parse(matches); let validator_vp_code_path = VALIDATOR_CODE_PATH .parse(matches) .unwrap_or_else(|| PathBuf::from(VP_USER_WASM)); @@ -4216,6 +4223,7 @@ pub mod args { description, website, discord_handle, + avatar, validator_vp_code_path, unsafe_dont_encrypt, tx_init_account_code_path, @@ -4268,6 +4276,7 @@ pub mod args { .arg(DESCRIPTION_OPT.def().help("The validator's description.")) .arg(WEBSITE_OPT.def().help("The validator's website.")) .arg(DISCORD_OPT.def().help("The validator's discord handle.")) + .arg(AVATAR_OPT.def().help("The validator's avatar.")) .arg(VALIDATOR_CODE_PATH.def().help( "The path to the validity predicate WASM code to be used \ for the validator account. Uses the default validator VP \ @@ -5331,6 +5340,7 @@ pub mod args { description: self.description, website: self.website, discord_handle: self.discord_handle, + avatar: self.avatar, commission_rate: self.commission_rate, tx_code_path: self.tx_code_path.to_path_buf(), } @@ -5345,6 +5355,7 @@ pub mod args { let description = DESCRIPTION_OPT.parse(matches); let website = WEBSITE_OPT.parse(matches); let discord_handle = DISCORD_OPT.parse(matches); + let avatar = AVATAR_OPT.parse(matches); let commission_rate = COMMISSION_RATE_OPT.parse(matches); let tx_code_path = PathBuf::from(TX_CHANGE_METADATA_WASM); Self { @@ -5354,6 +5365,7 @@ pub mod args { description, website, discord_handle, + avatar, commission_rate, tx_code_path, } @@ -5382,6 +5394,10 @@ pub mod args { existing discord handle, pass an empty string to this \ argument.", )) + .arg(AVATAR_OPT.def().help( + "The desired new validator avatar url. To remove the \ + existing avatar, pass an empty string to this argument.", + )) .arg( COMMISSION_RATE_OPT .def() @@ -6707,6 +6723,7 @@ pub mod args { pub description: Option, pub website: Option, pub discord_handle: Option, + pub avatar: Option, pub address: EstablishedAddress, pub tx_path: PathBuf, } @@ -6726,6 +6743,7 @@ pub mod args { let description = DESCRIPTION_OPT.parse(matches); let website = WEBSITE_OPT.parse(matches); let discord_handle = DISCORD_OPT.parse(matches); + let avatar = AVATAR_OPT.parse(matches); let address = RAW_ADDRESS_ESTABLISHED.parse(matches); let tx_path = PATH.parse(matches); Self { @@ -6740,6 +6758,7 @@ pub mod args { description, website, discord_handle, + avatar, tx_path, address, } @@ -6797,6 +6816,9 @@ pub mod args { "The validator's discord handle. This is an optional \ parameter.", )) + .arg(AVATAR_OPT.def().help( + "The validator's avatar. This is an optional parameter.", + )) } } diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 8a1052b097..0e431d1b93 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -1999,6 +1999,7 @@ pub async fn query_and_print_metadata( description, website, discord_handle, + avatar, }) => { display_line!( context.io(), @@ -2025,6 +2026,11 @@ pub async fn query_and_print_metadata( } else { display_line!(context.io(), "No discord handle"); } + if let Some(avatar) = avatar { + display_line!(context.io(), "Avatar: {}", avatar); + } else { + display_line!(context.io(), "No avatar"); + } } None => display_line!( context.io(), diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index f57cddd4eb..d76b12a7b5 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -472,6 +472,7 @@ pub async fn submit_become_validator( website, description, discord_handle, + avatar, unsafe_dont_encrypt, tx_code_path, }: args::TxBecomeValidator, @@ -710,6 +711,7 @@ pub async fn submit_become_validator( description, website, discord_handle, + avatar, }; // Put together all the PKs that we have to sign with to verify ownership @@ -845,6 +847,7 @@ pub async fn submit_init_validator( website, description, discord_handle, + avatar, validator_vp_code_path, unsafe_dont_encrypt, tx_init_account_code_path, @@ -896,6 +899,7 @@ pub async fn submit_init_validator( description, website, discord_handle, + avatar, tx_code_path: tx_become_validator_code_path, unsafe_dont_encrypt, }, diff --git a/apps/src/lib/client/utils.rs b/apps/src/lib/client/utils.rs index d334a8281b..c1a96620dd 100644 --- a/apps/src/lib/client/utils.rs +++ b/apps/src/lib/client/utils.rs @@ -806,6 +806,7 @@ pub fn init_genesis_validator( description, website, discord_handle, + avatar, tx_path, address, }: args::InitGenesisValidator, @@ -884,6 +885,7 @@ pub fn init_genesis_validator( description, website, discord_handle, + avatar, }, &validator_wallet, ); diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index 8649d2a4d1..83963a7d38 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -468,6 +468,7 @@ pub fn make_dev_genesis( description: None, website: None, discord_handle: None, + avatar: None, }, net_address: SocketAddr::new( IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), diff --git a/apps/src/lib/config/genesis/transactions.rs b/apps/src/lib/config/genesis/transactions.rs index ae8d892a29..210230b14e 100644 --- a/apps/src/lib/config/genesis/transactions.rs +++ b/apps/src/lib/config/genesis/transactions.rs @@ -144,6 +144,7 @@ pub struct GenesisValidatorData { pub description: Option, pub website: Option, pub discord_handle: Option, + pub avatar: Option, } /// Panics if given `txs.validator_accounts` is not empty, because validator @@ -269,6 +270,7 @@ pub fn init_validator( description, website, discord_handle, + avatar, }: GenesisValidatorData, validator_wallet: &ValidatorWallet, ) -> (Address, UnsignedTransactions) { @@ -302,6 +304,7 @@ pub fn init_validator( description, website, discord_handle, + avatar, }, }; let unsigned_validator_addr = @@ -613,6 +616,7 @@ impl TxToSign for ValidatorAccountTx { description: self.metadata.description.clone(), website: self.metadata.website.clone(), discord_handle: self.metadata.discord_handle.clone(), + avatar: self.metadata.avatar.clone(), }, ) } diff --git a/benches/txs.rs b/benches/txs.rs index 523cd48489..e6493d6bbf 100644 --- a/benches/txs.rs +++ b/benches/txs.rs @@ -627,6 +627,7 @@ fn become_validator(c: &mut Criterion) { description: None, website: None, discord_handle: None, + avatar: None, }; let tx = shell.generate_tx( TX_BECOME_VALIDATOR_WASM, @@ -722,6 +723,7 @@ fn change_validator_metadata(c: &mut Criterion) { description: Some("I will change this piece of data".to_string()), website: None, discord_handle: None, + avatar: None, commission_rate: None, }; diff --git a/core/src/types/transaction/pos.rs b/core/src/types/transaction/pos.rs index 1ac5fc8a38..c486a86263 100644 --- a/core/src/types/transaction/pos.rs +++ b/core/src/types/transaction/pos.rs @@ -44,6 +44,9 @@ pub struct BecomeValidator { pub website: Option, /// The validator's discord handle pub discord_handle: Option, + /// URL that points to a picture (e.g. PNG), + /// identifying the validator + pub avatar: Option, } /// A bond is a validator's self-bond or a delegation from non-validator to a @@ -183,6 +186,8 @@ pub struct MetaDataChange { pub website: Option, /// Validator's discord handle pub discord_handle: Option, + /// Validator's avatar url + pub avatar: Option, /// Validator's commission rate pub commission_rate: Option, } @@ -267,6 +272,7 @@ pub mod tests { description in option::of("[a-zA-Z0-9_]*"), website in option::of("[a-zA-Z0-9_]*"), discord_handle in option::of("[a-zA-Z0-9_]*"), + avatar in option::of("[a-zA-Z0-9_]*"), commission_rate in option::of(arb_dec()), ) -> MetaDataChange { MetaDataChange { @@ -275,6 +281,7 @@ pub mod tests { description, website, discord_handle, + avatar, commission_rate, } } @@ -307,6 +314,7 @@ pub mod tests { description in option::of("[a-zA-Z0-9_]*"), website in option::of("[a-zA-Z0-9_]*"), discord_handle in option::of("[a-zA-Z0-9_]*"), + avatar in option::of("[a-zA-Z0-9_]*"), ) -> BecomeValidator { BecomeValidator { address, @@ -320,6 +328,7 @@ pub mod tests { description, website, discord_handle, + avatar, } } } diff --git a/genesis/localnet/src/pre-genesis/validator-0/signed-transactions.toml b/genesis/localnet/src/pre-genesis/validator-0/signed-transactions.toml index 90c00d639c..dd6102dd22 100644 --- a/genesis/localnet/src/pre-genesis/validator-0/signed-transactions.toml +++ b/genesis/localnet/src/pre-genesis/validator-0/signed-transactions.toml @@ -12,29 +12,29 @@ net_address = "127.0.0.1:27656" [validator_account.consensus_key] pk = "tpknam1qr9u5py97pdmcvnrxhzuuv79ydv5rw7r9z402sucwt6h0lvmmmwqy2wrweg" -authorization = "signam1qqxx2kpu0yla6jy958t58c649m5gf8v5rjqk5rqemkvxa3xynfxgznjwx2yzre20l7n7mvul2p4vdpsunmjt33fu9c94tu26caevrvgdllk4mx" +authorization = "signam1qqxsgw7k5yu200657evhu7kuenf5zw86lvx8nw7wk9zrgt0tx3xpf492f2k054ml6zu6x7p6d3j6rmdm33747ptwrxtvmwdaenyqvvcp3v9gpn" [validator_account.protocol_key] pk = "tpknam1qrenhfdphzpszlr7fzand6qgmppge430g3a2lquqzhz64fkve5mq2hdfjaa" -authorization = "signam1qqfz3hym66j8my6exzfv88c8e829gulwr4eycyzn5j4a5tns8wj6md6j2kwly4kdr2ts8jwln7mta6weacgf2xjh08scykl3yzqeers899d9fn" +authorization = "signam1qpnjzsgnvgfnfjld89wtjtn7w843dhw39tktuxgw49tcyj678u03hdaufdsf6rakmrjfa6ywjnngy7hmwvwsgkjr60330w9jwl7azus20tcmwj" [validator_account.tendermint_node_key] pk = "tpknam1qpgcgptcjl22hl2te2uqnp33aqjmvfaud3a3f3sgtxezg7uu5rsv6d2flr3" -authorization = "signam1qpev0rmgakenvsec34xlyz8e2cpyc64u0fex6dv2us2x0yg9plyfrmfwcpxg2png7xl8404fmm6vp4h0afz25jclvc0yavjrc2vncps8wqqgds" +authorization = "signam1qq8d8k43779lxsz3hx3pkpucetdd4kzrl56hk52nwa5dfu97y2gq9axxffftx726e5n8ch2na4c8wn98yxcjpzzdpdkj86655g4hynsgwf35af" [validator_account.eth_hot_key] pk = "tpknam1qypnh98mexms8edj8rcwu0cayx0459p39dwzsffxrr394mf4cse707qcctyrx" -authorization = "signam1qy04m2dqtvwrkk7rjxzpeua89hag2yc38rm3z9ljeg9c7ymp35tuqfd7yk0w4epnsydfvdyd682lvcrtzf28rdkd8snzmmyj846r36zyqq77j5s0" +authorization = "signam1q8t7shqzpmdwtfumrahs022mt96mazwq2d843sad4dgcvz9prlj6x37qp7nvhpluclq8ta4sjkg7da77e9xlvyh4eujc3v936egqqf4eqytrku9n" [validator_account.eth_cold_key] pk = "tpknam1qypz8zr0w8lsz3s98vh4p974xuxeedpecj9s2l3326r3kdz4tc0snrcpnc8yv" -authorization = "signam1q8pmqdqcqh9v8djeqlrzy4mv845v9harnm9u5z54nwlrhwkyvc9y79sv0ucaa08c7y3xypcn7wqj6qvjqzfzs9h89hesuxttlujswz5wqyux24m9" +authorization = "signam1qxe6c5h7sfrsampxlhsxdkq33zmetwg45y2lqxxhf4v7sy7sa933xat3m9f4g76dpzvpnaedrdy4d7t5xfmwmu0u9cdgdu8pgwqx9k8zqyzkce99" [validator_account.metadata] email = "null@null.net" [validator_account.signatures] -tpknam1qpg2tsrplvhu3fd7z7tq5ztc2ne3s7e2ahjl2a2cddufrzdyr752g666ytj = "signam1qz3ylgxm3sr9d68ktwf078vt89dn9v8xj0c7tnytevnsd0v29kh6wmnqsfxd5mke8nrdlwve850l3uwcquxw2e7nltqt8eufds6ns6cqfzastc" +tpknam1qpg2tsrplvhu3fd7z7tq5ztc2ne3s7e2ahjl2a2cddufrzdyr752g666ytj = "signam1qqtljxqp03xhfcfg7vpnmxeuke97kqz5etmdzwvkmr08r4x9wj4ckzf6wlqzse8d05p3t70yyn3pd34kjvx87fzp3sytn4vp6adajrgx5uk8my" [[bond]] source = "tnam1q9vhfdur7gadtwx4r223agpal0fvlqhywylf2mzx" diff --git a/genesis/localnet/transactions.toml b/genesis/localnet/transactions.toml index 49c07a000e..3a2b5b6b18 100644 --- a/genesis/localnet/transactions.toml +++ b/genesis/localnet/transactions.toml @@ -19,29 +19,29 @@ net_address = "127.0.0.1:27656" [validator_account.consensus_key] pk = "tpknam1qr9u5py97pdmcvnrxhzuuv79ydv5rw7r9z402sucwt6h0lvmmmwqy2wrweg" -authorization = "signam1qqxx2kpu0yla6jy958t58c649m5gf8v5rjqk5rqemkvxa3xynfxgznjwx2yzre20l7n7mvul2p4vdpsunmjt33fu9c94tu26caevrvgdllk4mx" +authorization = "signam1qqxsgw7k5yu200657evhu7kuenf5zw86lvx8nw7wk9zrgt0tx3xpf492f2k054ml6zu6x7p6d3j6rmdm33747ptwrxtvmwdaenyqvvcp3v9gpn" [validator_account.protocol_key] pk = "tpknam1qrenhfdphzpszlr7fzand6qgmppge430g3a2lquqzhz64fkve5mq2hdfjaa" -authorization = "signam1qqfz3hym66j8my6exzfv88c8e829gulwr4eycyzn5j4a5tns8wj6md6j2kwly4kdr2ts8jwln7mta6weacgf2xjh08scykl3yzqeers899d9fn" +authorization = "signam1qpnjzsgnvgfnfjld89wtjtn7w843dhw39tktuxgw49tcyj678u03hdaufdsf6rakmrjfa6ywjnngy7hmwvwsgkjr60330w9jwl7azus20tcmwj" [validator_account.tendermint_node_key] pk = "tpknam1qpgcgptcjl22hl2te2uqnp33aqjmvfaud3a3f3sgtxezg7uu5rsv6d2flr3" -authorization = "signam1qpev0rmgakenvsec34xlyz8e2cpyc64u0fex6dv2us2x0yg9plyfrmfwcpxg2png7xl8404fmm6vp4h0afz25jclvc0yavjrc2vncps8wqqgds" +authorization = "signam1qq8d8k43779lxsz3hx3pkpucetdd4kzrl56hk52nwa5dfu97y2gq9axxffftx726e5n8ch2na4c8wn98yxcjpzzdpdkj86655g4hynsgwf35af" [validator_account.eth_hot_key] pk = "tpknam1qypnh98mexms8edj8rcwu0cayx0459p39dwzsffxrr394mf4cse707qcctyrx" -authorization = "signam1qy04m2dqtvwrkk7rjxzpeua89hag2yc38rm3z9ljeg9c7ymp35tuqfd7yk0w4epnsydfvdyd682lvcrtzf28rdkd8snzmmyj846r36zyqq77j5s0" +authorization = "signam1q8t7shqzpmdwtfumrahs022mt96mazwq2d843sad4dgcvz9prlj6x37qp7nvhpluclq8ta4sjkg7da77e9xlvyh4eujc3v936egqqf4eqytrku9n" [validator_account.eth_cold_key] pk = "tpknam1qypz8zr0w8lsz3s98vh4p974xuxeedpecj9s2l3326r3kdz4tc0snrcpnc8yv" -authorization = "signam1q8pmqdqcqh9v8djeqlrzy4mv845v9harnm9u5z54nwlrhwkyvc9y79sv0ucaa08c7y3xypcn7wqj6qvjqzfzs9h89hesuxttlujswz5wqyux24m9" +authorization = "signam1qxe6c5h7sfrsampxlhsxdkq33zmetwg45y2lqxxhf4v7sy7sa933xat3m9f4g76dpzvpnaedrdy4d7t5xfmwmu0u9cdgdu8pgwqx9k8zqyzkce99" [validator_account.metadata] email = "null@null.net" [validator_account.signatures] -tpknam1qpg2tsrplvhu3fd7z7tq5ztc2ne3s7e2ahjl2a2cddufrzdyr752g666ytj = "signam1qz3ylgxm3sr9d68ktwf078vt89dn9v8xj0c7tnytevnsd0v29kh6wmnqsfxd5mke8nrdlwve850l3uwcquxw2e7nltqt8eufds6ns6cqfzastc" +tpknam1qpg2tsrplvhu3fd7z7tq5ztc2ne3s7e2ahjl2a2cddufrzdyr752g666ytj = "signam1qqtljxqp03xhfcfg7vpnmxeuke97kqz5etmdzwvkmr08r4x9wj4ckzf6wlqzse8d05p3t70yyn3pd34kjvx87fzp3sytn4vp6adajrgx5uk8my" [[bond]] source = "tnam1q9vhfdur7gadtwx4r223agpal0fvlqhywylf2mzx" diff --git a/light_sdk/src/transaction/pos.rs b/light_sdk/src/transaction/pos.rs index a436e1588a..e042d80c5a 100644 --- a/light_sdk/src/transaction/pos.rs +++ b/light_sdk/src/transaction/pos.rs @@ -133,6 +133,7 @@ impl BecomeValidator { description: Option, website: Option, discord_handle: Option, + avatar: Option, args: GlobalArgs, ) -> Self { let update_account = @@ -148,6 +149,7 @@ impl BecomeValidator { description, website, discord_handle, + avatar, }; Self(transaction::build_tx( @@ -333,12 +335,14 @@ pub struct ChangeMetaData(Tx); impl ChangeMetaData { /// Build a raw ChangeMetadata transaction from the given parameters + #[allow(clippy::too_many_arguments)] pub fn new( validator: Address, email: Option, description: Option, website: Option, discord_handle: Option, + avatar: Option, commission_rate: Option, args: GlobalArgs, ) -> Self { @@ -349,6 +353,7 @@ impl ChangeMetaData { description, website, discord_handle, + avatar, commission_rate, }; diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index a390d6ec00..afdf94a39b 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -68,7 +68,7 @@ use crate::storage::{ validator_slashes_handle, validator_state_handle, validator_total_redelegated_bonded_handle, validator_total_redelegated_unbonded_handle, write_last_reward_claim_epoch, - write_pos_params, write_validator_address_raw_hash, + write_pos_params, write_validator_address_raw_hash, write_validator_avatar, write_validator_description, write_validator_discord_handle, write_validator_email, write_validator_max_commission_rate_change, write_validator_metadata, write_validator_website, @@ -2558,6 +2558,7 @@ pub fn change_validator_metadata( description: Option, website: Option, discord_handle: Option, + avatar: Option, commission_rate: Option, current_epoch: Epoch, ) -> storage_api::Result<()> @@ -2576,6 +2577,9 @@ where if let Some(discord) = discord_handle { write_validator_discord_handle(storage, validator, &discord)?; } + if let Some(avatar) = avatar { + write_validator_avatar(storage, validator, &avatar)?; + } if let Some(commission_rate) = commission_rate { change_validator_commission_rate( storage, diff --git a/proof_of_stake/src/storage.rs b/proof_of_stake/src/storage.rs index bab677b3a0..5aac88a041 100644 --- a/proof_of_stake/src/storage.rs +++ b/proof_of_stake/src/storage.rs @@ -753,6 +753,35 @@ where } } +/// Read PoS validator's avatar. +pub fn read_validator_avatar( + storage: &S, + validator: &Address, +) -> storage_api::Result> +where + S: StorageRead, +{ + storage.read(&storage_key::validator_avatar_key(validator)) +} + +/// Write PoS validator's avatar. If the provided arg is an empty +/// string, remove the data. +pub fn write_validator_avatar( + storage: &mut S, + validator: &Address, + avatar: &String, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let key = storage_key::validator_avatar_key(validator); + if avatar.is_empty() { + storage.delete(&key) + } else { + storage.write(&key, avatar) + } +} + /// Write validator's metadata. pub fn write_validator_metadata( storage: &mut S, @@ -774,6 +803,9 @@ where if let Some(discord) = metadata.discord_handle.as_ref() { write_validator_discord_handle(storage, validator, discord)?; } + if let Some(avatar) = metadata.avatar.as_ref() { + write_validator_avatar(storage, validator, avatar)?; + } Ok(()) } diff --git a/proof_of_stake/src/storage_key.rs b/proof_of_stake/src/storage_key.rs index 2991526760..90e3506a70 100644 --- a/proof_of_stake/src/storage_key.rs +++ b/proof_of_stake/src/storage_key.rs @@ -54,6 +54,7 @@ const VALIDATOR_EMAIL_KEY: &str = "email"; const VALIDATOR_DESCRIPTION_KEY: &str = "description"; const VALIDATOR_WEBSITE_KEY: &str = "website"; const VALIDATOR_DISCORD_KEY: &str = "discord_handle"; +const VALIDATOR_AVATAR_KEY: &str = "avatar"; const LIVENESS_PREFIX: &str = "liveness"; const LIVENESS_MISSED_VOTES: &str = "missed_votes"; const LIVENESS_MISSED_VOTES_SUM: &str = "sum_missed_votes"; @@ -271,6 +272,7 @@ pub fn is_validator_metadata_key(key: &Key) -> Option<&Address> { | VALIDATOR_DESCRIPTION_KEY | VALIDATOR_WEBSITE_KEY | VALIDATOR_DISCORD_KEY + | VALIDATOR_AVATAR_KEY ) => { Some(validator) @@ -1015,6 +1017,13 @@ pub fn validator_discord_key(validator: &Address) -> Key { .expect("Cannot obtain a storage key") } +/// Storage key for a validator's avatar +pub fn validator_avatar_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_AVATAR_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + /// Storage prefix for the liveness data of the cosnensus validator set. pub fn liveness_data_prefix() -> Key { Key::from(ADDRESS.to_db_key()) diff --git a/proof_of_stake/src/types/mod.rs b/proof_of_stake/src/types/mod.rs index 2d297bd72f..eb3bbb45fb 100644 --- a/proof_of_stake/src/types/mod.rs +++ b/proof_of_stake/src/types/mod.rs @@ -362,6 +362,9 @@ pub struct ValidatorMetaData { pub website: Option, /// Validator's discord handle pub discord_handle: Option, + /// URL that points to a picture (e.g. PNG), + /// identifying the validator + pub avatar: Option, } #[cfg(any(test, feature = "testing"))] @@ -372,6 +375,7 @@ impl Default for ValidatorMetaData { description: Default::default(), website: Default::default(), discord_handle: Default::default(), + avatar: Default::default(), } } } diff --git a/sdk/src/args.rs b/sdk/src/args.rs index c12ecdf537..b0327c0990 100644 --- a/sdk/src/args.rs +++ b/sdk/src/args.rs @@ -748,6 +748,8 @@ pub struct TxBecomeValidator { pub website: Option, /// The validator's discord handle pub discord_handle: Option, + /// The validator's avatar + pub avatar: Option, /// Path to the TX WASM code file pub tx_code_path: PathBuf, /// Don't encrypt the keypair @@ -785,6 +787,8 @@ pub struct TxInitValidator { pub website: Option, /// The validator's discord handle pub discord_handle: Option, + /// The validator's avatar + pub avatar: Option, /// Path to the VP WASM code file pub validator_vp_code_path: PathBuf, /// Path to the TX WASM code file @@ -1456,6 +1460,8 @@ pub struct MetaDataChange { pub website: Option, /// New validator discord handle pub discord_handle: Option, + /// New validator avatar url + pub avatar: Option, /// New validator commission rate pub commission_rate: Option, /// Path to the TX WASM code file @@ -1487,6 +1493,54 @@ impl MetaDataChange { ..self } } + + /// New validator email + pub fn email(self, email: String) -> Self { + Self { + email: Some(email), + ..self + } + } + + /// New validator description + pub fn description(self, description: String) -> Self { + Self { + description: Some(description), + ..self + } + } + + /// New validator website + pub fn website(self, website: String) -> Self { + Self { + website: Some(website), + ..self + } + } + + /// New validator discord handle + pub fn discord_handle(self, discord_handle: String) -> Self { + Self { + discord_handle: Some(discord_handle), + ..self + } + } + + /// New validator avatar url + pub fn avatar(self, avatar: String) -> Self { + Self { + avatar: Some(avatar), + ..self + } + } + + /// New validator commission rate + pub fn commission_rate(self, commission_rate: Dec) -> Self { + Self { + commission_rate: Some(commission_rate), + ..self + } + } } impl MetaDataChange { diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 65b4a45cf4..f3cc1fbe01 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -338,23 +338,15 @@ pub trait Namada: Sized + MaybeSync + MaybeSend { /// Make a CommissionRateChange builder from the given minimum set of /// arguments - #[allow(clippy::too_many_arguments)] - fn new_change_metadata( - &self, - validator: Address, - email: Option, - description: Option, - website: Option, - discord_handle: Option, - commission_rate: Option, - ) -> args::MetaDataChange { + fn new_change_metadata(&self, validator: Address) -> args::MetaDataChange { args::MetaDataChange { validator, - email, - description, - website, - discord_handle, - commission_rate, + email: None, + description: None, + website: None, + discord_handle: None, + avatar: None, + commission_rate: None, tx_code_path: PathBuf::from(TX_CHANGE_METADATA_WASM), tx: self.tx_builder(), } @@ -384,6 +376,7 @@ pub trait Namada: Sized + MaybeSync + MaybeSend { description: None, website: None, discord_handle: None, + avatar: None, } } @@ -415,6 +408,7 @@ pub trait Namada: Sized + MaybeSync + MaybeSend { description: None, website: None, discord_handle: None, + avatar: None, } } diff --git a/sdk/src/queries/vp/pos.rs b/sdk/src/queries/vp/pos.rs index ff1b073dc4..47a1d6bd0b 100644 --- a/sdk/src/queries/vp/pos.rs +++ b/sdk/src/queries/vp/pos.rs @@ -22,7 +22,7 @@ use namada_proof_of_stake::storage::{ bond_handle, read_all_validator_addresses, read_below_capacity_validator_set_addresses_with_stake, read_consensus_validator_set_addresses_with_stake, read_pos_params, - read_total_stake, read_validator_description, + read_total_stake, read_validator_avatar, read_validator_description, read_validator_discord_handle, read_validator_email, read_validator_last_slash_epoch, read_validator_max_commission_rate_change, read_validator_stake, read_validator_website, unbond_handle, @@ -266,6 +266,7 @@ where let website = read_validator_website(ctx.wl_storage, &validator)?; let discord_handle = read_validator_discord_handle(ctx.wl_storage, &validator)?; + let avatar = read_validator_avatar(ctx.wl_storage, &validator)?; // Email is the only required field for a validator in storage match email { @@ -274,6 +275,7 @@ where description, website, discord_handle, + avatar, })), _ => Ok(None), } diff --git a/sdk/src/tx.rs b/sdk/src/tx.rs index de0474615b..a53ac8f8bf 100644 --- a/sdk/src/tx.rs +++ b/sdk/src/tx.rs @@ -694,6 +694,7 @@ pub async fn build_validator_metadata_change( description, website, discord_handle, + avatar, commission_rate, tx_code_path, }: &args::MetaDataChange, @@ -797,6 +798,7 @@ pub async fn build_validator_metadata_change( website: website.clone(), description: description.clone(), discord_handle: discord_handle.clone(), + avatar: avatar.clone(), commission_rate: *commission_rate, }; diff --git a/tx_prelude/src/proof_of_stake.rs b/tx_prelude/src/proof_of_stake.rs index 3b7883361d..acaa86e1ae 100644 --- a/tx_prelude/src/proof_of_stake.rs +++ b/tx_prelude/src/proof_of_stake.rs @@ -127,6 +127,7 @@ impl Ctx { description, website, discord_handle, + avatar, }: BecomeValidator, ) -> EnvResult
{ let current_epoch = self.get_block_epoch()?; @@ -151,6 +152,7 @@ impl Ctx { description, website, discord_handle, + avatar, }, offset_opt: None, }, @@ -180,6 +182,7 @@ impl Ctx { description: Option, website: Option, discord_handle: Option, + avatar: Option, commission_rate: Option, ) -> TxResult { let current_epoch = self.get_block_epoch()?; @@ -190,6 +193,7 @@ impl Ctx { description, website, discord_handle, + avatar, commission_rate, current_epoch, ) diff --git a/wasm/wasm_source/src/tx_change_validator_metadata.rs b/wasm/wasm_source/src/tx_change_validator_metadata.rs index f2ba6a0267..0a0806fd2c 100644 --- a/wasm/wasm_source/src/tx_change_validator_metadata.rs +++ b/wasm/wasm_source/src/tx_change_validator_metadata.rs @@ -15,6 +15,7 @@ fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { description, website, discord_handle, + avatar, commission_rate, } = transaction::pos::MetaDataChange::try_from_slice(&data[..]) .wrap_err("failed to decode Dec value")?; @@ -24,6 +25,7 @@ fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { description, website, discord_handle, + avatar, commission_rate, ) } diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index 29c861d04d..e9e148405f 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -703,6 +703,7 @@ mod tests { description: None, website: None, discord_handle: None, + avatar: None, }; tx::ctx().become_validator(args).unwrap(); }); @@ -792,6 +793,7 @@ mod tests { Some("desc".to_owned()), Some("website".to_owned()), Some("discord".to_owned()), + Some("avatar".to_owned()), Some(Dec::new(6, 2).unwrap()), ) .unwrap(); @@ -972,6 +974,7 @@ mod tests { description: None, website: None, discord_handle: None, + avatar: None, }; tx::ctx().become_validator(args).unwrap(); }); @@ -1076,6 +1079,7 @@ mod tests { Some("desc".to_owned()), Some("website".to_owned()), Some("discord".to_owned()), + Some("avatar".to_owned()), Some(Dec::new(6, 2).unwrap()), ) .unwrap();