diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 53fde4aa..d3a95818 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -32,7 +32,7 @@ jobs: toolchain: 1.69.0 override: true components: rustfmt, clippy - + - name: Install wasm32 toolchain if: env.GIT_DIFF run: rustup target add wasm32-unknown-unknown diff --git a/contracts/oracle/src/lib.rs b/contracts/oracle/src/lib.rs index f000e44e..e7073f9c 100644 --- a/contracts/oracle/src/lib.rs +++ b/contracts/oracle/src/lib.rs @@ -1,5 +1,5 @@ use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; -use near_sdk::collections::{LazyOption, UnorderedSet}; +use near_sdk::collections::{LazyOption, LookupMap, UnorderedSet}; use near_sdk::serde::Serialize; use near_sdk::{ env, near_bindgen, require, AccountId, Balance, Gas, PanicOnDefault, Promise, PromiseError, @@ -20,6 +20,7 @@ pub use crate::storage::*; pub use crate::util::*; mod errors; +mod migrate; mod storage; mod util; @@ -52,6 +53,9 @@ pub struct Contract { /// used for backend key rotation pub admins: UnorderedSet, + + /// class metadata + pub class_metadata: LookupMap, } // Implement the contract structure @@ -85,6 +89,7 @@ impl Contract { authority_pubkey: pubkey_from_b64(authority), used_identities: UnorderedSet::new(StorageKey::UsedIdentities), admins, + class_metadata: LookupMap::new(StorageKey::ClassMetadata), } } @@ -106,6 +111,11 @@ impl Contract { self.used_identities.contains(&normalised_id) } + /// Returns `ClassMetadata` by class. Returns none if the class is not found. + pub fn class_metadata(&self, class: ClassId) -> Option { + self.class_metadata.get(&class) + } + // all SBT queries should be done through registry /********** @@ -307,6 +317,22 @@ impl Contract { ); } + /// Allows admin to update class metadata. + /// Panics if not admin or the class is not found (Currently oracle only supports classes: [1,2]) + #[handle_result] + pub fn set_class_metadata( + &mut self, + class: ClassId, + metadata: ClassMetadata, + ) -> Result<(), CtrError> { + self.assert_admin(); + if class != 1 && class != 2 { + return Err(CtrError::BadRequest("class not found".to_string())); + } + self.class_metadata.insert(&class, &metadata); + Ok(()) + } + // TODO: // - fn sbt_renew } @@ -335,6 +361,7 @@ mod checks; pub mod tests { use crate::*; use ed25519_dalek::Keypair; + use near_sdk::test_utils::test_env::alice; use near_sdk::test_utils::VMContextBuilder; use near_sdk::{testing_env, VMContext}; @@ -369,6 +396,16 @@ pub mod tests { 11 * SECOND } + fn class_metadata() -> ClassMetadata { + ClassMetadata { + name: "test_1".to_string(), + symbol: None, + icon: None, + reference: None, + reference_hash: None, + } + } + /// SBT claim ttl in seconds const CLAIM_TTL: u64 = 2; @@ -610,4 +647,31 @@ pub mod tests { assert!(res.is_err()); assert_bad_request(res, "IAH SBT cannot be mint during the elections period"); } + + #[test] + #[should_panic(expected = "not an admin")] + fn set_class_metadata_not_admin() { + let (_, mut ctr, _) = setup(&alice(), &alice()); + let _ = ctr.set_class_metadata(1, class_metadata()); + } + + #[test] + fn set_class_metadata_wrong_class() { + let (_, mut ctr, _) = setup(&alice(), &acc_admin()); + match ctr.set_class_metadata(3, class_metadata()) { + Err(CtrError::BadRequest(_)) => (), + Err(error) => panic!("expected BadRequest, got: {:?}", error), + Ok(_) => panic!("expected BadRequest, got: Ok"), + } + } + + #[test] + fn set_class_metadata() { + let (_, mut ctr, _) = setup(&alice(), &acc_admin()); + match ctr.set_class_metadata(1, class_metadata()) { + Ok(_) => (), + Err(error) => panic!("expected Ok, got: {:?}", error), + } + assert_eq!(ctr.class_metadata(1).unwrap(), class_metadata()); + } } diff --git a/contracts/oracle/src/migrate.rs b/contracts/oracle/src/migrate.rs new file mode 100644 index 00000000..18c4f5db --- /dev/null +++ b/contracts/oracle/src/migrate.rs @@ -0,0 +1,41 @@ +use crate::*; + +// registry/v1.3.0 +#[derive(BorshDeserialize, PanicOnDefault)] +pub struct OldState { + pub metadata: LazyOption, + pub registry: AccountId, + pub claim_ttl: u64, + pub sbt_ttl_ms: u64, + pub authority_pubkey: [u8; PUBLIC_KEY_LEN], + pub used_identities: UnorderedSet>, + pub admins: UnorderedSet, +} + +#[near_bindgen] +impl Contract { + #[private] + #[init(ignore_state)] + /* pub */ + pub fn migrate(class_metadata: Vec<(ClassId, ClassMetadata)>) -> Self { + let old_state: OldState = env::state_read().expect("failed"); + // new field in the smart contract : + // + class_metadata: LookupMap + + let mut c_metadata = LookupMap::new(StorageKey::ClassMetadata); + for (class_id, class_metadata) in class_metadata { + c_metadata.insert(&class_id, &class_metadata); + } + + Self { + metadata: old_state.metadata, + registry: old_state.registry, + claim_ttl: old_state.claim_ttl, + sbt_ttl_ms: old_state.sbt_ttl_ms, + authority_pubkey: old_state.authority_pubkey, + used_identities: old_state.used_identities, + admins: old_state.admins, + class_metadata: c_metadata, + } + } +} diff --git a/contracts/oracle/src/storage.rs b/contracts/oracle/src/storage.rs index 2ee08aa5..489b0041 100644 --- a/contracts/oracle/src/storage.rs +++ b/contracts/oracle/src/storage.rs @@ -9,4 +9,5 @@ pub enum StorageKey { ContractMetadata, UsedIdentities, Admins, + ClassMetadata, } diff --git a/contracts/oracle/tests/integration_test.rs b/contracts/oracle/tests/integration_test.rs index a582085f..5f3d908b 100644 --- a/contracts/oracle/tests/integration_test.rs +++ b/contracts/oracle/tests/integration_test.rs @@ -3,6 +3,7 @@ use std::str::FromStr; use chrono::Utc; use near_crypto::{SecretKey, Signature}; use near_sdk::ONE_NEAR; +use near_units::parse_near; use serde_json::json; use test_util::{ deploy_contract, gen_user_account, @@ -13,7 +14,7 @@ use workspaces::{types::Balance, Account, AccountId, Contract, DevNetwork, Worke use near_sdk::borsh::BorshSerialize; use oracle_sbt::{Claim, MINT_TOTAL_COST}; -use sbt::ContractMetadata; +use sbt::{ClassMetadata, ContractMetadata}; const AUTHORITY_KEY: &str = "zqMwV9fTRoBOLXwt1mHxBAF3d0Rh9E9xwSAXR3/KL5E="; const CLAIM_TTL: u64 = 3600 * 24 * 365 * 100; @@ -341,3 +342,90 @@ async fn try_sbt_mint( } } } + +#[tokio::test] +async fn migration_mainnet() -> anyhow::Result<()> { + let worker_sandbox = workspaces::sandbox().await?; + let worker_mainnet = workspaces::mainnet().await?; + let oracle_address: AccountId = "fractal.i-am-human.near".parse()?; + let oracle = worker_sandbox + .import_contract(&oracle_address, &worker_mainnet) + .initial_balance(parse_near!("10000000 N")) + .transact() + .await?; + + let admin = worker_sandbox.dev_create_account().await?; + let registry = worker_sandbox.dev_create_account().await?; + + // init the contract + let res = oracle + .call("new") + .args_json(json!({ + "authority": &String::from(AUTHORITY_KEY), + "admin": admin.id(), + "registry": registry.id(), + "claim_ttl": CLAIM_TTL, + "metadata": ContractMetadata{spec: "sbt".to_owned(), name: "oracle".to_owned(), symbol: "iah".to_owned(), icon: None, base_uri: None, reference: None, reference_hash: None}, + })) + .max_gas() + .transact() + .await?; + + assert!(res.is_success(), "{:?}", res.receipt_failures()); + + // deploy the new contract + let res = oracle + .as_account() + .deploy(include_bytes!("../../res/oracle_sbt.wasm")) + .await?; + + assert!(res.is_success()); + + let new_oracle = res.into_result()?; + + let class_metadata_1 = ClassMetadata { + name: "test_1".to_string(), + symbol: None, + icon: None, + reference: None, + reference_hash: None, + }; + let class_metadata_2 = ClassMetadata { + name: "test_2".to_string(), + symbol: None, + icon: None, + reference: None, + reference_hash: None, + }; + + // call the migrate method + let res = new_oracle + .call("migrate") + .args_json(json!({"class_metadata": [[1, class_metadata_1], [2, class_metadata_2]]})) + .max_gas() + .transact() + .await?; + assert!(res.is_success(), "{:?}", res.receipt_failures()); + + let class_metdata: Option = new_oracle + .call("class_metadata") + .args_json(json!({"class": 1})) + .max_gas() + .transact() + .await? + .json()?; + + assert_eq!(class_metdata, Some(class_metadata_1)); + + let class_metdata: Option = new_oracle + .call("class_metadata") + .args_json(json!({"class": 2})) + .max_gas() + .transact() + .await? + .json()?; + + assert_eq!(class_metdata, Some(class_metadata_2)); + + Ok(()) +}