1use crate::{
2 content::poap_ownership_verification::PoapOwnershipVerificationContent as Ctnt,
3 proof::poap_ownership_verification::PoapOwnershipVerificationProof as Prf,
4 statement::poap_ownership_verification::PoapOwnershipVerificationStatement as Stmt,
5 types::{
6 defs::{Flow, Instructions, Issuer, Proof, Statement, StatementResponse, Subject},
7 enums::subject::{Pkh, Subjects},
8 error::FlowError,
9 },
10};
11
12use async_trait::async_trait;
13use chrono::{DateTime, Duration, Utc};
14use reqwest::{
15 header::{HeaderMap, HeaderName, HeaderValue},
16 Client,
17};
18use schemars::schema_for;
19use serde::{Deserialize, Serialize};
20use tsify::Tsify;
21use url::Url;
22use wasm_bindgen::prelude::*;
23
24#[derive(Clone, Debug, Deserialize, Serialize, Tsify)]
25#[tsify(into_wasm_abi, from_wasm_abi)]
26pub struct PoapOwnershipVerificationFlow {
27 pub api_key: String,
28 pub challenge_delimiter: String,
29 pub max_elapsed_minutes: i64,
34}
35
36impl PoapOwnershipVerificationFlow {
37 pub fn sanity_check(&self, timestamp: &str) -> Result<(), FlowError> {
41 if self.max_elapsed_minutes <= 0 {
42 return Err(FlowError::Validation(
43 "Max elapsed minutes must be set to a number greater than 0".to_string(),
44 ));
45 }
46
47 let now = Utc::now();
48 let then = DateTime::parse_from_rfc3339(timestamp)
49 .map_err(|e| FlowError::Validation(e.to_string()))?;
50
51 if then > now {
52 return Err(FlowError::Validation(
53 "Timestamp provided comes from the future".to_string(),
54 ));
55 }
56
57 if now - Duration::minutes(self.max_elapsed_minutes) > then {
58 return Err(FlowError::Validation(
59 "Validation window has expired".to_string(),
60 ));
61 };
62 Ok(())
63 }
64}
65
66#[derive(Deserialize, Serialize)]
67struct PoapResEntry {
68 event: PoapEventEntry,
69 #[serde(rename = "tokenId")]
70 token_id: String,
71 owner: String,
72 chain: String,
73 created: String,
75}
76
77#[derive(Deserialize, Serialize)]
78struct PoapEventEntry {
79 id: i64,
80 fancy_id: String,
81 name: String,
82 event_url: String,
83 image_url: String,
84 country: String,
85 city: String,
86 description: String,
87 year: u64,
88 start_date: String,
90 end_date: String,
91 expiry_date: String,
93 supply: u64,
94}
95
96#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
97#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
98impl Flow<Ctnt, Stmt, Prf> for PoapOwnershipVerificationFlow {
99 fn instructions(&self) -> Result<Instructions, FlowError> {
100 Ok(Instructions {
101 statement: "Enter the event id of the POAP you want to verify ownership of."
102 .to_string(),
103 signature: "Sign a statement attesting to ownership of the POAP.".to_string(),
104 witness: "Send the attestation and the signature to the witness and issue a credential"
105 .to_string(),
106 statement_schema: schema_for!(Stmt),
107 witness_schema: schema_for!(Prf),
108 })
109 }
110
111 async fn statement<I: Issuer + Send + Clone>(
112 &self,
113 stmt: Stmt,
114 issuer: I,
115 ) -> Result<StatementResponse, FlowError> {
116 self.sanity_check(&stmt.issued_at)?;
117
118 if let Subjects::Pkh(Pkh::Eip155(_)) = stmt.subject {
121 } else {
122 return Err(FlowError::Validation(
123 "Currently only supports Ethereum Addresses for POAP Ownership flow".to_string(),
124 ));
125 }
126
127 let s = stmt.generate_statement()?;
128
129 let f = issuer.sign(&s);
135 let sig = f.await?;
136 Ok(StatementResponse {
137 statement: format!("{}{}{}", s, self.challenge_delimiter, sig),
138 delimiter: None,
139 })
140 }
141
142 async fn validate_proof<I: Issuer + Send>(
143 &self,
144 proof: Prf,
145 issuer: I,
146 ) -> Result<Ctnt, FlowError> {
147 self.sanity_check(&proof.statement.issued_at)?;
148
149 let u = Url::parse(&format!(
150 "https://api.poap.tech/actions/scan/{}",
151 proof.statement.subject.display_id()?
152 ))
153 .map_err(|e| FlowError::BadLookup(format!("Failed in API request: {}", e)))?;
154
155 let mut headers = HeaderMap::new();
156 let hv: HeaderValue = self
157 .api_key
158 .parse()
159 .map_err(|_e| FlowError::BadLookup("Could not parse Header value".to_string()))?;
160 let hn: HeaderName = "X-API-KEY"
161 .to_string()
162 .parse()
163 .map_err(|_e| FlowError::BadLookup("Could not parse Header name".to_string()))?;
164 headers.insert(hn, hv);
165
166 let client = Client::new();
167 let f = client.get(u).headers(headers).send();
168 let h = f.await.map_err(|e| FlowError::BadLookup(e.to_string()))?;
169 let f = h.json();
170 let res: Vec<PoapResEntry> = f.await.map_err(|e| FlowError::BadLookup(e.to_string()))?;
171
172 let mut found = false;
173 for entry in res {
174 if entry.event.id == proof.statement.event_id {
175 found = true;
176 break;
177 }
178 }
179
180 if !found {
181 return Err(FlowError::BadLookup(format!(
182 "Found no event with id {} in user's POAPs.",
183 proof.statement.event_id
184 )));
185 }
186
187 let s = proof.statement.generate_statement()?;
188 let f = issuer.sign(&s);
189 let sig = f.await?;
190
191 proof
192 .statement
193 .subject
194 .valid_signature(
195 &format!("{}{}{}", s, &self.challenge_delimiter, sig),
200 &proof.signature,
201 )
202 .await?;
203
204 Ok(proof.to_content(&s, &proof.signature)?)
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use crate::{
212 test_util::util::{
213 test_eth_did, test_witness_signature, test_witness_statement, MockFlow, MockIssuer,
214 TestKey, TestWitness,
215 },
216 types::{
217 defs::{Issuer, Proof, Statement, Subject},
218 enums::subject::Subjects,
219 },
220 };
221
222 fn mock_proof(key: fn() -> Subjects, signature: String) -> Prf {
223 Prf {
224 statement: Stmt {
225 subject: key(),
226 event_id: 102213,
227 issued_at: "2023-09-27T16:36:33.696Z".to_string(),
228 },
229 signature,
230 }
231 }
232
233 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
234 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
235 impl Flow<Ctnt, Stmt, Prf> for MockFlow {
236 fn instructions(&self) -> Result<Instructions, FlowError> {
237 Ok(Instructions {
238 statement: "Unimplemented".to_string(),
239 statement_schema: schema_for!(Stmt),
240 signature: "Unimplemented".to_string(),
241 witness: "Unimplemented".to_string(),
242 witness_schema: schema_for!(Prf),
243 })
244 }
245
246 async fn statement<I: Issuer + Send + Clone>(
247 &self,
248 statement: Stmt,
249 _issuer: I,
250 ) -> Result<StatementResponse, FlowError> {
251 Ok(StatementResponse {
252 statement: statement.generate_statement()?,
253 delimiter: Some("\n\n".to_string()),
254 })
255 }
256
257 async fn validate_proof<I: Issuer + Send>(
258 &self,
259 proof: Prf,
260 _issuer: I,
261 ) -> Result<Ctnt, FlowError> {
262 proof
263 .statement
264 .subject
265 .valid_signature(&self.statement, &self.signature)
266 .await?;
267
268 Ok(proof
269 .to_content(&self.statement, &self.signature)
270 .map_err(FlowError::Proof)?)
271 }
272 }
273
274 #[tokio::test]
275 async fn mock_poap_ownership() {
276 let signature = test_witness_signature(TestWitness::NftOwnership, TestKey::Eth).unwrap();
277 let statement = test_witness_statement(TestWitness::NftOwnership, TestKey::Eth).unwrap();
278
279 let p = mock_proof(test_eth_did, signature.clone());
280
281 let flow = MockFlow {
282 statement,
283 signature,
284 };
285
286 let i = MockIssuer {};
287 flow.unsigned_credential(p, test_eth_did(), i)
288 .await
289 .unwrap();
290 }
291}