Skip to content

Commit

Permalink
feat: add created_at and created_by to collections backfill based on …
Browse files Browse the repository at this point in the history
…drop. add new fields to collection when drop created.
  • Loading branch information
kespinola committed Jul 18, 2023
1 parent a7fa3fd commit fe25471
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 139 deletions.
3 changes: 2 additions & 1 deletion api/src/entities/collections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ pub struct Model {
#[sea_orm(nullable)]
pub signature: Option<String>,
pub seller_fee_basis_points: i16,
// TODO: add to collections and backfill from the drops
pub created_by: Uuid,
pub created_at: DateTimeWithTimeZone,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
Expand Down
1 change: 1 addition & 0 deletions api/src/mutations/collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ impl Mutation {
supply: Set(Some(0)),
creation_status: Set(CreationStatus::Pending),
project_id: Set(input.project),
created_by: Set(user_id),
..Default::default()
};

Expand Down
151 changes: 16 additions & 135 deletions api/src/mutations/drop.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
use std::str::FromStr;

use async_graphql::{Context, Error, InputObject, Object, Result, SimpleObject};
use hub_core::{chrono::Utc, credits::CreditsClient, producer::Producer};
use reqwest::Url;
use sea_orm::{prelude::*, JoinType, ModelTrait, QuerySelect, Set, TransactionTrait};
use serde::{Deserialize, Serialize};
use solana_program::pubkey::Pubkey;

use super::collection::{validate_creators, validate_json, validate_solana_creator_verification};
use crate::{
blockchains::{polygon::Polygon, solana::Solana, DropEvent},
collection::Collection,
Expand Down Expand Up @@ -77,6 +74,8 @@ impl Mutation {
supply: Set(input.supply.map(TryFrom::try_from).transpose()?),
creation_status: Set(CreationStatus::Pending),
seller_fee_basis_points: Set(seller_fee_basis_points.try_into()?),
created_by: Set(user_id),
project_id: Set(input.project),
..Default::default()
};

Expand Down Expand Up @@ -627,12 +626,18 @@ async fn submit_pending_deduction(
blockchain,
action,
} = params;
let conn = db.get();

let drop_model = drops::Entity::find_by_id(drop)
.one(db.get())
let (drop_model, collection) = Drops::find()
.join(JoinType::InnerJoin, drops::Relation::Collections.def())
.select_also(Collections)
.filter(drops::Column::Id.eq(drop))
.one(conn)
.await?
.ok_or(Error::new("drop not found"))?;

let collection = collection.ok_or(Error::new("collection not found"))?;

if drop_model.credits_deduction_id.is_some() {
return Ok(());
}
Expand All @@ -651,8 +656,12 @@ async fn submit_pending_deduction(
let deduction_id = id.ok_or(Error::new("Organization does not have enough credits"))?;

let mut drop: drops::ActiveModel = drop_model.into();
let mut collection: collections::ActiveModel = collection.into();
drop.credits_deduction_id = Set(Some(deduction_id.0));
drop.update(db.get()).await?;
collection.credits_deduction_id = Set(Some(deduction_id.0));

collection.update(conn).await?;
drop.update(conn).await?;

Ok(())
}
Expand Down Expand Up @@ -739,134 +748,6 @@ fn validate_end_time(end_time: &Option<DateTimeWithTimeZone>) -> Result<()> {
Ok(())
}

fn validate_solana_creator_verification(
project_treasury_wallet_address: &str,
creators: &Vec<Creator>,
) -> Result<()> {
for creator in creators {
if creator.verified.unwrap_or_default()
&& creator.address != project_treasury_wallet_address
{
return Err(Error::new(format!(
"Only the project treasury wallet of {project_treasury_wallet_address} can be verified in the mutation. Other creators must be verified independently. See the Metaplex documentation for more details."
)));
}
}

Ok(())
}

/// Validates the addresses of the creators for a given blockchain.
/// # Returns
/// - Ok(()) if all creator addresses are valid blockchain addresses.
///
/// # Errors
/// - Err with an appropriate error message if any creator address is not a valid address.
/// - Err if the blockchain is not supported.
fn validate_creators(blockchain: BlockchainEnum, creators: &Vec<Creator>) -> Result<()> {
let royalty_share = creators.iter().map(|c| c.share).sum::<u8>();

if royalty_share != 100 {
return Err(Error::new(
"The sum of all creator shares must be equal to 100",
));
}

match blockchain {
BlockchainEnum::Solana => {
if creators.len() > 5 {
return Err(Error::new(
"Maximum number of creators is 5 for Solana Blockchain",
));
}

for creator in creators {
validate_solana_address(&creator.address)?;
}
},
BlockchainEnum::Polygon => {
if creators.len() != 1 {
return Err(Error::new(
"Only one creator is allowed for Polygon Blockchain",
));
}

let address = &creators[0].clone().address;
validate_evm_address(address)?;
},
BlockchainEnum::Ethereum => return Err(Error::new("Blockchain not supported yet")),
}

Ok(())
}

pub fn validate_solana_address(address: &str) -> Result<()> {
if Pubkey::from_str(address).is_err() {
return Err(Error::new(format!(
"{address} is not a valid Solana address"
)));
}

Ok(())
}

pub fn validate_evm_address(address: &str) -> Result<()> {
let err = Err(Error::new(format!("{address} is not a valid EVM address")));

// Ethereum address must start with '0x'
if !address.starts_with("0x") {
return err;
}

// Ethereum address must be exactly 40 characters long after removing '0x'
if address.len() != 42 {
return err;
}

// Check that the address contains only hexadecimal characters
if !address[2..].chars().all(|c| c.is_ascii_hexdigit()) {
return err;
}

Ok(())
}

/// Validates the JSON metadata input for the NFT drop.
/// # Returns
/// - Ok(()) if all JSON fields are valid.
///
/// # Errors
/// - Err with an appropriate error message if any JSON field is invalid.
fn validate_json(blockchain: BlockchainEnum, json: &MetadataJsonInput) -> Result<()> {
json.animation_url
.as_ref()
.map(|animation_url| Url::from_str(animation_url))
.transpose()
.map_err(|_| Error::new("Invalid animation url"))?;

json.external_url
.as_ref()
.map(|external_url| Url::from_str(external_url))
.transpose()
.map_err(|_| Error::new("Invalid external url"))?;

Url::from_str(&json.image).map_err(|_| Error::new("Invalid image url"))?;

if blockchain != BlockchainEnum::Solana {
return Ok(());
}

if json.name.chars().count() > 32 {
return Err(Error::new("Name must be less than 32 characters"));
}

if json.symbol.chars().count() > 10 {
return Err(Error::new("Symbol must be less than 10 characters"));
}

Ok(())
}

#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
pub struct RetryDropInput {
pub drop: Uuid,
Expand Down
1 change: 0 additions & 1 deletion api/src/mutations/mint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,6 @@ impl Mutation {
let collection = collection.ok_or(Error::new("collection not found"))?;

let recipient = collection_mint_model.owner.clone();
let _edition = collection_mint_model.edition;
let project_id = collection.project_id;
let blockchain = collection.blockchain;

Expand Down
2 changes: 1 addition & 1 deletion api/src/mutations/transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use hub_core::credits::CreditsClient;
use sea_orm::{prelude::*, JoinType, QuerySelect, Set};
use serde::{Deserialize, Serialize};

use super::drop::{validate_evm_address, validate_solana_address};
use super::collection::{validate_evm_address, validate_solana_address};
use crate::{
blockchains::{polygon::Polygon, solana::Solana, TransferEvent},
db::Connection,
Expand Down
16 changes: 16 additions & 0 deletions api/src/objects/collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ pub struct Collection {
pub seller_fee_basis_points: i16,
pub project_id: Uuid,
pub credits_deduction_id: Option<Uuid>,
pub created_at: DateTimeWithTimeZone,
pub created_by: Uuid,
}

#[Object]
Expand Down Expand Up @@ -62,6 +64,16 @@ impl Collection {
self.project_id
}

/// The date and time in UTC when the collection was created.
async fn created_at(&self) -> DateTimeWithTimeZone {
self.created_at
}

/// The user id of the person who created the collection.
async fn created_by_id(&self) -> Uuid {
self.created_by
}

async fn credits_deduction_id(&self) -> Option<Uuid> {
self.credits_deduction_id
}
Expand Down Expand Up @@ -162,6 +174,8 @@ impl From<Model> for Collection {
address,
project_id,
credits_deduction_id,
created_at,
created_by,
}: Model,
) -> Self {
Self {
Expand All @@ -175,6 +189,8 @@ impl From<Model> for Collection {
seller_fee_basis_points,
project_id,
credits_deduction_id,
created_at,
created_by,
}
}
}
2 changes: 2 additions & 0 deletions migration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ mod m20230706_134402_drop_column_credits_deduction_id_from_nft_transfers;
mod m20230706_142939_add_columns_project_id_and_credits_deduction_id_to_collections;
mod m20230713_151414_create_mint_creators_table;
mod m20230713_163043_add_column_compressed_to_collection_mints;
mod m20230718_111347_add_created_at_and_created_by_columns_to_collections;

pub struct Migrator;

Expand Down Expand Up @@ -101,6 +102,7 @@ impl MigratorTrait for Migrator {
Box::new(m20230706_142939_add_columns_project_id_and_credits_deduction_id_to_collections::Migration),
Box::new(m20230713_151414_create_mint_creators_table::Migration),
Box::new(m20230713_163043_add_column_compressed_to_collection_mints::Migration),
Box::new(m20230718_111347_add_created_at_and_created_by_columns_to_collections::Migration),
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ impl MigrationTrait for Migration {

let stmt = Statement::from_string(
manager.get_database_backend(),
r#"UPDATE collections SET credits_deduction_id = drops.credits_deduction_id, project_id = drops.project_id FROM collections c INNER JOIN drops ON c.id = drops.collection_id;"#.to_string(),
r#"UPDATE collections SET credits_deduction_id = drops.credits_deduction_id, project_id = drops.project_id FROM drops WHERE drops.collection_id = collections.id;"#.to_string(),
);

db.execute(stmt).await?;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use sea_orm::{ConnectionTrait, Statement};
use sea_orm_migration::prelude::*;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Collections::Table)
.add_column_if_not_exists(ColumnDef::new(Collections::CreatedBy).uuid())
.add_column_if_not_exists(
ColumnDef::new(Collections::CreatedAt)
.timestamp_with_time_zone()
.extra("default now()".to_string()),
)
.to_owned(),
)
.await?;

let db = manager.get_connection();

let stmt = Statement::from_string(
manager.get_database_backend(),
r#"UPDATE collections SET created_by = drops.created_by, created_at = drops.created_at FROM drops WHERE drops.collection_id = collections.id;"#.to_string(),
);

db.execute(stmt).await?;

manager
.alter_table(
Table::alter()
.table(Collections::Table)
.modify_column(ColumnDef::new(Collections::CreatedBy).not_null())
.modify_column(ColumnDef::new(Collections::CreatedAt).not_null())
.to_owned(),
)
.await
}

async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Collections::Table)
.drop_column(Collections::CreatedBy)
.drop_column(Collections::CreatedAt)
.to_owned(),
)
.await
}
}

/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
enum Collections {
Table,
CreatedBy,
CreatedAt,
}

0 comments on commit fe25471

Please sign in to comment.