Skip to content

Commit

Permalink
Request and submit KYC api endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
Filip-L authored and kacperzuk-neti committed Jun 6, 2024
1 parent b8a5f7d commit 7ef514a
Show file tree
Hide file tree
Showing 13 changed files with 3,137 additions and 525 deletions.
3,091 changes: 2,577 additions & 514 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM rust:1.72 AS builder
FROM rust:1.76 AS builder
COPY ./fplus-lib /fplus-lib
COPY ./fplus-http-server/Cargo.toml /fplus-http-server/Cargo.toml
COPY ./Cargo.lock /fplus-http-server/Cargo.lock
Expand All @@ -11,4 +11,4 @@ RUN cargo build --release
FROM debian:bookworm
RUN apt-get update && apt-get install -y libssl-dev ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /fplus-http-server/target/release/fplus-http-server /target/release/fplus-http-server
CMD ["/target/release/fplus-http-server"]
CMD ["/target/release/fplus-http-server"]
5 changes: 4 additions & 1 deletion fplus-http-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@ anyhow = "1.0.75"
async-trait = "0.1.73"
uuidv4 = "1.0.0"
log = "0.4.20"
cron = "0.12.1"
cron = "0.12.1"
alloy = { git = "https://github.com/alloy-rs/alloy", version = "0.1.0", features = [
"signers",
] }
5 changes: 4 additions & 1 deletion fplus-http-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ async fn main() -> std::io::Result<()> {
.service(router::application::decline)
.service(router::application::additional_info_required)
.service(router::application::trigger_ssa)
.service(router::application::request_kyc)
)
.service(router::application::merged)
.service(router::application::active)
Expand All @@ -97,6 +98,8 @@ async fn main() -> std::io::Result<()> {
.service(router::application::delete_branch)
.service(router::application::cache_renewal)
.service(router::application::update_from_issue)
.service(router::application::trigger_ssa)
.service(router::application::submit_kyc)
.service(router::blockchain::address_allowance)
.service(router::blockchain::verified_clients)
.service(router::verifier::verifiers)
Expand All @@ -111,4 +114,4 @@ async fn main() -> std::io::Result<()> {
.bind(("0.0.0.0", 8080))?
.run()
.await
}
}
36 changes: 35 additions & 1 deletion fplus-http-server/src/router/application.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use actix_web::{get, post, web, HttpResponse, Responder};
use fplus_lib::core::{
application::file::VerifierInput, ApplicationQueryParams, BranchDeleteInfo, CompleteGovernanceReviewInfo, CompleteNewApplicationApprovalInfo, CompleteNewApplicationProposalInfo, CreateApplicationInfo, DcReachedInfo, GithubQueryParams, LDNApplication, MoreInfoNeeded, RefillInfo, ValidationPullRequestData, VerifierActionsQueryParams, TriggerSSAInfo
application::file::VerifierInput, ApplicationQueryParams, BranchDeleteInfo, CompleteGovernanceReviewInfo, CompleteNewApplicationApprovalInfo, CompleteNewApplicationProposalInfo, CreateApplicationInfo, DcReachedInfo, GithubQueryParams, LDNApplication, MoreInfoNeeded, RefillInfo, ValidationPullRequestData, VerifierActionsQueryParams, SubmitKYCInfo, TriggerSSAInfo
};


Expand Down Expand Up @@ -451,11 +451,45 @@ pub async fn check_for_changes(
}
}

#[post("application/submit_kyc")]
pub async fn submit_kyc(info: web::Json<SubmitKYCInfo>) -> impl Responder {
let ldn_application = match LDNApplication::load(info.message.client_id.clone(),
info.message.allocator_repo_owner.clone(),
info.message.allocator_repo_name.clone()).await {
Ok(app) => app,
Err(e) => return HttpResponse::BadRequest().body(e.to_string()),
};
match ldn_application.submit_kyc(&info.into_inner()).await {
Ok(()) => {
return HttpResponse::Ok().body(serde_json::to_string_pretty("Address verified with score").unwrap())
}
Err(e) => return HttpResponse::BadRequest().body(e.to_string()),
};
}

