From 22545d26600c274f7e02f4184a96e7110bb0ca39 Mon Sep 17 00:00:00 2001 From: K Rhoda Date: Wed, 8 Feb 2023 14:09:14 -0700 Subject: [PATCH] Adds a prototype NFT Ownership flow that uses Alchemy + Eth implementation. Lacks full documentation or appearance in demo dapp because breaking changes are expected. Is implemented in NPM lib and worker for use in seperate demo.t --- demo/witness/secrets.md | 8 + demo/witness/worker/worker.js | 10 + rust/rebase/README.md | 2 + rust/rebase/src/content/mod.rs | 1 + rust/rebase/src/content/nft_ownership.rs | 72 +++++++ rust/rebase/src/flow/email.rs | 3 + rust/rebase/src/flow/mod.rs | 1 + rust/rebase/src/flow/nft_ownership.rs | 238 +++++++++++++++++++++ rust/rebase/src/lib.rs | 2 +- rust/rebase/src/proof/mod.rs | 1 + rust/rebase/src/proof/nft_ownership.rs | 34 +++ rust/rebase/src/statement/mod.rs | 1 + rust/rebase/src/statement/nft_ownership.rs | 33 +++ rust/rebase_witness_sdk/README.md | 2 + rust/rebase_witness_sdk/src/types.rs | 53 ++++- 15 files changed, 453 insertions(+), 8 deletions(-) create mode 100644 rust/rebase/src/content/nft_ownership.rs create mode 100644 rust/rebase/src/flow/nft_ownership.rs create mode 100644 rust/rebase/src/proof/nft_ownership.rs create mode 100644 rust/rebase/src/statement/nft_ownership.rs diff --git a/demo/witness/secrets.md b/demo/witness/secrets.md index 5309180a..61d87ecd 100644 --- a/demo/witness/secrets.md +++ b/demo/witness/secrets.md @@ -45,6 +45,14 @@ The `SENDGRID_MAX_ELAPSED_MINS` is a number set to something greater than 0. It ### GITHUB_USER_AGENT (GitHub Flow) The `GITHUB_USER_AGENT` secret will be the user agent sent to GitHub when querying it's public API. +## NFT Ownership Flow + +NOTE: The NFT Ownership route is not in a complete state and will encounter breaking changes before final release, use in production at your own peril! +### ALCHEMY_API_KEY (NFT Ownership Flow) +The `ALCHEMY_API_KEY` secret will be used to access the alchemy api for querying about NFT ownership. An API key can be gained by signing up [here](https://docs.alchemy.com/reference/api-overview). + +### ALCHEMY_MAX_ELAPSED_MINS +The `ALCHEMY_MAX_ELAPSED_MINS` is a number set to something greater than 0. It represents how many minutes can ellapse from the email challenge being sent and the end-user pasting it back before the witness considers the challenge expired. If set to not a number, a negative number, or 0 it will error out. ## Twitter Flow ### TWITTER_BEARER_TOKEN (Twitter Flow) The `TWITTER_BEARER_TOKEN` is the bearer token given from Twitter to the application developer using the [Twitter API](https://developer.twitter.com/en/docs/twitter-api) and will be used (as described [here](https://developer.twitter.com/en/docs/authentication/oauth-2-0/bearer-tokens)) when querying the API. diff --git a/demo/witness/worker/worker.js b/demo/witness/worker/worker.js index 85d69650..e1d8c56c 100644 --- a/demo/witness/worker/worker.js +++ b/demo/witness/worker/worker.js @@ -216,6 +216,16 @@ function witnessOpts() { } } + let useAlchemy = ALCHEMY_API_KEY + && ALCHEMY_MAX_ELAPSED_MINS + && !isNaN(parseInt(ALCHEMY_MAX_ELAPSED_MINS)); + if (useAlchemy) { + o.nft_ownership = { + api_key: ALCHEMY_API_KEY, + max_elapsed_minutes: parseInt(ALCHEMY_MAX_ELAPSED_MINS) + } + } + return o }; diff --git a/rust/rebase/README.md b/rust/rebase/README.md index a7e0670f..565188df 100644 --- a/rust/rebase/README.md +++ b/rust/rebase/README.md @@ -27,6 +27,7 @@ Current credentials supported: * same (links two Subject instances) * soundcloud * twitter +* nft_ownership (NOT PRODUCTION READY) Current Witness flows: * dns @@ -36,6 +37,7 @@ Current Witness flows: * same (links two Subject instances) * soundcloud * twitter +* nft_ownership (NOT PRODUCTION READY) Current Subjects: * ethereum diff --git a/rust/rebase/src/content/mod.rs b/rust/rebase/src/content/mod.rs index 3fa19dc5..65067c50 100644 --- a/rust/rebase/src/content/mod.rs +++ b/rust/rebase/src/content/mod.rs @@ -3,6 +3,7 @@ pub mod basic_profile; pub mod dns; pub mod email; pub mod github; +pub mod nft_ownership; pub mod reddit; pub mod same; pub mod soundcloud; diff --git a/rust/rebase/src/content/nft_ownership.rs b/rust/rebase/src/content/nft_ownership.rs new file mode 100644 index 00000000..2135b3ec --- /dev/null +++ b/rust/rebase/src/content/nft_ownership.rs @@ -0,0 +1,72 @@ +use crate::types::{ + enums::subject::Subjects, + error::ContentError, + types::{Content, Subject}, +}; +use chrono::{SecondsFormat, Utc}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use ssi::{one_or_many::OneOrMany, vc::Evidence}; + +#[derive(Clone, Deserialize, JsonSchema, Serialize)] +pub struct NftOwnership { + pub contract_address: String, + pub subject: Subjects, + pub statement: String, + pub signature: String, +} + +impl Content for NftOwnership { + fn context(&self) -> Result { + Ok(json!([ + "https://www.w3.org/2018/credentials/v1", + "https://spec.rebase.xyz/contexts/v1" + ])) + } + + fn evidence(&self) -> Result>, ContentError> { + let mut evidence_map = std::collections::HashMap::new(); + evidence_map.insert( + "contract_address".to_string(), + serde_json::Value::String(self.contract_address.clone()), + ); + + evidence_map.insert( + "statement".to_string(), + serde_json::Value::String(self.contract_address.clone()), + ); + + evidence_map.insert( + "signature".to_string(), + serde_json::Value::String(self.contract_address.clone()), + ); + + evidence_map.insert( + "timestamp".to_string(), + serde_json::Value::String(Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true)), + ); + + let evidence = Evidence { + id: None, + type_: vec!["NftOwnershipMessage".to_string()], + property_set: Some(evidence_map), + }; + + Ok(Some(OneOrMany::One(evidence))) + } + + fn subject(&self) -> Result { + Ok(json!({ + "id": self.subject.did()?, + "owns_asset_from": self.contract_address.clone(), + })) + } + + fn types(&self) -> Result, ContentError> { + Ok(vec![ + "VerifiableCredential".to_owned(), + "NftOwnershipVerification".to_owned(), + ]) + } +} diff --git a/rust/rebase/src/flow/email.rs b/rust/rebase/src/flow/email.rs index 8323a8ff..61f74917 100644 --- a/rust/rebase/src/flow/email.rs +++ b/rust/rebase/src/flow/email.rs @@ -19,6 +19,7 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use url::Url; +// TODO: When revamping for publication, add challenge-parsing delimitor to config handle all on this side. #[derive(Clone, Deserialize, Serialize)] pub struct SendGridBasic { api_key: String, @@ -142,6 +143,8 @@ impl Flow for SendGridBasic { ))); } + // TODO: Fix this part to have the delimitor part of the config. + // TODO: Add parsing logic based on that config. let t = format!( "{}:::{}", proof.statement.generate_statement()?, diff --git a/rust/rebase/src/flow/mod.rs b/rust/rebase/src/flow/mod.rs index 521811e8..42dd54e0 100644 --- a/rust/rebase/src/flow/mod.rs +++ b/rust/rebase/src/flow/mod.rs @@ -1,6 +1,7 @@ pub mod dns; pub mod email; pub mod github; +pub mod nft_ownership; pub mod reddit; pub mod same; pub mod soundcloud; diff --git a/rust/rebase/src/flow/nft_ownership.rs b/rust/rebase/src/flow/nft_ownership.rs new file mode 100644 index 00000000..2038d79e --- /dev/null +++ b/rust/rebase/src/flow/nft_ownership.rs @@ -0,0 +1,238 @@ +use crate::{ + content::nft_ownership::NftOwnership as Ctnt, + proof::nft_ownership::NftOwnership as Prf, + statement::nft_ownership::NftOwnership as Stmt, + types::{ + enums::subject::{Pkh, Subjects}, + error::FlowError, + types::{Flow, FlowResponse, Instructions, Issuer, Proof, Statement, Subject}, + }, +}; + +use async_trait::async_trait; +use chrono::{DateTime, Duration, Utc}; +use reqwest::Client; +use schemars::schema_for; +use serde::{Deserialize, Serialize}; +use url::Url; + +// TODO: Make this an Enum of which Alchemy is the only impl. +// TODO: Make Alchemy variant be configurable by chain + per-chain configs. +// NOTE: For now, this is just a wrapper around the alchemy API. +#[derive(Clone, Deserialize, Serialize)] +pub struct NftOwnership { + api_key: String, + // The amount of time that can pass before the witness + // wants a new flow initiated. In demo, set to 15 mins. + // This is checked for a negative value or 0 and errs if one is found + // Alternative is casting u64 to i64 and risking UB. + max_elapsed_minutes: i64, +} + +pub struct PageResult { + next_page: Option, + found: bool, + // If more data is needed about the NFT pass this structure around. + // res: AlchemyNftRes, +} + +impl NftOwnership { + // NOTE: This method would be vulnerable to someone foward-dating signatures. + // It likely wouldn't occur, but could be mitigated by doing a Challenge { challenge: string, timestamp: string} + // and attaching it to FlowResponse with a challenge: Option field. + // Then, we generate the TS here, like in email, but send it back over the wire as part of the statement. + // That said, there's no motivation for commiting that style of attack in the current NFT gating demo situation. + // NOTE: People with clocks that are off might mess this up too. + // TODO: When moving to post-demo impl, rework this to use the above strategy + pub fn sanity_check(&self, timestamp: &str) -> Result<(), FlowError> { + if self.max_elapsed_minutes <= 0 { + return Err(FlowError::Validation( + "Max elapsed minutes must be set to a number greater than 0".to_string(), + )); + } + + let now = Utc::now(); + let then = DateTime::parse_from_rfc3339(timestamp) + .map_err(|e| FlowError::Validation(e.to_string()))?; + + if then > now { + return Err(FlowError::Validation(format!( + "Timestamp provided comes from the future" + ))); + } + + if now - Duration::minutes(self.max_elapsed_minutes) > then { + return Err(FlowError::Validation(format!( + "Validation window has expired" + ))); + }; + Ok(()) + } + + pub async fn process_page( + &self, + client: &reqwest::Client, + u: url::Url, + contract_address: &str, + ) -> Result { + let res: AlchemyNftRes = client + .get(u) + .send() + .await + .map_err(|e| FlowError::BadLookup(e.to_string()))? + .json() + .await + .map_err(|e| FlowError::BadLookup(e.to_string()))?; + + let mut result: PageResult = PageResult { + next_page: res.page_key.clone(), + found: false, + // res: res.clone(), + }; + + for nft in res.owned_nfts { + if nft.contract.address == contract_address { + result.found = true; + break; + } + } + + Ok(result) + } +} + +#[async_trait(?Send)] +impl Flow for NftOwnership { + fn instructions(&self) -> Result { + Ok(Instructions { + statement: "Enter the contract address and network of asset".to_string(), + signature: "Sign a statement attesting to ownership of the asset".to_string(), + witness: "Send the attestation and the signature to the witness and issue a credential" + .to_string(), + statement_schema: schema_for!(Stmt), + witness_schema: schema_for!(Prf), + }) + } + + async fn statement( + &self, + stmt: &Stmt, + _issuer: &I, + ) -> Result { + self.sanity_check(&stmt.issued_at)?; + + // TODO: Adjust this when adding additional Alchemy flows. + match stmt.subject { + Subjects::Pkh(Pkh::Eip155(_)) => {} + _ => { + return Err(FlowError::Validation( + "Currently only supports Ethereum NFTs".to_string(), + )) + } + } + + Ok(FlowResponse { + statement: stmt.generate_statement()?, + delimitor: None, + }) + } + + async fn validate_proof(&self, proof: &Prf, _issuer: &I) -> Result { + self.sanity_check(&proof.statement.issued_at)?; + + let u = Url::parse(&format!( + "https://{}-{}.g.alchemy.com/nft/v2/{}/getNFTs?owner={}&withMetadata=false", + // TODO: Replace with enum. + "eth".to_string(), + // TODO: Replace with enum. + proof.statement.network, + self.api_key, + proof.statement.subject.display_id()? + )) + .map_err(|e| FlowError::BadLookup(e.to_string()))?; + + let client = Client::new(); + // NOTE: Intentionally re-assigning before read, thus using: #[allow(unused_assignments)] + #[allow(unused_assignments)] + let mut page_key: Option = None; + #[allow(unused_assignments)] + let mut res: PageResult = PageResult { + next_page: None, + found: false, + // res: AlchemyNftRes { + // owned_nfts: Vec::new(), + // page_key: None, + // total_count: 0, + // block_hash: "initial value".to_string(), + // }, + }; + + let mut next_u: url::Url = u.clone(); + + loop { + res = self + .process_page(&client, next_u.clone(), &proof.statement.contract_address) + .await?; + + if res.found { + break; + } + + page_key = res.next_page.clone(); + match page_key { + None => break, + Some(s) => { + next_u = u.join(&format!("&pageKey={}", s)).map_err(|_e| { + FlowError::BadLookup("Could not follow paginated results".to_string()) + })?; + } + } + } + + if !res.found { + return Err(FlowError::BadLookup(format!( + "Found no owned NFTs from contract {}", + proof.statement.contract_address + ))); + } + + let s = proof.statement.generate_statement()?; + // NOTE: We would generate and append the challenge + // here if using that scheme. + proof + .statement + .subject + .valid_signature(&s, &proof.signature) + .await?; + + Ok(proof.to_content(&s, &proof.signature)?) + } +} + +#[derive(Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +struct AlchemyNftRes { + owned_nfts: Vec, + page_key: Option, + total_count: i64, + block_hash: String, +} + +#[derive(Clone, Deserialize, Serialize)] +struct AlchemyNftEntry { + contract: AlchemyNftContractEntry, + id: AlchemyTokenId, + // NOTE: Balance always seems to be a number + balance: String, +} + +#[derive(Clone, Deserialize, Serialize)] +struct AlchemyNftContractEntry { + address: String, +} + +#[derive(Clone, Deserialize, Serialize)] +struct AlchemyTokenId { + #[serde(rename = "tokenId")] + token_id: String, +} diff --git a/rust/rebase/src/lib.rs b/rust/rebase/src/lib.rs index 5d45f1eb..8c0a4d62 100644 --- a/rust/rebase/src/lib.rs +++ b/rust/rebase/src/lib.rs @@ -1,4 +1,4 @@ -//! For more information please see the documentation at https://spurceid.dev/rebase +//! For more information please see the documentation at https://spruceid.dev/rebase/rebase pub mod content; pub mod flow; pub mod issuer; diff --git a/rust/rebase/src/proof/mod.rs b/rust/rebase/src/proof/mod.rs index 521811e8..42dd54e0 100644 --- a/rust/rebase/src/proof/mod.rs +++ b/rust/rebase/src/proof/mod.rs @@ -1,6 +1,7 @@ pub mod dns; pub mod email; pub mod github; +pub mod nft_ownership; pub mod reddit; pub mod same; pub mod soundcloud; diff --git a/rust/rebase/src/proof/nft_ownership.rs b/rust/rebase/src/proof/nft_ownership.rs new file mode 100644 index 00000000..ad7cc4cf --- /dev/null +++ b/rust/rebase/src/proof/nft_ownership.rs @@ -0,0 +1,34 @@ +use crate::{ + content::nft_ownership::NftOwnership as Ctnt, + statement::nft_ownership::NftOwnership as Stmt, + types::{ + error::{ProofError, StatementError}, + types::{Proof, Statement}, + }, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Deserialize, JsonSchema, Serialize)] +#[serde(rename = "proof")] +pub struct NftOwnership { + pub signature: String, + pub statement: Stmt, +} + +impl Statement for NftOwnership { + fn generate_statement(&self) -> Result { + self.statement.generate_statement() + } +} + +impl Proof for NftOwnership { + fn to_content(&self, statement: &str, signature: &str) -> Result { + Ok(Ctnt { + contract_address: self.statement.contract_address.clone(), + subject: self.statement.subject.clone(), + statement: statement.to_owned(), + signature: signature.to_owned(), + }) + } +} diff --git a/rust/rebase/src/statement/mod.rs b/rust/rebase/src/statement/mod.rs index 521811e8..42dd54e0 100644 --- a/rust/rebase/src/statement/mod.rs +++ b/rust/rebase/src/statement/mod.rs @@ -1,6 +1,7 @@ pub mod dns; pub mod email; pub mod github; +pub mod nft_ownership; pub mod reddit; pub mod same; pub mod soundcloud; diff --git a/rust/rebase/src/statement/nft_ownership.rs b/rust/rebase/src/statement/nft_ownership.rs new file mode 100644 index 00000000..5e80ad9e --- /dev/null +++ b/rust/rebase/src/statement/nft_ownership.rs @@ -0,0 +1,33 @@ +use crate::types::{ + enums::subject::Subjects, + error::StatementError, + types::{Statement, Subject}, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +// TODO: Change this to an enum of possible chains / details. +// Will match an Alchemy specific instance of NftOwnership +// As SendGrid is to Email. +#[derive(Clone, Deserialize, JsonSchema, Serialize)] +#[serde(rename = "statement")] +pub struct NftOwnership { + pub contract_address: String, + pub subject: Subjects, + pub network: String, + pub issued_at: String, +} + +impl Statement for NftOwnership { + fn generate_statement(&self) -> Result { + // TODO: Parse issued_at for valid date. + Ok(format!( + "The {} {} owns an asset from the contract {} on the network {} at time of {}", + self.subject.statement_title()?, + self.subject.display_id()?, + self.contract_address, + self.network, + self.issued_at, + )) + } +} diff --git a/rust/rebase_witness_sdk/README.md b/rust/rebase_witness_sdk/README.md index 4f948a48..614eba6f 100644 --- a/rust/rebase_witness_sdk/README.md +++ b/rust/rebase_witness_sdk/README.md @@ -40,6 +40,8 @@ impl Flow for WitnessFlow { ``` The `Contents`, `Statements`, and `Proofs` enums are each wrappers around all structs implementing the associated trait (`Content`, `Statement`, and `Proof`) exposed in the underlying Rebase lib. As an example, `Statements` looks like: +(NOTE: This does not include the NftOwnership flow which is currently implemented but NOT production ready). + ```rust #[derive(Clone, Deserialize, Serialize)] #[serde(rename = "opts")] diff --git a/rust/rebase_witness_sdk/src/types.rs b/rust/rebase_witness_sdk/src/types.rs index 8afe91bc..e3a7f4e8 100644 --- a/rust/rebase_witness_sdk/src/types.rs +++ b/rust/rebase_witness_sdk/src/types.rs @@ -2,21 +2,25 @@ pub use rebase::issuer; use rebase::{ content::{ dns::Dns as DnsCtnt, email::Email as EmailCtnt, github::GitHub as GitHubCtnt, - reddit::Reddit as RedditCtnt, same::Same as SameCtnt, - soundcloud::SoundCloud as SoundCloudCtnt, twitter::Twitter as TwitterCtnt, + nft_ownership::NftOwnership as NftOwnershipCtnt, reddit::Reddit as RedditCtnt, + same::Same as SameCtnt, soundcloud::SoundCloud as SoundCloudCtnt, + twitter::Twitter as TwitterCtnt, }, flow::{ - dns::DnsFlow, email::SendGridBasic as EmailFlow, github::GitHubFlow, reddit::RedditFlow, - same::SameFlow, soundcloud::SoundCloudFlow, twitter::TwitterFlow, + dns::DnsFlow, email::SendGridBasic as EmailFlow, github::GitHubFlow, + nft_ownership::NftOwnership as NftOwnershipFlow, reddit::RedditFlow, same::SameFlow, + soundcloud::SoundCloudFlow, twitter::TwitterFlow, }, proof::{ - email::Email as EmailProof, github::GitHub as GitHubProof, same::Same as SameProof, + email::Email as EmailProof, github::GitHub as GitHubProof, + nft_ownership::NftOwnership as NftOwnershipProof, same::Same as SameProof, twitter::Twitter as TwitterProof, }, statement::{ dns::Dns as DnsStmt, email::Email as EmailStmt, github::GitHub as GitHubStmt, - reddit::Reddit as RedditStmt, same::Same as SameStmt, - soundcloud::SoundCloud as SoundCloudStmt, twitter::Twitter as TwitterStmt, + nft_ownership::NftOwnership as NftOwnershipStmt, reddit::Reddit as RedditStmt, + same::Same as SameStmt, soundcloud::SoundCloud as SoundCloudStmt, + twitter::Twitter as TwitterStmt, }, types::{ error::{ContentError, FlowError, ProofError, StatementError}, @@ -39,6 +43,8 @@ pub enum InstructionsType { Email, #[serde(rename = "github")] GitHub, + #[serde(rename = "nft_ownership")] + NftOwnership, #[serde(rename = "reddit")] Reddit, #[serde(rename = "same")] @@ -54,6 +60,7 @@ pub enum Contents { Dns(DnsCtnt), Email(EmailCtnt), GitHub(GitHubCtnt), + NftOwnership(NftOwnershipCtnt), Reddit(RedditCtnt), Same(SameCtnt), SoundCloud(SoundCloudCtnt), @@ -67,6 +74,7 @@ impl Content for Contents { Contents::Dns(x) => x.context(), Contents::Email(x) => x.context(), Contents::GitHub(x) => x.context(), + Contents::NftOwnership(x) => x.context(), Contents::Reddit(x) => x.context(), Contents::Same(x) => x.context(), Contents::SoundCloud(x) => x.context(), @@ -79,6 +87,7 @@ impl Content for Contents { Contents::Dns(x) => x.evidence(), Contents::Email(x) => x.evidence(), Contents::GitHub(x) => x.evidence(), + Contents::NftOwnership(x) => x.evidence(), Contents::Reddit(x) => x.evidence(), Contents::Same(x) => x.evidence(), Contents::SoundCloud(x) => x.evidence(), @@ -91,6 +100,7 @@ impl Content for Contents { Contents::Dns(x) => x.subject(), Contents::Email(x) => x.subject(), Contents::GitHub(x) => x.subject(), + Contents::NftOwnership(x) => x.subject(), Contents::Reddit(x) => x.subject(), Contents::Same(x) => x.subject(), Contents::SoundCloud(x) => x.subject(), @@ -103,6 +113,7 @@ impl Content for Contents { Contents::Dns(x) => x.types(), Contents::Email(x) => x.types(), Contents::GitHub(x) => x.types(), + Contents::NftOwnership(x) => x.types(), Contents::Reddit(x) => x.types(), Contents::Same(x) => x.types(), Contents::SoundCloud(x) => x.types(), @@ -120,6 +131,8 @@ pub enum Statements { Email(EmailStmt), #[serde(rename = "github")] GitHub(GitHubStmt), + #[serde(rename = "nft_ownership")] + NftOwnership(NftOwnershipStmt), #[serde(rename = "reddit")] Reddit(RedditStmt), #[serde(rename = "same")] @@ -136,6 +149,7 @@ impl Statement for Statements { Statements::Dns(x) => x.generate_statement(), Statements::Email(x) => x.generate_statement(), Statements::GitHub(x) => x.generate_statement(), + Statements::NftOwnership(x) => x.generate_statement(), Statements::Reddit(x) => x.generate_statement(), Statements::Same(x) => x.generate_statement(), Statements::SoundCloud(x) => x.generate_statement(), @@ -153,6 +167,8 @@ pub enum Proofs { Email(EmailProof), #[serde(rename = "github")] GitHub(GitHubProof), + #[serde(rename = "nft_ownership")] + NftOwnership(NftOwnershipProof), #[serde(rename = "reddit")] Reddit(RedditStmt), #[serde(rename = "same")] @@ -169,6 +185,7 @@ impl Statement for Proofs { Proofs::Dns(x) => x.generate_statement(), Proofs::Email(x) => x.generate_statement(), Proofs::GitHub(x) => x.generate_statement(), + Proofs::NftOwnership(x) => x.generate_statement(), Proofs::Reddit(x) => x.generate_statement(), Proofs::Same(x) => x.generate_statement(), Proofs::SoundCloud(x) => x.generate_statement(), @@ -183,6 +200,9 @@ impl Proof for Proofs { Proofs::Dns(x) => Ok(Contents::Dns(x.to_content(statement, signature)?)), Proofs::Email(x) => Ok(Contents::Email(x.to_content(statement, signature)?)), Proofs::GitHub(x) => Ok(Contents::GitHub(x.to_content(statement, signature)?)), + Proofs::NftOwnership(x) => { + Ok(Contents::NftOwnership(x.to_content(statement, signature)?)) + } Proofs::Reddit(x) => Ok(Contents::Reddit(x.to_content(statement, signature)?)), Proofs::Same(x) => Ok(Contents::Same(x.to_content(statement, signature)?)), Proofs::SoundCloud(x) => Ok(Contents::SoundCloud(x.to_content(statement, signature)?)), @@ -196,6 +216,7 @@ pub struct WitnessFlow { dns: Option, email: Option, github: Option, + nft_ownership: Option, reddit: Option, same: Option, soundcloud: Option, @@ -229,6 +250,12 @@ impl Flow for WitnessFlow { "no github flow configured".to_owned(), )), }, + Statements::NftOwnership(s) => match &self.nft_ownership { + Some(x) => Ok(x.statement(&s, issuer).await?), + None => Err(FlowError::Validation( + "no nft_ownership flow configured".to_owned(), + )), + }, Statements::Reddit(s) => match &self.reddit { Some(x) => Ok(x.statement(&s, issuer).await?), None => Err(FlowError::Validation( @@ -274,6 +301,12 @@ impl Flow for WitnessFlow { "no github flow configured".to_owned(), )), }, + Proofs::NftOwnership(p) => match &self.nft_ownership { + Some(x) => Ok(Contents::NftOwnership(x.validate_proof(&p, issuer).await?)), + None => Err(FlowError::Validation( + "no nft_ownership flow configured".to_owned(), + )), + }, Proofs::Reddit(p) => match &self.reddit { Some(x) => Ok(Contents::Reddit(x.validate_proof(&p, issuer).await?)), None => Err(FlowError::Validation( @@ -343,6 +376,12 @@ impl WitnessFlow { "no github flow configured".to_owned(), )), }, + InstructionsType::NftOwnership => match &self.nft_ownership { + Some(x) => x.instructions(), + _ => Err(FlowError::Validation( + "no nft_ownership flow configured".to_owned(), + )), + }, InstructionsType::Reddit => match &self.reddit { Some(x) => x.instructions(), _ => Err(FlowError::Validation(