diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 6ce89ed..f1445e3 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -46,3 +46,18 @@ jobs: - name: Run tests if: env.GIT_DIFF run: npm run test + - uses: actions/upload-artifact@v4 + if: env.GIT_DIFF + with: + name: secret-network-contract.tar.gz + path: contracts/secret_contract/contract.wasm.gz + if-no-files-found: error + overwrite: true + - uses: actions/upload-artifact@v4 + if: env.GIT_DIFF + with: + name: snapshot-contract + path: target/near/voting_snapshot/* + if-no-files-found: error + overwrite: true + diff --git a/.gitignore b/.gitignore index 975f4d2..b0f424f 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,6 @@ data node_modules stake* -snapshot* +snapshot*.json* .env diff --git a/contracts/secret_contract/src/contract.rs b/contracts/secret_contract/src/contract.rs index 6d91eef..8e17970 100644 --- a/contracts/secret_contract/src/contract.rs +++ b/contracts/secret_contract/src/contract.rs @@ -14,6 +14,10 @@ pub fn instantiate( _info: MessageInfo, msg: InstantiateMsg, ) -> Result { + if !MY_KEYS.is_empty(deps.storage) { + return Err(ContractError::AlreadyInitialized); + } + create_keys(deps, env, msg.end_time)?; Ok(Response::default()) @@ -57,7 +61,9 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result Result { - let my_keys = MY_KEYS.load(deps.storage)?; + let my_keys = MY_KEYS + .load(deps.storage) + .map_err(|_| ContractError::NotInitialized)?; let private = (env.block.time > my_keys.end_time).then(|| my_keys.private_key); Ok(KeysResponse { diff --git a/contracts/secret_contract/src/error.rs b/contracts/secret_contract/src/error.rs index c9f8d4c..4611838 100644 --- a/contracts/secret_contract/src/error.rs +++ b/contracts/secret_contract/src/error.rs @@ -8,4 +8,10 @@ pub enum ContractError { #[error("Invalid End Time")] InvalidEndTime, + + #[error("Already initialized")] + AlreadyInitialized, + + #[error("Not initialized yet")] + NotInitialized, } diff --git a/contracts/secret_contract/src/tests.rs b/contracts/secret_contract/src/tests.rs index 34fe4f8..c340bcd 100644 --- a/contracts/secret_contract/src/tests.rs +++ b/contracts/secret_contract/src/tests.rs @@ -3,6 +3,7 @@ use cosmwasm_std::{testing::*, Timestamp}; use secp256k1::{PublicKey, Secp256k1}; use crate::contract::{instantiate, query}; +use crate::error::ContractError; use crate::msg::{InstantiateMsg, KeysResponse, QueryMsg}; fn setup_contract( @@ -43,6 +44,43 @@ fn proper_initialization() { assert!(PublicKey::from_slice(&value.public).is_ok()); } +#[test] +fn user_cant_reinitialize() { + let time = Timestamp::from_seconds(500); + let mut mock_env = mock_env(); + + mock_env.block.time = time.minus_seconds(5); + + let mut deps = setup_contract(time, mock_env.clone()); + + let info = mock_info( + "creator", + &[Coin { + denom: "earth".to_string(), + amount: Uint128::new(1000), + }], + ); + + let res = query(deps.as_ref(), mock_env.clone(), QueryMsg::GetKeys {}).unwrap(); + let init: KeysResponse = from_binary(&res).unwrap(); + + assert_eq!( + instantiate( + deps.as_mut(), + mock_env.clone(), + info, + InstantiateMsg { + end_time: time.plus_seconds(10), + }, + ), + Err(ContractError::AlreadyInitialized) + ); + let res = query(deps.as_ref(), mock_env, QueryMsg::GetKeys {}).unwrap(); + let upd: KeysResponse = from_binary(&res).unwrap(); + + assert_eq!(init, upd); +} + #[test] fn invalid_time_is_failure() { let time = Timestamp::from_seconds(500); @@ -60,7 +98,10 @@ fn invalid_time_is_failure() { let init_msg = InstantiateMsg { end_time: time }; - instantiate(deps.as_mut(), mock_env, info, init_msg).expect_err("Should fail"); + assert_eq!( + instantiate(deps.as_mut(), mock_env, info, init_msg), + Err(ContractError::InvalidEndTime) + ); } #[test] @@ -92,3 +133,14 @@ fn secret_revealed_after_time_pass() { PublicKey::from_slice(&value.public).unwrap() ); } + +#[test] +fn cannot_fetch_uninitialized_contract() { + let deps = mock_dependencies(); + let mock_env = mock_env(); + + assert_eq!( + query(deps.as_ref(), mock_env, QueryMsg::GetKeys {}), + Err(ContractError::NotInitialized) + ); +} diff --git a/contracts/voting_snapshot/README.md b/contracts/voting_snapshot/README.md index 54f1d3b..ec55116 100644 --- a/contracts/voting_snapshot/README.md +++ b/contracts/voting_snapshot/README.md @@ -24,6 +24,14 @@ To deploy manually, install [`cargo-near`](https://github.com/near/cargo-near) a cargo near deploy ``` +## How to load snapshot? + +To load snapshot use js script prepared for it. + +```bash +node ../../snapshotter/loadSnapshot.js --contract contractID --json ../../snapshot-108194270.json --network testnet --account adminId +``` + ## Contract interface ```rust @@ -60,6 +68,9 @@ pub fn is_nominee(self, nominee: &AccountId) -> bool pub fn is_eligible_voter(self, voter: &AccountId) -> bool pub fn get_voter_information(self, voter: &AccountId) -> VoterInformation pub fn get_voters_info(self, voters: Vec) -> Vec<(AccountId, VoterInformation)> +pub fn get_total_eligible_users(&self) -> u32 +pub fn get_total_voters(&self) -> u32 +pub fn get_eligible_voter_info(&self, account_id: &AccountId) -> Option // Callbacks: pub fn on_refund_success(self, account_id: AccountId) -> () diff --git a/contracts/voting_snapshot/src/admin.rs b/contracts/voting_snapshot/src/admin.rs index 8ececb7..3a4da5f 100644 --- a/contracts/voting_snapshot/src/admin.rs +++ b/contracts/voting_snapshot/src/admin.rs @@ -30,7 +30,13 @@ impl Contract { self.assert_initialization(); self.assert_admin(); - self.eligible_voters.extend(voters); + let mut new_accounts = 0; + for (key, value) in voters.into_iter() { + if self.eligible_voters.insert(key, value).is_none() { + new_accounts += 1; + } + } + self.total_eligible_users += new_accounts; self.eligible_voters.flush(); require!( @@ -210,6 +216,8 @@ mod tests { contract.bulk_load_voters(voters.clone()); assert!(contract.is_eligible_voter(&voters[0].0)); + + assert_eq!(contract.get_total_eligible_users(), 4); } #[test] diff --git a/contracts/voting_snapshot/src/lib.rs b/contracts/voting_snapshot/src/lib.rs index ea5526d..0a858be 100644 --- a/contracts/voting_snapshot/src/lib.rs +++ b/contracts/voting_snapshot/src/lib.rs @@ -39,12 +39,15 @@ pub struct Contract { // for full snapshot, please refer to the IPFS storage // Will be cleaned on halt. eligible_voters: LookupMap, + total_eligible_users: u32, // We need to collect the ones who want to participate in the vote process // We collect the public key of the voter to verify the signature // in the encoded message. // Also, this user indicates that he/she accepts conduct of fair voting voters: LookupMap, + total_voters: u32, + nominees: LookupSet, // People can deposit NEAR to challenge snapshot @@ -74,6 +77,8 @@ impl Contract { process_config, vote_config, end_time_in_millis: 0, + total_voters: 0, + total_eligible_users: 0, eligible_voters: LookupMap::new(StorageKey::EligibleVoters), voters: LookupMap::new(StorageKey::Voters), nominees: LookupSet::new(StorageKey::Nominees), @@ -103,6 +108,7 @@ impl Contract { self.assert_eligible_voter(&signer); self.voters.insert(signer, env::signer_account_pk()); + self.total_voters += 1; self.voters.flush(); require!( @@ -129,6 +135,7 @@ impl Contract { require!(!self.voters.contains_key(&user), ALREADY_REGISTERED); self.voters.insert(user, public_key); + self.total_voters += 1; self.voters.flush(); diff --git a/contracts/voting_snapshot/src/view.rs b/contracts/voting_snapshot/src/view.rs index 97473e2..3c320bf 100644 --- a/contracts/voting_snapshot/src/view.rs +++ b/contracts/voting_snapshot/src/view.rs @@ -91,13 +91,28 @@ impl Contract { .filter_map(|voter| self.get_voter_information(&voter).map(|info| (voter, info))) .collect() } + + /// *View*: Returns amount of eligible users to become voters + pub fn get_total_eligible_users(&self) -> u32 { + self.total_eligible_users + } + + /// *View*: Shows total number of voters that are registered + pub fn get_total_voters(&self) -> u32 { + self.total_voters + } + + /// *View*: displays information about snapshot data for particular account + pub fn get_eligible_voter_info(&self, account_id: &AccountId) -> Option { + self.eligible_voters.get(account_id).cloned() + } } #[cfg(test)] mod tests { use near_sdk::{testing_env, NearToken}; - use crate::test_utils::*; + use crate::{test_utils::*, types::UserData}; #[test] fn user_can_get_vote_config() { @@ -116,6 +131,13 @@ mod tests { let (_context, contract) = setup_ctr(); let vote_power = contract.get_vote_power(&acc(1)).unwrap(); + assert_eq!( + Some(UserData { + stake: NearToken::from_near(1), + active_months: 1, + }), + contract.get_eligible_voter_info(&acc(1)) + ); assert_eq!(vote_power, 11); } @@ -135,6 +157,8 @@ mod tests { contract.register_as_voter(); + assert_eq!(contract.get_total_voters(), 1); + let voter_info = contract.get_voter_information(&acc(1)).unwrap(); assert_eq!(voter_info.vote_weight, 11); assert_eq!(voter_info.public_key, pk()); diff --git a/package-lock.json b/package-lock.json index 4b711de..0c71ea2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,8 @@ "p-retry": "^6.2.0", "pg": "^8.11.3", "secp256k1": "^5.0.0", - "secretjs": "^1.12.0" + "secretjs": "^1.12.0", + "wasm-opt": "^1.4.0" }, "devDependencies": { "cargo-near": "^0.6.1", @@ -839,6 +840,14 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", @@ -952,6 +961,28 @@ "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==" }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/google-protobuf": { "version": "3.21.2", "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz", @@ -1068,11 +1099,53 @@ "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/miscreant": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/miscreant/-/miscreant-0.3.2.tgz", "integrity": "sha512-fL9KxsQz9BJB2KGPMHFrReioywkiomBiuaLk6EuChijK0BsJsIKJXdVomR+/bPj5mvbFD6wM0CM3bZio9g7OHA==" }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mustache": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.0.0.tgz", @@ -1911,6 +1984,22 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tiny-secp256k1": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-1.1.6.tgz", @@ -1968,6 +2057,38 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/wasm-opt": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/wasm-opt/-/wasm-opt-1.4.0.tgz", + "integrity": "sha512-wIsxxp0/FOSphokH4VOONy1zPkVREQfALN+/JTvJPK8gFSKbsmrcfECu2hT7OowqPfb4WEMSMceHgNL0ipFRyw==", + "hasInstallScript": true, + "dependencies": { + "node-fetch": "^2.6.9", + "tar": "^6.1.13" + }, + "bin": { + "wasm-opt": "bin/wasm-opt.js" + } + }, + "node_modules/wasm-opt/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -1997,6 +2118,11 @@ "engines": { "node": ">=0.4" } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } } diff --git a/package.json b/package.json index a152778..0d960bf 100644 --- a/package.json +++ b/package.json @@ -16,16 +16,17 @@ "@supercharge/promise-pool": "^2.3.2", "big.js": "^6.1.1", "commander": "^12.0.0", + "dotenv": "^16.3.1", "fs": "^0.0.1security", "near-api-js": "3.0.4", "p-retry": "^6.2.0", "pg": "^8.11.3", - "dotenv": "^16.3.1", "secp256k1": "^5.0.0", - "secretjs": "^1.12.0" + "secretjs": "^1.12.0", + "wasm-opt": "^1.4.0" }, "devDependencies": { - "near-cli-rs": "^0.8.1", - "cargo-near": "^0.6.1" + "cargo-near": "^0.6.1", + "near-cli-rs": "^0.8.1" } } diff --git a/snapshotter/loadSnapshot.js b/snapshotter/loadSnapshot.js new file mode 100644 index 0000000..4f6ff1c --- /dev/null +++ b/snapshotter/loadSnapshot.js @@ -0,0 +1,112 @@ +import fs from 'fs'; +import { program } from 'commander'; +import { keyStores, connect, transactions, Contract } from 'near-api-js'; +import { parseNearAmount } from 'near-api-js/lib/utils/format.js'; +import os from 'os'; +import path from 'path'; +import { exit } from 'process'; +import { BN } from 'bn.js'; + +program + .description('Load the snapshot data on the contract during the initialization phase.') + .option('--contract ', 'Contract address to load the snapshot', process.env.CONTRACT) + .option('--json ', 'Path to the json snapshot', process.env.JSON_PATH) + .option('--network ', 'Testnet or Mainnet', process.env.NETWORK) + .option('--account ', 'Account from keystore to use', process.env.ACCOUNT) + .option('--start ', 'Start loading from butch X', 0); + +program.parse(process.argv); +const options = program.opts(); + +// 300TGAS +const GAS = "300000000000000"; +const DEPOSIT = parseNearAmount("2"); + + +let contractId = options.contract; +let jsonPath = options.json; +let network = options.network; +let accountId = options.account; +let index = options.start; + +const snapshotToContractRecord = (snapshotRecord) => ([snapshotRecord.account_id, { + active_months: snapshotRecord.active_months, + stake: snapshotRecord.stake +}]) + +function chunkArray(array, chunkSize, mapper) { + const chunks = []; + for (let i = 0; i < array.length; i += chunkSize) { + chunks.push(array.slice(i, i + chunkSize).map( + mapper + )); + } + return chunks; +} + +const snapshot = JSON.parse(fs.readFileSync(jsonPath, 'utf-8')).data; +const transactionsChunks = chunkArray(snapshot, 500, snapshotToContractRecord); + +// Load credentials; + +const homedir = os.homedir(); +const CREDENTIALS_DIR = ".near-credentials"; +const credentialsPath = path.join(homedir, CREDENTIALS_DIR); +const keyStore = new keyStores.UnencryptedFileSystemKeyStore(credentialsPath); + +let connectionConfig; + +if (network === "Mainnet") { + connectionConfig = { + networkId: "mainnet", + keyStore, + nodeUrl: "https://rpc.mainnet.near.org", + walletUrl: "https://wallet.mainnet.near.org", + helperUrl: "https://helper.mainnet.near.org", + explorerUrl: "https://nearblocks.io", + }; +} else { + connectionConfig = { + networkId: "testnet", + keyStore, + nodeUrl: "https://rpc.testnet.near.org", + walletUrl: "https://testnet.mynearwallet.com/", + helperUrl: "https://helper.testnet.near.org", + explorerUrl: "https://testnet.nearblocks.io", + }; +} +const nearConnection = await connect(connectionConfig); + +const account = await nearConnection.account(accountId); +const contract = new Contract(account, contractId, { + changeMethods: ['bulk_load_voters'], + viewMethods: ['get_status', 'get_total_eligible_users'] +}); + +let status = await contract.get_status(); +if (status.Initialization === undefined) { + console.error("Wrong contract state"); + exit(0); +} +console.log("Start loading data from the snapshot"); +console.log(`The contract is at ${status.Initialization} attempt`) + +for (let i = index; i < transactionsChunks.length; i++) { + let args = transactionsChunks[i]; + try { + const result = await contract.bulk_load_voters({ + voters: args + }, GAS, DEPOSIT); + console.log('Loaded butch', i, 'with', result); + } catch (e) { + console.log("Error:", e); + console.log("Error at index: ", i) + break; + } +} + +const total_on_contract = await contract.get_total_eligible_users(); +const total_in_snapshot = snapshot.length; + +console.log(`In contract: ${total_on_contract}`); +console.log(`In snapshot: ${total_in_snapshot}`); diff --git a/snapshotter/prepareSnapshot.js b/snapshotter/prepareSnapshot.js index 0cb24d9..1d71120 100644 --- a/snapshotter/prepareSnapshot.js +++ b/snapshotter/prepareSnapshot.js @@ -5,7 +5,7 @@ import { program } from 'commander'; import assert from 'assert'; program - .description('Load and process staking pools data from NEAR blockchain.') + .description('Combine staking pool and activity data to create snapshot.') .option('--block ', 'Block height of the snapshot', process.env.BLOCK) .option('--dbname ', 'Database name', process.env.DB_NAME) .option('--user ', 'Database user', process.env.DB_USER)