#[get("/health")]
pub async fn health() -> impl Responder {
HttpResponse::Ok().body("OK")
}

#[post("application/request_kyc")]
pub async fn request_kyc(
query: web::Query<VerifierActionsQueryParams>,
) -> impl Responder {
let ldn_application =
match LDNApplication::load(query.id.clone(), query.owner.clone(), query.repo.clone()).await
{
Ok(app) => app,
Err(e) => return HttpResponse::BadRequest().body(e.to_string()),
};
match ldn_application.request_kyc(&query.id, &query.owner, &query.repo).await {
Ok(()) => {
return HttpResponse::Ok().body(serde_json::to_string_pretty("Success").unwrap())
}
Err(e) => return HttpResponse::BadRequest().body(e.to_string()),
};
}

#[post("application/trigger_ssa")]
pub async fn trigger_ssa(query: web::Query<VerifierActionsQueryParams>, info: web::Json<TriggerSSAInfo>) -> impl Responder {
match LDNApplication::trigger_ssa(&query.id, &query.owner, &query.repo, info.into_inner()).await {
Expand Down
21 changes: 20 additions & 1 deletion fplus-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,23 @@ once_cell = "1.19.0"
fplus-database = { path = "../fplus-database", version = "1.8.4"}
pem = "1.0"
anyhow = "1.0"
regex = "1.0"
regex = "1.0"
alloy = { git = "https://github.com/alloy-rs/alloy", version = "0.1.0", features = [
"signers",
"providers",
"node-bindings",
"sol-types",
"json",
"network",
"rpc-types-eth",
"provider-http",
"dyn-abi",
"eip712",
] }
tempfile = "3.10.1"

[dev-dependencies]
actix-rt = "2.9.0"

[features]
online-tests = []
6 changes: 5 additions & 1 deletion fplus-lib/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ pub fn default_env_vars() -> &'static HashMap<&'static str, &'static str> {
m.insert("FILPLUS_ENV", "staging");
m.insert("GLIF_NODE_URL", "http://electric-publicly-adder.ngrok-free.app/rpc/v0");
m.insert("ISSUE_TEMPLATE_VERSION", "1.3");

m.insert("GITCOIN_PASSPORT_DECODER", "5558D441779Eca04A329BcD6b47830D2C6607769");
m.insert("PASSPORT_VERIFIER_CHAIN_ID", "10");
m.insert("GITCOIN_MINIMUM_SCORE", "30");
m.insert("KYC_URL", "https://kyc.allocator.tech");
m.insert("RPC_URL", "https://mainnet.optimism.io");
m
})
}
Expand Down
1 change: 1 addition & 0 deletions fplus-lib/src/core/application/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ pub struct Provider {
pub enum AppState {
AdditionalInfoRequired,
AdditionalInfoSubmitted,
KYCRequested,
Submitted,
ChangesRequested,
ReadyToSign,
Expand Down
179 changes: 179 additions & 0 deletions fplus-lib/src/core/application/gitcoin_interaction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
use std::str::FromStr;
use serde::Deserialize;

use alloy::{
network::TransactionBuilder,
primitives::{address, Address, Bytes, U256},
providers::{Provider, ProviderBuilder},
rpc::types::eth::{BlockId, TransactionRequest},
signers::Signature,
sol,
sol_types::{SolCall, eip712_domain, SolStruct},
};

use anyhow::Result;
use crate::config::get_env_var_or_default;
use crate::error::LDNError;

sol! {
#[allow(missing_docs)]
function getScore(address user) view returns (uint256);

#[derive(Deserialize)]
struct KycApproval {
string message;
string client_id;
string allocator_repo_name;
string allocator_repo_owner;
string issued_at;
string expires_at;
}
}

pub async fn verify_on_gitcoin(address_from_signature: &Address) -> Result<f64, LDNError> {
let rpc_url = get_env_var_or_default("RPC_URL");
let score = get_gitcoin_score_for_address(&rpc_url, address_from_signature.clone()).await?;

let minimum_score = get_env_var_or_default("GITCOIN_MINIMUM_SCORE");
let minimum_score = minimum_score.parse::<f64>().map_err(|e| LDNError::New(format!("Parse minimum score to f64 failed: {e:?}")))?;

if score <= minimum_score {
return Err(LDNError::New(format!(
"For address: {}, Gitcoin passport score is too low ({}). Minimum value is: {}",
address_from_signature, score, minimum_score)));
}
Ok(score)
}

async fn get_gitcoin_score_for_address(rpc_url: &str, address: Address) -> Result<f64, LDNError> {
let provider = ProviderBuilder::new().on_builtin(rpc_url).await.map_err(|e| LDNError::New(format!("Invalid RPC URL: {e:?}")))?;
let gitcoin_passport_decoder = Address::from_str(
&get_env_var_or_default("GITCOIN_PASSPORT_DECODER")).map_err(|e| LDNError::New(format!("Parse GITCOIN PASSPORT DECODER failed: {e:?}")))?;
let call = getScoreCall { user: address }.abi_encode();
let input = Bytes::from(call);
let tx = TransactionRequest::default()
.with_to(gitcoin_passport_decoder)
.with_input(input);

match provider.call(&tx).block(BlockId::latest()).await {
Ok(response) => Ok(calculate_score(response)),
Err(_) => Ok(0.0),
}
}

fn calculate_score(response: Bytes) -> f64 {
let score = U256::from_str(&response.to_string()).unwrap().to::<u128>();
score as f64 / 10000.0
}

pub fn get_address_from_signature(
message: &KycApproval, signature: &str
) -> Result<Address, LDNError> {
let domain = eip712_domain! {
name: "Fil+ KYC",
version: "1",
chain_id: get_env_var_or_default("PASSPORT_VERIFIER_CHAIN_ID").parse().map_err(|_| LDNError::New(format!("Parse chain Id to u64 failed")))?, // Filecoin Chain Id
verifying_contract: address!("0000000000000000000000000000000000000000"),
};
let hash = message.eip712_signing_hash(&domain);
let signature = Signature::from_str(&signature).map_err(|e| LDNError::New(format!("Signature parsing failed: {e:?}")))?;
Ok(signature.recover_address_from_prehash(&hash).map_err(|e| LDNError::New(format!("Recover address from prehash failed: {e:?}")))?)
}

#[cfg(test)]
mod tests {
use std::str::FromStr;
use std::env;

use alloy::node_bindings::{Anvil, AnvilInstance};

use super::*;

const SIGNATURE: &str = "0x0d65d92f0f6774ca40a232422329421183dca5479a17b552a9f2d98ad0bb22ac65618c83061d988cd657c239754253bf66ce6e169252710894041b345797aaa21b";

#[actix_rt::test]
#[cfg(feature = "online-tests")]
async fn getting_score_from_gitcoin_passport_decoder_works() {
env::set_var("GITCOIN_PASSPORT_DECODER", "e53C60F8069C2f0c3a84F9B3DB5cf56f3100ba56");
let anvil = init_anvil();

let test_address = address!("907F988126Fd7e3BB5F46412b6Db6775B3dC3F9b");
let result = get_gitcoin_score_for_address(&anvil.endpoint(), test_address).await;

assert!(result.is_ok());
let result = result.unwrap();

// getScore returns 10410 for input address on block 12507578
assert_eq!(result, 104.09999999999999);
}

#[actix_rt::test]
#[cfg(feature = "online-tests")]
async fn getting_score_with_not_verified_score_should_return_zero() {
env::set_var("GITCOIN_PASSPORT_DECODER", "e53C60F8069C2f0c3a84F9B3DB5cf56f3100ba56");
let anvil = init_anvil();

let test_address = address!("79E214f3Aa3101997ffE810a57eCA4586e3bdeb2");
let result = get_gitcoin_score_for_address(&anvil.endpoint(), test_address).await;

assert!(result.is_ok());
let result = result.unwrap();

assert_eq!(result, 0.0);
}

#[actix_rt::test]
#[cfg(feature = "online-tests")]
async fn verifier_returns_valid_address_for_valid_message() {
env::set_var("PASSPORT_VERIFIER_CHAIN_ID", "11155420");
let signature_message: KycApproval = KycApproval {
message: "Connect your Fil+ application with your wallet and give access to your Gitcoin passport".into(),
client_id: "test".into(),
issued_at: "2024-05-28T09:02:51.126Z".into(),
expires_at: "2024-05-29T09:02:51.126Z".into(),
allocator_repo_name: "test".into(),
allocator_repo_owner: "test".into()
};
let address_from_signature =
get_address_from_signature(&signature_message, &SIGNATURE).unwrap();

let expected_address= address!("7638462f3a5f2cdb49609bf4947ae396f9088949");

assert_eq!(expected_address, address_from_signature);
}

#[actix_rt::test]
#[cfg(feature = "online-tests")]
async fn verifier_returns_invalid_address_for_invalid_message() {
env::set_var("PASSPORT_VERIFIER_CHAIN_ID", "11155420");
let message: KycApproval = KycApproval {
message: "Connect your Fil+ application with your wallet and give access to your Gitcoin passport".into(),
client_id: "test".into(),
issued_at: "2024-05-28T09:02:51.126Z".into(),
expires_at: "2024-05-29T09:02:51.126Z".into(),
allocator_repo_name: "test".into(),
allocator_repo_owner: "test".into()
};

let address_from_signature =
get_address_from_signature(&message, &SIGNATURE).unwrap();

let expected_address =
Address::from_str("0x79e214f3aa3101997ffe810a57eca4586e3bdeb2").unwrap();

assert_ne!(expected_address, address_from_signature);
}

fn init_anvil() -> AnvilInstance {
let rpc_url = "https://sepolia.optimism.io/";
let block_number = 12507578;

let anvil = Anvil::new()
.fork(rpc_url)
.fork_block_number(block_number)
.try_spawn()
.unwrap();

anvil
}
}
16 changes: 16 additions & 0 deletions fplus-lib/src/core/application/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ impl AppState {
AppState::AdditionalInfoRequired => "additional information required",
AppState::AdditionalInfoSubmitted => "additional information submitted",
AppState::Submitted => "validated",
AppState::KYCRequested => "kyc requested",
AppState::ChangesRequested => "application changes requested",
AppState::ReadyToSign => "ready to sign",
AppState::StartSignDatacap => "start sign datacap",
Expand All @@ -34,6 +35,14 @@ impl LifeCycle {
}
}

pub fn kyc_request(&self) -> Self {
LifeCycle {
state: AppState::KYCRequested,
updated_at: Utc::now().to_string(),
..self.clone()
}
}

/// Change Application state to Proposal from Governance Review
/// Actor input is the actor who is changing the state
pub fn finish_governance_review(&self, actor: String, current_allocation_id: String) -> Self {
Expand Down Expand Up @@ -109,4 +118,11 @@ impl LifeCycle {
..self
}
}
pub fn move_back_to_submit_state(self) -> Self {
LifeCycle {
state: AppState::Submitted,
updated_at: Utc::now().to_string(),
..self.clone()
}
}
}
20 changes: 20 additions & 0 deletions fplus-lib/src/core/application/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod client;
pub mod datacap;
pub mod file;
pub mod lifecycle;
pub mod gitcoin_interaction;

impl file::ApplicationFile {
pub async fn new(
Expand Down Expand Up @@ -132,6 +133,25 @@ impl file::ApplicationFile {
..self.clone()
}
}

pub fn move_back_to_submit_state(self) -> Self {
let new_life_cycle = self.lifecycle.clone().move_back_to_submit_state();
Self {
lifecycle: new_life_cycle,
..self.clone()
}
}

pub fn kyc_request(&self) -> Self {
let new_life_cycle = self
.lifecycle
.clone()
.kyc_request();
Self {
lifecycle: new_life_cycle,
..self.clone()
}
}
}

impl std::str::FromStr for file::ApplicationFile {
Expand Down
Loading

0 comments on commit 7ef514a

Please sign in to comment.