├── .dockerignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── client.rs ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── src │ ├── client │ │ ├── amount.rs │ │ ├── configuration.rs │ │ ├── entities │ │ │ ├── address.rs │ │ │ ├── asset.rs │ │ │ ├── case.rs │ │ │ ├── category.rs │ │ │ ├── mod.rs │ │ │ ├── network.rs │ │ │ └── reporter.rs │ │ ├── events.rs │ │ ├── implementations │ │ │ ├── evm │ │ │ │ ├── abi │ │ │ │ │ ├── HapiCore.json │ │ │ │ │ └── Token.json │ │ │ │ ├── client.rs │ │ │ │ ├── conversion.rs │ │ │ │ ├── error.rs │ │ │ │ ├── mod.rs │ │ │ │ └── token.rs │ │ │ ├── mod.rs │ │ │ ├── near │ │ │ │ ├── client.rs │ │ │ │ ├── conversion.rs │ │ │ │ ├── mod.rs │ │ │ │ └── token.rs │ │ │ └── solana │ │ │ │ ├── account_macro.rs │ │ │ │ ├── client.rs │ │ │ │ ├── conversion.rs │ │ │ │ ├── instruction_data.rs │ │ │ │ ├── instruction_decoder.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── test_helpers.rs │ │ │ │ ├── token.rs │ │ │ │ └── utils.rs │ │ ├── interface.rs │ │ ├── mod.rs │ │ ├── result.rs │ │ └── token.rs │ ├── commands.rs │ ├── commands │ │ ├── context.rs │ │ └── matcher.rs │ ├── lib.rs │ └── main.rs └── tests │ ├── assert.rs │ ├── cli-near.rs │ ├── cli-solana.rs │ ├── cli_evm.rs │ ├── cmd_utils.rs │ ├── common_fixtures.rs │ ├── evm │ ├── fixtures.rs │ ├── mod.rs │ ├── setup.rs │ └── util.rs │ ├── near │ ├── mod.rs │ └── setup.rs │ ├── solana │ ├── fixtures.rs │ ├── keys │ │ ├── token_1.json │ │ ├── token_2.json │ │ ├── wallet_1.json │ │ └── wallet_2.json │ ├── mod.rs │ ├── setup.rs │ └── validator_utils.rs │ └── util.rs ├── client.ts ├── .gitignore ├── package-lock.json ├── package.json ├── src │ ├── cmd.ts │ ├── implementations │ │ ├── evm │ │ │ ├── index.ts │ │ │ └── token.ts │ │ ├── index.ts │ │ ├── near.ts │ │ └── solana.ts │ ├── index.ts │ ├── interface.ts │ └── util.ts ├── test │ ├── helpers.ts │ ├── keys │ │ ├── token.json │ │ ├── wallet_1.json │ │ ├── wallet_2.json │ │ └── wallet_3.json │ ├── setup.ts │ └── solana_cli.test.ts └── tsconfig.json ├── docker ├── README.md ├── explorer │ └── Dockerfile └── indexer │ └── Dockerfile ├── evm ├── .gitignore ├── README.md ├── contracts │ ├── HapiCore.sol │ └── Token.sol ├── hardhat.config.ts ├── package-lock.json ├── package.json ├── test │ ├── hapi_core │ │ ├── address.ts │ │ ├── asset.ts │ │ ├── case.ts │ │ ├── configuration.ts │ │ ├── deployment.ts │ │ ├── reporter_mgmt.ts │ │ └── reporter_staking.ts │ ├── setup.ts │ └── util.ts └── tsconfig.json ├── explorer ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── configuration.sample.toml ├── src │ ├── application.rs │ ├── configuration.rs │ ├── entity │ │ ├── address │ │ │ ├── mod.rs │ │ │ ├── model.rs │ │ │ ├── query_utils.rs │ │ │ └── resolver.rs │ │ ├── asset │ │ │ ├── mod.rs │ │ │ ├── model.rs │ │ │ ├── query_utils.rs │ │ │ └── resolver.rs │ │ ├── case │ │ │ ├── mod.rs │ │ │ ├── model.rs │ │ │ ├── query_utils.rs │ │ │ └── resolver.rs │ │ ├── indexer.rs │ │ ├── mod.rs │ │ ├── network │ │ │ ├── mod.rs │ │ │ ├── model.rs │ │ │ ├── query_utils.rs │ │ │ └── resolver.rs │ │ ├── pagination.rs │ │ ├── reporter │ │ │ ├── mod.rs │ │ │ ├── model.rs │ │ │ ├── query_utils.rs │ │ │ └── resolver.rs │ │ ├── statistics.rs │ │ └── types.rs │ ├── error.rs │ ├── lib.rs │ ├── main.rs │ ├── migrations │ │ ├── m20231127_140636_create_address.rs │ │ ├── m20231127_160838_create_asset.rs │ │ ├── m20231127_161317_create_reporter.rs │ │ ├── m20231127_162130_create_case.rs │ │ ├── m20231127_162603_create_category_type.rs │ │ ├── m20231127_165849_create_reporter_role_type.rs │ │ ├── m20231127_170357_create_reporter_status_type.rs │ │ ├── m20231127_170630_create_case_status_type.rs │ │ ├── m20231205_131413_create_indexer.rs │ │ ├── m20231205_131413_create_network.rs │ │ ├── m20231211_164133_create_network_backend.rs │ │ └── mod.rs │ ├── observability │ │ ├── metrics_setup.rs │ │ ├── mod.rs │ │ └── tracing_setup.rs │ ├── server │ │ ├── app_server.rs │ │ ├── handlers │ │ │ ├── events.rs │ │ │ ├── graphql.rs │ │ │ ├── health.rs │ │ │ ├── indexer.rs │ │ │ ├── jwt_auth.rs │ │ │ ├── mod.rs │ │ │ └── stats.rs │ │ ├── mod.rs │ │ └── schema.rs │ └── service │ │ ├── mod.rs │ │ ├── mutation.rs │ │ └── query.rs └── tests │ ├── cli │ └── mod.rs │ ├── graphql │ ├── address_query.rs │ ├── asset_query.rs │ ├── case_query.rs │ ├── mod.rs │ ├── network_query.rs │ ├── reporter_query.rs │ └── statistics_query.rs │ ├── helpers │ ├── jwt.rs │ ├── mod.rs │ ├── request_sender.rs │ ├── test_app.rs │ └── test_data.rs │ ├── mod.rs │ └── routes │ ├── cors.rs │ ├── health_check.rs │ ├── indexer.rs │ ├── metrics.rs │ ├── mod.rs │ └── webhook_processing.rs ├── indexer ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── configuration.sample.toml ├── src │ ├── configuration.rs │ ├── indexer │ │ ├── client │ │ │ ├── evm.rs │ │ │ ├── indexer_client.rs │ │ │ ├── mod.rs │ │ │ ├── near.rs │ │ │ └── solana.rs │ │ ├── heartbeat.rs │ │ ├── jobs.rs │ │ ├── jwt.rs │ │ ├── logic.rs │ │ ├── mod.rs │ │ ├── persistence.rs │ │ ├── push.rs │ │ ├── server.rs │ │ └── state.rs │ ├── lib.rs │ ├── main.rs │ └── observability.rs └── tests │ ├── indexer_test.rs │ ├── jwt.rs │ ├── mocks │ ├── evm_mock.rs │ ├── mod.rs │ ├── near_mock.rs │ ├── solana_mock.rs │ └── webhook_mock.rs │ └── simple_listener.rs ├── near ├── .gitignore ├── Cargo.toml ├── Dockerfile ├── README.md ├── README_contract.md ├── build_docker.sh ├── contract │ ├── Cargo.toml │ ├── LICENSE │ ├── build_local.sh │ ├── src │ │ ├── address │ │ │ ├── management.rs │ │ │ ├── mod.rs │ │ │ ├── v_address.rs │ │ │ └── view.rs │ │ ├── assets │ │ │ ├── management.rs │ │ │ ├── mod.rs │ │ │ ├── v_asset.rs │ │ │ └── view.rs │ │ ├── case │ │ │ ├── management.rs │ │ │ ├── mod.rs │ │ │ ├── v_case.rs │ │ │ └── view.rs │ │ ├── configuration.rs │ │ ├── errors.rs │ │ ├── lib.rs │ │ ├── reporter │ │ │ ├── management.rs │ │ │ ├── mod.rs │ │ │ ├── v_reporter.rs │ │ │ └── view.rs │ │ ├── reward │ │ │ └── mod.rs │ │ ├── stake │ │ │ └── mod.rs │ │ ├── token_transferer.rs │ │ └── utils.rs │ └── tests │ │ ├── address │ │ ├── helpers.rs │ │ └── mod.rs │ │ ├── asset │ │ ├── helpers.rs │ │ └── mod.rs │ │ ├── case │ │ ├── helpers.rs │ │ └── mod.rs │ │ ├── configuration │ │ ├── helpers.rs │ │ └── mod.rs │ │ ├── context │ │ └── mod.rs │ │ ├── errors.rs │ │ ├── lib.rs │ │ ├── reporter │ │ ├── helpers.rs │ │ └── mod.rs │ │ └── utils │ │ ├── execution_extension.rs │ │ ├── mod.rs │ │ ├── token_extension.rs │ │ └── utils.rs └── res │ ├── fungible_token.wasm │ └── hapi_core_near.wasm ├── solana ├── .gitignore ├── .prettierignore ├── Anchor.toml ├── Cargo.lock ├── Cargo.toml ├── README.md ├── lib │ ├── buffer.ts │ ├── enums.ts │ ├── hapi-core.ts │ └── index.ts ├── migrations │ └── deploy.ts ├── package-lock.json ├── package.json ├── programs │ └── hapi_core_solana │ │ ├── Cargo.toml │ │ ├── LICENSE │ │ ├── Xargo.toml │ │ └── src │ │ ├── context.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ └── state │ │ ├── address.rs │ │ ├── asset.rs │ │ ├── case.rs │ │ ├── confirmation.rs │ │ ├── mod.rs │ │ ├── network.rs │ │ ├── reporter.rs │ │ └── utils.rs ├── scripts │ ├── create-network.ts │ └── utils.ts ├── tests │ ├── address.spec.ts │ ├── asset.spec.ts │ ├── case.spec.ts │ ├── general.spec.ts │ ├── network.spec.ts │ ├── reporter.spec.ts │ ├── test_keypair.json │ ├── test_script.sh │ └── util │ │ ├── console.ts │ │ ├── crypto.ts │ │ ├── error.ts │ │ ├── setup.ts │ │ └── token.ts ├── tsconfig.json └── yarn.lock └── solana_legacy ├── .clippy.toml ├── .eslintignore ├── .eslintrc ├── .gitignore ├── Anchor.toml ├── Cargo.lock ├── Cargo.toml ├── README.md ├── lib ├── buffer.ts ├── enums.ts ├── hapi-core.ts ├── idl │ └── hapi_core.ts └── index.ts ├── migrations ├── deploy.ts └── program-migrations │ ├── Cargo.toml │ ├── README.md │ ├── config.sample.yaml │ └── src │ ├── cli.rs │ ├── configuration.rs │ ├── main.rs │ └── migration_list.rs ├── npm-shrinkwrap.json ├── package.json ├── programs └── hapi-core │ ├── Cargo.toml │ ├── Xargo.toml │ └── src │ ├── checker │ ├── address_data.rs │ └── mod.rs │ ├── context.rs │ ├── error.rs │ ├── lib.rs │ ├── state │ ├── address.rs │ ├── asset.rs │ ├── case.rs │ ├── community.rs │ ├── deprecated │ │ ├── deprecated_address.rs │ │ ├── deprecated_asset.rs │ │ ├── deprecated_case.rs │ │ ├── deprecated_community.rs │ │ ├── deprecated_network.rs │ │ ├── deprecated_reporter.rs │ │ ├── deprecated_reporter_reward.rs │ │ └── mod.rs │ ├── mod.rs │ ├── network.rs │ └── reporter.rs │ └── utils.rs ├── rollup.config.js ├── tests ├── hapi-core │ ├── address.spec.ts │ ├── asset.spec.ts │ ├── case.spec.ts │ ├── community.spec.ts │ ├── general.spec.ts │ ├── network.spec.ts │ └── reporter.spec.ts ├── lib │ └── buffer.spec.ts └── util │ ├── console.ts │ ├── crypto.ts │ ├── error.ts │ └── token.ts └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | explorer/target 2 | client.rs/target 3 | indexer/target 4 | solana/target 5 | near/target -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 HAPI Protocol. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HAPI Core 2 | 3 | HAPI Core is a set of contracts and libraries to interact with them. 4 | 5 | ## HAPI Protocol 6 | 7 | If you want to know more about HAPI Protocol, please visit the [official site](https://hapi.one/) and our [gitbook](https://hapi-one.gitbook.io/hapi-protocol). 8 | 9 | ## Contribution 10 | 11 | If you want to propose any changes to this smart contract, please visit our [governance forum](https://gov.hapi.one/). Suggestions for the client library enhancements are welcome. 12 | -------------------------------------------------------------------------------- /client.rs/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .vscode 3 | -------------------------------------------------------------------------------- /client.rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hapi-core" 3 | authors = ["HAPI "] 4 | version = "0.3.0" 5 | edition = "2021" 6 | description = "Rust Client for HAPI Protocol contracts" 7 | license-file = "LICENSE" 8 | repository = "https://github.com/HAPIprotocol/hapi-core" 9 | homepage = "https://hapi.one" 10 | 11 | [lib] 12 | crate-type = ["lib"] 13 | name = "hapi_core" 14 | 15 | [features] 16 | decode = [] 17 | 18 | [dependencies] 19 | async-trait = "0.1.72" 20 | thiserror = "1.0.44" 21 | tokio = { version = "1.28.0", features = ["full"] } 22 | anyhow = { version = "1.0.72" } 23 | clap = { version = "4.3.19", features = ["env"] } 24 | regex = "1.9.1" 25 | serde = "1.0.177" 26 | serde_json = "1.0.104" 27 | uuid = { version = "1.4.1", features = ["serde"] } 28 | enum_extract = "0.1" 29 | dirs = "5.0.1" 30 | borsh = { version = "0.10.3" } 31 | bs58 = "0.5.0" 32 | sha2 = "0.10.7" 33 | 34 | # EVM dependencies 35 | ethers = "=2.0.8" 36 | ethers-providers = "=2.0.8" 37 | ethers-signers = "=2.0.8" 38 | ethers-contract = "=2.0.8" 39 | ethers-core = "=2.0.8" 40 | 41 | # NEAR dependencies 42 | 43 | near-crypto = "0.19.0" 44 | near-primitives = "0.19.0" 45 | near-chain-configs = "0.19.0" 46 | near-jsonrpc-client = "0.7.0" 47 | near-jsonrpc-primitives = "0.19.0" 48 | hapi-core-near = "0.3.0" 49 | 50 | # TODO: Replace with the latest stable version of near-sdk-rs 51 | near-sdk = "5.0.0-alpha.2" 52 | 53 | # Solana dependencies 54 | 55 | [dependencies.hapi-core-solana] 56 | path = "../solana/programs/hapi_core_solana" 57 | version = "0.3.0" 58 | 59 | [dependencies.anchor-client] 60 | git = "https://github.com/hlgltvnnk/anchor.git" 61 | branch = "update-dependencies" 62 | features = ["async"] 63 | 64 | [dependencies.solana-cli-config] 65 | git = "https://github.com/hlgltvnnk/solana.git" 66 | branch = "update-dependencies" 67 | 68 | [dependencies.solana-account-decoder] 69 | git = "https://github.com/hlgltvnnk/solana.git" 70 | branch = "update-dependencies" 71 | 72 | [dependencies.solana-transaction-status] 73 | git = "https://github.com/hlgltvnnk/solana.git" 74 | branch = "update-dependencies" 75 | 76 | [dependencies.spl-associated-token-account] 77 | git = "https://github.com/hlgltvnnk/solana-program-library.git" 78 | 79 | [dependencies.spl-token] 80 | git = "https://github.com/hlgltvnnk/solana-program-library.git" 81 | 82 | [dev-dependencies] 83 | bs58 = "0.5.0" 84 | -------------------------------------------------------------------------------- /client.rs/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 HAPI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /client.rs/src/client/amount.rs: -------------------------------------------------------------------------------- 1 | use ethers::types::U256; 2 | use near_sdk::json_types::U128; 3 | use serde::{de, Deserialize, Serialize}; 4 | use std::str::FromStr; 5 | 6 | #[derive(Default, Clone, Debug, PartialEq, PartialOrd, Eq)] 7 | pub struct Amount(U256); 8 | 9 | impl Amount { 10 | pub fn normalize_to_u64(&self, decimals: usize) -> u64 { 11 | let unit: U256 = U256::exp10(decimals); 12 | 13 | (self.0 / unit).as_u64() 14 | } 15 | } 16 | 17 | impl Serialize for Amount { 18 | fn serialize(&self, serializer: S) -> Result { 19 | self.0.to_string().serialize(serializer) 20 | } 21 | } 22 | 23 | impl<'de> Deserialize<'de> for Amount { 24 | fn deserialize>(deserializer: D) -> Result { 25 | let s = String::deserialize(deserializer)?; 26 | U256::from_dec_str(&s) 27 | .map(Amount) 28 | .map_err(de::Error::custom) 29 | } 30 | } 31 | 32 | impl std::fmt::Display for Amount { 33 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 34 | self.0.fmt(f) 35 | } 36 | } 37 | 38 | impl From for Amount { 39 | fn from(value: U256) -> Self { 40 | Self(value) 41 | } 42 | } 43 | 44 | impl From for Amount { 45 | fn from(value: u64) -> Self { 46 | Self(value.into()) 47 | } 48 | } 49 | 50 | impl From for Amount { 51 | fn from(value: U128) -> Self { 52 | Self(value.0.into()) 53 | } 54 | } 55 | 56 | impl From for U256 { 57 | fn from(value: Amount) -> Self { 58 | value.0 59 | } 60 | } 61 | 62 | impl From for U128 { 63 | fn from(value: Amount) -> Self { 64 | U128(value.0.as_u128()) 65 | } 66 | } 67 | 68 | impl From for u64 { 69 | fn from(value: Amount) -> Self { 70 | value.0.as_u64() 71 | } 72 | } 73 | 74 | impl FromStr for Amount { 75 | type Err = anyhow::Error; 76 | 77 | fn from_str(s: &str) -> Result { 78 | Ok(Self(U256::from_dec_str(s)?)) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /client.rs/src/client/configuration.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::amount::Amount; 4 | 5 | #[derive(Default, Clone, Debug, Serialize, Deserialize)] 6 | pub struct StakeConfiguration { 7 | pub token: String, 8 | pub unlock_duration: u64, 9 | pub validator_stake: Amount, 10 | pub tracer_stake: Amount, 11 | pub publisher_stake: Amount, 12 | pub authority_stake: Amount, 13 | } 14 | 15 | #[derive(Default, Clone, Debug, Serialize, Deserialize)] 16 | pub struct RewardConfiguration { 17 | pub token: String, 18 | pub address_confirmation_reward: Amount, 19 | pub address_tracer_reward: Amount, 20 | pub asset_confirmation_reward: Amount, 21 | pub asset_tracer_reward: Amount, 22 | } 23 | -------------------------------------------------------------------------------- /client.rs/src/client/entities/address.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use uuid::Uuid; 3 | 4 | use super::category::Category; 5 | 6 | #[derive(Clone, Debug, Serialize, Deserialize)] 7 | pub struct CreateAddressInput { 8 | pub address: String, 9 | pub case_id: Uuid, 10 | pub risk: u8, 11 | pub category: Category, 12 | } 13 | 14 | #[derive(Clone, Debug, Serialize, Deserialize)] 15 | pub struct UpdateAddressInput { 16 | pub address: String, 17 | pub case_id: Uuid, 18 | pub risk: u8, 19 | pub category: Category, 20 | } 21 | 22 | #[derive(Clone, Debug, Serialize, Deserialize)] 23 | pub struct ConfirmAddressInput { 24 | pub address: String, 25 | } 26 | 27 | #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq)] 28 | pub struct Address { 29 | pub address: String, 30 | pub case_id: Uuid, 31 | pub reporter_id: Uuid, 32 | pub risk: u8, 33 | pub category: Category, 34 | pub confirmations: u64, 35 | } 36 | -------------------------------------------------------------------------------- /client.rs/src/client/entities/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod address; 2 | pub mod asset; 3 | pub mod case; 4 | pub mod category; 5 | pub mod network; 6 | pub mod reporter; 7 | -------------------------------------------------------------------------------- /client.rs/src/client/entities/network.rs: -------------------------------------------------------------------------------- 1 | use { 2 | serde::{ 3 | de::{self, Visitor}, 4 | Deserialize, Serialize, 5 | }, 6 | std::{fmt, str::FromStr}, 7 | }; 8 | 9 | #[derive(Serialize, Default, Debug, Clone, PartialEq)] 10 | pub enum HapiCoreNetwork { 11 | #[default] 12 | Sepolia, 13 | Ethereum, 14 | Bsc, 15 | Solana, 16 | Bitcoin, 17 | Near, 18 | } 19 | 20 | impl FromStr for HapiCoreNetwork { 21 | type Err = anyhow::Error; 22 | 23 | fn from_str(s: &str) -> Result { 24 | match s { 25 | "Sepolia" | "sepolia" => Ok(Self::Sepolia), 26 | "Ethereum" | "ethereum" => Ok(Self::Ethereum), 27 | "Bsc" | "bsc" => Ok(Self::Bsc), 28 | "Solana" | "solana" => Ok(Self::Solana), 29 | "Bitcoin" | "bitcoin" => Ok(Self::Bitcoin), 30 | "Near" | "near" => Ok(Self::Near), 31 | _ => Err(anyhow::anyhow!("Invalid network: {}", s)), 32 | } 33 | } 34 | } 35 | 36 | // Solana network naming is related to this 37 | impl fmt::Display for HapiCoreNetwork { 38 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 39 | match self { 40 | HapiCoreNetwork::Sepolia => write!(f, "sepolia"), 41 | HapiCoreNetwork::Ethereum => write!(f, "ethereum"), 42 | HapiCoreNetwork::Bsc => write!(f, "bsc"), 43 | HapiCoreNetwork::Solana => write!(f, "solana"), 44 | HapiCoreNetwork::Bitcoin => write!(f, "bitcoin"), 45 | HapiCoreNetwork::Near => write!(f, "near"), 46 | } 47 | } 48 | } 49 | 50 | struct HapiCoreNetworkVisitor; 51 | 52 | impl<'de> Visitor<'de> for HapiCoreNetworkVisitor { 53 | type Value = HapiCoreNetwork; 54 | 55 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 56 | formatter.write_str("a valid string for HapiCoreNetwork") 57 | } 58 | 59 | fn visit_str(self, value: &str) -> Result { 60 | HapiCoreNetwork::from_str(value).map_err(E::custom) 61 | } 62 | } 63 | 64 | impl<'de> Deserialize<'de> for HapiCoreNetwork { 65 | fn deserialize>(deserializer: D) -> Result { 66 | deserializer.deserialize_str(HapiCoreNetworkVisitor) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /client.rs/src/client/implementations/evm/error.rs: -------------------------------------------------------------------------------- 1 | use ethers_contract::ContractError; 2 | 3 | use crate::client::result::ClientError; 4 | 5 | pub(super) fn map_ethers_error( 6 | caller: &str, 7 | e: ContractError, 8 | ) -> ClientError { 9 | match e { 10 | ContractError::Revert(e) => match e { 11 | _ if e.is_empty() => { 12 | ClientError::Ethers(format!("`{caller}` reverted with empty message")) 13 | } 14 | // TODO: get rid of black magic parsing 15 | _ if e.len() > 64 => ClientError::Ethers(format!( 16 | "`{caller}` reverted with: {}", 17 | String::from_utf8_lossy(&e[64..]) 18 | .chars() 19 | .filter(|c| !c.is_control()) 20 | .collect::() 21 | )), 22 | e => ClientError::Ethers(format!( 23 | "`{caller}` reverted with: {}", 24 | String::from_utf8_lossy(&e) 25 | .chars() 26 | .filter(|c| !c.is_control()) 27 | .collect::() 28 | )), 29 | }, 30 | _ => ClientError::Ethers(format!("`{caller}` failed: {e}")), 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client.rs/src/client/implementations/evm/mod.rs: -------------------------------------------------------------------------------- 1 | mod client; 2 | mod conversion; 3 | mod error; 4 | pub mod token; 5 | 6 | pub use client::{HapiCoreEvm, LogHeader}; 7 | pub use token::TokenContractEvm; 8 | -------------------------------------------------------------------------------- /client.rs/src/client/implementations/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod evm; 2 | pub mod near; 3 | pub mod solana; 4 | 5 | pub use evm::{token::TokenContractEvm, HapiCoreEvm}; 6 | pub use near::{HapiCoreNear, TokenContractNear}; 7 | pub use solana::{HapiCoreSolana, TokenContractSolana}; 8 | -------------------------------------------------------------------------------- /client.rs/src/client/implementations/near/mod.rs: -------------------------------------------------------------------------------- 1 | mod client; 2 | mod conversion; 3 | mod token; 4 | 5 | pub use client::{ 6 | HapiCoreNear, DELAY_AFTER_TX_EXECUTION, PERIOD_CHECK_TX_STATUS, TRANSACTION_TIMEOUT, 7 | }; 8 | pub use token::TokenContractNear; 9 | 10 | pub const GAS_FOR_TX: u64 = 50_000_000_000_000; // 50 TeraGas 11 | -------------------------------------------------------------------------------- /client.rs/src/client/implementations/solana/account_macro.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! get_solana_account { 3 | ($self:expr, $address:expr, $account:ident) => { 4 | <$account>::try_from( 5 | $self 6 | .get_account_data::($address) 7 | .await?, 8 | ) 9 | }; 10 | } 11 | 12 | #[macro_export] 13 | macro_rules! get_solana_accounts { 14 | ($self:expr, $account:ident) => {{ 15 | let data = $self 16 | .get_accounts::(hapi_core_solana::$account::LEN) 17 | .await?; 18 | 19 | let mut result: Vec<$account> = vec![]; 20 | 21 | for (_, acc) in data { 22 | if acc.network == $self.network { 23 | result.push(<$account>::try_from(acc)?); 24 | } 25 | } 26 | 27 | Ok(result) 28 | }}; 29 | } 30 | 31 | #[macro_export] 32 | macro_rules! get_solana_account_count { 33 | ($self:expr, $account:ident) => {{ 34 | let accounts: Result> = get_solana_accounts!($self, $account); 35 | Ok(accounts?.len() as u64) 36 | }}; 37 | } 38 | -------------------------------------------------------------------------------- /client.rs/src/client/implementations/solana/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod account_macro; 2 | mod client; 3 | mod conversion; 4 | mod instruction_data; 5 | pub mod instruction_decoder; 6 | pub mod token; 7 | mod utils; 8 | 9 | pub mod test_helpers; 10 | pub use test_helpers::create_test_tx; 11 | 12 | pub use client::HapiCoreSolana; 13 | pub use token::TokenContractSolana; 14 | 15 | pub use instruction_data::{DecodedInstructionData, InstructionData}; 16 | pub use instruction_decoder::DecodedInstruction; 17 | pub use utils::{byte_array_from_str, get_network_address}; 18 | -------------------------------------------------------------------------------- /client.rs/src/client/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod amount; 2 | pub mod configuration; 3 | pub mod entities; 4 | pub mod events; 5 | pub mod implementations; 6 | pub mod interface; 7 | pub mod result; 8 | pub mod token; 9 | 10 | pub use implementations::*; 11 | -------------------------------------------------------------------------------- /client.rs/src/client/token.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | 3 | use super::{ 4 | amount::Amount, 5 | result::{Result, Tx}, 6 | }; 7 | 8 | #[async_trait] 9 | pub trait TokenContract { 10 | /// Whether the HAPI Core contract requires approval before token transfer 11 | fn is_approve_needed(&self) -> bool; 12 | 13 | /// Transfer tokens to another address 14 | async fn transfer(&self, to: &str, amount: Amount) -> Result; 15 | 16 | /// Approve another address to spend tokens 17 | async fn approve(&self, spender: &str, amount: Amount) -> Result; 18 | 19 | /// Get the amount of tokens on this address 20 | async fn balance(&self, addr: &str) -> Result; 21 | } 22 | -------------------------------------------------------------------------------- /client.rs/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | 3 | pub use client::{ 4 | amount::Amount, 5 | entities::network::HapiCoreNetwork, 6 | implementations::{ 7 | HapiCoreEvm, HapiCoreNear, HapiCoreSolana, TokenContractEvm, TokenContractNear, 8 | TokenContractSolana, 9 | }, 10 | interface::{HapiCore, HapiCoreOptions}, 11 | token::TokenContract, 12 | }; 13 | -------------------------------------------------------------------------------- /client.rs/tests/cmd_utils.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | #[derive(Debug)] 4 | pub struct CmdOutput { 5 | pub success: bool, 6 | pub stdout: String, 7 | pub stderr: String, 8 | } 9 | 10 | pub fn ensure_cmd(command: &mut Command) -> anyhow::Result<()> { 11 | let output = command.output(); 12 | 13 | println!( 14 | "Exec: {} {}", 15 | command.get_program().to_string_lossy(), 16 | command 17 | .get_args() 18 | .map(|s| format!("\"{}\"", s.to_string_lossy())) 19 | .collect::>() 20 | .join(" ") 21 | ); 22 | 23 | if let Err(e) = output { 24 | panic!("Failed to execute command: {e}"); 25 | } 26 | 27 | let output = output.unwrap(); 28 | 29 | let stderr = String::from_utf8_lossy(&output.stderr); 30 | let stdout = String::from_utf8_lossy(&output.stdout); 31 | 32 | if !stderr.trim().is_empty() { 33 | println!("STDERR:\n{stderr}"); 34 | } 35 | if !stdout.trim().is_empty() { 36 | println!("STDOUT:\n{stdout}"); 37 | } 38 | 39 | if !output.status.success() { 40 | return Err(anyhow::anyhow!("Failed to execute command {:?}", command)); 41 | } 42 | 43 | Ok(()) 44 | } 45 | 46 | pub fn wrap_cmd(command: &mut Command) -> anyhow::Result { 47 | let output = command.output()?; 48 | 49 | println!( 50 | "Exec: {} {}", 51 | command.get_program().to_string_lossy(), 52 | command 53 | .get_args() 54 | .map(|s| format!("\"{}\"", s.to_string_lossy())) 55 | .collect::>() 56 | .join(" ") 57 | ); 58 | 59 | let stderr = String::from_utf8_lossy(&output.stderr); 60 | let stdout = String::from_utf8_lossy(&output.stdout); 61 | 62 | Ok(CmdOutput { 63 | success: output.status.success(), 64 | stdout: stdout.trim().to_owned(), 65 | stderr: stderr.trim().to_owned(), 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /client.rs/tests/common_fixtures.rs: -------------------------------------------------------------------------------- 1 | pub const REPORTER_UUID_1: &str = "0ca77383-0d2a-4090-98c9-31b69e105b12"; 2 | pub const REPORTER_UUID_2: &str = "2ef0a8f9-66c8-4be2-981a-b9236bb43f61"; 3 | 4 | pub const CASE_UUID_1: &str = "34b9d809-7511-46c5-9117-c7f80f379fad"; 5 | pub const CASE_NAME_1: &str = "HAPI Case 1"; 6 | pub const CASE_URL_1: &str = "https://hapi.one/case/1"; 7 | 8 | pub const ADDRESS_ADDR_1: &str = "0x9e833a87087efd527b1a842742eb0f3548cd82ab"; 9 | pub const ADDRESS_RISK_1: &str = "5"; 10 | pub const ADDRESS_CATEGORY_1: &str = "Ransomware"; 11 | 12 | pub const ASSET_ADDR_1: &str = "0xe9dbfa9e9d48393d9d22de10051dcbd91267b756"; 13 | pub const ASSET_ID_1: &str = "1"; 14 | pub const ASSET_RISK_1: &str = "7"; 15 | pub const ASSET_CATEGORY_1: &str = "Counterfeit"; 16 | -------------------------------------------------------------------------------- /client.rs/tests/evm/fixtures.rs: -------------------------------------------------------------------------------- 1 | pub const PUBLIC_KEY_1: &str = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"; 2 | pub const PRIVATE_KEY_1: &str = 3 | "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; 4 | 5 | pub const PUBLIC_KEY_2: &str = "0x70997970c51812dc3a010c7d01b50e0d17dc79c8"; 6 | pub const PRIVATE_KEY_2: &str = 7 | "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"; 8 | -------------------------------------------------------------------------------- /client.rs/tests/evm/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod fixtures; 2 | pub mod setup; 3 | pub mod util; 4 | -------------------------------------------------------------------------------- /client.rs/tests/evm/util.rs: -------------------------------------------------------------------------------- 1 | use ethers::{types::H160, utils::to_checksum as ethers_to_checksum}; 2 | use std::{net::TcpStream, thread, time::Duration}; 3 | 4 | pub fn to_checksum(value: &str) -> String { 5 | ethers_to_checksum(&value.parse::().expect("invalid address"), None) 6 | } 7 | 8 | pub fn wait_for_port(port: u16) { 9 | let addr = format!("localhost:{}", port); 10 | loop { 11 | thread::sleep(Duration::from_secs(1)); 12 | match TcpStream::connect(&addr) { 13 | Ok(_) => { 14 | println!("Successfully connected to the server at {}", &addr); 15 | break; 16 | } 17 | Err(e) => { 18 | println!("Failed to connect: {}", e); 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /client.rs/tests/near/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod setup; 2 | -------------------------------------------------------------------------------- /client.rs/tests/solana/fixtures.rs: -------------------------------------------------------------------------------- 1 | pub const VALIDATOR_PORT: u16 = 8899; 2 | 3 | pub const NETWORK: &str = "solana"; 4 | 5 | pub const PROGRAM_DIR: &str = "../solana"; 6 | pub const PROGRAM_NAME: &str = "hapi_core_solana"; 7 | pub const HAPI_CORE_KEYPAIR: &str = "tests/test_keypair.json"; 8 | pub const CONTRACT_ADDRESS: &str = "FgE5ySSi6fbnfYGGRyaeW8y6p8A5KybXPyQ2DdxPCNRk"; 9 | 10 | pub const KEYS_DIR: &str = "tests/solana/keys"; 11 | 12 | pub const STAKE_MINT_KEYPAIR: &str = "token_1.json"; 13 | pub const REWAED_MINT_KEYPAIR: &str = "token_2.json"; 14 | pub const AUTHORITY_KEYPAIR: &str = "wallet_1.json"; 15 | pub const PUBLISHER_KEYPAIR: &str = "wallet_2.json"; 16 | 17 | pub const ADDRESS_ADDR_1: &str = "8aqiaHSdGHcwnJQPJo95JqB2hPv4vzfuwc2zgAYHTWXz"; 18 | pub const ASSET_ADDR_1: &str = "6t4vnZsH5X8zcGm5Z5ZzY6TqHrsPtJcjWcNzL892XP37"; 19 | -------------------------------------------------------------------------------- /client.rs/tests/solana/keys/token_1.json: -------------------------------------------------------------------------------- 1 | [220,45,86,59,26,204,47,228,111,92,150,7,113,33,117,110,198,106,70,92,194,70,188,22,18,169,181,184,51,106,30,77,7,133,168,151,114,134,53,187,214,61,182,253,40,121,174,212,91,52,8,99,90,146,141,98,178,163,228,196,226,33,183,143] -------------------------------------------------------------------------------- /client.rs/tests/solana/keys/token_2.json: -------------------------------------------------------------------------------- 1 | [206,81,227,93,114,154,165,212,118,234,204,107,42,224,219,237,225,255,37,37,123,194,33,90,154,233,14,180,43,225,45,191,31,229,238,215,103,61,36,252,125,208,18,173,12,49,12,42,158,240,31,157,48,165,178,192,62,246,129,236,224,120,224,56] -------------------------------------------------------------------------------- /client.rs/tests/solana/keys/wallet_1.json: -------------------------------------------------------------------------------- 1 | [139,229,82,171,250,163,189,84,115,163,243,142,169,178,73,153,165,129,131,243,237,53,36,1,198,203,56,228,202,57,215,108,5,242,129,51,64,223,9,165,109,10,149,40,170,29,152,130,83,133,108,200,104,101,6,155,151,147,137,245,196,59,143,253] -------------------------------------------------------------------------------- /client.rs/tests/solana/keys/wallet_2.json: -------------------------------------------------------------------------------- 1 | [184,247,99,224,131,37,127,73,95,3,238,192,43,232,163,9,9,16,76,111,32,207,170,101,99,52,147,47,186,41,253,33,165,8,6,86,48,15,233,234,112,226,71,227,94,9,16,207,57,233,124,24,2,37,239,49,40,186,87,154,133,50,215,128] -------------------------------------------------------------------------------- /client.rs/tests/solana/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod fixtures; 2 | pub mod setup; 3 | mod validator_utils; 4 | -------------------------------------------------------------------------------- /client.rs/tests/util.rs: -------------------------------------------------------------------------------- 1 | use std::{net::TcpStream, thread, time::Duration}; 2 | 3 | pub fn wait_for_port(port: u16) { 4 | let addr = format!("localhost:{}", port); 5 | let mut count = 0; 6 | loop { 7 | thread::sleep(Duration::from_secs(1)); 8 | match TcpStream::connect(&addr) { 9 | Ok(_) => { 10 | println!("Successfully connected to the server at {}", &addr); 11 | break; 12 | } 13 | Err(e) => { 14 | println!("Failed to connect: {}", e); 15 | count += 1; 16 | if count > 10 { 17 | panic!("Failed to connect to the server at {}", &addr); 18 | } 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /client.ts/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /client.ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-core", 3 | "version": "0.1.0", 4 | "description": "HAPI Core Typescript/Javascript client", 5 | "main": "index.js", 6 | "scripts": { 7 | "cmd": "ts-node src/cmd", 8 | "test": "mocha --timeout 20000 --require ts-node/register ./test/*.test.ts" 9 | }, 10 | "author": "HAPI Protocol ", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@coral-xyz/anchor": "^0.28.0", 14 | "@solana/spl-token": "^0.3.8", 15 | "@types/node": "^20.4.6", 16 | "@types/uuid": "^9.0.2", 17 | "@types/yargs": "^17.0.24", 18 | "chai": "^4.3.7", 19 | "chalk": "^4.1.2", 20 | "ethers": "^6.7.0", 21 | "hapi-core-evm": "file:../evm", 22 | "mocha": "^10.2.0", 23 | "ts-node": "^10.9.1", 24 | "uuid": "^9.0.0", 25 | "yargs": "^17.7.2" 26 | }, 27 | "devDependencies": { 28 | "@types/mocha": "^10.0.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client.ts/src/implementations/evm/token.ts: -------------------------------------------------------------------------------- 1 | import { Signer, Provider, Contract } from "ethers"; 2 | 3 | export function getTokenContract( 4 | tokenAddress: string, 5 | provider: Signer | Provider 6 | ) { 7 | const abi = [ 8 | // ERC20 Optional 9 | "function name() view returns (string)", 10 | "function symbol() view returns (string)", 11 | 12 | // ERC20 Required 13 | "function totalSupply() view returns (uint256)", 14 | "function balanceOf(address) view returns (uint256)", 15 | "function transfer(address, uint256) returns (boolean)", 16 | "function allowance(address, address) view returns (uint256)", 17 | "function approve(address, uint256) returns (boolean)", 18 | "function transferFrom(address, address, uint256) returns (boolean)", 19 | "event Transfer(address indexed from, address indexed to, uint256 value)", 20 | "event Approval(address indexed owner, address indexed spender, uint256 value)", 21 | ]; 22 | 23 | return new Contract(tokenAddress, abi, provider); 24 | } 25 | -------------------------------------------------------------------------------- /client.ts/src/implementations/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./evm"; 2 | export * from "./near"; 3 | export * from "./solana"; 4 | -------------------------------------------------------------------------------- /client.ts/src/index.ts: -------------------------------------------------------------------------------- 1 | import { HapiCoreEvm, HapiCoreNear, HapiCoreSolana } from "./implementations"; 2 | import { HapiCoreConnectionOptions, HapiCoreNetwork } from "./interface"; 3 | 4 | export function connectHapiCore(options: HapiCoreConnectionOptions) { 5 | switch (options.network) { 6 | case HapiCoreNetwork.Ethereum || HapiCoreNetwork.BSC: 7 | return new HapiCoreEvm(options); 8 | case HapiCoreNetwork.Solana || HapiCoreNetwork.Bitcoin: 9 | return new HapiCoreSolana(options); 10 | case HapiCoreNetwork.NEAR: 11 | return new HapiCoreNear(options); 12 | default: 13 | throw new Error(`Unsupported network: ${options.network}`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client.ts/test/keys/token.json: -------------------------------------------------------------------------------- 1 | [220,45,86,59,26,204,47,228,111,92,150,7,113,33,117,110,198,106,70,92,194,70,188,22,18,169,181,184,51,106,30,77,7,133,168,151,114,134,53,187,214,61,182,253,40,121,174,212,91,52,8,99,90,146,141,98,178,163,228,196,226,33,183,143] -------------------------------------------------------------------------------- /client.ts/test/keys/wallet_1.json: -------------------------------------------------------------------------------- 1 | [139,229,82,171,250,163,189,84,115,163,243,142,169,178,73,153,165,129,131,243,237,53,36,1,198,203,56,228,202,57,215,108,5,242,129,51,64,223,9,165,109,10,149,40,170,29,152,130,83,133,108,200,104,101,6,155,151,147,137,245,196,59,143,253] -------------------------------------------------------------------------------- /client.ts/test/keys/wallet_2.json: -------------------------------------------------------------------------------- 1 | [184,247,99,224,131,37,127,73,95,3,238,192,43,232,163,9,9,16,76,111,32,207,170,101,99,52,147,47,186,41,253,33,165,8,6,86,48,15,233,234,112,226,71,227,94,9,16,207,57,233,124,24,2,37,239,49,40,186,87,154,133,50,215,128] -------------------------------------------------------------------------------- /client.ts/test/keys/wallet_3.json: -------------------------------------------------------------------------------- 1 | [206,160,108,22,216,166,207,163,89,109,4,60,221,220,41,132,147,180,226,145,149,214,168,11,9,106,110,237,119,58,63,138,64,83,55,148,233,213,36,101,55,178,31,236,182,86,175,28,71,159,224,24,2,226,20,35,184,66,38,109,103,46,22,15] -------------------------------------------------------------------------------- /client.ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": false, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true, 10 | } 11 | } -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # Building the HAPI Explorer image 2 | 3 | ```sh 4 | docker build -t hapi-explorer -f ./explorer/Dockerfile .. 5 | ``` 6 | 7 | # Building the HAPI Indexer image 8 | 9 | ```sh 10 | docker build -t ars9/hapi-indexer -f ./indexer/Dockerfile .. 11 | ``` 12 | -------------------------------------------------------------------------------- /docker/explorer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Builder image 2 | FROM rust:1.75-slim AS builder 3 | 4 | # Install OpenSSL 5 | RUN apt-get update \ 6 | && apt-get install -y pkg-config libssl-dev \ 7 | && rm -rf /var/lib/apt/lists/* 8 | 9 | # Copy source code 10 | COPY client.rs /usr/src/hapi-core/client.rs 11 | COPY explorer /usr/src/hapi-core/explorer 12 | COPY indexer /usr/src/hapi-core/indexer 13 | COPY solana /usr/src/hapi-core/solana 14 | COPY near /usr/src/hapi-core/near 15 | 16 | # Set working directory 17 | WORKDIR /usr/src/hapi-core/explorer 18 | 19 | # Build from source code 20 | RUN cargo build --release --locked 21 | 22 | # Runtime image 23 | FROM busybox AS runtime 24 | 25 | # Copy the built binary 26 | COPY --from=builder /usr/src/hapi-core/explorer/target/release/hapi-explorer /bin/hapi-explorer 27 | 28 | # Copy libssl from the builder stage 29 | COPY --from=builder /usr/lib/ssl /usr/lib/ssl 30 | COPY --from=builder \ 31 | /lib/x86_64-linux-gnu/libssl.so \ 32 | /lib/x86_64-linux-gnu/libssl.so.3 \ 33 | /lib/x86_64-linux-gnu/libcrypto.so.3 \ 34 | /lib/x86_64-linux-gnu/libgcc_s.so.1 \ 35 | /lib/ 36 | 37 | # Make it the default command 38 | CMD ["/bin/hapi-explorer"] 39 | -------------------------------------------------------------------------------- /docker/indexer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Builder image 2 | FROM rust:1.75-slim AS builder 3 | 4 | # Install OpenSSL 5 | RUN apt-get update \ 6 | && apt-get install -y pkg-config libssl-dev \ 7 | && rm -rf /var/lib/apt/lists/* 8 | 9 | # Copy source code 10 | COPY client.rs /usr/src/hapi-core/client.rs 11 | COPY explorer /usr/src/hapi-core/explorer 12 | COPY indexer /usr/src/hapi-core/indexer 13 | COPY solana /usr/src/hapi-core/solana 14 | COPY near /usr/src/hapi-core/near 15 | 16 | # Set working directory 17 | WORKDIR /usr/src/hapi-core/indexer 18 | 19 | # Build from source code 20 | RUN cargo build --release --locked 21 | 22 | # Runtime image 23 | FROM busybox AS runtime 24 | 25 | # Copy the built binary 26 | COPY --from=builder /usr/src/hapi-core/indexer/target/release/hapi-indexer /bin/hapi-indexer 27 | 28 | # Copy libssl from the builder stage 29 | COPY --from=builder /etc/ssl /etc/ssl 30 | COPY --from=builder /usr/lib/ssl /usr/lib/ssl 31 | COPY --from=builder \ 32 | /lib/x86_64-linux-gnu/libssl.so \ 33 | /lib/x86_64-linux-gnu/libssl.so.3 \ 34 | /lib/x86_64-linux-gnu/libcrypto.so.3 \ 35 | /lib/x86_64-linux-gnu/libgcc_s.so.1 \ 36 | /lib/ 37 | 38 | # Make it the default command 39 | CMD ["/bin/hapi-indexer"] 40 | -------------------------------------------------------------------------------- /evm/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | coverage 4 | coverage.json 5 | typechain 6 | typechain-types 7 | .openzeppelin 8 | 9 | # Hardhat files 10 | cache 11 | artifacts 12 | 13 | -------------------------------------------------------------------------------- /evm/contracts/Token.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.22; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract Token is ERC20 { 7 | constructor() ERC20("Test Token", "TEST") { 8 | _mint(msg.sender, 1000000000000000000000000); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /evm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-core-evm", 3 | "version": "0.1.0", 4 | "description": "HAPI Core for EVM", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "hardhat compile", 8 | "deploy": "hardhat deploy", 9 | "deploy-test-token": "hardhat deploy-test-token", 10 | "upgrade": "hardhat upgrade", 11 | "test": "hardhat test" 12 | }, 13 | "author": "HAPI Protocol ", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "@nomicfoundation/hardhat-network-helpers": "^1.0.10", 17 | "@nomicfoundation/hardhat-toolbox": "^4.0.0", 18 | "@nomicfoundation/hardhat-ethers": "^3.0.5", 19 | "@openzeppelin/contracts": "^5.0.1", 20 | "@openzeppelin/contracts-upgradeable": "^5.0.1", 21 | "@openzeppelin/hardhat-upgrades": "^3.0.2", 22 | "ethers": "^6.10.0", 23 | "hardhat": "^2.19.5" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /evm/test/hapi_core/deployment.ts: -------------------------------------------------------------------------------- 1 | import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; 2 | import { expect } from "chai"; 3 | import { ethers } from "hardhat"; 4 | 5 | import { setupContract } from "../setup"; 6 | 7 | describe("HapiCore: Deployment", function () { 8 | async function basicFixture() { 9 | let setup = await setupContract(); 10 | 11 | const [owner, authority, nobody] = await ethers.getSigners(); 12 | 13 | return { ...setup, owner, authority, nobody }; 14 | } 15 | 16 | it("Should set the right owner and authority", async function () { 17 | const { hapiCore, owner } = await loadFixture(basicFixture); 18 | 19 | const authorityRole = await hapiCore.AUTHORITY_ROLE(); 20 | 21 | expect(await hapiCore.owner()).to.equal(owner.address); 22 | 23 | expect(await hapiCore.hasRole(authorityRole, owner.address)).to.equal(true); 24 | }); 25 | 26 | it("Should correctly set authority from owner", async function () { 27 | const { hapiCore, owner, authority } = await loadFixture(basicFixture); 28 | 29 | const authorityRole = await hapiCore.AUTHORITY_ROLE(); 30 | 31 | await expect(await hapiCore.grantRole(authorityRole, authority.address)) 32 | .to.emit(hapiCore, "RoleGranted") 33 | .withArgs(authorityRole, authority.address, owner.address); 34 | 35 | expect(await hapiCore.hasRole(authorityRole, authority.address)).to.equal( 36 | true 37 | ); 38 | }); 39 | 40 | it("Should not allow setting authority from non-owner/non-authority", async function () { 41 | const { hapiCore, authority, nobody } = await loadFixture(basicFixture); 42 | 43 | const authorityRole = await hapiCore.AUTHORITY_ROLE(); 44 | 45 | await expect( 46 | hapiCore.connect(nobody).grantRole(authorityRole, authority.address) 47 | ).to.be.revertedWithCustomError(hapiCore, "AccessControlUnauthorizedAccount"); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /evm/test/util.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from "ethers"; 2 | 3 | function uint8ArrayToBigInt(array: Uint8Array) { 4 | let hex = [...array] 5 | .map(b => b.toString(16).padStart(2, '0')) 6 | .join(''); 7 | return BigInt('0x' + hex); 8 | } 9 | 10 | export function randomId() { 11 | return uint8ArrayToBigInt(randomBytes(16)); 12 | } 13 | 14 | export enum ReporterRole { 15 | Validator = 0, 16 | Tracer = 1, 17 | Publisher = 2, 18 | Authority = 3, 19 | } 20 | 21 | export enum ReporterStatus { 22 | Inactive = 0, 23 | Active = 1, 24 | Unstaking = 2, 25 | } 26 | 27 | export enum CaseStatus { 28 | Closed = 0, 29 | Open = 1, 30 | } 31 | 32 | export enum Category { 33 | None = 0, 34 | WalletService = 1, 35 | MerchantService = 2, 36 | MiningPool = 3, 37 | Exchange = 4, 38 | DeFi = 5, 39 | OTCBroker = 6, 40 | ATM = 7, 41 | Gambling = 8, 42 | IllicitOrganization = 9, 43 | Mixer = 10, 44 | DarknetService = 11, 45 | Scam = 12, 46 | Ransomware = 13, 47 | Theft = 14, 48 | Counterfeit = 15, 49 | TerroristFinancing = 16, 50 | Sanctions = 17, 51 | ChildAbuse = 18, 52 | Hacker = 19, 53 | HighRiskJurisdiction = 20, 54 | } 55 | -------------------------------------------------------------------------------- /evm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /explorer/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /configuration.toml 3 | /secret.toml -------------------------------------------------------------------------------- /explorer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hapi-explorer" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [[bin]] 7 | name = "hapi-explorer" 8 | path = "src/main.rs" 9 | 10 | [dependencies] 11 | axum = "0.6.13" 12 | tower-http = { version = "0.4.0", features = ["cors", "trace"] } 13 | 14 | tokio = { version = "1", features = ["full"] } 15 | tracing = "0.1" 16 | tracing-subscriber = { version = "0.3", features = [ 17 | "fmt", 18 | "json", 19 | "env-filter", 20 | ] } 21 | 22 | config = "0.13.4" 23 | serde = { version = "1", features = ["derive"] } 24 | serde_with = { version = "3", features = ["chrono"] } 25 | serde_json = "1" 26 | clap = { version = "4.4.13", features = ["cargo"] } 27 | 28 | anyhow = "1" 29 | uuid = "1" 30 | chrono = { version = "0.4", features = ["serde"] } 31 | web3 = "0.19.0" 32 | 33 | metrics-exporter-prometheus = "0.12.1" 34 | metrics = "0.21.1" 35 | 36 | sea-orm-migration = { version = "0.12.0", features = [ 37 | "runtime-tokio-rustls", 38 | "sqlx-postgres", 39 | ] } 40 | sea-orm = "0.12.0" 41 | sea-orm-cli = "0.12.0" 42 | 43 | async-graphql = { version = "6.0.11", features = ["uuid", "chrono"] } 44 | async-graphql-axum = "6.0.11" 45 | 46 | hapi_indexer = { path = "../indexer" } 47 | hapi-core = { path = "../client.rs" } 48 | 49 | #jwt dependencies 50 | jsonwebtoken = "9.1.0" 51 | axum-extra = { version = "0.8.0", features = ["cookie"] } 52 | secrecy = "0.8.0" 53 | 54 | [dev-dependencies] 55 | reqwest = "0.11.12" 56 | rand = "0.8.5" 57 | -------------------------------------------------------------------------------- /explorer/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 HAPI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /explorer/configuration.sample.toml: -------------------------------------------------------------------------------- 1 | log_level = "info" 2 | is_json_logging = true 3 | listener = "0.0.0.0:3000" 4 | database_url = "postgres://postgres:postgres@localhost:5432/explorer" 5 | jwt_secret = "my_secret" 6 | -------------------------------------------------------------------------------- /explorer/src/entity/address/mod.rs: -------------------------------------------------------------------------------- 1 | pub(super) mod model; 2 | pub(super) mod query_utils; 3 | pub(super) mod resolver; 4 | 5 | pub use model::{ActiveModel, Entity, Model}; 6 | pub(crate) use resolver::AddressQuery; 7 | -------------------------------------------------------------------------------- /explorer/src/entity/address/query_utils.rs: -------------------------------------------------------------------------------- 1 | use { 2 | async_graphql::{Enum, InputObject}, 3 | uuid::Uuid, 4 | }; 5 | 6 | use super::model::Column; 7 | use crate::entity::types::Category; 8 | 9 | /// Conditions to filter address listings by 10 | #[derive(Clone, Eq, PartialEq, InputObject, Debug, Default)] 11 | pub struct AddressFilter { 12 | pub network_id: Option, 13 | pub case_id: Option, 14 | pub reporter_id: Option, 15 | pub category: Option, 16 | pub risk: Option, 17 | pub confirmations: Option, 18 | } 19 | 20 | /// Available ordering values for address 21 | #[derive(Enum, Copy, Clone, Eq, PartialEq, Default, Debug)] 22 | pub enum AddressCondition { 23 | /// Order by network 24 | NetworkId, 25 | /// Order by address 26 | Address, 27 | /// Order by case id 28 | CaseId, 29 | /// Order by reporter id 30 | ReporterId, 31 | /// Order by category 32 | Category, 33 | /// Order by risk 34 | Risk, 35 | /// Order by confirmation count 36 | Confirmations, 37 | /// Order by the time when entity was created 38 | CreatedAt, 39 | /// Order by the time when entity was updated 40 | #[default] 41 | UpdatedAt, 42 | } 43 | 44 | impl From for Column { 45 | fn from(condition: AddressCondition) -> Self { 46 | match condition { 47 | AddressCondition::NetworkId => Column::NetworkId, 48 | AddressCondition::Address => Column::Address, 49 | AddressCondition::CaseId => Column::CaseId, 50 | AddressCondition::ReporterId => Column::ReporterId, 51 | AddressCondition::Category => Column::Category, 52 | AddressCondition::Risk => Column::Risk, 53 | AddressCondition::Confirmations => Column::Confirmations, 54 | AddressCondition::CreatedAt => Column::CreatedAt, 55 | AddressCondition::UpdatedAt => Column::UpdatedAt, 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /explorer/src/entity/address/resolver.rs: -------------------------------------------------------------------------------- 1 | use { 2 | async_graphql::{Context, Object, Result}, 3 | sea_orm::DatabaseConnection, 4 | tracing::instrument, 5 | }; 6 | 7 | use super::{ 8 | model::Model, 9 | query_utils::{AddressCondition, AddressFilter}, 10 | }; 11 | 12 | use crate::{ 13 | entity::pagination::{EntityInput, EntityPage}, 14 | service::EntityQuery, 15 | }; 16 | 17 | /// The GraphQl Query segment 18 | #[derive(Default)] 19 | pub struct AddressQuery {} 20 | 21 | /// Queries for the `Address` model 22 | #[Object] 23 | impl AddressQuery { 24 | /// Get a single address 25 | #[instrument(level = "debug", skip(self, ctx))] 26 | pub async fn get_address( 27 | &self, 28 | ctx: &Context<'_>, 29 | #[graphql(desc = "Address address")] address: String, 30 | #[graphql(desc = "Address network")] network_id: String, 31 | ) -> Result> { 32 | let db = ctx.data_unchecked::(); 33 | let address = 34 | EntityQuery::find_entity_by_id::(db, (network_id, address)) 35 | .await?; 36 | 37 | Ok(address) 38 | } 39 | 40 | /// Get multiple addresses 41 | #[instrument(level = "debug", skip(self, ctx), fields(input = ?input))] 42 | pub async fn get_many_addresses( 43 | &self, 44 | ctx: &Context<'_>, 45 | #[graphql(desc = "Address input parameters")] input: EntityInput< 46 | AddressFilter, 47 | AddressCondition, 48 | >, 49 | ) -> Result> { 50 | let db = ctx.data_unchecked::(); 51 | let page = EntityQuery::find_many::(db, input).await?; 52 | 53 | Ok(page) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /explorer/src/entity/asset/mod.rs: -------------------------------------------------------------------------------- 1 | pub(super) mod model; 2 | pub(super) mod query_utils; 3 | pub(super) mod resolver; 4 | 5 | pub use model::{ActiveModel, Entity, Model}; 6 | pub(crate) use resolver::AssetQuery; 7 | -------------------------------------------------------------------------------- /explorer/src/entity/asset/query_utils.rs: -------------------------------------------------------------------------------- 1 | use { 2 | async_graphql::{Enum, InputObject}, 3 | uuid::Uuid, 4 | }; 5 | 6 | use super::model::Column; 7 | use crate::entity::types::Category; 8 | 9 | /// Conditions to filter address listings by 10 | #[derive(Clone, Eq, PartialEq, InputObject, Debug, Default)] 11 | pub struct AssetFilter { 12 | pub network_id: Option, 13 | pub address: Option, 14 | pub case_id: Option, 15 | pub reporter_id: Option, 16 | pub category: Option, 17 | pub risk: Option, 18 | pub confirmations: Option, 19 | } 20 | 21 | /// Available ordering values for asset 22 | #[derive(Enum, Copy, Clone, Eq, PartialEq, Default, Debug)] 23 | pub enum AssetCondition { 24 | /// Order by network 25 | NetworkId, 26 | /// Order by address 27 | Address, 28 | /// Order by case id 29 | CaseId, 30 | /// Order by reporter id 31 | ReporterId, 32 | /// Order by category 33 | Category, 34 | /// Order by risk 35 | Risk, 36 | /// Order by confirmation count 37 | Confirmations, 38 | /// Order by the time when entity was created 39 | CreatedAt, 40 | /// Order by the time when entity was updated 41 | #[default] 42 | UpdatedAt, 43 | } 44 | 45 | impl From for Column { 46 | fn from(condition: AssetCondition) -> Self { 47 | match condition { 48 | AssetCondition::NetworkId => Column::NetworkId, 49 | AssetCondition::Address => Column::Address, 50 | AssetCondition::CaseId => Column::CaseId, 51 | AssetCondition::ReporterId => Column::ReporterId, 52 | AssetCondition::Category => Column::Category, 53 | AssetCondition::Risk => Column::Risk, 54 | AssetCondition::Confirmations => Column::Confirmations, 55 | AssetCondition::CreatedAt => Column::CreatedAt, 56 | AssetCondition::UpdatedAt => Column::UpdatedAt, 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /explorer/src/entity/asset/resolver.rs: -------------------------------------------------------------------------------- 1 | use { 2 | async_graphql::{Context, Object, Result}, 3 | sea_orm::DatabaseConnection, 4 | tracing::instrument, 5 | }; 6 | 7 | use super::{ 8 | model::Model, 9 | query_utils::{AssetCondition, AssetFilter}, 10 | }; 11 | 12 | use crate::{ 13 | entity::pagination::{EntityInput, EntityPage}, 14 | service::EntityQuery, 15 | }; 16 | 17 | /// The GraphQl Query segment 18 | #[derive(Default)] 19 | pub struct AssetQuery {} 20 | 21 | /// Queries for the `Asset` model 22 | #[Object] 23 | impl AssetQuery { 24 | /// Get a single asset 25 | #[instrument(level = "debug", skip(self, ctx))] 26 | pub async fn get_asset( 27 | &self, 28 | ctx: &Context<'_>, 29 | #[graphql(desc = "Asset address")] address: String, 30 | #[graphql(desc = "Asset id")] id: String, 31 | #[graphql(desc = "Asset network")] network_id: String, 32 | ) -> Result> { 33 | let db = ctx.data_unchecked::(); 34 | let address = EntityQuery::find_entity_by_id::( 35 | db, 36 | (network_id, address, id), 37 | ) 38 | .await?; 39 | 40 | Ok(address) 41 | } 42 | 43 | /// Get multiple assets 44 | #[instrument(level = "debug", skip(self, ctx), fields(input = ?input))] 45 | pub async fn get_many_assets( 46 | &self, 47 | ctx: &Context<'_>, 48 | #[graphql(desc = "Asset input parameters")] input: EntityInput, 49 | ) -> Result> { 50 | let db = ctx.data_unchecked::(); 51 | let page = EntityQuery::find_many::(db, input).await?; 52 | 53 | Ok(page) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /explorer/src/entity/case/mod.rs: -------------------------------------------------------------------------------- 1 | pub(super) mod model; 2 | pub(super) mod query_utils; 3 | pub(super) mod resolver; 4 | 5 | pub use model::{ActiveModel, Entity, Model}; 6 | pub(crate) use resolver::CaseQuery; 7 | -------------------------------------------------------------------------------- /explorer/src/entity/case/query_utils.rs: -------------------------------------------------------------------------------- 1 | use { 2 | async_graphql::{Enum, InputObject}, 3 | uuid::Uuid, 4 | }; 5 | 6 | use super::model::Column; 7 | use crate::entity::types::CaseStatus; 8 | 9 | /// Conditions to filter address listings by 10 | #[derive(Clone, Eq, PartialEq, InputObject, Debug, Default)] 11 | pub struct CaseFilter { 12 | pub network_id: Option, 13 | pub name: Option, 14 | pub url: Option, 15 | pub status: Option, 16 | pub reporter_id: Option, 17 | } 18 | 19 | /// Available ordering values for asset 20 | #[derive(Enum, Copy, Clone, Eq, PartialEq, Default, Debug)] 21 | pub enum CaseCondition { 22 | /// Order by network 23 | NetworkId, 24 | /// Order by case id 25 | Id, 26 | /// Order by name 27 | Name, 28 | /// Order by url 29 | Url, 30 | /// Order by status 31 | Status, 32 | /// Order by reporter id 33 | ReporterId, 34 | /// Order by the time when entity was created 35 | CreatedAt, 36 | /// Order by the time when entity was updated 37 | #[default] 38 | UpdatedAt, 39 | /// Order by address count 40 | AddressCount, 41 | /// Order by asset count 42 | AssetCount, 43 | } 44 | 45 | impl From for Column { 46 | fn from(condition: CaseCondition) -> Self { 47 | match condition { 48 | CaseCondition::NetworkId => Column::NetworkId, 49 | CaseCondition::Id => Column::Id, 50 | CaseCondition::Name => Column::Name, 51 | CaseCondition::Url => Column::Url, 52 | CaseCondition::Status => Column::Status, 53 | CaseCondition::ReporterId => Column::ReporterId, 54 | CaseCondition::CreatedAt => Column::CreatedAt, 55 | CaseCondition::UpdatedAt => Column::UpdatedAt, 56 | _ => unreachable!("Invalid condition: {:?}", condition), 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /explorer/src/entity/case/resolver.rs: -------------------------------------------------------------------------------- 1 | use { 2 | async_graphql::{Context, Object, Result}, 3 | sea_orm::DatabaseConnection, 4 | tracing::instrument, 5 | uuid::Uuid, 6 | }; 7 | 8 | use super::{ 9 | model::Model, 10 | query_utils::{CaseCondition, CaseFilter}, 11 | }; 12 | 13 | use crate::{ 14 | entity::pagination::{EntityInput, EntityPage}, 15 | service::EntityQuery, 16 | }; 17 | 18 | /// The GraphQl Query segment 19 | #[derive(Default)] 20 | pub struct CaseQuery {} 21 | 22 | /// Queries for the `Case` model 23 | #[Object] 24 | impl CaseQuery { 25 | /// Get a single case 26 | #[instrument(level = "debug", skip(self, ctx))] 27 | pub async fn get_case( 28 | &self, 29 | ctx: &Context<'_>, 30 | #[graphql(desc = "Case id")] id: Uuid, 31 | #[graphql(desc = "Case network")] network_id: String, 32 | ) -> Result> { 33 | let db = ctx.data_unchecked::(); 34 | let address = 35 | EntityQuery::find_entity_by_id::(db, (network_id, id)).await?; 36 | 37 | Ok(address) 38 | } 39 | 40 | /// Get multiple cases 41 | #[instrument(level = "debug", skip(self, ctx), fields(input = ?input))] 42 | pub async fn get_many_cases( 43 | &self, 44 | ctx: &Context<'_>, 45 | #[graphql(desc = "Case input parameters")] input: EntityInput, 46 | ) -> Result> { 47 | let db = ctx.data_unchecked::(); 48 | let page = EntityQuery::find_many::(db, input).await?; 49 | 50 | Ok(page) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /explorer/src/entity/indexer.rs: -------------------------------------------------------------------------------- 1 | use {sea_orm::entity::prelude::*, serde::Serialize}; 2 | 3 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize)] 4 | #[sea_orm(table_name = "indexer")] 5 | pub struct Model { 6 | #[sea_orm(primary_key, auto_increment = false)] 7 | pub id: Uuid, 8 | pub network_id: String, 9 | pub created_at: DateTime, 10 | pub last_heartbeat: DateTime, 11 | pub cursor: String, 12 | } 13 | 14 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 15 | pub enum Relation {} 16 | 17 | impl ActiveModelBehavior for ActiveModel {} 18 | -------------------------------------------------------------------------------- /explorer/src/entity/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod address; 2 | pub mod asset; 3 | pub mod case; 4 | pub mod indexer; 5 | pub mod network; 6 | pub mod pagination; 7 | pub mod reporter; 8 | pub mod statistics; 9 | pub mod types; 10 | 11 | use self::pagination::{order_by_column, Ordering}; 12 | use sea_orm::{prelude::DateTime, EntityTrait, Select}; 13 | 14 | pub trait FromPayload: Sized { 15 | fn from( 16 | network_id: String, 17 | created_at: Option, 18 | updated_at: Option, 19 | value: T, 20 | ) -> Self; 21 | } 22 | 23 | // Trait for Filtering query 24 | pub trait EntityFilter: Sized + EntityTrait { 25 | type Filter; 26 | type Condition; 27 | 28 | fn filter(selected: Select, filter_options: &Self::Filter) -> Select; 29 | 30 | fn columns_for_search() -> Vec; 31 | 32 | fn order( 33 | selected: Select, 34 | ordering: Option, 35 | condition: Option, 36 | ) -> Select 37 | where 38 | Self::Column: From, 39 | Self::Condition: Default, 40 | { 41 | order_by_column(selected, ordering, condition) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /explorer/src/entity/network/mod.rs: -------------------------------------------------------------------------------- 1 | pub(super) mod model; 2 | pub(super) mod query_utils; 3 | pub(super) mod resolver; 4 | 5 | pub use model::{ActiveModel, Column, Entity, Model}; 6 | pub(crate) use resolver::NetworkQuery; 7 | -------------------------------------------------------------------------------- /explorer/src/entity/network/model.rs: -------------------------------------------------------------------------------- 1 | use super::query_utils::{NetworkCondition, NetworkFilter}; 2 | use crate::entity::{types::NetworkBackend, EntityFilter}; 3 | 4 | use {async_graphql::SimpleObject, sea_orm::entity::prelude::*}; 5 | 6 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, SimpleObject)] 7 | #[graphql(name = "Network")] 8 | #[sea_orm(table_name = "network")] 9 | pub struct Model { 10 | #[sea_orm(primary_key, auto_increment = false)] 11 | pub id: String, 12 | pub name: String, 13 | pub backend: NetworkBackend, 14 | pub chain_id: Option, 15 | pub authority: String, 16 | pub stake_token: String, 17 | pub created_at: DateTime, 18 | pub updated_at: DateTime, 19 | } 20 | 21 | impl EntityFilter for Entity { 22 | type Filter = NetworkFilter; 23 | type Condition = NetworkCondition; 24 | 25 | // Filtering query 26 | fn filter(selected: Select, filter_options: &NetworkFilter) -> Select { 27 | let mut query = selected; 28 | 29 | if let Some(name) = &filter_options.name { 30 | query = query.filter(Column::Name.contains(name)); 31 | } 32 | 33 | if let Some(backend) = filter_options.backend { 34 | query = query.filter(Column::Backend.eq(backend)); 35 | } 36 | 37 | if let Some(authority) = &filter_options.authority { 38 | query = query.filter(Column::Authority.eq(authority)); 39 | } 40 | 41 | if let Some(stake_token) = &filter_options.stake_token { 42 | query = query.filter(Column::StakeToken.eq(stake_token)); 43 | } 44 | 45 | query 46 | } 47 | 48 | /// Columns for search 49 | fn columns_for_search() -> Vec { 50 | vec![ 51 | String::from("id"), 52 | String::from("name"), 53 | String::from("backend::text"), 54 | String::from("chain_id"), 55 | String::from("authority"), 56 | String::from("stake_token"), 57 | String::from("created_at::text"), 58 | String::from("updated_at::text"), 59 | ] 60 | } 61 | } 62 | 63 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 64 | pub enum Relation {} 65 | 66 | impl ActiveModelBehavior for ActiveModel {} 67 | -------------------------------------------------------------------------------- /explorer/src/entity/network/query_utils.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{Enum, InputObject}; 2 | 3 | use super::model::Column; 4 | use crate::entity::types::NetworkBackend; 5 | 6 | /// Conditions to filter address listings by 7 | #[derive(Clone, Eq, PartialEq, InputObject, Debug, Default)] 8 | pub struct NetworkFilter { 9 | pub name: Option, 10 | pub backend: Option, 11 | pub authority: Option, 12 | pub stake_token: Option, 13 | } 14 | 15 | /// Available ordering values for asset 16 | #[derive(Enum, Copy, Clone, Eq, PartialEq, Default, Debug)] 17 | pub enum NetworkCondition { 18 | /// Order by id 19 | Id, 20 | /// Order by name 21 | Name, 22 | /// Order by network backend 23 | Backend, 24 | /// Order by network stake token 25 | StakeToken, 26 | /// Order by network authority 27 | Authority, 28 | /// Order by the time when entity was created 29 | CreatedAt, 30 | /// Order by the time when entity was updated 31 | #[default] 32 | UpdatedAt, 33 | } 34 | 35 | impl From for Column { 36 | fn from(condition: NetworkCondition) -> Self { 37 | match condition { 38 | NetworkCondition::Id => Column::Id, 39 | NetworkCondition::Name => Column::Name, 40 | NetworkCondition::Backend => Column::Backend, 41 | NetworkCondition::StakeToken => Column::StakeToken, 42 | NetworkCondition::Authority => Column::Authority, 43 | NetworkCondition::CreatedAt => Column::CreatedAt, 44 | NetworkCondition::UpdatedAt => Column::UpdatedAt, 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /explorer/src/entity/network/resolver.rs: -------------------------------------------------------------------------------- 1 | use { 2 | async_graphql::{Context, Object, Result}, 3 | sea_orm::DatabaseConnection, 4 | tracing::instrument, 5 | }; 6 | 7 | use super::{ 8 | model::Model, 9 | query_utils::{NetworkCondition, NetworkFilter}, 10 | }; 11 | 12 | use crate::{ 13 | entity::pagination::{EntityInput, EntityPage}, 14 | service::EntityQuery, 15 | }; 16 | 17 | /// The GraphQl Query segment 18 | #[derive(Default)] 19 | pub struct NetworkQuery {} 20 | 21 | /// Queries for the `Network` model 22 | #[Object] 23 | impl NetworkQuery { 24 | /// Get a single network 25 | #[instrument(level = "debug", skip(self, ctx))] 26 | pub async fn get_network( 27 | &self, 28 | ctx: &Context<'_>, 29 | #[graphql(desc = "Network id")] id: String, 30 | ) -> Result> { 31 | let db = ctx.data_unchecked::(); 32 | let address = EntityQuery::find_entity_by_id::(db, id).await?; 33 | 34 | Ok(address) 35 | } 36 | 37 | /// Get multiple networks 38 | #[instrument(level = "debug", skip(self, ctx), fields(input = ?input))] 39 | pub async fn get_many_networks( 40 | &self, 41 | ctx: &Context<'_>, 42 | #[graphql(desc = "Network input parameters")] input: EntityInput< 43 | NetworkFilter, 44 | NetworkCondition, 45 | >, 46 | ) -> Result> { 47 | let db = ctx.data_unchecked::(); 48 | let page = EntityQuery::find_many::(db, input).await?; 49 | 50 | Ok(page) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /explorer/src/entity/reporter/mod.rs: -------------------------------------------------------------------------------- 1 | pub(super) mod model; 2 | pub(super) mod query_utils; 3 | pub(super) mod resolver; 4 | 5 | pub use model::{ActiveModel, Column, Entity, Model}; 6 | pub(crate) use resolver::ReporterQuery; 7 | -------------------------------------------------------------------------------- /explorer/src/entity/reporter/query_utils.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{Enum, InputObject}; 2 | 3 | use super::model::Column; 4 | use crate::entity::types::{ReporterRole, ReporterStatus}; 5 | 6 | /// Conditions to filter address listings by 7 | #[derive(Clone, Eq, PartialEq, InputObject, Debug, Default)] 8 | pub struct ReporterFilter { 9 | pub network_id: Option, 10 | pub account: Option, 11 | pub role: Option, 12 | pub status: Option, 13 | pub name: Option, 14 | pub url: Option, 15 | } 16 | 17 | /// Available ordering values for asset 18 | #[derive(Enum, Copy, Clone, Eq, PartialEq, Default, Debug)] 19 | pub enum ReporterCondition { 20 | /// Order by network 21 | NetworkId, 22 | /// Order by reporter id 23 | Id, 24 | /// Order by reporter account 25 | Account, 26 | /// Order by reporter role 27 | Role, 28 | /// Order by reporter status 29 | Status, 30 | /// Order by name 31 | Name, 32 | /// Order by url 33 | Url, 34 | /// Order by stake 35 | Stake, 36 | /// Order by unlock timestamp 37 | UnlockTimestamp, 38 | /// Order by the time when entity was created 39 | CreatedAt, 40 | /// Order by the time when entity was updated 41 | #[default] 42 | UpdatedAt, 43 | } 44 | 45 | impl From for Column { 46 | fn from(condition: ReporterCondition) -> Self { 47 | match condition { 48 | ReporterCondition::NetworkId => Column::NetworkId, 49 | ReporterCondition::Id => Column::Id, 50 | ReporterCondition::Account => Column::Account, 51 | ReporterCondition::Role => Column::Role, 52 | ReporterCondition::Status => Column::Status, 53 | ReporterCondition::Name => Column::Name, 54 | ReporterCondition::Url => Column::Url, 55 | ReporterCondition::Stake => Column::Stake, 56 | ReporterCondition::UnlockTimestamp => Column::UnlockTimestamp, 57 | ReporterCondition::CreatedAt => Column::CreatedAt, 58 | ReporterCondition::UpdatedAt => Column::UpdatedAt, 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /explorer/src/entity/reporter/resolver.rs: -------------------------------------------------------------------------------- 1 | use { 2 | async_graphql::{Context, Object, Result}, 3 | sea_orm::DatabaseConnection, 4 | tracing::instrument, 5 | uuid::Uuid, 6 | }; 7 | 8 | use super::{ 9 | model::Model, 10 | query_utils::{ReporterCondition, ReporterFilter}, 11 | }; 12 | 13 | use crate::{ 14 | entity::pagination::{EntityInput, EntityPage}, 15 | service::EntityQuery, 16 | }; 17 | 18 | /// The GraphQl Query segment 19 | #[derive(Default)] 20 | pub struct ReporterQuery {} 21 | 22 | /// Queries for the `Reporter` model 23 | #[Object] 24 | impl ReporterQuery { 25 | /// Get a single reporter 26 | #[instrument(level = "debug", skip(self, ctx))] 27 | pub async fn get_reporter( 28 | &self, 29 | ctx: &Context<'_>, 30 | #[graphql(desc = "Reporter id")] id: Uuid, 31 | #[graphql(desc = "Reporter network")] network_id: String, 32 | ) -> Result> { 33 | let db = ctx.data_unchecked::(); 34 | let address = 35 | EntityQuery::find_entity_by_id::(db, (network_id, id)).await?; 36 | 37 | Ok(address) 38 | } 39 | 40 | /// Get multiple reporters 41 | #[instrument(level = "debug", skip(self, ctx), fields(input = ?input))] 42 | pub async fn get_many_reporters( 43 | &self, 44 | ctx: &Context<'_>, 45 | #[graphql(desc = "Reporter input parameters")] input: EntityInput< 46 | ReporterFilter, 47 | ReporterCondition, 48 | >, 49 | ) -> Result> { 50 | let db = ctx.data_unchecked::(); 51 | let page = EntityQuery::find_many::(db, input).await?; 52 | 53 | Ok(page) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /explorer/src/error.rs: -------------------------------------------------------------------------------- 1 | use { 2 | axum::{ 3 | http::StatusCode, 4 | response::{IntoResponse, Response}, 5 | }, 6 | std::fmt::Display, 7 | }; 8 | 9 | #[derive(Debug)] 10 | pub struct AppError { 11 | pub code: StatusCode, 12 | pub description: String, 13 | } 14 | 15 | impl AppError { 16 | pub fn new(code: StatusCode, description: String) -> Self { 17 | AppError { code, description } 18 | } 19 | 20 | pub fn invalid_request(description: &str) -> Self { 21 | AppError::new(StatusCode::BAD_REQUEST, description.to_string()) 22 | } 23 | } 24 | 25 | impl IntoResponse for AppError { 26 | fn into_response(self) -> Response { 27 | tracing::error!(code = ?self.code, description = ?self.description); 28 | (self.code, self.description).into_response() 29 | } 30 | } 31 | 32 | impl From for AppError 33 | where 34 | E: Into + Display, 35 | { 36 | fn from(err: E) -> Self { 37 | Self::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /explorer/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod application; 2 | pub mod configuration; 3 | pub mod entity; 4 | pub mod error; 5 | pub mod migrations; 6 | pub mod observability; 7 | pub mod server; 8 | pub mod service; 9 | -------------------------------------------------------------------------------- /explorer/src/migrations/m20231127_162603_create_category_type.rs: -------------------------------------------------------------------------------- 1 | use { 2 | sea_orm::{EnumIter, Iterable}, 3 | sea_orm_migration::{prelude::*, sea_query::extension::postgres::Type}, 4 | }; 5 | 6 | #[derive(DeriveMigrationName)] 7 | pub struct Migration; 8 | 9 | #[async_trait::async_trait] 10 | impl MigrationTrait for Migration { 11 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 12 | manager 13 | .create_type( 14 | Type::create() 15 | .as_enum(Category::Type) 16 | .values(Category::iter().skip(1)) 17 | .to_owned(), 18 | ) 19 | .await 20 | } 21 | 22 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 23 | manager 24 | .drop_type(Type::drop().name(Category::Type).to_owned()) 25 | .await 26 | } 27 | } 28 | 29 | #[derive(Iden, EnumIter)] 30 | pub enum Category { 31 | #[iden = "category"] 32 | Type, 33 | None, 34 | WalletService, 35 | MerchantService, 36 | MiningPool, 37 | Exchange, 38 | DeFi, 39 | OTCBroker, 40 | Atm, 41 | Gambling, 42 | IllicitOrganization, 43 | Mixer, 44 | DarknetService, 45 | Scam, 46 | Ransomware, 47 | Theft, 48 | Counterfeit, 49 | TerroristFinancing, 50 | Sanctions, 51 | ChildAbuse, 52 | Hacker, 53 | HighRiskJurisdiction, 54 | } 55 | -------------------------------------------------------------------------------- /explorer/src/migrations/m20231127_165849_create_reporter_role_type.rs: -------------------------------------------------------------------------------- 1 | use { 2 | sea_orm::{EnumIter, Iterable}, 3 | sea_orm_migration::{prelude::*, sea_query::extension::postgres::Type}, 4 | }; 5 | 6 | #[derive(DeriveMigrationName)] 7 | pub struct Migration; 8 | 9 | #[async_trait::async_trait] 10 | impl MigrationTrait for Migration { 11 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 12 | manager 13 | .create_type( 14 | Type::create() 15 | .as_enum(ReporterRole::Type) 16 | .values(ReporterRole::iter().skip(1)) 17 | .to_owned(), 18 | ) 19 | .await 20 | } 21 | 22 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 23 | manager 24 | .drop_type(Type::drop().name(ReporterRole::Type).to_owned()) 25 | .await 26 | } 27 | } 28 | 29 | #[derive(Iden, EnumIter)] 30 | pub enum ReporterRole { 31 | #[iden = "reporter_role"] 32 | Type, 33 | Validator, 34 | Tracer, 35 | Publisher, 36 | Authority, 37 | } 38 | -------------------------------------------------------------------------------- /explorer/src/migrations/m20231127_170357_create_reporter_status_type.rs: -------------------------------------------------------------------------------- 1 | use { 2 | sea_orm::{EnumIter, Iterable}, 3 | sea_orm_migration::{prelude::*, sea_query::extension::postgres::Type}, 4 | }; 5 | 6 | #[derive(DeriveMigrationName)] 7 | pub struct Migration; 8 | 9 | #[async_trait::async_trait] 10 | impl MigrationTrait for Migration { 11 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 12 | manager 13 | .create_type( 14 | Type::create() 15 | .as_enum(ReporterStatus::Type) 16 | .values(ReporterStatus::iter().skip(1)) 17 | .to_owned(), 18 | ) 19 | .await 20 | } 21 | 22 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 23 | manager 24 | .drop_type(Type::drop().name(ReporterStatus::Type).to_owned()) 25 | .await 26 | } 27 | } 28 | 29 | #[derive(Iden, EnumIter)] 30 | pub enum ReporterStatus { 31 | #[iden = "reporter_status"] 32 | Type, 33 | Inactive, 34 | Active, 35 | Unstaking, 36 | } 37 | -------------------------------------------------------------------------------- /explorer/src/migrations/m20231127_170630_create_case_status_type.rs: -------------------------------------------------------------------------------- 1 | use { 2 | sea_orm::{EnumIter, Iterable}, 3 | sea_orm_migration::{prelude::*, sea_query::extension::postgres::Type}, 4 | }; 5 | 6 | #[derive(DeriveMigrationName)] 7 | pub struct Migration; 8 | 9 | #[async_trait::async_trait] 10 | impl MigrationTrait for Migration { 11 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 12 | manager 13 | .create_type( 14 | Type::create() 15 | .as_enum(CaseStatus::Type) 16 | .values(CaseStatus::iter().skip(1)) 17 | .to_owned(), 18 | ) 19 | .await 20 | } 21 | 22 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 23 | manager 24 | .drop_type(Type::drop().name(CaseStatus::Type).to_owned()) 25 | .await 26 | } 27 | } 28 | 29 | #[derive(Iden, EnumIter)] 30 | pub enum CaseStatus { 31 | #[iden = "case_status"] 32 | Type, 33 | Closed, 34 | Open, 35 | } 36 | -------------------------------------------------------------------------------- /explorer/src/migrations/m20231205_131413_create_indexer.rs: -------------------------------------------------------------------------------- 1 | use super::Network; 2 | use sea_orm_migration::prelude::*; 3 | 4 | #[derive(DeriveMigrationName)] 5 | pub struct Migration; 6 | 7 | #[async_trait::async_trait] 8 | impl MigrationTrait for Migration { 9 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 10 | manager 11 | .create_table( 12 | Table::create() 13 | .table(Indexer::Table) 14 | .if_not_exists() 15 | .col(ColumnDef::new(Indexer::Id).uuid().not_null().primary_key()) 16 | .col(ColumnDef::new(Indexer::NetworkId).string().not_null()) 17 | .col(ColumnDef::new(Indexer::CreatedAt).timestamp().not_null()) 18 | .col( 19 | ColumnDef::new(Indexer::LastHeartbeat) 20 | .timestamp() 21 | .not_null(), 22 | ) 23 | .col(ColumnDef::new(Indexer::Cursor).string().not_null()) 24 | .foreign_key( 25 | ForeignKey::create() 26 | .name("fk-indexer_network_id") 27 | .from(Indexer::Table, Indexer::NetworkId) 28 | .to(Network::Table, Network::Id) 29 | .on_delete(ForeignKeyAction::NoAction) 30 | .on_update(ForeignKeyAction::Cascade), 31 | ) 32 | .to_owned(), 33 | ) 34 | .await 35 | } 36 | 37 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 38 | manager 39 | .drop_table(Table::drop().table(Indexer::Table).to_owned()) 40 | .await 41 | } 42 | } 43 | 44 | #[derive(DeriveIden)] 45 | pub(crate) enum Indexer { 46 | Table, 47 | Id, 48 | NetworkId, 49 | CreatedAt, 50 | LastHeartbeat, 51 | Cursor, 52 | } 53 | -------------------------------------------------------------------------------- /explorer/src/migrations/m20231205_131413_create_network.rs: -------------------------------------------------------------------------------- 1 | use super::NetworkBackend; 2 | use {sea_orm::Iterable, sea_orm_migration::prelude::*}; 3 | 4 | #[derive(DeriveMigrationName)] 5 | pub struct Migration; 6 | 7 | #[async_trait::async_trait] 8 | impl MigrationTrait for Migration { 9 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 10 | manager 11 | .create_table( 12 | Table::create() 13 | .table(Network::Table) 14 | .if_not_exists() 15 | .col( 16 | ColumnDef::new(Network::Id) 17 | .string() 18 | .not_null() 19 | .primary_key(), 20 | ) 21 | .col(ColumnDef::new(Network::Name).string().not_null()) 22 | .col( 23 | ColumnDef::new(Network::Backend) 24 | .enumeration(NetworkBackend::Type, NetworkBackend::iter().skip(1)) 25 | .not_null(), 26 | ) 27 | .col(ColumnDef::new(Network::ChainId).string()) 28 | .col(ColumnDef::new(Network::Authority).string().not_null()) 29 | .col(ColumnDef::new(Network::StakeToken).string().not_null()) 30 | .col(ColumnDef::new(Network::CreatedAt).timestamp().not_null()) 31 | .col(ColumnDef::new(Network::UpdatedAt).timestamp().not_null()) 32 | .to_owned(), 33 | ) 34 | .await 35 | } 36 | 37 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 38 | manager 39 | .drop_table(Table::drop().table(Network::Table).to_owned()) 40 | .await 41 | } 42 | } 43 | 44 | #[derive(DeriveIden)] 45 | pub(crate) enum Network { 46 | Table, 47 | Id, 48 | Name, 49 | Backend, 50 | ChainId, 51 | Authority, 52 | StakeToken, 53 | CreatedAt, 54 | UpdatedAt, 55 | } 56 | -------------------------------------------------------------------------------- /explorer/src/migrations/m20231211_164133_create_network_backend.rs: -------------------------------------------------------------------------------- 1 | use { 2 | sea_orm::{EnumIter, Iterable}, 3 | sea_orm_migration::{prelude::*, sea_query::extension::postgres::Type}, 4 | }; 5 | 6 | #[derive(DeriveMigrationName)] 7 | pub struct Migration; 8 | 9 | #[async_trait::async_trait] 10 | impl MigrationTrait for Migration { 11 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 12 | manager 13 | .create_type( 14 | Type::create() 15 | .as_enum(NetworkBackend::Type) 16 | .values(NetworkBackend::iter().skip(1)) 17 | .to_owned(), 18 | ) 19 | .await 20 | } 21 | 22 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 23 | manager 24 | .drop_type(Type::drop().name(NetworkBackend::Type).to_owned()) 25 | .await 26 | } 27 | } 28 | 29 | #[derive(Iden, EnumIter)] 30 | pub enum NetworkBackend { 31 | #[iden = "network_backend"] 32 | Type, 33 | Evm, 34 | Solana, 35 | Near, 36 | } 37 | -------------------------------------------------------------------------------- /explorer/src/migrations/mod.rs: -------------------------------------------------------------------------------- 1 | mod m20231127_140636_create_address; 2 | mod m20231127_160838_create_asset; 3 | mod m20231127_161317_create_reporter; 4 | mod m20231127_162130_create_case; 5 | mod m20231127_162603_create_category_type; 6 | mod m20231127_165849_create_reporter_role_type; 7 | mod m20231127_170357_create_reporter_status_type; 8 | mod m20231127_170630_create_case_status_type; 9 | mod m20231205_131413_create_indexer; 10 | mod m20231205_131413_create_network; 11 | mod m20231211_164133_create_network_backend; 12 | 13 | pub(super) use m20231127_162603_create_category_type::Category; 14 | pub(super) use m20231127_165849_create_reporter_role_type::ReporterRole; 15 | pub(super) use m20231127_170357_create_reporter_status_type::ReporterStatus; 16 | pub(super) use m20231127_170630_create_case_status_type::CaseStatus; 17 | pub(super) use m20231205_131413_create_network::Network; 18 | pub(super) use m20231211_164133_create_network_backend::NetworkBackend; 19 | 20 | pub(super) use m20231127_161317_create_reporter::Reporter; 21 | pub(super) use m20231127_162130_create_case::Case; 22 | 23 | use sea_orm_migration::prelude::*; 24 | 25 | pub struct Migrator; 26 | 27 | #[async_trait::async_trait] 28 | impl MigratorTrait for Migrator { 29 | fn migrations() -> Vec> { 30 | vec![ 31 | Box::new(m20231211_164133_create_network_backend::Migration), 32 | Box::new(m20231127_162603_create_category_type::Migration), 33 | Box::new(m20231127_165849_create_reporter_role_type::Migration), 34 | Box::new(m20231127_170357_create_reporter_status_type::Migration), 35 | Box::new(m20231127_170630_create_case_status_type::Migration), 36 | Box::new(m20231205_131413_create_network::Migration), 37 | Box::new(m20231205_131413_create_indexer::Migration), 38 | Box::new(m20231127_161317_create_reporter::Migration), 39 | Box::new(m20231127_162130_create_case::Migration), 40 | Box::new(m20231127_140636_create_address::Migration), 41 | Box::new(m20231127_160838_create_asset::Migration), 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /explorer/src/observability/mod.rs: -------------------------------------------------------------------------------- 1 | mod metrics_setup; 2 | mod tracing_setup; 3 | 4 | pub(crate) use metrics_setup::{ 5 | setup_metrics, track_metrics, update_address_metrics, update_asset_metrics, 6 | update_case_metrics, update_network_metrics, update_reporter_metrics, MetricOp, 7 | }; 8 | pub use tracing_setup::setup_tracing; 9 | -------------------------------------------------------------------------------- /explorer/src/observability/tracing_setup.rs: -------------------------------------------------------------------------------- 1 | use { 2 | anyhow::{anyhow, Result}, 3 | tracing::subscriber, 4 | tracing_subscriber::{fmt::Subscriber, EnvFilter}, 5 | }; 6 | 7 | pub fn setup_tracing(log_level: &str, is_json_logging: bool) -> Result<()> { 8 | let builder = Subscriber::builder() 9 | .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| { 10 | format!("{}={log_level}", to_snake_case(env!("CARGO_PKG_NAME"))).into() 11 | })) 12 | .with_writer(std::io::stdout); 13 | 14 | let result = if is_json_logging { 15 | subscriber::set_global_default(builder.json().flatten_event(true).finish()) 16 | } else { 17 | subscriber::set_global_default(builder.finish()) 18 | }; 19 | 20 | result.map_err(|e| anyhow!("Failed to set up tracing subscriber: {:?}", e)) 21 | } 22 | 23 | fn to_snake_case(s: &str) -> String { 24 | s.to_lowercase().replace(['-', ' '], "_") 25 | } 26 | -------------------------------------------------------------------------------- /explorer/src/server/handlers/graphql.rs: -------------------------------------------------------------------------------- 1 | use { 2 | async_graphql_axum::{GraphQLRequest, GraphQLResponse}, 3 | axum::{ 4 | response::{Html, IntoResponse}, 5 | Extension, 6 | }, 7 | }; 8 | 9 | use crate::server::schema::AppSchema; 10 | 11 | /// Handle GraphQL playground 12 | pub(crate) async fn graphiql_playground() -> impl IntoResponse { 13 | Html(async_graphql::http::playground_source( 14 | async_graphql::http::GraphQLPlaygroundConfig::new("/graphql"), 15 | )) 16 | } 17 | 18 | /// Handle GraphQL Requests 19 | pub(crate) async fn graphql_handler( 20 | schema: Extension, 21 | req: GraphQLRequest, 22 | ) -> GraphQLResponse { 23 | schema.execute(req.into_inner()).await.into() 24 | } 25 | -------------------------------------------------------------------------------- /explorer/src/server/handlers/health.rs: -------------------------------------------------------------------------------- 1 | use axum::{http::StatusCode, response::IntoResponse}; 2 | 3 | /// Handle health Requests 4 | pub(crate) async fn health_handler() -> impl IntoResponse { 5 | StatusCode::OK 6 | } 7 | -------------------------------------------------------------------------------- /explorer/src/server/handlers/indexer.rs: -------------------------------------------------------------------------------- 1 | use { 2 | axum::{ 3 | extract::{Path, Query, State}, 4 | response::IntoResponse, 5 | Json, 6 | }, 7 | sea_orm::{ActiveModelTrait, EntityTrait, PaginatorTrait, Set}, 8 | uuid::Uuid, 9 | }; 10 | 11 | use crate::{application::AppState, entity::indexer, error::AppError}; 12 | 13 | const DEFAULT_PAGE_SIZE: u64 = 25; 14 | 15 | #[derive(serde::Deserialize)] 16 | pub struct PaginationParams { 17 | page: Option, 18 | page_size: Option, 19 | } 20 | 21 | pub(crate) async fn indexer_handler( 22 | state: State, 23 | pagination: Query, 24 | ) -> Result { 25 | tracing::info!("Received indexer request"); 26 | let db = &state.database_conn; 27 | 28 | let page = pagination.page.unwrap_or_default(); 29 | let page_size = pagination.page_size.unwrap_or(DEFAULT_PAGE_SIZE); 30 | 31 | let indexers_count = indexer::Entity::find().count(db).await?; 32 | 33 | let result = indexer::Entity::find() 34 | .paginate(db, page_size) 35 | .fetch_page(page) 36 | .await?; 37 | 38 | let json_response = serde_json::json!({ 39 | "data": result, 40 | "meta": { 41 | "total": indexers_count, 42 | "page": page, 43 | "page_size": page_size, 44 | } 45 | }); 46 | 47 | Ok(Json(json_response)) 48 | } 49 | 50 | pub(crate) async fn indexer_heartbeat_handler( 51 | state: State, 52 | Path(id): Path, 53 | cursor: String, 54 | ) -> Result { 55 | let db = &state.database_conn; 56 | 57 | indexer::ActiveModel { 58 | id: Set(id), 59 | last_heartbeat: Set(chrono::Utc::now().naive_utc()), 60 | cursor: Set(cursor), 61 | ..Default::default() 62 | } 63 | .update(db) 64 | .await?; 65 | 66 | let json_response = serde_json::json!({ 67 | "status": "success" 68 | }); 69 | 70 | Ok(Json(json_response)) 71 | } 72 | -------------------------------------------------------------------------------- /explorer/src/server/handlers/jwt_auth.rs: -------------------------------------------------------------------------------- 1 | use { 2 | axum::{ 3 | extract::State, 4 | http::Request, 5 | http::{header, StatusCode}, 6 | middleware::Next, 7 | response::IntoResponse, 8 | }, 9 | axum_extra::extract::cookie::CookieJar, 10 | jsonwebtoken::{decode, DecodingKey, Validation}, 11 | secrecy::ExposeSecret, 12 | serde::{Deserialize, Serialize}, 13 | }; 14 | 15 | use crate::{application::AppState, error::AppError}; 16 | 17 | #[derive(Debug, Serialize, Deserialize)] 18 | pub struct TokenClaims { 19 | pub id: String, 20 | pub iat: usize, 21 | pub exp: usize, 22 | } 23 | 24 | pub(crate) async fn auth_handler( 25 | state: State, 26 | cookie_jar: CookieJar, 27 | req: Request, 28 | next: Next, 29 | ) -> Result { 30 | let jwt_secret = state.jwt_secret.clone(); 31 | let token = cookie_jar 32 | .get("token") 33 | .map(|cookie| cookie.value().to_string()) 34 | .or_else(|| { 35 | req.headers() 36 | .get(header::AUTHORIZATION) 37 | .and_then(|auth_header| auth_header.to_str().ok()) 38 | .and_then(|auth_value| { 39 | auth_value 40 | .strip_prefix("Bearer ") 41 | .map(|payload| payload.to_owned()) 42 | }) 43 | }); 44 | 45 | let token = token.ok_or_else(|| { 46 | AppError::new( 47 | StatusCode::UNAUTHORIZED, 48 | "You are not authenticated, please provide token".to_string(), 49 | ) 50 | })?; 51 | 52 | decode::( 53 | &token, 54 | &DecodingKey::from_secret(jwt_secret.expose_secret().as_ref()), 55 | &Validation::default(), 56 | ) 57 | .map_err(|_| AppError::new(StatusCode::UNAUTHORIZED, "Invalid token".to_string()))?; 58 | 59 | Ok(next.run(req).await) 60 | } 61 | -------------------------------------------------------------------------------- /explorer/src/server/handlers/mod.rs: -------------------------------------------------------------------------------- 1 | mod events; 2 | mod graphql; 3 | mod health; 4 | mod indexer; 5 | mod jwt_auth; 6 | mod stats; 7 | 8 | pub(crate) use events::event_handler; 9 | pub(crate) use graphql::{graphiql_playground, graphql_handler}; 10 | pub(crate) use health::health_handler; 11 | pub(crate) use indexer::{indexer_handler, indexer_heartbeat_handler}; 12 | pub(crate) use jwt_auth::auth_handler; 13 | pub(crate) use stats::stats_handler; 14 | 15 | pub use jwt_auth::TokenClaims; 16 | -------------------------------------------------------------------------------- /explorer/src/server/handlers/stats.rs: -------------------------------------------------------------------------------- 1 | use axum::response::IntoResponse; 2 | 3 | /// Handle stats Requests 4 | pub(crate) async fn stats_handler() -> impl IntoResponse { 5 | unimplemented!() 6 | } 7 | -------------------------------------------------------------------------------- /explorer/src/server/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod app_server; 2 | pub(crate) mod handlers; 3 | pub(crate) mod schema; 4 | 5 | pub use handlers::TokenClaims; 6 | -------------------------------------------------------------------------------- /explorer/src/server/schema.rs: -------------------------------------------------------------------------------- 1 | use { 2 | anyhow::Result, 3 | async_graphql::{EmptyMutation, EmptySubscription, MergedObject, Schema}, 4 | sea_orm::DatabaseConnection, 5 | }; 6 | 7 | use crate::entity::{ 8 | address::AddressQuery, asset::AssetQuery, case::CaseQuery, network::NetworkQuery, 9 | reporter::ReporterQuery, statistics::StatisticsQuery, 10 | }; 11 | 12 | /// Top-level application Query type 13 | #[derive(Default, MergedObject)] 14 | pub struct Query( 15 | AddressQuery, 16 | AssetQuery, 17 | CaseQuery, 18 | ReporterQuery, 19 | NetworkQuery, 20 | StatisticsQuery, 21 | ); 22 | 23 | /// Top-level merged application schema 24 | pub type AppSchema = Schema; 25 | 26 | /// Building the GraphQL application schema, attaching the Database to the context 27 | pub(crate) fn create_graphql_schema(db: DatabaseConnection) -> Result { 28 | Ok( 29 | Schema::build(Query::default(), EmptyMutation, EmptySubscription) 30 | .data(db) 31 | .finish(), 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /explorer/src/service/mod.rs: -------------------------------------------------------------------------------- 1 | mod mutation; 2 | mod query; 3 | 4 | pub use mutation::EntityMutation; 5 | pub use query::{count_rows_per_week, get_network_id, EntityQuery}; 6 | -------------------------------------------------------------------------------- /explorer/tests/graphql/mod.rs: -------------------------------------------------------------------------------- 1 | mod address_query; 2 | mod asset_query; 3 | mod case_query; 4 | mod network_query; 5 | mod reporter_query; 6 | mod statistics_query; 7 | 8 | pub use case_query::check_case; 9 | 10 | pub(super) fn replacer(v: &V) -> String { 11 | v.to_string() 12 | .replace("\"", "") 13 | .replace("_", "") 14 | .to_lowercase() 15 | } 16 | -------------------------------------------------------------------------------- /explorer/tests/helpers/jwt.rs: -------------------------------------------------------------------------------- 1 | use { 2 | hapi_explorer::server::TokenClaims, 3 | jsonwebtoken::{encode, EncodingKey, Header}, 4 | }; 5 | 6 | pub(crate) fn create_jwt(secret: &str) -> String { 7 | let claims = TokenClaims { 8 | id: get_jwt_id(), 9 | iat: 1, 10 | exp: 10000000000, 11 | }; 12 | 13 | encode( 14 | &Header::default(), 15 | &claims, 16 | &EncodingKey::from_secret(secret.as_ref()), 17 | ) 18 | .expect("Failed to generate JWT") 19 | } 20 | 21 | pub fn get_jwt_id() -> String { 22 | "1466cf4f-1d71-4153-b9ad-4a9c1b48101e".to_string() 23 | } 24 | -------------------------------------------------------------------------------- /explorer/tests/helpers/mod.rs: -------------------------------------------------------------------------------- 1 | mod jwt; 2 | mod request_sender; 3 | mod test_app; 4 | mod test_data; 5 | 6 | pub(crate) use jwt::create_jwt; 7 | pub(crate) use request_sender::RequestSender; 8 | pub(crate) use test_app::{ 9 | FromTestPayload, TestApp, TestNetwork, METRICS_ENV_VAR, MIGRATION_COUNT, WAITING_INTERVAL, 10 | }; 11 | pub(crate) use test_data::{ 12 | create_address_data, create_asset_data, create_reporter_data, get_test_data, TestData, 13 | }; 14 | -------------------------------------------------------------------------------- /explorer/tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod cli; 2 | mod graphql; 3 | mod helpers; 4 | mod routes; 5 | -------------------------------------------------------------------------------- /explorer/tests/routes/cors.rs: -------------------------------------------------------------------------------- 1 | use crate::helpers::{RequestSender, TestApp}; 2 | 3 | #[tokio::test] 4 | async fn cors_test() { 5 | let origins = vec![ 6 | "http://allowed-origin1.com".to_string(), 7 | "http://allowed-origin2.com".to_string(), 8 | ]; 9 | 10 | let test_app = TestApp::start(Some(origins.clone())).await; 11 | let client = RequestSender::new(test_app.server_addr.to_owned()); 12 | 13 | for host in origins { 14 | let response = client 15 | .web_client 16 | .get(format!("{}/health", test_app.server_addr)) 17 | .header("Origin", host.clone()) 18 | .send() 19 | .await 20 | .expect("Failed to send request"); 21 | 22 | assert!(response 23 | .headers() 24 | .contains_key("Access-Control-Allow-Origin")); 25 | 26 | let cors_header = response 27 | .headers() 28 | .get("Access-Control-Allow-Origin") 29 | .unwrap() 30 | .to_str() 31 | .unwrap(); 32 | assert_eq!(cors_header, host); 33 | } 34 | 35 | let response = client 36 | .web_client 37 | .get(format!("{}/health", test_app.server_addr)) 38 | .header("Origin", "http://forbidden-origin.com") 39 | .send() 40 | .await 41 | .expect("Failed to send request"); 42 | 43 | assert!(!response 44 | .headers() 45 | .contains_key("Access-Control-Allow-Origin")); 46 | } 47 | -------------------------------------------------------------------------------- /explorer/tests/routes/health_check.rs: -------------------------------------------------------------------------------- 1 | use crate::helpers::{RequestSender, TestApp}; 2 | 3 | #[tokio::test] 4 | async fn health_check_test() { 5 | let test_app = TestApp::start(None).await; 6 | let client = RequestSender::new(test_app.server_addr.to_owned()); 7 | 8 | client 9 | .get("health") 10 | .await 11 | .expect("Failed to get health check"); 12 | } 13 | -------------------------------------------------------------------------------- /explorer/tests/routes/indexer.rs: -------------------------------------------------------------------------------- 1 | use crate::helpers::{create_jwt, RequestSender, TestApp}; 2 | 3 | /* 4 | Test cases: 5 | - heartbeat indexer 6 | - heartbeat indexer with wrong token 7 | - get indexers 8 | - check count of indexers 9 | */ 10 | #[tokio::test] 11 | async fn indexer_processing_test() { 12 | let test_app = TestApp::start(None).await; 13 | let indexer_mock = RequestSender::new(test_app.server_addr.to_owned()); 14 | 15 | for network in &test_app.networks { 16 | // heartbeat indexer 17 | indexer_mock.send_heartbeat(&network.token).await.unwrap(); 18 | } 19 | 20 | // heartbeat indexer with wrong token 21 | assert!(indexer_mock 22 | .send_heartbeat(&create_jwt("invalid_token")) 23 | .await 24 | .is_err()); 25 | 26 | // get indexers 27 | let response = indexer_mock.get("indexer").await.unwrap(); 28 | 29 | // check count of indexers 30 | let indexers: Vec = response["data"].as_array().unwrap().to_vec(); 31 | assert_eq!(indexers.len(), test_app.networks.len()); 32 | } 33 | -------------------------------------------------------------------------------- /explorer/tests/routes/mod.rs: -------------------------------------------------------------------------------- 1 | mod cors; 2 | mod health_check; 3 | mod indexer; 4 | mod metrics; 5 | mod webhook_processing; 6 | -------------------------------------------------------------------------------- /explorer/tests/routes/webhook_processing.rs: -------------------------------------------------------------------------------- 1 | use crate::helpers::{create_jwt, get_test_data, RequestSender, TestApp, WAITING_INTERVAL}; 2 | use tokio::time::{sleep, Duration}; 3 | 4 | #[tokio::test] 5 | async fn webhook_processing_test() { 6 | let test_app = TestApp::start(None).await; 7 | let indexer_mock = RequestSender::new(test_app.server_addr.clone()); 8 | let token = create_jwt("my_ultra_secure_secret"); 9 | 10 | for network in &test_app.networks { 11 | let test_data = get_test_data(&network.network, network.model.chain_id.clone()); 12 | 13 | for payload in test_data { 14 | indexer_mock 15 | .send("events", &payload, &token) 16 | .await 17 | .expect("Failed to send event"); 18 | sleep(Duration::from_millis(WAITING_INTERVAL)).await; 19 | 20 | test_app 21 | .check_entity(payload.data, network.model.id.clone()) 22 | .await; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /indexer/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /data 3 | /configuration.toml 4 | /secret.toml 5 | .vscode 6 | -------------------------------------------------------------------------------- /indexer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hapi_indexer" 3 | authors = ["HAPI"] 4 | version = "0.1.0" 5 | edition = "2021" 6 | description = "Indexer for HAPI Protocol contracts" 7 | 8 | [[bin]] 9 | name = "hapi-indexer" 10 | path = "src/main.rs" 11 | 12 | [lib] 13 | crate-type = ["lib"] 14 | name = "hapi_indexer" 15 | 16 | [features] 17 | manual-helper = [] 18 | 19 | [dependencies] 20 | hapi-core = { path = "../client.rs" } 21 | anyhow = "1" 22 | axum = "0.6" 23 | config = "0.13" 24 | hyper = { version = "0.14", features = ["tcp"] } 25 | serde = { version = "1", features = ["derive"] } 26 | serde_json = { version = "1" } 27 | serde_with = { version = "3", features = ["chrono_0_4"] } 28 | tokio = { version = "1", features = ["full"] } 29 | tracing = "0.1" 30 | tracing-subscriber = { version = "0.3", features = [ 31 | "fmt", 32 | "json", 33 | "env-filter", 34 | ] } 35 | uuid = { version = "1", features = ["serde", "v4"] } 36 | reqwest = "0.11" 37 | lazy_static = "1" 38 | enum_extract = "0.1" 39 | jsonwebtoken = "9.2.0" 40 | base64 = "0.21.5" 41 | 42 | # Evm dependencies 43 | ethers = "=2.0.8" 44 | 45 | # NEAR dependencies 46 | near-jsonrpc-primitives = "0.19.0" 47 | near-jsonrpc-client = "0.7.0" 48 | near-primitives = "0.19.0" 49 | hapi-core-near = { path = "../near/contract/" } 50 | near-crypto = "0.19.0" 51 | 52 | # Solana dependencies 53 | [dependencies.solana-client] 54 | git = "https://github.com/hlgltvnnk/solana.git" 55 | branch = "update-dependencies" 56 | 57 | [dependencies.solana-sdk] 58 | git = "https://github.com/hlgltvnnk/solana.git" 59 | branch = "update-dependencies" 60 | 61 | [dependencies.solana-transaction-status] 62 | git = "https://github.com/hlgltvnnk/solana.git" 63 | branch = "update-dependencies" 64 | 65 | [dependencies.solana-account-decoder] 66 | git = "https://github.com/hlgltvnnk/solana.git" 67 | branch = "update-dependencies" 68 | 69 | [dependencies.hapi-core-solana] 70 | path = "../solana/programs/hapi_core_solana" 71 | features = ["no-entrypoint"] 72 | 73 | [dev-dependencies] 74 | mockito = "1.2" 75 | hex = "0.4" 76 | rand = "0.8" 77 | 78 | [dev-dependencies.anchor-lang] 79 | git = "https://github.com/hlgltvnnk/anchor.git" 80 | branch = "update-dependencies" 81 | -------------------------------------------------------------------------------- /indexer/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 HAPI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /indexer/README.md: -------------------------------------------------------------------------------- 1 | # HAPI indexer 2 | 3 | This project provides a transaction indexing application for HAPI smart contracts, that serves Solana, EVM and NEAR blockchains. 4 | 5 | --- 6 | 7 | State machine of the indexer: 8 | 9 | 1. Initialize application: load persisted application state from a file (or database). 10 | 2. Check if something has been added to the contract. 11 | 3. Process received events and submit new data to output endpoint. 12 | 4. Wait for the updates. 13 | 14 | --- 15 | 16 | ## Usage 17 | 18 | It is required to set indexer configuration before start. Define config file in CONFIG_PATH env variable. 19 | Configuration file must contain fields: 20 | 21 | ```toml 22 | 23 | log_level # Tracing level 24 | is_json_logging # Tracing format 25 | listener # Address for the listener server 26 | 27 | [indexer] 28 | network # Indexed network [Sepolia, Ethereum, Bsc, Solana, Bitcoin, Near] 29 | chain_id # Network chain ID (optional) 30 | rpc_node_url # HTTP URL of the rpc node for the network 31 | webhook_url # HTTP URL of the webhook server 32 | contract_address # The HAPI Core contract address 33 | wait_interval_ms # Timeout in milliseconds between wait checks (default 1000 millis) 34 | state_file # The file to persist the indexer state in (default data/state.json) 35 | 36 | ``` 37 | 38 | To configure the indexing page limit, set the INDEXER_PAGE_SIZE env variable (default 500) 39 | 40 | Run indexer with: 41 | 42 | ``` 43 | cargo run 44 | ``` 45 | 46 | ## Testing 47 | 48 | To enable indexer tracing in tests, set the ENABLE_TRACING env variable to 1 49 | 50 | ``` 51 | cargo test 52 | ``` 53 | 54 | ## Manual testing 55 | 56 | Steps: 57 | 58 | 1. Prepare the environment for the selected network: run validator, deploy contract and create test data (all instructions are in the contact directory). 59 | 2. Start a listener server for webhooks (a simple listener server is available in tests, run `cargo test run_simple_listener -- --nocapture`). 60 | 3. Set indexer configuration and define cfg path in CONFIG_PATH env variable. 61 | 4. Run indexer with ` cargo run` command. 62 | 5. Compare the resulting payload with the test data 63 | 64 | ## License 65 | 66 | HAPI indexer is distributed under the terms of the MIT license. 67 | -------------------------------------------------------------------------------- /indexer/configuration.sample.toml: -------------------------------------------------------------------------------- 1 | log_level = "info" 2 | is_json_logging = true 3 | listener = "0.0.0.0:3000" 4 | 5 | [indexer] 6 | network = "ethereum" 7 | rpc_node_url = "http://localhost:8545" 8 | webhook_url = "http://localhost:3000" 9 | fetching_delay = 100 10 | contract_address = "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" 11 | state_file = "data/state.json" 12 | jwt_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjFhODgzNDhlLTEwNzItNDVlYi04MWZlLWMzOTNhM2Q2YmFhMCIsImlhdCI6MTcwNDI5MTExNSwiZXhwIjoxNzM1ODI3MTE1fQ._cc3zp58ZSPsyFHNjZUkj4jluXp2BbZjL0grDtmN12w" 13 | -------------------------------------------------------------------------------- /indexer/src/indexer/client/mod.rs: -------------------------------------------------------------------------------- 1 | mod evm; 2 | mod indexer_client; 3 | mod near; 4 | mod solana; 5 | 6 | pub(crate) use indexer_client::IndexerClient; 7 | pub use near::NearReceipt; 8 | -------------------------------------------------------------------------------- /indexer/src/indexer/heartbeat.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Result}; 2 | 3 | use crate::{Indexer, IndexingCursor}; 4 | 5 | impl Indexer { 6 | pub(crate) async fn send_heartbeat(&self, cursor: &IndexingCursor) -> Result<()> { 7 | let url = format!( 8 | "{}/indexer/{}/heartbeat", 9 | self.webhook_url, 10 | self.client.get_id() 11 | ); 12 | 13 | let response = self 14 | .web_client 15 | .put(&url) 16 | .bearer_auth(self.jwt_token.as_str()) 17 | .json(&cursor) 18 | .send() 19 | .await?; 20 | 21 | if !response.status().is_success() { 22 | bail!("Heartbeat request failed: {:?}", response.text().await?); 23 | } 24 | 25 | Ok(()) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /indexer/src/indexer/jobs.rs: -------------------------------------------------------------------------------- 1 | use super::client::NearReceipt; 2 | use { 3 | ethers::types::Log, 4 | serde::{Deserialize, Serialize}, 5 | }; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub(crate) enum IndexerJob { 9 | Transaction(String), 10 | Log(Log), 11 | TransactionReceipt(NearReceipt), 12 | } 13 | -------------------------------------------------------------------------------- /indexer/src/indexer/jwt.rs: -------------------------------------------------------------------------------- 1 | use { 2 | anyhow::{anyhow, Result}, 3 | base64::{ 4 | alphabet, 5 | engine::{self, general_purpose}, 6 | Engine as _, 7 | }, 8 | serde::{Deserialize, Serialize}, 9 | uuid::Uuid, 10 | }; 11 | 12 | #[derive(Debug, Serialize, Deserialize)] 13 | pub struct TokenClaims { 14 | pub id: Uuid, 15 | pub iat: usize, 16 | pub exp: usize, 17 | } 18 | 19 | pub fn get_id_from_jwt(token: &str) -> Result { 20 | let token_data = token.split('.').nth(1).ok_or(anyhow!("Invalid token"))?; 21 | 22 | let bytes = engine::GeneralPurpose::new(&alphabet::STANDARD, general_purpose::NO_PAD) 23 | .decode(token_data)?; 24 | 25 | let claims: TokenClaims = serde_json::from_slice(&bytes)?; 26 | 27 | Ok(claims.id) 28 | } 29 | 30 | #[cfg(test)] 31 | mod tests { 32 | use super::*; 33 | 34 | #[test] 35 | fn test_jwt() { 36 | let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjE0NjZjZjRmLTFkNzEtNDE1My1iOWFkLTRhOWMxYjQ4MTAxZSIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjM5MDIyfQ.weKNyTDqRCHMnEmN1RNsKI5vD24w-qesqf9EMoqJz1M"; 37 | 38 | let id = get_id_from_jwt(token).unwrap(); 39 | assert_eq!( 40 | id, 41 | Uuid::parse_str("1466cf4f-1d71-4153-b9ad-4a9c1b48101e").unwrap() 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /indexer/src/indexer/mod.rs: -------------------------------------------------------------------------------- 1 | use { 2 | anyhow::Result, 3 | std::{ 4 | collections::VecDeque, 5 | path::PathBuf, 6 | sync::Arc, 7 | time::{Duration, SystemTime, UNIX_EPOCH}, 8 | }, 9 | tokio::sync::Mutex, 10 | }; 11 | 12 | pub(crate) mod client; 13 | pub(crate) mod heartbeat; 14 | pub(crate) mod jobs; 15 | pub(crate) mod jwt; 16 | pub(crate) mod logic; 17 | pub(crate) mod persistence; 18 | pub(crate) mod push; 19 | pub(crate) mod server; 20 | pub(crate) mod state; 21 | 22 | pub(crate) use { 23 | client::IndexerClient, 24 | jobs::IndexerJob, 25 | persistence::PersistedState, 26 | state::{IndexerState, IndexingCursor}, 27 | }; 28 | 29 | fn now() -> Result { 30 | Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs()) 31 | } 32 | 33 | pub struct Indexer { 34 | /// Current state of the indexer 35 | state: Arc>, 36 | 37 | /// Stack of transactions to index 38 | jobs: VecDeque, 39 | 40 | /// The number of milliseconds between wait checks 41 | wait_interval_ms: Duration, 42 | 43 | /// Abstract client to access blockchain data 44 | client: IndexerClient, 45 | 46 | /// The file to persist the indexer state in 47 | state_file: PathBuf, 48 | 49 | /// The HTTP client to use for webhooks 50 | web_client: reqwest::Client, 51 | 52 | /// The URL to send webhooks to 53 | webhook_url: String, 54 | 55 | /// JWT token to use for webhooks 56 | jwt_token: String, 57 | } 58 | -------------------------------------------------------------------------------- /indexer/src/indexer/persistence.rs: -------------------------------------------------------------------------------- 1 | use { 2 | anyhow::Result, 3 | serde::{Deserialize, Serialize}, 4 | std::{ 5 | fs, 6 | path::{Path, PathBuf}, 7 | }, 8 | }; 9 | 10 | use super::IndexingCursor; 11 | 12 | #[derive(Serialize, Deserialize)] 13 | pub struct PersistedState { 14 | pub cursor: IndexingCursor, 15 | } 16 | 17 | impl PersistedState { 18 | pub fn from_file(path: &Path) -> Result { 19 | let state = fs::read_to_string(path)?; 20 | Ok(serde_json::from_str(&state)?) 21 | } 22 | 23 | pub fn to_file(&self, path: &Path) -> Result<()> { 24 | // Create the parent directory if it doesn't exist 25 | if let Some(dir) = path.ancestors().nth(1) { 26 | if !PathBuf::from(dir).exists() { 27 | fs::create_dir_all(dir)?; 28 | } 29 | } 30 | let state = serde_json::to_string(self)?; 31 | fs::write(path, state)?; 32 | Ok(()) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /indexer/src/indexer/server.rs: -------------------------------------------------------------------------------- 1 | use { 2 | anyhow::Result, 3 | axum::{ 4 | extract::State, 5 | routing::{get, put}, 6 | Json, Router, Server, 7 | }, 8 | serde::Serialize, 9 | std::{future::Future, sync::Arc, time::Duration}, 10 | tokio::{ 11 | sync::Mutex, 12 | task::{spawn, JoinHandle}, 13 | time::sleep, 14 | }, 15 | }; 16 | 17 | use super::{state::IndexerState, Indexer}; 18 | 19 | impl Indexer { 20 | async fn shutdown_signal(&self) -> impl Future { 21 | let shared_state = self.state.clone(); 22 | async move { 23 | loop { 24 | sleep(Duration::from_secs(1)).await; 25 | if matches!(*shared_state.lock().await, IndexerState::Stopped { .. }) { 26 | break; 27 | } 28 | } 29 | } 30 | } 31 | 32 | fn create_router(&self) -> Router { 33 | Router::new() 34 | .route("/state", get(get_state)) 35 | .route("/stop", put(stop)) 36 | .with_state(self.state.clone()) 37 | } 38 | 39 | pub async fn spawn_server(&self, addr: &str) -> Result>> { 40 | tracing::debug!(?addr, "Start server"); 41 | 42 | let server = Server::bind(&addr.parse()?) 43 | .serve(self.create_router().into_make_service()) 44 | .with_graceful_shutdown(self.shutdown_signal().await); 45 | 46 | Ok(spawn( 47 | async move { server.await.map_err(anyhow::Error::from) }, 48 | )) 49 | } 50 | } 51 | 52 | #[derive(Serialize)] 53 | struct GetStateOutput { 54 | state: IndexerState, 55 | } 56 | 57 | async fn get_state(State(shared_state): State>>) -> Json { 58 | let state = shared_state.lock().await.clone(); 59 | 60 | Json(GetStateOutput { state }) 61 | } 62 | 63 | #[derive(Serialize)] 64 | struct StopOutput { 65 | success: bool, 66 | } 67 | 68 | async fn stop(State(shared_state): State>>) -> Json { 69 | shared_state.lock().await.transition(IndexerState::Stopped { 70 | message: "Stopped by user".to_string(), 71 | }); 72 | 73 | Json(StopOutput { success: true }) 74 | } 75 | -------------------------------------------------------------------------------- /indexer/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod configuration; 2 | mod indexer; 3 | pub mod observability; 4 | 5 | pub use indexer::{ 6 | jwt::get_id_from_jwt, 7 | persistence::PersistedState, 8 | push::{NetworkData, PushData, PushEvent, PushPayload}, 9 | state::IndexingCursor, 10 | Indexer, 11 | }; 12 | -------------------------------------------------------------------------------- /indexer/src/main.rs: -------------------------------------------------------------------------------- 1 | use { 2 | anyhow::{bail, Result}, 3 | tokio::{ 4 | select, 5 | task::{spawn, JoinError}, 6 | }, 7 | }; 8 | 9 | use hapi_indexer::{ 10 | configuration::get_configuration, 11 | observability::{setup_json_tracing, setup_tracing}, 12 | Indexer, 13 | }; 14 | 15 | #[tokio::main] 16 | async fn main() -> Result<()> { 17 | let cfg = 18 | get_configuration().map_err(|e| anyhow::anyhow!("Configuration parsing error: {e}"))?; 19 | 20 | if cfg.is_json_logging { 21 | setup_json_tracing(&cfg.log_level)?; 22 | } else { 23 | setup_tracing(&cfg.log_level)?; 24 | } 25 | 26 | tracing::info!( 27 | "Starting {} v{}", 28 | env!("CARGO_PKG_NAME"), 29 | env!("CARGO_PKG_VERSION") 30 | ); 31 | 32 | let mut indexer = Indexer::new(cfg.indexer)?; 33 | 34 | let server_task = indexer.spawn_server(&cfg.listener).await?; 35 | let indexer_task = spawn(async move { indexer.run().await }); 36 | 37 | select! { 38 | server_result = server_task => { 39 | handle_result(server_result).await 40 | } 41 | indexer_result = indexer_task => { 42 | handle_result(indexer_result).await 43 | } 44 | } 45 | } 46 | 47 | async fn handle_result(result: Result, JoinError>) -> Result<()> { 48 | match result { 49 | Ok(Ok(_)) => Ok(()), 50 | Ok(Err(e)) => { 51 | tracing::error!(?e, "Indexer failed"); 52 | bail!("Indexer failed with error: {:?}", e); 53 | } 54 | Err(e) => { 55 | tracing::error!(?e, "Task failed to execute to completion"); 56 | bail!("Task failed to execute to completion: {:?}", e); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /indexer/src/observability.rs: -------------------------------------------------------------------------------- 1 | use { 2 | anyhow::{anyhow, Result}, 3 | tracing::subscriber, 4 | tracing_subscriber::{fmt::Subscriber, EnvFilter}, 5 | }; 6 | 7 | pub fn setup_tracing(log_level: &str) -> Result<()> { 8 | let subscriber = Subscriber::builder() 9 | .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| { 10 | format!("{}={log_level}", to_snake_case(env!("CARGO_PKG_NAME"))).into() 11 | })) 12 | .with_writer(std::io::stdout) 13 | .finish(); 14 | 15 | subscriber::set_global_default(subscriber) 16 | .map_err(|e| anyhow!("Failed to set up tracing subscriber: {:?}", e)) 17 | } 18 | 19 | pub fn setup_json_tracing(log_level: &str) -> Result<()> { 20 | let subscriber = Subscriber::builder() 21 | .json() 22 | .flatten_event(true) 23 | .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| { 24 | format!("{}={log_level}", to_snake_case(env!("CARGO_PKG_NAME"))).into() 25 | })) 26 | .with_writer(std::io::stdout) 27 | .finish(); 28 | 29 | subscriber::set_global_default(subscriber) 30 | .map_err(|e| anyhow!("Failed to set up tracing subscriber: {:?}", e)) 31 | } 32 | 33 | fn to_snake_case(s: &str) -> String { 34 | s.to_lowercase().replace(['-', ' '], "_") 35 | } 36 | -------------------------------------------------------------------------------- /indexer/tests/jwt.rs: -------------------------------------------------------------------------------- 1 | // returns builded jwt token 2 | pub fn get_jwt() -> String { 3 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjE0NjZjZjRmLTFkNzEtNDE1My1iOWFkLTRhOWMxYjQ4MTAxZSIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjM5MDIyfQ.weKNyTDqRCHMnEmN1RNsKI5vD24w-qesqf9EMoqJz1M".to_string() 4 | } 5 | 6 | // returns id used in jwt token 7 | pub fn get_jwt_id() -> String { 8 | "1466cf4f-1d71-4153-b9ad-4a9c1b48101e".to_string() 9 | } 10 | -------------------------------------------------------------------------------- /indexer/tests/mocks/webhook_mock.rs: -------------------------------------------------------------------------------- 1 | use { 2 | hapi_core::client::events::EventName, 3 | hapi_indexer::{PushEvent, PushPayload}, 4 | mockito::{Matcher, Mock, Server, ServerGuard}, 5 | }; 6 | 7 | use super::TestBatch; 8 | 9 | pub struct WebhookServiceMock { 10 | mocks: Vec, 11 | pub server: ServerGuard, 12 | } 13 | 14 | impl WebhookServiceMock { 15 | pub fn new() -> Self { 16 | Self { 17 | mocks: vec![], 18 | server: Server::new(), 19 | } 20 | } 21 | pub fn set_mocks(&mut self, batch: &TestBatch) { 22 | for event in batch { 23 | if let Some(data) = &event.data { 24 | if event.name != EventName::ConfirmAddress && event.name != EventName::ConfirmAsset 25 | { 26 | let payload = PushPayload { 27 | network_data: event.network_data.clone(), 28 | event: PushEvent { 29 | name: event.name.clone(), 30 | tx_hash: event.hash.clone(), 31 | tx_index: 0, 32 | timestamp: 123, 33 | }, 34 | data: data.clone(), 35 | }; 36 | 37 | let mock = self 38 | .server 39 | .mock("POST", "/events") 40 | .with_status(200) 41 | .match_body(Matcher::PartialJsonString( 42 | serde_json::to_string(&payload).expect("Failed to serialize payload"), 43 | )) 44 | .expect(1) 45 | .create(); 46 | 47 | self.mocks.push(mock); 48 | } 49 | } 50 | } 51 | } 52 | 53 | pub fn check_mocks(&self) { 54 | for mock in &self.mocks { 55 | mock.assert(); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /indexer/tests/simple_listener.rs: -------------------------------------------------------------------------------- 1 | use { 2 | axum::{response::IntoResponse, routing::post, Router, Server}, 3 | reqwest::StatusCode, 4 | std::net::SocketAddr, 5 | }; 6 | 7 | async fn webhook_handler(body: String) -> impl IntoResponse { 8 | println!("Received webhook: {}\n", body); 9 | 10 | (StatusCode::OK, "Received webhook") 11 | } 12 | 13 | #[tokio::test] 14 | async fn run_simple_listener() { 15 | let app = Router::new() 16 | .route("/", post(webhook_handler)) 17 | .into_make_service(); 18 | let addr = SocketAddr::from(([127, 0, 0, 1], 8080)); 19 | 20 | println!("Listening on {}", addr); 21 | 22 | Server::bind(&addr) 23 | .serve(app) 24 | .await 25 | .expect("Failed to start server"); 26 | } 27 | -------------------------------------------------------------------------------- /near/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock -------------------------------------------------------------------------------- /near/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["contract"] 3 | resolver = "2" 4 | 5 | [profile.release] 6 | codegen-units = 1 7 | opt-level = "z" 8 | lto = true 9 | debug = false 10 | panic = "abort" 11 | overflow-checks = true 12 | -------------------------------------------------------------------------------- /near/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use NEAR maintained image as a base 2 | FROM nearprotocol/contract-builder:latest 3 | 4 | # Setup tooling 5 | RUN rustup toolchain install stable-2023-04-20 && \ 6 | rustup default stable-2023-04-20 && \ 7 | rustup target add wasm32-unknown-unknown 8 | 9 | # Define working directory (instead of root) 10 | WORKDIR /builder 11 | 12 | # Copy source code to the builder directory 13 | COPY . /builder 14 | 15 | # Define custom flags for Rust compiler 16 | ENV RUSTFLAGS="-C link-arg=-s" 17 | 18 | # Define custom cargo home directory 19 | ENV CARGO_HOME=/var/cache/cargo 20 | 21 | # Define a volume for built artifacts and dependency cache 22 | VOLUME "/var/cache/cargo" 23 | 24 | # Define a volume for the output artifacts 25 | VOLUME "/output" 26 | 27 | # Build the contract when the container is started 28 | # Removing rlib first to reduce the size of wasm file 29 | CMD cargo build --target wasm32-unknown-unknown --release && \ 30 | cp target/wasm32-unknown-unknown/release/hapi_core_near.wasm /output/hapi_core_near.wasm 31 | -------------------------------------------------------------------------------- /near/build_docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | printf "\033[0;34m=> [pre-build] formatting and clippy\n\033[0m" 5 | cargo fmt && cargo clippy -- -D warnings 6 | 7 | CONTAINER_NAME="hapi-core-near-builder" 8 | CARGO_CACHE_VOLUME="$CONTAINER_NAME-cargo-cache" 9 | 10 | # Get the directory path of the current script 11 | SCRIPT_DIR=$(dirname "$0") 12 | 13 | # Change the current directory to the script directory 14 | cd "$SCRIPT_DIR" 15 | 16 | # Make sure we have the latest version of the base image 17 | docker build -t $CONTAINER_NAME . 18 | 19 | # Check if the container is running 20 | if [ "$(docker ps -q -f name=$CONTAINER_NAME)" ]; then 21 | # If the container is running, stop it. 22 | echo "Container $CONTAINER_NAME is running. Attempting to stop..." 23 | docker stop $CONTAINER_NAME 24 | fi 25 | 26 | # Check if the container exists 27 | if [ "$(docker ps -aq -f status=exited -f name=$CONTAINER_NAME)" ]; then 28 | # If the container exists, but is stopped, remove it. 29 | echo "Container $CONTAINER_NAME exists, but is not running. Removing..." 30 | docker rm $CONTAINER_NAME 31 | fi 32 | 33 | # Run the builder and mount the ./res directory for artifact output 34 | docker run \ 35 | -it \ 36 | -v $PWD/res:/output \ 37 | --name $CONTAINER_NAME \ 38 | --volume $CARGO_CACHE_VOLUME:/var/cache/cargo \ 39 | $CONTAINER_NAME 40 | -------------------------------------------------------------------------------- /near/contract/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hapi-core-near" 3 | authors = ["HAPI "] 4 | version = "0.3.1" 5 | edition = "2021" 6 | license-file = "LICENSE" 7 | description = "NEAR implementation of HAPI Protocol contract" 8 | repository = "https://github.com/HAPIprotocol/hapi-core" 9 | homepage = "https://hapi.one" 10 | 11 | [lib] 12 | crate-type = ["cdylib", "lib"] 13 | 14 | [dependencies] 15 | borsh = { version = "1.0.0", features = ["derive"] } 16 | 17 | # TODO: Replace with the latest stable version of near-sdk-rs 18 | near-sdk = "5.0.0-alpha.2" 19 | near-contract-standards = "5.0.0-alpha.2" 20 | 21 | [dev-dependencies] 22 | uuid = { version = "1.4.0", features = [ 23 | "v4", # Lets you generate random UUIDs 24 | "fast-rng", # Use a faster (but still sufficiently random) RNG 25 | "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs 26 | ] } 27 | workspaces = "0.7.0" 28 | tokio = { version = "1.28.0", features = ["full"] } 29 | anyhow = "1.0.71" 30 | -------------------------------------------------------------------------------- /near/contract/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 HAPI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /near/contract/build_local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release 5 | cp target/wasm32-unknown-unknown/release/hapi_core_near.wasm ./res/hapi_core_near_local.wasm 6 | -------------------------------------------------------------------------------- /near/contract/src/address/mod.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::{ 2 | borsh::{self, BorshDeserialize, BorshSerialize}, 3 | collections::UnorderedSet, 4 | AccountId, 5 | }; 6 | 7 | use crate::{CaseId, Category, ReporterId, RiskScore}; 8 | 9 | mod management; 10 | mod v_address; 11 | mod view; 12 | pub use v_address::VAddress; 13 | pub use view::AddressView; 14 | 15 | #[derive(BorshDeserialize, BorshSerialize)] 16 | pub struct Address { 17 | address: AccountId, 18 | category: Category, 19 | risk_score: RiskScore, 20 | case_id: CaseId, 21 | reporter_id: ReporterId, 22 | confirmations: UnorderedSet, 23 | } 24 | -------------------------------------------------------------------------------- /near/contract/src/address/v_address.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; 2 | 3 | use super::Address; 4 | 5 | #[derive(BorshDeserialize, BorshSerialize)] 6 | pub enum VAddress { 7 | Current(Address), 8 | } 9 | 10 | impl From
for VAddress { 11 | fn from(address: Address) -> Self { 12 | VAddress::Current(address) 13 | } 14 | } 15 | 16 | impl From for Address { 17 | fn from(v_address: VAddress) -> Self { 18 | match v_address { 19 | VAddress::Current(address) => address, 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /near/contract/src/address/view.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | address::{Address, VAddress}, 3 | CaseId, Category, Contract, ContractExt, ReporterId, RiskScore, ERROR_ADDRESS_NOT_FOUND, 4 | }; 5 | use near_sdk::{ 6 | borsh::{self, BorshDeserialize, BorshSerialize}, 7 | near_bindgen, 8 | serde::{Deserialize, Serialize}, 9 | AccountId, 10 | }; 11 | 12 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] 13 | #[serde(crate = "near_sdk::serde")] 14 | pub struct AddressView { 15 | pub address: AccountId, 16 | pub category: Category, 17 | pub risk_score: RiskScore, 18 | pub case_id: CaseId, 19 | pub reporter_id: ReporterId, 20 | pub confirmations_count: u64, 21 | } 22 | 23 | impl From for AddressView { 24 | fn from(v_address: VAddress) -> Self { 25 | let address: Address = v_address.into(); 26 | Self { 27 | address: address.address, 28 | category: address.category, 29 | risk_score: address.risk_score, 30 | case_id: address.case_id, 31 | reporter_id: address.reporter_id, 32 | confirmations_count: address.confirmations.len(), 33 | } 34 | } 35 | } 36 | 37 | #[near_bindgen] 38 | impl Contract { 39 | pub fn get_address(&self, address: &AccountId) -> AddressView { 40 | self.addresses 41 | .get(address) 42 | .expect(ERROR_ADDRESS_NOT_FOUND) 43 | .into() 44 | } 45 | 46 | pub fn get_addresses(&self, take: u64, skip: u64) -> Vec { 47 | self.addresses 48 | .iter() 49 | .skip(skip as _) 50 | .take(take as _) 51 | .map(|(_, address)| address.into()) 52 | .collect() 53 | } 54 | 55 | pub fn get_address_count(&self) -> u64 { 56 | self.addresses.len() 57 | } 58 | } 59 | 60 | impl Contract { 61 | pub fn get_address_internal(&self, address: &AccountId) -> Address { 62 | self.addresses 63 | .get(address) 64 | .expect(ERROR_ADDRESS_NOT_FOUND) 65 | .into() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /near/contract/src/assets/mod.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::{ 2 | borsh::{self, BorshDeserialize, BorshSerialize}, 3 | collections::UnorderedSet, 4 | json_types::U64, 5 | AccountId, 6 | }; 7 | 8 | use crate::{CaseId, Category, ReporterId, RiskScore}; 9 | 10 | mod management; 11 | mod v_asset; 12 | mod view; 13 | 14 | pub use v_asset::VAsset; 15 | pub use view::AssetView; 16 | 17 | pub type AssetId = String; 18 | 19 | #[derive(BorshDeserialize, BorshSerialize)] 20 | pub struct Asset { 21 | address: AccountId, 22 | id: U64, 23 | category: Category, 24 | risk_score: RiskScore, 25 | case_id: CaseId, 26 | reporter_id: ReporterId, 27 | confirmations: UnorderedSet, 28 | } 29 | -------------------------------------------------------------------------------- /near/contract/src/assets/v_asset.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; 2 | 3 | use super::Asset; 4 | 5 | #[derive(BorshDeserialize, BorshSerialize)] 6 | pub enum VAsset { 7 | Current(Asset), 8 | } 9 | 10 | impl From for VAsset { 11 | fn from(asset: Asset) -> Self { 12 | VAsset::Current(asset) 13 | } 14 | } 15 | 16 | impl From for Asset { 17 | fn from(v_asset: VAsset) -> Self { 18 | match v_asset { 19 | VAsset::Current(asset) => asset, 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /near/contract/src/assets/view.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; 2 | use near_sdk::json_types::U64; 3 | use near_sdk::serde::{Deserialize, Serialize}; 4 | use near_sdk::{near_bindgen, AccountId}; 5 | 6 | use super::{management::get_asset_id, Asset, AssetId, VAsset}; 7 | use crate::{ 8 | CaseId, Category, Contract, ContractExt, ReporterId, RiskScore, ERROR_ASSET_NOT_FOUND, 9 | }; 10 | 11 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] 12 | #[serde(crate = "near_sdk::serde")] 13 | pub struct AssetView { 14 | pub address: AccountId, 15 | pub id: U64, 16 | pub category: Category, 17 | pub risk_score: RiskScore, 18 | pub case_id: CaseId, 19 | pub reporter_id: ReporterId, 20 | pub confirmations_count: u64, 21 | } 22 | 23 | impl From for AssetView { 24 | fn from(asset: Asset) -> Self { 25 | Self { 26 | address: asset.address, 27 | id: asset.id, 28 | category: asset.category, 29 | risk_score: asset.risk_score, 30 | case_id: asset.case_id, 31 | reporter_id: asset.reporter_id, 32 | confirmations_count: asset.confirmations.len(), 33 | } 34 | } 35 | } 36 | 37 | impl From for AssetView { 38 | fn from(v_asset: VAsset) -> Self { 39 | let asset: Asset = v_asset.into(); 40 | Self { 41 | address: asset.address, 42 | id: asset.id, 43 | category: asset.category, 44 | risk_score: asset.risk_score, 45 | case_id: asset.case_id, 46 | reporter_id: asset.reporter_id, 47 | confirmations_count: asset.confirmations.len(), 48 | } 49 | } 50 | } 51 | 52 | #[near_bindgen] 53 | impl Contract { 54 | pub fn get_asset(&self, address: AccountId, id: U64) -> AssetView { 55 | let id: AssetId = get_asset_id(&address, &id); 56 | self.assets.get(&id).expect(ERROR_ASSET_NOT_FOUND).into() 57 | } 58 | 59 | pub fn get_assets(&self, take: u64, skip: u64) -> Vec { 60 | self.assets 61 | .iter() 62 | .skip(skip as _) 63 | .take(take as _) 64 | .map(|(_, asset)| asset.into()) 65 | .collect() 66 | } 67 | 68 | pub fn get_asset_count(&self) -> u64 { 69 | self.assets.len() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /near/contract/src/case/management.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | reporter::Role, utils::MAX_NAME_LENGTH, Contract, ContractExt, ERROR_CASE_ALREADY_EXISTS, 3 | ERROR_CASE_NOT_FOUND, ERROR_INVALID_ROLE, ERROR_LONG_NAME, ERROR_REPORTER_IS_INACTIVE, 4 | }; 5 | use near_sdk::{env, near_bindgen, require}; 6 | 7 | use super::{Case, CaseId, CaseStatus}; 8 | 9 | #[near_bindgen] 10 | impl Contract { 11 | pub fn create_case(&mut self, id: CaseId, name: String, url: String) { 12 | let reporter = self.get_reporter_by_account(env::predecessor_account_id()); 13 | 14 | require!(name.len() <= MAX_NAME_LENGTH, ERROR_LONG_NAME); 15 | 16 | match reporter.role { 17 | Role::Publisher | Role::Authority => { 18 | require!(reporter.is_active(), ERROR_REPORTER_IS_INACTIVE); 19 | } 20 | _ => { 21 | env::panic_str(ERROR_INVALID_ROLE); 22 | } 23 | } 24 | 25 | require!(self.cases.get(&id).is_none(), ERROR_CASE_ALREADY_EXISTS); 26 | 27 | let case = Case { 28 | id, 29 | name, 30 | reporter_id: reporter.id, 31 | status: CaseStatus::Open, 32 | url, 33 | }; 34 | 35 | self.cases.insert(&id, &case.into()); 36 | } 37 | 38 | pub fn update_case(&mut self, id: CaseId, name: String, status: CaseStatus, url: String) { 39 | let reporter = self.get_reporter_by_account(env::predecessor_account_id()); 40 | 41 | require!(name.len() <= MAX_NAME_LENGTH, ERROR_LONG_NAME); 42 | 43 | let mut case: Case = self.cases.get(&id).expect(ERROR_CASE_NOT_FOUND).into(); 44 | 45 | match reporter.role { 46 | Role::Publisher | Role::Authority => { 47 | require!(reporter.is_active(), ERROR_REPORTER_IS_INACTIVE); 48 | require!(case.reporter_id == reporter.id, ERROR_INVALID_ROLE); 49 | } 50 | _ => { 51 | env::panic_str(ERROR_INVALID_ROLE); 52 | } 53 | } 54 | 55 | case.name = name; 56 | case.status = status; 57 | case.url = url; 58 | 59 | self.cases.insert(&id, &case.into()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /near/contract/src/case/mod.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::{ 2 | borsh::{self, BorshDeserialize, BorshSerialize}, 3 | serde::{Deserialize, Serialize}, 4 | }; 5 | 6 | use crate::{utils::UUID, ReporterId}; 7 | 8 | mod management; 9 | mod v_case; 10 | mod view; 11 | pub use v_case::VCase; 12 | 13 | pub type CaseId = UUID; 14 | 15 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] 16 | #[serde(crate = "near_sdk::serde")] 17 | pub enum CaseStatus { 18 | Closed, 19 | Open, 20 | } 21 | 22 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] 23 | #[serde(crate = "near_sdk::serde")] 24 | pub struct Case { 25 | pub id: CaseId, 26 | pub name: String, 27 | pub reporter_id: ReporterId, 28 | pub status: CaseStatus, 29 | pub url: String, 30 | } 31 | -------------------------------------------------------------------------------- /near/contract/src/case/v_case.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; 2 | 3 | use super::Case; 4 | 5 | #[derive(BorshDeserialize, BorshSerialize)] 6 | pub enum VCase { 7 | Current(Case), 8 | } 9 | 10 | impl From for VCase { 11 | fn from(case: Case) -> Self { 12 | VCase::Current(case) 13 | } 14 | } 15 | 16 | impl From for Case { 17 | fn from(v_case: VCase) -> Self { 18 | match v_case { 19 | VCase::Current(case) => case, 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /near/contract/src/case/view.rs: -------------------------------------------------------------------------------- 1 | use super::{Case, CaseId}; 2 | use crate::{Contract, ContractExt, ERROR_CASE_NOT_FOUND}; 3 | use near_sdk::near_bindgen; 4 | 5 | #[near_bindgen] 6 | impl Contract { 7 | pub fn get_case(&self, id: CaseId) -> Case { 8 | self.cases.get(&id).expect(ERROR_CASE_NOT_FOUND).into() 9 | } 10 | 11 | pub fn get_cases(&self, skip: u64, take: u64) -> Vec { 12 | self.cases 13 | .iter() 14 | .skip(skip as _) 15 | .take(take as _) 16 | .map(|(_, case)| case.into()) 17 | .collect() 18 | } 19 | 20 | pub fn get_case_count(&self) -> u64 { 21 | self.cases.len() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /near/contract/src/configuration.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::{env, near_bindgen, require, AccountId}; 2 | 3 | use crate::{ 4 | reward::RewardConfiguration, stake::StakeConfiguration, Contract, ContractExt, 5 | ERROR_CHANGE_TOKEN, ERROR_ONLY_AUTHORITY, ERROR_REWARD_CONFIGURATION_NOT_SET, 6 | ERROR_STAKE_CONFIGURATION_NOT_SET, 7 | }; 8 | 9 | #[near_bindgen] 10 | impl Contract { 11 | pub fn update_stake_configuration(&mut self, stake_configuration: StakeConfiguration) { 12 | self.assert_authority(); 13 | require!( 14 | self.stake_configuration.is_default() 15 | || stake_configuration 16 | .get_token() 17 | .eq(self.stake_configuration.get_token()), 18 | ERROR_CHANGE_TOKEN 19 | ); 20 | 21 | self.stake_configuration = stake_configuration; 22 | } 23 | 24 | pub fn update_reward_configuration(&mut self, reward_configuration: RewardConfiguration) { 25 | self.assert_authority(); 26 | require!( 27 | self.reward_configuration.is_default() 28 | || reward_configuration 29 | .get_token() 30 | .eq(self.reward_configuration.get_token()), 31 | ERROR_CHANGE_TOKEN 32 | ); 33 | 34 | self.reward_configuration = reward_configuration; 35 | } 36 | 37 | pub fn set_authority(&mut self, authority: AccountId) { 38 | self.assert_authority(); 39 | self.authority = authority; 40 | } 41 | 42 | pub fn get_stake_configuration(&self) -> StakeConfiguration { 43 | require!( 44 | !self.stake_configuration.is_default(), 45 | ERROR_STAKE_CONFIGURATION_NOT_SET 46 | ); 47 | self.stake_configuration.clone() 48 | } 49 | 50 | pub fn get_reward_configuration(&self) -> RewardConfiguration { 51 | require!( 52 | !self.reward_configuration.is_default(), 53 | ERROR_REWARD_CONFIGURATION_NOT_SET 54 | ); 55 | 56 | self.reward_configuration.clone() 57 | } 58 | 59 | pub fn get_authority(&self) -> AccountId { 60 | self.authority.clone() 61 | } 62 | } 63 | 64 | impl Contract { 65 | pub(crate) fn assert_authority(&self) { 66 | require!( 67 | env::predecessor_account_id().eq(&self.authority), 68 | ERROR_ONLY_AUTHORITY 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /near/contract/src/errors.rs: -------------------------------------------------------------------------------- 1 | pub const ERROR_ONLY_AUTHORITY: &str = "Only authority can call this method"; 2 | 3 | pub const ERROR_REPORTER_NOT_FOUND: &str = "Reporter not found"; 4 | pub const ERROR_REPORTER_EXISTS: &str = "Reporter already exists"; 5 | pub const ERROR_INVALID_STAKE_TOKEN: &str = "Invalid stake token"; 6 | pub const ERROR_INVALID_REWARD_TOKEN: &str = "Invalid reward token"; 7 | pub const ERROR_INVALID_STAKE_AMOUNT: &str = "Invalid stake amount"; 8 | pub const ERROR_REPORTER_IS_ACTIVE: &str = "Reporter is active"; 9 | pub const ERROR_REPORTER_IS_INACTIVE: &str = "Reporter is inactive"; 10 | pub const ERROR_UNLOCK_DURATION_NOT_PASSED: &str = "Unlock duration not passed"; 11 | pub const ERROR_LONG_NAME: &str = "Name is too long"; 12 | pub const ERROR_CHANGE_TOKEN: &str = "Token cannot be changed"; 13 | pub const ERROR_REPORT_CONFIRMATION: &str = "Reporter can't confirm report reported by himself"; 14 | 15 | // Case errors 16 | pub const ERROR_INVALID_ROLE: &str = "Invalid role"; 17 | pub const ERROR_CASE_NOT_FOUND: &str = "Case not found"; 18 | pub const ERROR_CASE_ALREADY_EXISTS: &str = "Case already exists"; 19 | 20 | // Address errors 21 | pub const ERROR_INVALID_RISK_SCORE: &str = "Invalid risk score"; 22 | pub const ERROR_ADDRESS_NOT_FOUND: &str = "Address not found"; 23 | pub const ERROR_ALREADY_CONFIRMED: &str = "Already confirmed"; 24 | pub const ERROR_ADDRESS_ALREADY_EXISTS: &str = "Address already exists"; 25 | 26 | // Configuration errors 27 | pub const ERROR_STAKE_CONFIGURATION_NOT_SET: &str = "Stake configuration is not set"; 28 | pub const ERROR_REWARD_CONFIGURATION_NOT_SET: &str = "Reward configuration is not set"; 29 | 30 | // Asset errors 31 | pub const ERROR_ASSET_NOT_FOUND: &str = "Asset not found"; 32 | pub const ERROR_ASSET_ALREADY_EXISTS: &str = "Asset already exists"; 33 | -------------------------------------------------------------------------------- /near/contract/src/reporter/mod.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::{ 2 | borsh::{self, BorshDeserialize, BorshSerialize}, 3 | json_types::U128, 4 | serde::{Deserialize, Serialize}, 5 | AccountId, Timestamp, 6 | }; 7 | 8 | mod management; 9 | mod v_reporter; 10 | mod view; 11 | 12 | pub use v_reporter::VReporter; 13 | 14 | use crate::utils::UUID; 15 | 16 | pub type ReporterId = UUID; 17 | 18 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Debug)] 19 | #[serde(crate = "near_sdk::serde")] 20 | pub enum Role { 21 | Validator, 22 | Tracer, 23 | Publisher, 24 | Authority, 25 | Appraiser, 26 | } 27 | 28 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, PartialEq)] 29 | #[serde(crate = "near_sdk::serde")] 30 | pub enum ReporterStatus { 31 | Inactive, 32 | Active, 33 | Unstaking, 34 | } 35 | 36 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] 37 | #[serde(crate = "near_sdk::serde")] 38 | pub struct Reporter { 39 | pub id: ReporterId, 40 | pub account_id: AccountId, 41 | pub name: String, 42 | pub role: Role, 43 | pub status: ReporterStatus, 44 | pub stake: U128, 45 | pub url: String, 46 | pub unlock_timestamp: Timestamp, 47 | } 48 | 49 | impl Reporter { 50 | pub fn is_active(&self) -> bool { 51 | self.status == ReporterStatus::Active 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /near/contract/src/reporter/v_reporter.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; 2 | 3 | use crate::reporter::Reporter; 4 | 5 | #[derive(BorshDeserialize, BorshSerialize)] 6 | pub enum VReporter { 7 | Current(Reporter), 8 | } 9 | 10 | impl From for Reporter { 11 | fn from(v: VReporter) -> Self { 12 | match v { 13 | VReporter::Current(reporter) => reporter, 14 | } 15 | } 16 | } 17 | 18 | impl From for VReporter { 19 | fn from(reporter: Reporter) -> Self { 20 | VReporter::Current(reporter) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /near/contract/src/reporter/view.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::{near_bindgen, AccountId}; 2 | 3 | use super::{Reporter, ReporterId}; 4 | use crate::{Contract, ContractExt, ERROR_REPORTER_NOT_FOUND}; 5 | 6 | #[near_bindgen] 7 | impl Contract { 8 | pub fn get_reporter(&self, id: ReporterId) -> Reporter { 9 | self.reporters 10 | .get(&id) 11 | .expect(ERROR_REPORTER_NOT_FOUND) 12 | .into() 13 | } 14 | 15 | pub fn get_reporters(&self, take: u64, skip: u64) -> Vec { 16 | self.reporters 17 | .iter() 18 | .skip(skip as _) 19 | .take(take as _) 20 | .map(|(_, reporter)| reporter.into()) 21 | .collect() 22 | } 23 | 24 | pub fn get_reporter_count(&self) -> u64 { 25 | self.reporters.len() 26 | } 27 | 28 | pub fn get_reporter_by_account(&self, account_id: AccountId) -> Reporter { 29 | let id = self 30 | .reporters_by_account 31 | .get(&account_id) 32 | .expect(ERROR_REPORTER_NOT_FOUND); 33 | self.reporters 34 | .get(&id) 35 | .expect(ERROR_REPORTER_NOT_FOUND) 36 | .into() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /near/contract/src/reward/mod.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::{ 2 | borsh::{self, BorshDeserialize, BorshSerialize}, 3 | env, 4 | json_types::U128, 5 | serde::{Deserialize, Serialize}, 6 | AccountId, 7 | }; 8 | 9 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Clone)] 10 | #[serde(crate = "near_sdk::serde")] 11 | pub struct RewardConfiguration { 12 | token: AccountId, 13 | address_confirmation_reward: U128, 14 | address_tracer_reward: U128, 15 | asset_confirmation_reward: U128, 16 | asset_tracer_reward: U128, 17 | } 18 | 19 | impl Default for RewardConfiguration { 20 | fn default() -> Self { 21 | Self { 22 | token: env::current_account_id(), 23 | address_confirmation_reward: 0.into(), 24 | address_tracer_reward: 0.into(), 25 | asset_confirmation_reward: 0.into(), 26 | asset_tracer_reward: 0.into(), 27 | } 28 | } 29 | } 30 | 31 | impl RewardConfiguration { 32 | pub fn get_token(&self) -> &AccountId { 33 | &self.token 34 | } 35 | 36 | pub fn is_default(&self) -> bool { 37 | self.token == env::current_account_id() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /near/contract/src/token_transferer.rs: -------------------------------------------------------------------------------- 1 | use near_contract_standards::fungible_token::{core::ext_ft_core, receiver::FungibleTokenReceiver}; 2 | use near_sdk::{ 3 | env, ext_contract, is_promise_success, json_types::U128, near_bindgen, AccountId, Gas, 4 | NearToken, Promise, PromiseOrValue, 5 | }; 6 | 7 | const GAS_FOR_FT_TRANSFER: Gas = Gas::from_tgas(10); 8 | const GAS_FOR_AFTER_FT_TRANSFER: Gas = Gas::from_tgas(10); 9 | 10 | use crate::{reporter, Contract, ContractExt}; 11 | 12 | #[ext_contract(ext_self)] 13 | pub trait ExtSelf { 14 | fn after_transfer_stake(&mut self, reporter_account: AccountId, amount: U128); 15 | } 16 | 17 | #[near_bindgen] 18 | impl ExtSelf for Contract { 19 | #[private] 20 | fn after_transfer_stake(&mut self, reporter_account: AccountId, amount: U128) { 21 | if !is_promise_success() { 22 | let mut reporter = self.get_reporter_by_account(reporter_account); 23 | reporter.stake = amount; 24 | reporter.status = reporter::ReporterStatus::Unstaking; 25 | self.reporters 26 | .insert(&reporter.id.clone(), &reporter.into()); 27 | } 28 | } 29 | } 30 | 31 | #[near_bindgen] 32 | impl FungibleTokenReceiver for Contract { 33 | #[allow(unused_variables)] 34 | fn ft_on_transfer( 35 | &mut self, 36 | sender_id: AccountId, 37 | amount: U128, 38 | msg: String, 39 | ) -> PromiseOrValue { 40 | self.stake_configuration.assert_token_valid(); 41 | 42 | self.activate_reporter(sender_id, amount); 43 | PromiseOrValue::Value(U128(0)) 44 | } 45 | } 46 | 47 | impl Contract { 48 | pub(crate) fn transfer_stake( 49 | &mut self, 50 | receiver_id: AccountId, 51 | amount: U128, 52 | token_account_id: AccountId, 53 | ) -> Promise { 54 | ext_ft_core::ext(token_account_id.clone()) 55 | .with_static_gas(GAS_FOR_FT_TRANSFER) 56 | .with_attached_deposit(NearToken::from_yoctonear(1)) 57 | .ft_transfer( 58 | receiver_id.clone(), 59 | amount, 60 | Some(format!("Transfer {} of {token_account_id}", amount.0)), 61 | ) 62 | .then( 63 | ext_self::ext(env::current_account_id()) 64 | .with_static_gas(GAS_FOR_AFTER_FT_TRANSFER) 65 | .after_transfer_stake(receiver_id, amount), 66 | ) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /near/contract/src/utils.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::{json_types::U128, Timestamp}; 2 | 3 | pub type UUID = U128; 4 | 5 | const NS: u64 = 1_000_000_000; 6 | 7 | pub const MAX_NAME_LENGTH: usize = 64; 8 | 9 | pub trait TimestampExtension { 10 | fn to_sec(&self) -> u64; 11 | } 12 | 13 | impl TimestampExtension for Timestamp { 14 | fn to_sec(&self) -> u64 { 15 | self / NS 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /near/contract/tests/address/helpers.rs: -------------------------------------------------------------------------------- 1 | use crate::{case::CaseId, reporter::ReporterId}; 2 | use hapi_core_near::{Category, RiskScore}; 3 | use near_sdk::{ 4 | serde::{Deserialize, Serialize}, 5 | AccountId, 6 | }; 7 | 8 | #[derive(Serialize, Deserialize)] 9 | #[serde(crate = "near_sdk::serde")] 10 | pub struct Address { 11 | pub address: AccountId, 12 | pub category: Category, 13 | pub risk_score: RiskScore, 14 | pub case_id: CaseId, 15 | pub reporter_id: ReporterId, 16 | pub confirmations_count: u64, 17 | } 18 | -------------------------------------------------------------------------------- /near/contract/tests/asset/helpers.rs: -------------------------------------------------------------------------------- 1 | use crate::{case::CaseId, reporter::ReporterId}; 2 | use hapi_core_near::{Category, RiskScore}; 3 | use near_sdk::{ 4 | serde::{Deserialize, Serialize}, 5 | AccountId, 6 | }; 7 | 8 | #[derive(Serialize, Deserialize)] 9 | #[serde(crate = "near_sdk::serde")] 10 | pub struct Asset { 11 | pub address: AccountId, 12 | pub id: String, 13 | pub category: Category, 14 | pub risk_score: RiskScore, 15 | pub case_id: CaseId, 16 | pub reporter_id: ReporterId, 17 | pub confirmations_count: u64, 18 | } 19 | -------------------------------------------------------------------------------- /near/contract/tests/case/helpers.rs: -------------------------------------------------------------------------------- 1 | use crate::reporter::ReporterId; 2 | use near_sdk::{ 3 | json_types::U128, 4 | serde::{Deserialize, Serialize}, 5 | }; 6 | 7 | pub type CaseId = U128; 8 | 9 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 10 | #[serde(crate = "near_sdk::serde")] 11 | pub enum CaseStatus { 12 | Closed, 13 | Open, 14 | } 15 | 16 | #[derive(Serialize, Deserialize)] 17 | #[serde(crate = "near_sdk::serde")] 18 | pub struct Case { 19 | pub id: CaseId, 20 | pub name: String, 21 | pub reporter_id: ReporterId, 22 | pub status: CaseStatus, 23 | pub url: String, 24 | } 25 | -------------------------------------------------------------------------------- /near/contract/tests/configuration/helpers.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::{ 2 | json_types::U128, 3 | serde::{Deserialize, Serialize}, 4 | Timestamp, 5 | }; 6 | 7 | use workspaces::AccountId; 8 | 9 | use crate::context::TestContext; 10 | 11 | #[derive(Serialize, Deserialize)] 12 | #[serde(crate = "near_sdk::serde")] 13 | pub struct StakeConfiguration { 14 | pub token: AccountId, 15 | pub unlock_duration: Timestamp, 16 | pub validator_stake: U128, 17 | pub tracer_stake: U128, 18 | pub publisher_stake: U128, 19 | pub authority_stake: U128, 20 | } 21 | 22 | #[derive(Serialize, Deserialize)] 23 | #[serde(crate = "near_sdk::serde")] 24 | pub struct RewardConfiguration { 25 | pub token: AccountId, 26 | pub address_confirmation_reward: U128, 27 | pub address_tracer_reward: U128, 28 | pub asset_confirmation_reward: U128, 29 | pub asset_tracer_reward: U128, 30 | } 31 | 32 | pub const UNLOCK_DURATION: u64 = 60; // in seconds 33 | pub const VALIDATOR_STAKE: u128 = 30; 34 | pub const TRACER_STAKE: u128 = 20; 35 | pub const PUBLISHER_STAKE: u128 = 10; 36 | pub const AUTHORITY_STAKE: u128 = 50; 37 | 38 | impl TestContext { 39 | pub async fn get_stake_configuration(&self) -> StakeConfiguration { 40 | StakeConfiguration { 41 | token: self.stake_token.id.clone(), 42 | unlock_duration: UNLOCK_DURATION, 43 | validator_stake: U128(30), 44 | tracer_stake: U128(20), 45 | publisher_stake: U128(10), 46 | authority_stake: U128(50), 47 | } 48 | } 49 | 50 | pub async fn get_reward_configuration(&self) -> RewardConfiguration { 51 | RewardConfiguration { 52 | token: self.reward_token.id.clone(), 53 | address_confirmation_reward: 1.into(), 54 | address_tracer_reward: 2.into(), 55 | asset_confirmation_reward: 3.into(), 56 | asset_tracer_reward: 4.into(), 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /near/contract/tests/errors.rs: -------------------------------------------------------------------------------- 1 | pub const ERROR_ONLY_AUTHORITY: &str = "Only authority can call this method"; 2 | 3 | pub const ERROR_REPORTER_NOT_FOUND: &str = "Reporter not found"; 4 | pub const ERROR_REPORTER_EXISTS: &str = "Reporter already exists"; 5 | pub const ERROR_INVALID_STAKE_TOKEN: &str = "Invalid stake token"; 6 | pub const ERROR_INVALID_REWARD_TOKEN: &str = "Invalid reward token"; 7 | pub const ERROR_INVALID_STAKE_AMOUNT: &str = "Invalid stake amount"; 8 | pub const ERROR_REPORTER_IS_ACTIVE: &str = "Reporter is active"; 9 | pub const ERROR_REPORTER_IS_INACTIVE: &str = "Reporter is inactive"; 10 | pub const ERROR_UNLOCK_DURATION_NOT_PASSED: &str = "Unlock duration not passed"; 11 | pub const ERROR_LONG_NAME: &str = "Name is too long"; 12 | 13 | // Case errors 14 | pub const ERROR_INVALID_ROLE: &str = "Invalid role"; 15 | pub const ERROR_CASE_NOT_FOUND: &str = "Case not found"; 16 | pub const ERROR_CASE_ALREADY_EXISTS: &str = "Case already exists"; 17 | -------------------------------------------------------------------------------- /near/contract/tests/lib.rs: -------------------------------------------------------------------------------- 1 | mod address; 2 | mod asset; 3 | mod case; 4 | mod configuration; 5 | pub mod context; 6 | pub mod errors; 7 | mod reporter; 8 | mod utils; 9 | pub use configuration::*; 10 | pub use errors::*; 11 | pub use utils::*; 12 | 13 | pub const SHOW_LOGS: bool = false; 14 | pub const SHOW_DEFAULT_OUTPUT: bool = false; 15 | 16 | pub const CONTRACT: &[u8] = include_bytes!("../../res/hapi_core_near.wasm"); 17 | 18 | pub const INITIAL_USER_BALANCE: u128 = 1000; 19 | pub const INITIAL_NEAR_USER_BALANCE: u128 = 10000000000000000000000000; // 10 near 20 | -------------------------------------------------------------------------------- /near/contract/tests/reporter/helpers.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | utils::CallExecutionDetailsExtension, AUTHORITY_STAKE, PUBLISHER_STAKE, TRACER_STAKE, 3 | VALIDATOR_STAKE, 4 | }; 5 | use near_sdk::{ 6 | json_types::U128, 7 | serde::{Deserialize, Serialize}, 8 | serde_json::json, 9 | AccountId, Timestamp, 10 | }; 11 | use workspaces::Account; 12 | 13 | use crate::context::TestContext; 14 | pub type ReporterId = U128; 15 | 16 | #[derive(Serialize, Deserialize, Debug, PartialEq)] 17 | #[serde(crate = "near_sdk::serde")] 18 | pub enum Role { 19 | Validator, 20 | Tracer, 21 | Publisher, 22 | Authority, 23 | Appraiser, 24 | } 25 | 26 | #[derive(Serialize, Deserialize, Debug, PartialEq)] 27 | #[serde(crate = "near_sdk::serde")] 28 | pub enum ReporterStatus { 29 | Inactive, 30 | Active, 31 | Unstaking, 32 | } 33 | 34 | #[derive(Serialize, Deserialize)] 35 | #[serde(crate = "near_sdk::serde")] 36 | pub struct Reporter { 37 | pub id: ReporterId, 38 | pub account_id: AccountId, 39 | pub name: String, 40 | pub role: Role, 41 | pub status: ReporterStatus, 42 | pub stake: U128, 43 | pub url: String, 44 | pub unlock_timestamp: Timestamp, 45 | } 46 | 47 | impl TestContext { 48 | pub async fn prepare_reporter(&self, id: U128, account: &Account, role: Role) { 49 | let (role_str, amount) = match role { 50 | Role::Validator => ("Validator", U128(VALIDATOR_STAKE)), 51 | Role::Tracer => ("Tracer", U128(TRACER_STAKE)), 52 | Role::Publisher => ("Publisher", U128(PUBLISHER_STAKE)), 53 | Role::Authority => ("Authority", U128(AUTHORITY_STAKE)), 54 | Role::Appraiser => ("Appraiser", U128(0)), 55 | }; 56 | 57 | self.authority 58 | .call(&self.contract.id(), "create_reporter") 59 | .args_json(json!({"id": id, "account_id": account.id(), "name": role_str, "role": role_str, "url": role_str.to_lowercase() + ".com"})) 60 | .transact() 61 | .await 62 | .assert_success("create reporter"); 63 | 64 | if amount.0 > 0 { 65 | self.ft_transfer_call(account, &self.stake_token, amount.0) 66 | .await 67 | .assert_success("activate reporter"); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /near/contract/tests/utils/mod.rs: -------------------------------------------------------------------------------- 1 | mod execution_extension; 2 | mod token_extension; 3 | mod utils; 4 | 5 | pub use execution_extension::*; 6 | pub use token_extension::*; 7 | pub use utils::*; 8 | -------------------------------------------------------------------------------- /near/contract/tests/utils/utils.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::Timestamp; 2 | use std::ops::Mul; 3 | use workspaces::{network::Sandbox, Account, Contract, Worker}; 4 | 5 | use crate::INITIAL_NEAR_USER_BALANCE; 6 | 7 | const NS: u64 = 1_000_000_000; 8 | pub const ONE_TGAS: u64 = 1_000_000_000_000; 9 | 10 | pub trait U128Extension { 11 | fn to_decimals(self, decimals: u8) -> u128; 12 | } 13 | 14 | impl U128Extension for u128 { 15 | fn to_decimals(self, decimals: u8) -> u128 { 16 | self.mul(10_u128.pow(decimals.into())) 17 | } 18 | } 19 | 20 | pub trait GasExtension { 21 | fn to_tgas(self) -> u64; 22 | } 23 | 24 | impl GasExtension for u64 { 25 | fn to_tgas(self) -> u64 { 26 | self * ONE_TGAS 27 | } 28 | } 29 | 30 | pub trait TimestampExtension { 31 | fn sec_to_ns(self) -> Timestamp; 32 | fn ns_to_sec(self) -> Timestamp; 33 | fn minutes_to_sec(self) -> Timestamp; 34 | fn add_minutes(self, minutes: u64) -> Timestamp; 35 | } 36 | 37 | impl TimestampExtension for Timestamp { 38 | fn sec_to_ns(self) -> Timestamp { 39 | self * NS 40 | } 41 | 42 | fn ns_to_sec(self) -> Timestamp { 43 | self / NS 44 | } 45 | 46 | fn minutes_to_sec(self) -> Timestamp { 47 | self.mul(60) 48 | } 49 | 50 | fn add_minutes(self, minutes: u64) -> Timestamp { 51 | std::ops::Add::add(self, minutes.minutes_to_sec().sec_to_ns()) 52 | } 53 | } 54 | 55 | pub async fn create_account(worker: &Worker, account_prefix: &str) -> Account { 56 | worker 57 | .root_account() 58 | .unwrap() 59 | .create_subaccount(account_prefix) 60 | .initial_balance(INITIAL_NEAR_USER_BALANCE) 61 | .transact() 62 | .await 63 | .unwrap() 64 | .result 65 | } 66 | 67 | pub async fn deploy_contract( 68 | worker: &Worker, 69 | account_prefix: &str, 70 | wasm: &[u8], 71 | ) -> Contract { 72 | create_account(worker, account_prefix) 73 | .await 74 | .deploy(wasm) 75 | .await 76 | .unwrap() 77 | .result 78 | } 79 | -------------------------------------------------------------------------------- /near/res/fungible_token.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HAPIprotocol/hapi-core/682676b5069421441370f372a3bdc3bf26aeb5ec/near/res/fungible_token.wasm -------------------------------------------------------------------------------- /near/res/hapi_core_near.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HAPIprotocol/hapi-core/682676b5069421441370f372a3bdc3bf26aeb5ec/near/res/hapi_core_near.wasm -------------------------------------------------------------------------------- /solana/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .anchor 3 | .DS_Store 4 | target 5 | **/*.rs.bk 6 | node_modules 7 | test-ledger 8 | .env* 9 | -------------------------------------------------------------------------------- /solana/.prettierignore: -------------------------------------------------------------------------------- 1 | 2 | .anchor 3 | .DS_Store 4 | target 5 | node_modules 6 | dist 7 | build 8 | test-ledger 9 | -------------------------------------------------------------------------------- /solana/Anchor.toml: -------------------------------------------------------------------------------- 1 | [features] 2 | seeds = false 3 | skip-lint = false 4 | 5 | [registry] 6 | url = "https://api.apr.dev" 7 | 8 | [programs.localnet] 9 | hapi_core_solana = "FgE5ySSi6fbnfYGGRyaeW8y6p8A5KybXPyQ2DdxPCNRk" 10 | 11 | [programs.devnet] 12 | hapi_core_solana = "hapinwqwa5qRQ3Lqzy1zELDjXV9SkeNjiyBe9dwhk5W" 13 | 14 | [provider] 15 | cluster = "localnet" 16 | wallet = "~/.config/solana/id.json" 17 | 18 | [scripts] 19 | test = "yarn run jest --verbose" 20 | -------------------------------------------------------------------------------- /solana/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["programs/*"] 3 | resolver = "2" 4 | 5 | [profile.release] 6 | overflow-checks = true 7 | lto = "fat" 8 | codegen-units = 1 9 | 10 | [profile.release.build-override] 11 | opt-level = 3 12 | incremental = false 13 | codegen-units = 1 14 | -------------------------------------------------------------------------------- /solana/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./buffer"; 2 | export * from "./hapi-core"; 3 | export * from "./enums"; 4 | -------------------------------------------------------------------------------- /solana/migrations/deploy.ts: -------------------------------------------------------------------------------- 1 | // Migrations are an early feature. Currently, they're nothing more than this 2 | // single deploy script that's invoked from the CLI, injecting a provider 3 | // configured from the workspace's Anchor.toml. 4 | 5 | const anchor = require("@coral-xyz/anchor"); 6 | 7 | module.exports = async function (provider) { 8 | // Configure client to use the provider. 9 | anchor.setProvider(provider); 10 | 11 | // Add your deploy script here. 12 | }; 13 | -------------------------------------------------------------------------------- /solana/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hapi.one/core-cli", 3 | "version": "0.14.1", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "description": "Client library for HAPI core contract", 8 | "author": "HAPI Protocol", 9 | "license": "Apache-2.0", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/HAPIprotocol/hapi-core.git" 13 | }, 14 | "keywords": [ 15 | "HAPI", 16 | "protocol", 17 | "cybersecurity", 18 | "Solana", 19 | "client" 20 | ], 21 | "main": "out-lib/index.cjs.js", 22 | "module": "out-lib/index.esm.js", 23 | "types": "out-lib/index.d.ts", 24 | "files": [ 25 | "out-lib/**/*" 26 | ], 27 | "scripts": { 28 | "prebuild": "rimraf out-lib out-tsc", 29 | "build": "tsc && rollup -c", 30 | "prepublishOnly": "yarn run build", 31 | "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", 32 | "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check", 33 | "create-network": "ts-node scripts/create-network" 34 | }, 35 | "dependencies": { 36 | "@coral-xyz/anchor": "0.28.0", 37 | "@solana/spl-token": "^0.3.8", 38 | "@solana/web3.js": "^1.78.4", 39 | "@types/bn.js": "^5.1.1", 40 | "@types/jest": "^29.5.1", 41 | "@types/node": "^20.1.7", 42 | "@types/uuid": "^9.0.2", 43 | "bn.js": "^5.2.1", 44 | "chalk": "4.1.2", 45 | "dotenv": "^16.3.1", 46 | "eip55": "^2.1.0", 47 | "jest": "^29.3.1", 48 | "rimraf": "^5.0.0", 49 | "ts-jest": "^29.0.3" 50 | }, 51 | "devDependencies": { 52 | "prettier": "^2.6.2", 53 | "ts-node": "^10.9.1", 54 | "typescript": "^4.3.5" 55 | }, 56 | "jest": { 57 | "moduleFileExtensions": [ 58 | "ts", 59 | "js", 60 | "json" 61 | ], 62 | "transform": { 63 | "^.+\\.tsx?$": "ts-jest" 64 | }, 65 | "testRegex": "/tests/.*\\.spec\\.(ts|tsx)$", 66 | "testTimeout": 30000 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /solana/programs/hapi_core_solana/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hapi-core-solana" 3 | authors = ["HAPI "] 4 | version = "0.3.0" 5 | edition = "2021" 6 | license-file = "LICENSE" 7 | description = "Solana implementation of HAPI Protocol contract" 8 | repository = "https://github.com/HAPIprotocol/hapi-core" 9 | homepage = "https://hapi.one" 10 | 11 | [lib] 12 | crate-type = ["cdylib", "lib"] 13 | name = "hapi_core_solana" 14 | 15 | [features] 16 | no-entrypoint = [] 17 | no-idl = [] 18 | no-log-ix-name = [] 19 | cpi = ["no-entrypoint"] 20 | default = [] 21 | 22 | [dependencies] 23 | uuid = "1.3.4" 24 | 25 | [dependencies.anchor-lang] 26 | git = "https://github.com/hlgltvnnk/anchor.git" 27 | branch = "update-dependencies" 28 | version = "0.28.0" 29 | 30 | [dependencies.anchor-spl] 31 | git = "https://github.com/hlgltvnnk/anchor.git" 32 | branch = "update-dependencies" 33 | version = "0.28.0" 34 | -------------------------------------------------------------------------------- /solana/programs/hapi_core_solana/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 HAPI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /solana/programs/hapi_core_solana/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /solana/programs/hapi_core_solana/src/error.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[error_code] 4 | pub enum ErrorCode { 5 | #[msg("Invalid token account")] 6 | InvalidToken, 7 | #[msg("Authority mismatched")] 8 | AuthorityMismatch, 9 | #[msg("Account has illegal owner")] 10 | IllegalOwner, 11 | #[msg("Invalid program data account")] 12 | InvalidProgramData, 13 | #[msg("Invalid program account")] 14 | InvalidProgramAccount, 15 | #[msg("Invalid reporter account")] 16 | InvalidReporter, 17 | #[msg("Invalid reporter status")] 18 | InvalidReporterStatus, 19 | #[msg("Reporter account is not active")] 20 | InactiveReporter, 21 | #[msg("This reporter is frozen")] 22 | FrozenReporter, 23 | #[msg("Release epoch is in future")] 24 | ReleaseEpochInFuture, 25 | #[msg("Mint has already been updated")] 26 | UpdatedMint, 27 | #[msg("Account is not authorized to perform this action")] 28 | Unauthorized, 29 | #[msg("Invalid UUID")] 30 | InvalidUUID, 31 | #[msg("Invalid Data")] 32 | InvalidData, 33 | #[msg("Case closed")] 34 | CaseClosed, 35 | #[msg("Case mismatched")] 36 | CaseMismatch, 37 | #[msg("Risk score must be in 0..10 range")] 38 | RiskOutOfRange, 39 | } 40 | 41 | pub fn print_error(error: ErrorCode) -> Result<()> { 42 | msg!("Error: {}", error); 43 | Err(error.into()) 44 | } 45 | -------------------------------------------------------------------------------- /solana/programs/hapi_core_solana/src/state/address.rs: -------------------------------------------------------------------------------- 1 | use super::{utils::Category, DISCRIMINATOR_LENGTH}; 2 | use anchor_lang::prelude::*; 3 | 4 | #[account] 5 | pub struct Address { 6 | /// Account version 7 | pub version: u16, 8 | 9 | /// Seed bump for PDA 10 | pub bump: u8, 11 | 12 | /// Network account 13 | pub network: Pubkey, 14 | 15 | /// Actual address public key 16 | pub address: [u8; 64], 17 | 18 | /// Primary category of activity detected on the address 19 | pub category: Category, 20 | 21 | /// Estimated risk score on a scale from 0 to 10 (where 0 is safe and 10 is maximum risk) 22 | pub risk_score: u8, 23 | 24 | /// Case UUID 25 | pub case_id: u128, 26 | 27 | /// Reporter UUID 28 | pub reporter_id: u128, 29 | 30 | /// Confirmation count for this address 31 | pub confirmations: u64, 32 | } 33 | 34 | impl Address { 35 | pub const LEN: usize = DISCRIMINATOR_LENGTH + (2 + 1 + 32 + 64 + 1 + 1 + 16 + 16 + 8); 36 | pub const VERSION: u16 = 1; 37 | } 38 | -------------------------------------------------------------------------------- /solana/programs/hapi_core_solana/src/state/asset.rs: -------------------------------------------------------------------------------- 1 | use super::{utils::Category, DISCRIMINATOR_LENGTH}; 2 | use anchor_lang::prelude::*; 3 | 4 | #[account] 5 | pub struct Asset { 6 | /// Account version 7 | pub version: u16, 8 | 9 | /// Seed bump for PDA 10 | pub bump: u8, 11 | 12 | /// Network account 13 | pub network: Pubkey, 14 | 15 | /// Asset contract address (i.e. NFT) 16 | pub address: [u8; 64], 17 | 18 | /// Asset ID on it’s contract 19 | pub id: [u8; 32], 20 | 21 | /// Primary category of activity detected on the address 22 | pub category: Category, 23 | 24 | /// Estimated risk score on a scale from 0 to 10 (where 0 is safe and 10 is maximum risk) 25 | pub risk_score: u8, 26 | 27 | /// Case UUID 28 | pub case_id: u128, 29 | 30 | /// Reporter UUID 31 | pub reporter_id: u128, 32 | 33 | /// Confirmation count for this address 34 | pub confirmations: u64, 35 | } 36 | 37 | impl Asset { 38 | pub const LEN: usize = DISCRIMINATOR_LENGTH + (2 + 1 + 32 + 64 + 32 + 1 + 1 + 16 + 16 + 8); 39 | pub const VERSION: u16 = 1; 40 | } 41 | -------------------------------------------------------------------------------- /solana/programs/hapi_core_solana/src/state/case.rs: -------------------------------------------------------------------------------- 1 | use super::DISCRIMINATOR_LENGTH; 2 | use anchor_lang::prelude::*; 3 | 4 | #[account] 5 | pub struct Case { 6 | /// Account version 7 | pub version: u16, 8 | 9 | /// Seed bump for PDA 10 | pub bump: u8, 11 | 12 | /// Case UUID 13 | pub id: u128, 14 | 15 | /// Network account 16 | pub network: Pubkey, 17 | 18 | /// Short case description 19 | pub name: String, 20 | 21 | /// Reporter UUID 22 | pub reporter_id: u128, 23 | 24 | /// Case status 25 | pub status: CaseStatus, 26 | 27 | /// A link to publicly available case documentation 28 | pub url: String, 29 | } 30 | 31 | impl Case { 32 | pub const LEN: usize = DISCRIMINATOR_LENGTH + (2 + 1 + 16 + 32 + 128 + 32 + 1 + 128); 33 | pub const VERSION: u16 = 1; 34 | } 35 | 36 | #[derive(Default, Debug, Clone, PartialEq, AnchorDeserialize, AnchorSerialize)] 37 | pub enum CaseStatus { 38 | /// Investigations over this case are finished 39 | Closed = 0, 40 | 41 | /// The case is on-going 42 | #[default] 43 | Open = 1, 44 | } 45 | -------------------------------------------------------------------------------- /solana/programs/hapi_core_solana/src/state/confirmation.rs: -------------------------------------------------------------------------------- 1 | use super::DISCRIMINATOR_LENGTH; 2 | use anchor_lang::prelude::*; 3 | 4 | #[account] 5 | pub struct Confirmation { 6 | /// Account version 7 | pub version: u16, 8 | 9 | /// Seed bump for PDA 10 | pub bump: u8, 11 | 12 | /// Network account 13 | pub network: Pubkey, 14 | 15 | /// Confirmed account public key 16 | pub account: Pubkey, 17 | 18 | /// Reporter UUID 19 | pub reporter_id: u128, 20 | } 21 | 22 | impl Confirmation { 23 | pub const LEN: usize = DISCRIMINATOR_LENGTH + (2 + 1 + 32 + 32 + 16); 24 | pub const VERSION: u16 = 1; 25 | } 26 | -------------------------------------------------------------------------------- /solana/programs/hapi_core_solana/src/state/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod address; 2 | pub mod asset; 3 | pub mod case; 4 | pub mod confirmation; 5 | pub mod network; 6 | pub mod reporter; 7 | pub mod utils; 8 | 9 | /// Anchor discriminator length 10 | pub const DISCRIMINATOR_LENGTH: usize = 8; 11 | /// Account reserve space 12 | pub const ACCOUNT_RESERVE_SPACE: usize = 32; 13 | -------------------------------------------------------------------------------- /solana/programs/hapi_core_solana/src/state/network.rs: -------------------------------------------------------------------------------- 1 | use super::DISCRIMINATOR_LENGTH; 2 | use anchor_lang::prelude::*; 3 | 4 | #[account] 5 | pub struct Network { 6 | /// Account version 7 | pub version: u16, 8 | 9 | /// Seed bump for PDA 10 | pub bump: u8, 11 | 12 | /// Network authority 13 | pub authority: Pubkey, 14 | 15 | /// Network name (i.e. ethereum, solana, near) 16 | pub name: [u8; 32], 17 | 18 | /// Stake token mint account 19 | pub stake_mint: Pubkey, 20 | 21 | /// Stake configuration info 22 | pub stake_configuration: StakeConfiguration, 23 | 24 | /// Reward token mint account 25 | pub reward_mint: Pubkey, 26 | 27 | /// Reward configuration info 28 | pub reward_configuration: RewardConfiguration, 29 | } 30 | 31 | impl Network { 32 | pub const LEN: usize = DISCRIMINATOR_LENGTH + (2 + 1 + 32 + 32 + 32 + 48 + 32 + 32); 33 | pub const VERSION: u16 = 1; 34 | } 35 | 36 | #[derive(Default, Debug, Clone, PartialEq, AnchorDeserialize, AnchorSerialize)] 37 | pub struct StakeConfiguration { 38 | /// Duration in seconds of reporter suspension before the stake can be withdrawn 39 | pub unlock_duration: u64, 40 | 41 | /// Amount of stake required from a reporter of validator type 42 | pub validator_stake: u64, 43 | 44 | /// Amount of stake required from a reporter of tracer type 45 | pub tracer_stake: u64, 46 | 47 | /// Amount of stake required from a reporter of publisher type 48 | pub publisher_stake: u64, 49 | 50 | /// Amount of stake required from a reporter of authority type 51 | pub authority_stake: u64, 52 | 53 | /// Amount of stake required from a reporter of appraiser type 54 | pub appraiser_stake: u64, 55 | } 56 | 57 | #[derive(Default, Debug, Clone, PartialEq, AnchorDeserialize, AnchorSerialize)] 58 | pub struct RewardConfiguration { 59 | /// Reward amount for tracers that report addresses to this network 60 | pub address_tracer_reward: u64, 61 | 62 | /// Reward amount for tracers and validators that confirm addresses on this network 63 | pub address_confirmation_reward: u64, 64 | 65 | /// Reward amount for tracers that report assets to this network 66 | pub asset_tracer_reward: u64, 67 | 68 | /// Reward amount for tracers and validators that confirm assets on this network 69 | pub asset_confirmation_reward: u64, 70 | } 71 | -------------------------------------------------------------------------------- /solana/programs/hapi_core_solana/src/state/reporter.rs: -------------------------------------------------------------------------------- 1 | use super::DISCRIMINATOR_LENGTH; 2 | use anchor_lang::prelude::*; 3 | 4 | #[account] 5 | pub struct Reporter { 6 | /// Account version 7 | pub version: u16, 8 | 9 | /// Seed bump for PDA 10 | pub bump: u8, 11 | 12 | /// Reporter UUID 13 | pub id: u128, 14 | 15 | /// Network account 16 | pub network: Pubkey, 17 | 18 | /// Reporter's wallet account 19 | pub account: Pubkey, 20 | 21 | /// Short reporter description 22 | pub name: String, 23 | 24 | /// Reporter's type 25 | pub role: ReporterRole, 26 | 27 | /// Reporter account status 28 | pub status: ReporterStatus, 29 | 30 | /// Current deposited stake 31 | pub stake: u64, 32 | 33 | /// Duration starting from the deactivation moment 34 | pub unlock_timestamp: u64, 35 | 36 | /// A link to reporter’s public page 37 | pub url: String, 38 | } 39 | 40 | impl Reporter { 41 | pub const LEN: usize = 42 | DISCRIMINATOR_LENGTH + (2 + 1 + 16 + 32 + 32 + 128 + 1 + 1 + 8 + 8 + 128); 43 | pub const VERSION: u16 = 1; 44 | } 45 | 46 | #[derive(Default, Clone, PartialEq, AnchorDeserialize, AnchorSerialize)] 47 | pub enum ReporterStatus { 48 | /// Reporter is not active, but can activate after staking 49 | #[default] 50 | Inactive, 51 | 52 | /// Reporter is active and can report 53 | Active, 54 | 55 | /// Reporter has requested unstaking and can't report 56 | Unstaking, 57 | } 58 | 59 | #[derive(Default, Debug, Clone, PartialEq, AnchorDeserialize, AnchorSerialize)] 60 | pub enum ReporterRole { 61 | /// Validator - can validate addresses 62 | #[default] 63 | Validator = 0, 64 | 65 | /// Tracer - can report and validate addresses 66 | Tracer = 1, 67 | 68 | /// Publisher - can report cases and addresses 69 | Publisher = 2, 70 | 71 | /// Authority - can report and modify cases and addresses 72 | Authority = 3, 73 | 74 | /// Appraiser - can update replication price 75 | Appraiser = 4, 76 | } 77 | -------------------------------------------------------------------------------- /solana/programs/hapi_core_solana/src/state/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ErrorCode; 2 | use anchor_lang::prelude::*; 3 | 4 | #[derive(Default, Debug, Clone, PartialEq, AnchorDeserialize, AnchorSerialize)] 5 | pub enum Category { 6 | // Tier 0 7 | /// None 8 | #[default] 9 | None = 0, 10 | 11 | // Tier 1 - Low risk 12 | /// Wallet service - custodial or mixed wallets 13 | WalletService, 14 | 15 | /// Merchant service 16 | MerchantService, 17 | 18 | /// Mining pool 19 | MiningPool, 20 | 21 | // Tier 2 - Medium risk 22 | /// Exchange 23 | Exchange, 24 | 25 | /// DeFi application 26 | DeFi, 27 | 28 | /// OTC Broker 29 | OTCBroker, 30 | 31 | /// Cryptocurrency ATM 32 | ATM, 33 | 34 | /// Gambling 35 | Gambling, 36 | 37 | // Tier 3 - High risk 38 | /// Illicit organization 39 | IllicitOrganization, 40 | 41 | /// Mixer 42 | Mixer, 43 | 44 | /// Darknet market or service 45 | DarknetService, 46 | 47 | /// Scam 48 | Scam, 49 | 50 | /// Ransomware 51 | Ransomware, 52 | 53 | /// Theft - stolen funds 54 | Theft, 55 | 56 | /// Counterfeit - fake assets 57 | Counterfeit, 58 | 59 | // Tier 4 - Severe risk 60 | /// Terrorist financing 61 | TerroristFinancing, 62 | 63 | /// Sanctions 64 | Sanctions, 65 | 66 | /// Child abuse and porn materials 67 | ChildAbuse, 68 | 69 | /// The address belongs to a hacker or a group of hackers 70 | Hacker, 71 | 72 | /// Address belongs to a person or an organization from a high risk jurisdiction 73 | HighRiskJurisdiction, 74 | } 75 | 76 | pub fn bytes_to_string(arr: &[u8]) -> Result { 77 | let null_index = arr.iter().position(|&ch| ch == b'\0').unwrap_or(arr.len()); 78 | 79 | String::from_utf8(arr[0..null_index].to_vec()).map_err(|_| ErrorCode::InvalidData.into()) 80 | } 81 | -------------------------------------------------------------------------------- /solana/scripts/utils.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | 3 | export function errorHandler(error: T) { 4 | if (error["code"] && error["msg"]) { 5 | console.error("\n" + chalk.red(`Error ${error["code"]}: ${error["msg"]}`)); 6 | 7 | if (error["logs"]) { 8 | console.error("\n" + chalk.gray(error["logs"].join("\n"))); 9 | } 10 | } else { 11 | console.error("\n" + chalk.red(error)); 12 | } 13 | 14 | process.exit(1); 15 | } 16 | 17 | export function successHandler(output: String | object) { 18 | if (typeof output === "string") { 19 | console.log(chalk.green(`\nSuccess: ${output}`)); 20 | } else if (typeof output === "object") { 21 | console.log(JSON.stringify(output, undefined, 2)); 22 | } 23 | 24 | process.exit(0); 25 | } 26 | -------------------------------------------------------------------------------- /solana/tests/test_keypair.json: -------------------------------------------------------------------------------- 1 | [39,214,184,148,221,155,93,112,21,70,182,151,41,223,152,90,237,115,244,49,242,53,226,49,24,91,106,94,170,236,7,76,218,16,16,138,229,76,69,99,42,206,198,8,102,93,126,36,11,13,165,204,251,42,252,62,117,22,81,231,198,133,164,7] -------------------------------------------------------------------------------- /solana/tests/test_script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | KEYPAIR=./tests/test_keypair.json 4 | AUTHORITY=~/.config/solana/id.json 5 | 6 | exception() { 7 | echo "Error: $1" 8 | exit 1 9 | } 10 | 11 | run_validator() { 12 | echo "==> Running solana test validator" 13 | solana-test-validator -r 1> /dev/null & 14 | VALIDATOR_PID=$! 15 | echo "==> Solana-test-validator PID: $VALIDATOR_PID" 16 | } 17 | 18 | test() { 19 | sleep 5 20 | echo "==> Deploying program to test validator and running tests" 21 | export ANCHOR_WALLET=$AUTHORITY 22 | (anchor deploy --program-keypair $KEYPAIR --program-name hapi_core_solana 1> /dev/null && anchor test --skip-local-validator) 23 | } 24 | 25 | cleanup() { 26 | echo "==> Test validator shut down" 27 | kill -9 $VALIDATOR_PID 28 | } 29 | 30 | run_validator 31 | test 32 | cleanup 33 | -------------------------------------------------------------------------------- /solana/tests/util/console.ts: -------------------------------------------------------------------------------- 1 | import { web3 } from "@coral-xyz/anchor"; 2 | 3 | export function silenceConsole() { 4 | const logSpy = jest.spyOn(console, "log").mockImplementation(() => undefined); 5 | const errorSpy = jest 6 | .spyOn(console, "error") 7 | .mockImplementation(() => undefined); 8 | 9 | return { 10 | close: () => { 11 | logSpy.mockRestore(); 12 | errorSpy.mockRestore(); 13 | }, 14 | }; 15 | } 16 | 17 | export async function expectThrowError( 18 | fn: () => Promise, 19 | error?: string | jest.Constructable | RegExp | Error, 20 | isSilent = true 21 | ) { 22 | const silencer = isSilent ? silenceConsole() : undefined; 23 | 24 | await expect(fn).rejects.toThrowError(error); 25 | 26 | if (silencer) { 27 | silencer.close(); 28 | } 29 | } 30 | 31 | export function listenSolanaLogs(connection: web3.Connection) { 32 | const handle = connection.onLogs("all", (logs: web3.Logs) => { 33 | console.log(logs.logs.join("\n")); 34 | if (logs.err) { 35 | console.error(logs.err); 36 | } 37 | }); 38 | 39 | return { 40 | close: async () => { 41 | connection.removeOnLogsListener(handle); 42 | }, 43 | }; 44 | } 45 | 46 | export async function dumpAccounts( 47 | connection: web3.Connection, 48 | accounts: T 49 | ): Promise { 50 | const lines = []; 51 | for (const key of Object.keys(accounts)) { 52 | const account = accounts[key] as web3.PublicKey; 53 | const info = await connection.getAccountInfoAndContext(account); 54 | lines.push( 55 | [key, account.toBase58(), info.value?.owner?.toBase58() || "[none]"].join( 56 | " " 57 | ) 58 | ); 59 | } 60 | console.log(lines.join("\n")); 61 | return accounts; 62 | } 63 | -------------------------------------------------------------------------------- /solana/tests/util/crypto.ts: -------------------------------------------------------------------------------- 1 | import { web3 } from "@coral-xyz/anchor"; 2 | 3 | export function pubkeyFromHex(hex: string): web3.PublicKey { 4 | return web3.PublicKey.decodeUnchecked(Buffer.from(hex, "hex")); 5 | } 6 | -------------------------------------------------------------------------------- /solana/tests/util/error.ts: -------------------------------------------------------------------------------- 1 | import { errors } from "../../target/idl/hapi_core_solana.json"; 2 | 3 | export function errorRegexp(code: number, instruction = 0) { 4 | return new RegExp( 5 | `failed to send transaction: Transaction simulation failed: Error processing Instruction ${instruction}: custom program error: 0x${code.toString( 6 | 16 7 | )}` 8 | ); 9 | } 10 | 11 | export function programError(name: string): string { 12 | const error = errors.find((error) => error.name === name); 13 | if (!error) { 14 | throw new Error(`Error "${name}" is not found`); 15 | } 16 | 17 | return error.msg; 18 | } 19 | -------------------------------------------------------------------------------- /solana/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "noEmitOnError": true, 7 | "types": [ 8 | "jest" 9 | ], 10 | "lib": [ 11 | "es2017" 12 | ], 13 | "esModuleInterop": true, 14 | "resolveJsonModule": true, 15 | "allowSyntheticDefaultImports": true, 16 | "declaration": true, 17 | "sourceMap": true, 18 | "removeComments": false, 19 | "outDir": "out-tsc", 20 | "rootDir": "./" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /solana_legacy/.clippy.toml: -------------------------------------------------------------------------------- 1 | result_large_err = false -------------------------------------------------------------------------------- /solana_legacy/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /solana_legacy/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "prettier", 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "plugin:prettier/recommended", 10 | "eslint:recommended", 11 | "plugin:@typescript-eslint/eslint-recommended", 12 | "plugin:@typescript-eslint/recommended" 13 | ] 14 | } -------------------------------------------------------------------------------- /solana_legacy/.gitignore: -------------------------------------------------------------------------------- 1 | .anchor 2 | .DS_Store 3 | target 4 | **/*.rs.bk 5 | node_modules 6 | out-tsc 7 | out-lib 8 | -------------------------------------------------------------------------------- /solana_legacy/Anchor.toml: -------------------------------------------------------------------------------- 1 | anchor_version = "0.20.1" 2 | 3 | [programs.localnet] 4 | hapi_core = "hapiAwBQLYRXrjGn6FLCgC8FpQd2yWbKMqS6AYZ48g6" 5 | 6 | [programs.devnet] 7 | hapi_core = "hapiAwBQLYRXrjGn6FLCgC8FpQd2yWbKMqS6AYZ48g6" 8 | 9 | [registry] 10 | url = "https://anchor.projectserum.com" 11 | 12 | [provider] 13 | cluster = "localnet" 14 | wallet = "~/.config/solana/id.json" 15 | 16 | [scripts] 17 | test = "npx jest --verbose" 18 | 19 | [workspace] 20 | types = "lib/idl/" 21 | members = [ 22 | "programs/hapi-core", 23 | ] 24 | -------------------------------------------------------------------------------- /solana_legacy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "programs/*", 4 | "migrations/*" 5 | ] 6 | -------------------------------------------------------------------------------- /solana_legacy/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./buffer"; 2 | export * from "./hapi-core"; 3 | export * from "./enums"; 4 | -------------------------------------------------------------------------------- /solana_legacy/migrations/deploy.ts: -------------------------------------------------------------------------------- 1 | // Migrations are an early feature. Currently, they're nothing more than this 2 | // single deploy script that's invoked from the CLI, injecting a provider 3 | // configured from the workspace's Anchor.toml. 4 | 5 | import * as anchor from "@project-serum/anchor"; 6 | 7 | module.exports = async function (provider) { 8 | // Configure client to use the provider. 9 | anchor.setProvider(provider); 10 | 11 | // Add your deploy script here. 12 | }; 13 | -------------------------------------------------------------------------------- /solana_legacy/migrations/program-migrations/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "program-migrations" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anchor-client = "0.26.0" 8 | spl-token = "3.5" 9 | spl-associated-token-account = "1.1.0" 10 | 11 | serde = "1" 12 | serde_json = "1" 13 | serde_derive = "1" 14 | serde_with = "1" 15 | 16 | config = "0.13.3" 17 | anyhow = "1" 18 | 19 | colored = "2" 20 | 21 | hapi-core = {version = "^0.2", path = "../../programs/hapi-core", features = ["no-entrypoint"]} 22 | -------------------------------------------------------------------------------- /solana_legacy/migrations/program-migrations/README.md: -------------------------------------------------------------------------------- 1 | # HAPI Core migration 2 | 3 | HAPI Core migration script 4 | 5 | ## Dependencies 6 | 7 | To install everything you need to work with this project, you'll need to install dependencies as described in [Anchor](https://project-serum.github.io/anchor/getting-started/installation.html) documentation. 8 | 9 | ### Build 10 | 11 | To build the script, you need to execute this command: 12 | 13 | ```sh 14 | cargo build 15 | ``` 16 | 17 | ### Configure 18 | 19 | To run migration script set configuration before start. 20 | You can copy `config.sample.yaml` to `config.yaml` to use the template in the repo root or define other file in HAPI_CFG env variable to initialize the config file with required fields: 21 | ```yaml 22 | # This is configuration parameters for to HAPI CORE migration 23 | 24 | program_id: "" # The public key of the account containing a program 25 | # (optional, default - program id from the HAPI CORE crate) 26 | environment: "" # Solana environment cluster (must be one of [localnet, 27 | # testnet, mainnet, devnet] or be an http or https url, default - localnet) 28 | keypair_path: "" # Reporter keypair path 29 | communities: # HAPI CORE communities public keys 30 | - id: 1 # New community id 31 | pubkey: "" # Community pubkey 32 | 33 | migrate_accounts: [] # Define what accounts should be migrated (optional, default - All) 34 | # Variants: 35 | # - "Community" 36 | # - "Network" 37 | # - "Reporter" 38 | # - "ReporterReward" 39 | # - "Case" 40 | # - "Address" 41 | # - "Asset" 42 | # example: ["Community", "Reporter"] 43 | input_path: "" # Path to the file to store the migration list 44 | ``` 45 | 46 | ### Run 47 | 48 | To run the script, you need to execute this command: 49 | 50 | ```sh 51 | cargo run 52 | ``` -------------------------------------------------------------------------------- /solana_legacy/migrations/program-migrations/config.sample.yaml: -------------------------------------------------------------------------------- 1 | program_id: "hapiAwBQLYRXrjGn6FLCgC8FpQd2yWbKMqS6AYZ48g6" 2 | keypair_path: "~/.config/solana/id.json" 3 | communities: 4 | - id: 1 5 | pubkey: "B7JizsfxERnN5s1STwJyJoij68Zab9PQUV57QiYNoYYq" 6 | -------------------------------------------------------------------------------- /solana_legacy/migrations/program-migrations/src/configuration.rs: -------------------------------------------------------------------------------- 1 | use { 2 | config::{Config, ConfigError, File}, 3 | serde_derive::Deserialize, 4 | std::env, 5 | }; 6 | 7 | #[derive(Debug, Deserialize, Clone)] 8 | pub enum MigrateAccount { 9 | All, 10 | Community, 11 | Network, 12 | Reporter, 13 | ReporterReward, 14 | Case, 15 | Address, 16 | Asset, 17 | } 18 | 19 | #[derive(Debug, Deserialize, Clone, Default)] 20 | pub struct CommunityCfg { 21 | pub id: u64, 22 | pub pubkey: String, 23 | } 24 | 25 | #[derive(Debug, Deserialize, Clone, Default)] 26 | pub struct HapiCfg { 27 | pub keypair_path: String, 28 | #[serde(default)] 29 | pub program_id: String, 30 | #[serde(default = "localhost_node")] 31 | pub environment: String, 32 | pub communities: Vec, 33 | #[serde(default = "migrate_all")] 34 | pub migrate_accounts: Vec, 35 | #[serde(default = "default_input_path")] 36 | pub input_path: String, 37 | } 38 | 39 | impl HapiCfg { 40 | pub fn build() -> Result { 41 | let config = env::var("HAPI_CFG").unwrap_or_else(|_| "./config.yaml".into()); 42 | 43 | let s = Config::builder() 44 | .add_source(File::with_name(&config).required(true)) 45 | .build()?; 46 | 47 | s.try_deserialize().map_err(Into::into) 48 | } 49 | } 50 | 51 | fn localhost_node() -> String { 52 | "localnet".into() 53 | } 54 | 55 | fn default_input_path() -> String { 56 | "migration_list.json".into() 57 | } 58 | 59 | fn migrate_all() -> Vec { 60 | vec![MigrateAccount::All] 61 | } 62 | -------------------------------------------------------------------------------- /solana_legacy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hapi.one/core-cli", 3 | "version": "0.14.1", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "description": "Client library for HAPI core contract", 8 | "author": "HAPI Protocol", 9 | "license": "Apache-2.0", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/HAPIprotocol/hapi-core.git" 13 | }, 14 | "keywords": [ 15 | "HAPI", 16 | "protocol", 17 | "cybersecurity", 18 | "Solana", 19 | "client" 20 | ], 21 | "main": "out-lib/index.cjs.js", 22 | "module": "out-lib/index.esm.js", 23 | "types": "out-lib/index.d.ts", 24 | "files": [ 25 | "out-lib/**/*" 26 | ], 27 | "scripts": { 28 | "prebuild": "rimraf out-lib out-tsc", 29 | "build": "tsc && rollup -c", 30 | "prepublishOnly": "npm run build", 31 | "test": "anchor test", 32 | "lint": "eslint . --ext .ts" 33 | }, 34 | "dependencies": { 35 | "@project-serum/anchor": "^0.26.0", 36 | "@solana/spl-token": "^0.3.6", 37 | "@solana/web3.js": "^1.72.0", 38 | "@types/bn.js": "^5.1.1", 39 | "@types/jest": "^29.2.4", 40 | "bn.js": "^5.2.1", 41 | "eip55": "^2.1.0", 42 | "jest": "^29.3.1", 43 | "ts-jest": "^29.0.3" 44 | }, 45 | "devDependencies": { 46 | "@rollup/plugin-commonjs": "^21.0.1", 47 | "@rollup/plugin-json": "^4.1.0", 48 | "@rollup/plugin-node-resolve": "^13.1.1", 49 | "@rollup/plugin-typescript": "^8.3.0", 50 | "@typescript-eslint/eslint-plugin": "^5.7.0", 51 | "@typescript-eslint/parser": "^5.7.0", 52 | "eslint": "^8.4.1", 53 | "eslint-config-prettier": "^8.3.0", 54 | "eslint-plugin-prettier": "^4.0.0", 55 | "rollup": "^2.61.1", 56 | "rollup-plugin-dts": "^4.0.1", 57 | "typescript": "^4.3.5" 58 | }, 59 | "jest": { 60 | "moduleFileExtensions": [ 61 | "ts", 62 | "js", 63 | "json" 64 | ], 65 | "transform": { 66 | "^.+\\.tsx?$": "ts-jest" 67 | }, 68 | "testRegex": "/tests/.*\\.spec\\.(ts|tsx)$", 69 | "testTimeout": 30000 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /solana_legacy/programs/hapi-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hapi-core" 3 | version = "0.2.0" 4 | description = "Core contract of HAPI Protocol" 5 | homepage = "https://hapi.one" 6 | repository = "https://github.com/HAPIprotocol/hapi-core" 7 | documentation = "https://hapi-one.gitbook.io/" 8 | license-file = "../../LICENSE" 9 | readme = "../../README.md" 10 | edition = "2018" 11 | 12 | [lib] 13 | crate-type = ["cdylib", "lib"] 14 | name = "hapi_core" 15 | 16 | [features] 17 | no-entrypoint = [] 18 | no-idl = [] 19 | cpi = ["no-entrypoint"] 20 | default = [] 21 | 22 | [dependencies] 23 | anchor-lang = "0.26.0" 24 | anchor-spl = "0.26.0" 25 | spl-token = { version = "3.2.0", features = ["no-entrypoint"] } 26 | -------------------------------------------------------------------------------- /solana_legacy/programs/hapi-core/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /solana_legacy/programs/hapi-core/src/checker/address_data.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{ 2 | prelude::{AccountInfo, ProgramError}, 3 | AnchorDeserialize, 4 | }; 5 | 6 | use crate::state::address::Category; 7 | 8 | /// Byte index of bump in account data 9 | const OFFSET_BUMP: usize = 136; 10 | /// Risk index of bump in account data 11 | const OFFSET_RISK: usize = 178; 12 | /// Category index of bump in account data 13 | const OFFSET_CATEGORY: usize = 177; 14 | 15 | pub struct AddressData { 16 | pub bump: u8, 17 | pub risk: u8, 18 | pub category: Category, 19 | } 20 | 21 | impl AddressData { 22 | pub fn from(account_info: &AccountInfo) -> Result { 23 | let data = account_info.try_borrow_data()?; 24 | 25 | let category = Category::try_from_slice( 26 | &data[OFFSET_CATEGORY..OFFSET_CATEGORY + std::mem::size_of::()], 27 | )?; 28 | 29 | Ok(Self { 30 | bump: data[OFFSET_BUMP], 31 | risk: data[OFFSET_RISK], 32 | category, 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /solana_legacy/programs/hapi-core/src/error.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[error_code] 4 | pub enum ErrorCode { 5 | #[msg("Unexpected account has been used")] 6 | UnexpectedAccount, 7 | #[msg("Account is not authorized to perform this action")] 8 | Unauthorized, 9 | #[msg("Non-sequential case ID")] 10 | NonSequentialCaseId, 11 | #[msg("Release epoch is in future")] 12 | ReleaseEpochInFuture, 13 | #[msg("Invalid mint account")] 14 | InvalidMint, 15 | #[msg("Invalid reporter account")] 16 | InvalidReporter, 17 | #[msg("Reporter account is not active")] 18 | InactiveReporter, 19 | #[msg("Invalid token account")] 20 | InvalidToken, 21 | #[msg("Case closed")] 22 | CaseClosed, 23 | #[msg("Invalid reporter status")] 24 | InvalidReporterStatus, 25 | #[msg("Authority mismatched")] 26 | AuthorityMismatch, 27 | #[msg("Community mismatched")] 28 | CommunityMismatch, 29 | #[msg("This reporter is frozen")] 30 | FrozenReporter, 31 | #[msg("Risk score must be in 0..10 range")] 32 | RiskOutOfRange, 33 | #[msg("Network mismatched")] 34 | NetworkMismatch, 35 | #[msg("Case mismatched")] 36 | CaseMismatch, 37 | #[msg("Same address case")] 38 | SameCase, 39 | #[msg("There is no reward to claim")] 40 | NoReward, 41 | #[msg("Account has illegal owner")] 42 | IllegalOwner, 43 | #[msg("User account has high risk")] 44 | HighAccountRisk, 45 | #[msg("Unexpected account length")] 46 | UnexpectedLength, 47 | #[msg("Invalid account version")] 48 | InvalidAccountVersion, 49 | } 50 | 51 | pub fn print_error(error: ErrorCode) -> Result<()> { 52 | msg!("Error: {}", error); 53 | Err(error.into()) 54 | } 55 | -------------------------------------------------------------------------------- /solana_legacy/programs/hapi-core/src/state/asset.rs: -------------------------------------------------------------------------------- 1 | use super::address::Category; 2 | use crate::utils::DISCRIMINATOR_LENGTH; 3 | use anchor_lang::prelude::*; 4 | 5 | #[account] 6 | pub struct Asset { 7 | /// Account version 8 | pub version: u16, 9 | 10 | /// Community account, which this address belongs to 11 | pub community: Pubkey, 12 | 13 | /// Network account, which this address belongs to 14 | pub network: Pubkey, 15 | 16 | /// Asset mint account 17 | pub mint: [u8; 64], 18 | 19 | /// Asset ID 20 | pub asset_id: [u8; 32], 21 | 22 | /// Seed bump for PDA 23 | pub bump: u8, 24 | 25 | /// ID of the associated case 26 | pub case_id: u64, 27 | 28 | /// Reporter account public key 29 | pub reporter: Pubkey, 30 | 31 | /// Category of illicit activity identified with this address 32 | pub category: Category, 33 | 34 | /// Address risk score 0..10 (0 is safe, 10 is maximum risk) 35 | pub risk: u8, 36 | 37 | /// Confirmation count for this address 38 | pub confirmations: u8, 39 | 40 | /// Accumulated payment amount for report 41 | pub replication_bounty: u64, 42 | } 43 | 44 | impl Asset { 45 | pub const LEN: usize = 46 | DISCRIMINATOR_LENGTH + (2 + 32 + 32 + 64 + 32 + 1 + 8 + 32 + 1 + 1 + 1 + 8); 47 | 48 | pub const VERSION: u16 = 1; 49 | } 50 | -------------------------------------------------------------------------------- /solana_legacy/programs/hapi-core/src/state/case.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::DISCRIMINATOR_LENGTH; 2 | use anchor_lang::prelude::*; 3 | 4 | #[account] 5 | pub struct Case { 6 | /// Account version 7 | pub version: u16, 8 | 9 | /// Community account, which this case belongs to 10 | pub community: Pubkey, 11 | 12 | /// Seed bump for PDA 13 | pub bump: u8, 14 | 15 | /// Sequantial case ID 16 | pub id: u64, 17 | 18 | /// Case reporter's account 19 | pub reporter: Pubkey, 20 | 21 | /// Case status 22 | pub status: CaseStatus, 23 | 24 | /// Short case description 25 | pub name: [u8; 32], 26 | } 27 | 28 | impl Case { 29 | pub const LEN: usize = DISCRIMINATOR_LENGTH + (2 + 32 + 1 + 8 + 32 + 1 + 32); 30 | pub const VERSION: u16 = 1; 31 | } 32 | 33 | #[derive(Default, Clone, PartialEq, AnchorDeserialize, AnchorSerialize)] 34 | pub enum CaseStatus { 35 | Closed = 0, 36 | #[default] 37 | Open = 1, 38 | } 39 | -------------------------------------------------------------------------------- /solana_legacy/programs/hapi-core/src/state/community.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::DISCRIMINATOR_LENGTH; 2 | use anchor_lang::prelude::*; 3 | 4 | #[account] 5 | pub struct Community { 6 | /// Account version 7 | pub version: u16, 8 | 9 | /// Community authority wallet 10 | pub authority: Pubkey, 11 | 12 | /// Community ID 13 | pub id: u64, 14 | 15 | /// Seed bump for PDA 16 | pub bump: u8, 17 | 18 | /// Community case counter 19 | pub cases: u64, 20 | 21 | /// Number of confirmations needed for address to be considered confirmed 22 | pub confirmation_threshold: u8, 23 | 24 | /// Number of epochs reporter must wait to retrieve their stake 25 | pub stake_unlock_epochs: u64, 26 | 27 | /// Stake token mint account 28 | pub stake_mint: Pubkey, 29 | 30 | /// Amount of stake required from a reporter of validator type 31 | pub validator_stake: u64, 32 | 33 | /// Amount of stake required from a reporter of tracer type 34 | pub tracer_stake: u64, 35 | 36 | /// Amount of stake required from a reporter of full type 37 | pub full_stake: u64, 38 | 39 | /// Amount of stake required from a reporter of authority type 40 | pub authority_stake: u64, 41 | 42 | /// Amount of stake required from a reporter of appraiser type 43 | pub appraiser_stake: u64, 44 | } 45 | 46 | impl Community { 47 | pub const LEN: usize = DISCRIMINATOR_LENGTH + (2 + 32 + 8 + 1 + 8 + 1 + 8 + 32 + 8 + 8 + 8 + 8 + 8); 48 | pub const VERSION: u16 = 1; 49 | } 50 | -------------------------------------------------------------------------------- /solana_legacy/programs/hapi-core/src/state/deprecated/deprecated_address.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::ErrorCode, 3 | state::address::{Address, Category}, 4 | }; 5 | use {anchor_lang::prelude::*, std::convert::TryInto}; 6 | 7 | impl Address { 8 | pub fn from_deprecated(account_data: &mut &[u8]) -> Result
{ 9 | // TODO: current account version must be less than deprecated account version (exept V0) 10 | let address: Address = match Address::VERSION { 11 | // Warning! V0 migration can be performed only once 12 | 1 => AddressV0::try_deserialize_unchecked(account_data)?, 13 | _ => return Err(ErrorCode::InvalidAccountVersion.into()), 14 | } 15 | .try_into()?; 16 | 17 | Ok(address) 18 | } 19 | } 20 | 21 | #[account] 22 | pub struct AddressV0 { 23 | pub community: Pubkey, 24 | pub network: Pubkey, 25 | pub address: [u8; 64], 26 | pub bump: u8, 27 | pub case_id: u64, 28 | pub reporter: Pubkey, 29 | pub category: Category, 30 | pub risk: u8, 31 | pub confirmations: u8, 32 | } 33 | 34 | impl TryInto
for AddressV0 { 35 | type Error = Error; 36 | fn try_into(self) -> Result
{ 37 | Ok(Address { 38 | version: Address::VERSION, 39 | community: self.community, 40 | network: self.network, 41 | address: self.address, 42 | bump: self.bump, 43 | case_id: self.case_id, 44 | reporter: self.reporter, 45 | category: self.category, 46 | risk: self.risk, 47 | confirmations: self.confirmations, 48 | replication_bounty: 0, 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /solana_legacy/programs/hapi-core/src/state/deprecated/deprecated_asset.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::ErrorCode, 3 | state::{address::Category, asset::Asset}, 4 | }; 5 | use {anchor_lang::prelude::*, std::convert::TryInto}; 6 | 7 | impl Asset { 8 | pub fn from_deprecated(account_data: &mut &[u8]) -> Result { 9 | // TODO: current account version must be less than deprecated account version (exept V0) 10 | let asset: Asset = match Asset::VERSION { 11 | // Warning! V0 migration can be performed only once 12 | 1 => AssetV0::try_deserialize_unchecked(account_data)?, 13 | _ => return Err(ErrorCode::InvalidAccountVersion.into()), 14 | } 15 | .try_into()?; 16 | 17 | Ok(asset) 18 | } 19 | } 20 | 21 | #[account] 22 | pub struct AssetV0 { 23 | pub community: Pubkey, 24 | pub network: Pubkey, 25 | pub mint: [u8; 64], 26 | pub asset_id: [u8; 32], 27 | pub bump: u8, 28 | pub case_id: u64, 29 | pub reporter: Pubkey, 30 | pub category: Category, 31 | pub risk: u8, 32 | pub confirmations: u8, 33 | } 34 | 35 | impl TryInto for AssetV0 { 36 | type Error = Error; 37 | fn try_into(self) -> Result { 38 | Ok(Asset { 39 | version: Asset::VERSION, 40 | community: self.community, 41 | network: self.network, 42 | mint: self.mint, 43 | asset_id: self.asset_id, 44 | bump: self.bump, 45 | case_id: self.case_id, 46 | reporter: self.reporter, 47 | category: self.category, 48 | risk: self.risk, 49 | confirmations: self.confirmations, 50 | replication_bounty: 0, 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /solana_legacy/programs/hapi-core/src/state/deprecated/deprecated_case.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::ErrorCode, 3 | state::case::{Case, CaseStatus}, 4 | }; 5 | use {anchor_lang::prelude::*, std::convert::TryInto}; 6 | 7 | impl Case { 8 | pub fn from_deprecated(account_data: &mut &[u8]) -> Result { 9 | // TODO: current account version must be less than deprecated account version (exept V0) 10 | let reward: Case = match Case::VERSION { 11 | // Warning! V0 migration can be performed only once 12 | 1 => CaseV0::try_deserialize_unchecked(account_data)?, 13 | _ => return Err(ErrorCode::InvalidAccountVersion.into()), 14 | } 15 | .try_into()?; 16 | 17 | Ok(reward) 18 | } 19 | } 20 | 21 | #[account] 22 | pub struct CaseV0 { 23 | pub community: Pubkey, 24 | pub bump: u8, 25 | pub id: u64, 26 | pub reporter: Pubkey, 27 | pub status: CaseStatus, 28 | pub name: [u8; 32], 29 | } 30 | 31 | impl TryInto for CaseV0 { 32 | type Error = Error; 33 | fn try_into(self) -> Result { 34 | Ok(Case { 35 | version: Case::VERSION, 36 | community: self.community, 37 | bump: self.bump, 38 | id: self.id, 39 | reporter: self.reporter, 40 | status: self.status, 41 | name: self.name, 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /solana_legacy/programs/hapi-core/src/state/deprecated/deprecated_community.rs: -------------------------------------------------------------------------------- 1 | use crate::{error::ErrorCode, state::community::Community}; 2 | use {anchor_lang::prelude::*, std::convert::TryInto}; 3 | 4 | impl Community { 5 | pub fn from_deprecated(account_data: &mut &[u8]) -> Result { 6 | // TODO: current account version must be less than deprecated account version (exept V0) 7 | let community: Community = match Community::VERSION { 8 | // Warning! V0 migration can be performed only once 9 | 1 => CommunityV0::try_deserialize_unchecked(account_data)?, 10 | _ => return Err(ErrorCode::InvalidAccountVersion.into()), 11 | } 12 | .try_into()?; 13 | 14 | Ok(community) 15 | } 16 | } 17 | 18 | #[account] 19 | pub struct CommunityV0 { 20 | pub authority: Pubkey, 21 | pub cases: u64, 22 | pub confirmation_threshold: u8, 23 | pub stake_unlock_epochs: u64, 24 | pub stake_mint: Pubkey, 25 | pub token_signer: Pubkey, 26 | pub token_signer_bump: u8, 27 | pub token_account: Pubkey, 28 | pub validator_stake: u64, 29 | pub tracer_stake: u64, 30 | pub full_stake: u64, 31 | pub authority_stake: u64, 32 | } 33 | 34 | impl TryInto for CommunityV0 { 35 | type Error = Error; 36 | fn try_into(self) -> Result { 37 | Ok(Community { 38 | version: Community::VERSION, 39 | authority: self.authority, 40 | cases: self.cases, 41 | confirmation_threshold: self.confirmation_threshold, 42 | stake_unlock_epochs: self.stake_unlock_epochs, 43 | stake_mint: self.stake_mint, 44 | validator_stake: self.validator_stake, 45 | tracer_stake: self.tracer_stake, 46 | full_stake: self.full_stake, 47 | authority_stake: self.authority_stake, 48 | appraiser_stake: u64::MAX, 49 | id: 0, 50 | bump: 0, 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /solana_legacy/programs/hapi-core/src/state/deprecated/deprecated_network.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::ErrorCode, 3 | state::network::{Network, NetworkSchema}, 4 | }; 5 | use {anchor_lang::prelude::*, std::convert::TryInto}; 6 | 7 | impl Network { 8 | pub fn from_deprecated(account_data: &mut &[u8]) -> Result { 9 | // TODO: current account version must be less than deprecated account version (exept V0) 10 | let network: Network = match Network::VERSION { 11 | // Warning! V0 migration can be performed only once 12 | 1 => NetworkV0::try_deserialize_unchecked(account_data)?, 13 | _ => return Err(ErrorCode::InvalidAccountVersion.into()), 14 | } 15 | .try_into()?; 16 | 17 | Ok(network) 18 | } 19 | } 20 | 21 | #[account] 22 | pub struct NetworkV0 { 23 | pub community: Pubkey, 24 | pub bump: u8, 25 | pub name: [u8; 32], 26 | pub schema: NetworkSchema, 27 | pub reward_mint: Pubkey, 28 | pub reward_signer: Pubkey, 29 | pub reward_signer_bump: u8, 30 | pub address_tracer_reward: u64, 31 | pub address_confirmation_reward: u64, 32 | pub asset_tracer_reward: u64, 33 | pub asset_confirmation_reward: u64, 34 | } 35 | 36 | impl TryInto for NetworkV0 { 37 | type Error = Error; 38 | fn try_into(self) -> Result { 39 | Ok(Network { 40 | version: Network::VERSION, 41 | community: self.community, 42 | bump: self.bump, 43 | name: self.name, 44 | schema: self.schema, 45 | reward_mint: self.reward_mint, 46 | address_tracer_reward: self.address_tracer_reward, 47 | address_confirmation_reward: self.address_confirmation_reward, 48 | asset_tracer_reward: self.asset_tracer_reward, 49 | asset_confirmation_reward: self.asset_confirmation_reward, 50 | replication_price: 0, 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /solana_legacy/programs/hapi-core/src/state/deprecated/deprecated_reporter.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::ErrorCode, 3 | state::reporter::{Reporter, ReporterRole, ReporterStatus}, 4 | }; 5 | use {anchor_lang::prelude::*, std::convert::TryInto}; 6 | 7 | impl Reporter { 8 | pub fn from_deprecated(account_data: &mut &[u8]) -> Result { 9 | // TODO: current account version must be less than deprecated account version (exept V0) 10 | let reward: Reporter = match Reporter::VERSION { 11 | // Warning! V0 migration can be performed only once 12 | 1 => ReporterV0::try_deserialize_unchecked(account_data)?, 13 | _ => return Err(ErrorCode::InvalidAccountVersion.into()), 14 | } 15 | .try_into()?; 16 | 17 | Ok(reward) 18 | } 19 | } 20 | 21 | #[account] 22 | #[derive(Default)] 23 | pub struct ReporterV0 { 24 | pub community: Pubkey, 25 | pub bump: u8, 26 | pub is_frozen: bool, 27 | pub status: ReporterStatus, 28 | pub role: ReporterRole, 29 | pub pubkey: Pubkey, 30 | pub name: [u8; 32], 31 | pub stake: u64, 32 | pub unlock_epoch: u64, 33 | } 34 | 35 | impl TryInto for ReporterV0 { 36 | type Error = Error; 37 | fn try_into(self) -> Result { 38 | Ok(Reporter { 39 | version: Reporter::VERSION, 40 | community: self.community, 41 | bump: self.bump, 42 | is_frozen: self.is_frozen, 43 | status: self.status, 44 | role: self.role, 45 | pubkey: self.pubkey, 46 | name: self.name, 47 | stake: self.stake, 48 | unlock_epoch: self.unlock_epoch, 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /solana_legacy/programs/hapi-core/src/state/deprecated/deprecated_reporter_reward.rs: -------------------------------------------------------------------------------- 1 | use crate::{error::ErrorCode, state::reporter::ReporterReward}; 2 | use {anchor_lang::prelude::*, std::convert::TryInto}; 3 | 4 | impl ReporterReward { 5 | pub fn from_deprecated(account_data: &mut &[u8]) -> Result { 6 | // TODO: current account version must be less than deprecated account version (exept V0) 7 | let reward: ReporterReward = match ReporterReward::VERSION { 8 | // Warning! V0 migration can be performed only once 9 | 1 => ReporterRewardV0::try_deserialize_unchecked(account_data)?, 10 | _ => return Err(ErrorCode::InvalidAccountVersion.into()), 11 | } 12 | .try_into()?; 13 | 14 | Ok(reward) 15 | } 16 | } 17 | 18 | #[account(zero_copy)] 19 | #[derive(Default, Debug)] 20 | pub struct ReporterRewardV0 { 21 | pub reporter: Pubkey, 22 | pub network: Pubkey, 23 | pub bump: u8, 24 | pub address_tracer_counter: u64, 25 | pub address_confirmation_counter: u64, 26 | pub asset_tracer_counter: u64, 27 | pub asset_confirmation_counter: u64, 28 | } 29 | 30 | impl TryInto for ReporterRewardV0 { 31 | type Error = Error; 32 | fn try_into(self) -> Result { 33 | Ok(ReporterReward { 34 | version: ReporterReward::VERSION, 35 | reporter: self.reporter, 36 | network: self.network, 37 | bump: self.bump, 38 | address_tracer_counter: self.address_tracer_counter, 39 | address_confirmation_counter: self.address_confirmation_counter, 40 | asset_tracer_counter: self.asset_tracer_counter, 41 | asset_confirmation_counter: self.asset_confirmation_counter, 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /solana_legacy/programs/hapi-core/src/state/deprecated/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod deprecated_address; 2 | pub mod deprecated_asset; 3 | pub mod deprecated_case; 4 | pub mod deprecated_community; 5 | pub mod deprecated_network; 6 | pub mod deprecated_reporter; 7 | pub mod deprecated_reporter_reward; 8 | -------------------------------------------------------------------------------- /solana_legacy/programs/hapi-core/src/state/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod address; 2 | pub mod asset; 3 | pub mod case; 4 | pub mod community; 5 | pub mod deprecated; 6 | pub mod network; 7 | pub mod reporter; 8 | -------------------------------------------------------------------------------- /solana_legacy/programs/hapi-core/src/state/network.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::DISCRIMINATOR_LENGTH; 2 | use anchor_lang::prelude::*; 3 | 4 | #[account] 5 | pub struct Network { 6 | /// Account version 7 | pub version: u16, 8 | 9 | /// Community account, which this network belongs to 10 | pub community: Pubkey, 11 | 12 | /// Seed bump for PDA 13 | pub bump: u8, 14 | 15 | /// Network name (i.e. ethereum, solana, near) 16 | pub name: [u8; 32], 17 | 18 | // Network address schema 19 | pub schema: NetworkSchema, 20 | 21 | /// Reward token mint account 22 | pub reward_mint: Pubkey, 23 | 24 | /// Reward amount for tracers that report addresses to this network 25 | pub address_tracer_reward: u64, 26 | 27 | /// Reward amount for tracers and validators that confirm addresses on this network 28 | pub address_confirmation_reward: u64, 29 | 30 | /// Reward amount for tracers that report assets to this network 31 | pub asset_tracer_reward: u64, 32 | 33 | /// Reward amount for tracers and validators that confirm assets on this network 34 | pub asset_confirmation_reward: u64, 35 | 36 | /// Replication price amount 37 | pub replication_price: u64, 38 | } 39 | 40 | impl Network { 41 | pub const LEN: usize = DISCRIMINATOR_LENGTH + (2 + 32 + 1 + 32 + 1 + 32 + 8 + 8 + 8 + 8 + 8); 42 | pub const VERSION: u16 = 1; 43 | } 44 | 45 | #[derive(Default, Clone, PartialEq, AnchorDeserialize, AnchorSerialize)] 46 | pub enum NetworkSchema { 47 | #[default] 48 | Plain, 49 | Solana, 50 | Ethereum, 51 | Bitcoin, 52 | Near, 53 | } 54 | -------------------------------------------------------------------------------- /solana_legacy/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from "@rollup/plugin-node-resolve"; 2 | import commonjs from "@rollup/plugin-commonjs"; 3 | import json from "@rollup/plugin-json"; 4 | import dts from "rollup-plugin-dts"; 5 | 6 | export default [ 7 | { 8 | input: ["out-tsc/lib/index.js"], 9 | external: ["@project-serum/anchor", "@solana/spl-token"], 10 | output: [ 11 | { 12 | file: "out-lib/index.esm.js", 13 | format: "esm", 14 | sourcemap: true, 15 | }, 16 | ], 17 | plugins: [resolve(), commonjs(), json()], 18 | }, 19 | { 20 | input: ["out-tsc/lib/index.js"], 21 | external: ["@project-serum/anchor", "@solana/spl-token"], 22 | output: [ 23 | { 24 | file: "out-lib/index.cjs.js", 25 | format: "commonjs", 26 | sourcemap: true, 27 | }, 28 | ], 29 | plugins: [resolve(), commonjs(), json()], 30 | }, 31 | { 32 | input: "out-tsc/lib/index.d.ts", 33 | output: [{ file: "out-lib/index.d.ts", format: "es" }], 34 | plugins: [dts()], 35 | }, 36 | ]; 37 | -------------------------------------------------------------------------------- /solana_legacy/tests/util/console.ts: -------------------------------------------------------------------------------- 1 | import { web3 } from "@project-serum/anchor"; 2 | 3 | export function silenceConsole() { 4 | const logSpy = jest.spyOn(console, "log").mockImplementation(() => undefined); 5 | const errorSpy = jest 6 | .spyOn(console, "error") 7 | .mockImplementation(() => undefined); 8 | 9 | return { 10 | close: () => { 11 | logSpy.mockRestore(); 12 | errorSpy.mockRestore(); 13 | }, 14 | }; 15 | } 16 | 17 | export async function expectThrowError( 18 | fn: () => Promise, 19 | error?: string | jest.Constructable | RegExp | Error, 20 | isSilent = true 21 | ) { 22 | const silencer = isSilent ? silenceConsole() : undefined; 23 | 24 | await expect(fn).rejects.toThrowError(error); 25 | 26 | if (silencer) { 27 | silencer.close(); 28 | } 29 | } 30 | 31 | export function listenSolanaLogs(connection: web3.Connection) { 32 | const handle = connection.onLogs("all", (logs: web3.Logs) => { 33 | console.log(logs.logs.join("\n")); 34 | if (logs.err) { 35 | console.error(logs.err); 36 | } 37 | }); 38 | 39 | return { 40 | close: async () => { 41 | connection.removeOnLogsListener(handle); 42 | }, 43 | }; 44 | } 45 | 46 | export async function dumpAccounts( 47 | connection: web3.Connection, 48 | accounts: T 49 | ): Promise { 50 | const lines = []; 51 | for (const key of Object.keys(accounts)) { 52 | const account = accounts[key] as web3.PublicKey; 53 | const info = await connection.getAccountInfoAndContext(account); 54 | lines.push( 55 | [key, account.toBase58(), info.value?.owner?.toBase58() || "[none]"].join( 56 | " " 57 | ) 58 | ); 59 | } 60 | console.log(lines.join("\n")); 61 | return accounts; 62 | } 63 | -------------------------------------------------------------------------------- /solana_legacy/tests/util/crypto.ts: -------------------------------------------------------------------------------- 1 | import { web3 } from "@project-serum/anchor"; 2 | 3 | export function pubkeyFromHex(hex: string): web3.PublicKey { 4 | return web3.PublicKey.decodeUnchecked(Buffer.from(hex, "hex")); 5 | } 6 | -------------------------------------------------------------------------------- /solana_legacy/tests/util/error.ts: -------------------------------------------------------------------------------- 1 | import { errors } from "../../target/idl/hapi_core.json"; 2 | 3 | export function errorRegexp(code: number, instruction = 0) { 4 | return new RegExp( 5 | `failed to send transaction: Transaction simulation failed: Error processing Instruction ${instruction}: custom program error: 0x${code.toString( 6 | 16 7 | )}` 8 | ); 9 | } 10 | 11 | export function programError(name: string): string { 12 | const error = errors.find((error) => error.name === name); 13 | if (!error) { 14 | throw new Error(`Error "${name}" is not found`); 15 | } 16 | 17 | return error.msg; 18 | } 19 | -------------------------------------------------------------------------------- /solana_legacy/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "noEmitOnError": true, 7 | "types": [ 8 | "jest" 9 | ], 10 | "lib": [ 11 | "es2017" 12 | ], 13 | "esModuleInterop": true, 14 | "resolveJsonModule": true, 15 | "allowSyntheticDefaultImports": true, 16 | "declaration": true, 17 | "sourceMap": true, 18 | "removeComments": false, 19 | "outDir": "out-tsc", 20 | "rootDir": "./" 21 | } 22 | } --------------------------------------------------------------------------------