From 7fcec2e8f7f529ba8c106ebfe7483a42c0edff8e Mon Sep 17 00:00:00 2001 From: imabdulbasit Date: Wed, 23 Aug 2023 17:00:20 +0500 Subject: [PATCH 1/4] Switch mint's collection --- api/proto.lock | 12 +- api/proto.toml | 6 +- api/src/blockchains/mod.rs | 9 +- api/src/blockchains/solana.rs | 20 ++- api/src/entities/mod.rs | 1 + api/src/entities/prelude.rs | 1 + .../entities/switch_collection_histories.rs | 25 ++++ api/src/events.rs | 58 ++++++++- api/src/mutations/collection.rs | 102 ++++++++++++++- migration/src/lib.rs | 2 + ...reate_switch_collection_histories_table.rs | 119 ++++++++++++++++++ 11 files changed, 338 insertions(+), 17 deletions(-) create mode 100644 api/src/entities/switch_collection_histories.rs create mode 100644 migration/src/m20230821_131630_create_switch_collection_histories_table.rs diff --git a/api/proto.lock b/api/proto.lock index 2246705..dcd0349 100644 --- a/api/proto.lock +++ b/api/proto.lock @@ -5,8 +5,8 @@ sha512 = "d75800df0d4744c6b0f4d9a9952d3bfd0bb6b24a8babd19104cc11b54a525f85551b3c [[schemas]] subject = "nfts" -version = 26 -sha512 = "8191626dbd6e222f24914589a77855e5f0f5122b7adc655ef7e84af0d9ef104715408db97cdf7843882aaa65cfcacc436a01e01b336f6758b0863ec5e007b709" +version = 27 +sha512 = "ec52dffa482eb3beb72b1ae1745f3b8cb1b6d321d20fdc30078f4362c924b0f75ebe3dc53c54e5de92ea55163b64f09fe4e2b749c1bc5bc64d92f8356835eb90" [[schemas]] subject = "organization" @@ -20,8 +20,8 @@ sha512 = "c5ddf43d2958ec690ee2261d0ff9808b67ce810d2fc4b6077f96f561929a920f03509f [[schemas]] subject = "solana_nfts" -version = 9 -sha512 = "312a84e8ae8b9222c7ec2b307d036dae0bd8dac4363e813c2fcffd5d7fba8741bd802953b1ec0a96baf57a7ce852debb724fcccf3b0bd8a27a9e4cc60344a56f" +version = 10 +sha512 = "1bcb166ab5dfdf4841d60caa07a4098dcec03d7e3b0e63adb090ed2f5fe990c1e13826867e8df7521ec631027d5a931a08865fd2cf2daa905807c4b7dca40213" [[schemas]] subject = "timestamp" @@ -30,5 +30,5 @@ sha512 = "d167e0a143c813073eef8597f0b237e5a8eaf32abbf709724e8071b2dd73ce0438b82f [[schemas]] subject = "treasury" -version = 21 -sha512 = "734cff313b8b4854b9a4c03cfd6f95f07b1fd86f8678393ab466443d9da4d6e7c9fc400bdbcc718d83e6c7711857941d4b6dc0ea5d1d926f05a7859a65a15509" +version = 22 +sha512 = "bde788b07f818aa52e684dcbd91e1f1e3db82f242f616ec2a42ab6d412df33a1461677c229f2f9bae345938c2f325e6332a95caef2c7e01a47531af53e39bf03" diff --git a/api/proto.toml b/api/proto.toml index 7a0b3f9..a75af55 100644 --- a/api/proto.toml +++ b/api/proto.toml @@ -3,9 +3,9 @@ endpoint = "https://schemas.holaplex.tools" [schemas] organization = 5 -nfts = 26 +nfts = 27 customer = 2 -treasury = 21 -solana_nfts = 9 +treasury = 22 +solana_nfts = 10 polygon_nfts = 6 timestamp = 1 \ No newline at end of file diff --git a/api/src/blockchains/mod.rs b/api/src/blockchains/mod.rs index 294c46b..6aab281 100644 --- a/api/src/blockchains/mod.rs +++ b/api/src/blockchains/mod.rs @@ -3,7 +3,9 @@ pub mod solana; use hub_core::anyhow::Result; -use crate::proto::{NftEventKey, RetryUpdateSolanaMintPayload, UpdateSolanaMintPayload}; +use crate::proto::{ + NftEventKey, RetryUpdateSolanaMintPayload, SwitchCollectionPayload, UpdateSolanaMintPayload, +}; /// Represents a response from a transaction on the blockchain. This struct /// provides the serialized message and the signatures of the signed message. @@ -41,6 +43,11 @@ pub trait CollectionEvent { key: NftEventKey, payload: RetryUpdateSolanaMintPayload, ) -> Result<()>; + async fn switch_collection( + &self, + key: NftEventKey, + payload: SwitchCollectionPayload, + ) -> Result<()>; } #[async_trait::async_trait] diff --git a/api/src/blockchains/solana.rs b/api/src/blockchains/solana.rs index d5326fa..ac999a6 100644 --- a/api/src/blockchains/solana.rs +++ b/api/src/blockchains/solana.rs @@ -5,12 +5,13 @@ use crate::proto::{ nft_events::Event::{ SolanaCreateCollection, SolanaCreateDrop, SolanaMintDrop, SolanaMintToCollection, SolanaRetryCreateCollection, SolanaRetryDrop, SolanaRetryMintDrop, - SolanaRetryMintToCollection, SolanaRetryUpdatedCollectionMint, SolanaTransferAsset, - SolanaUpdateCollection, SolanaUpdateDrop, SolanaUpdatedCollectionMint, + SolanaRetryMintToCollection, SolanaRetryUpdatedCollectionMint, + SolanaSwitchMintCollectionRequested, SolanaTransferAsset, SolanaUpdateCollection, + SolanaUpdateDrop, SolanaUpdatedCollectionMint, }, MetaplexMasterEditionTransaction, MintMetaplexEditionTransaction, MintMetaplexMetadataTransaction, NftEventKey, NftEvents, RetryUpdateSolanaMintPayload, - TransferMetaplexAssetTransaction, UpdateSolanaMintPayload, + SwitchCollectionPayload, TransferMetaplexAssetTransaction, UpdateSolanaMintPayload, }; #[derive(Clone)] pub struct Solana { @@ -240,4 +241,17 @@ impl self.producer.send(Some(&event), Some(&key)).await?; Ok(()) } + + async fn switch_collection( + &self, + key: NftEventKey, + payload: SwitchCollectionPayload, + ) -> Result<()> { + let event = NftEvents { + event: Some(SolanaSwitchMintCollectionRequested(payload)), + }; + + self.producer.send(Some(&event), Some(&key)).await?; + Ok(()) + } } diff --git a/api/src/entities/mod.rs b/api/src/entities/mod.rs index f1b87a3..7fc278d 100644 --- a/api/src/entities/mod.rs +++ b/api/src/entities/mod.rs @@ -16,5 +16,6 @@ pub mod mint_histories; pub mod nft_transfers; pub mod project_wallets; pub mod sea_orm_active_enums; +pub mod switch_collection_histories; pub mod transfer_charges; pub mod update_histories; diff --git a/api/src/entities/prelude.rs b/api/src/entities/prelude.rs index 63461d0..3f2bd87 100644 --- a/api/src/entities/prelude.rs +++ b/api/src/entities/prelude.rs @@ -7,5 +7,6 @@ pub use super::{ metadata_json_files::Entity as MetadataJsonFiles, metadata_jsons::Entity as MetadataJsons, mint_creators::Entity as MintCreators, mint_histories::Entity as MintHistory, nft_transfers::Entity as NftTransfers, project_wallets::Entity as ProjectWallets, + switch_collection_histories::Entity as SwitchCollectionHistories, transfer_charges::Entity as TransferCharges, update_histories::Entity as UpdateHistories, }; diff --git a/api/src/entities/switch_collection_histories.rs b/api/src/entities/switch_collection_histories.rs new file mode 100644 index 0000000..2907648 --- /dev/null +++ b/api/src/entities/switch_collection_histories.rs @@ -0,0 +1,25 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3 + +use sea_orm::entity::prelude::*; + +use super::sea_orm_active_enums::CreationStatus; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "switch_collection_histories")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub collection_mint_id: Uuid, + pub collection_id: Uuid, + pub credit_deduction_id: Uuid, + #[sea_orm(column_type = "Text", nullable)] + pub signature: Option, + pub status: CreationStatus, + pub initiated_by: Uuid, + pub created_at: DateTime, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/api/src/events.rs b/api/src/events.rs index eb777d9..b4ab669 100644 --- a/api/src/events.rs +++ b/api/src/events.rs @@ -18,10 +18,13 @@ use crate::{ collection_creators, collection_mints, collections, customer_wallets, drops, metadata_json_attributes, metadata_json_files, metadata_jsons, mint_creators, mint_histories, nft_transfers, - prelude::{CollectionMints, Collections, Drops, MintHistory, UpdateHistories}, + prelude::{ + CollectionMints, Collections, Drops, MintHistory, SwitchCollectionHistories, + UpdateHistories, + }, project_wallets, sea_orm_active_enums::{Blockchain, CreationStatus}, - transfer_charges, update_histories, + switch_collection_histories, transfer_charges, update_histories, }, proto::{ nft_events::Event as NftEvent, @@ -132,6 +135,12 @@ enum UpdateResult { Failure, } +#[derive(Clone)] +enum SwitchCollectionResult { + Success(String), + Failure, +} + impl Processor { #[must_use] pub fn new( @@ -249,7 +258,13 @@ impl Processor { Some(SolanaNftsEvent::ImportedExternalMint(e)) => { self.index_mint(id, user_id, e).await }, - + Some(SolanaNftsEvent::SwitchMintCollectionSubmitted(payload)) => { + self.switch_collection_submitted( + id, + SwitchCollectionResult::Success(payload.signature), + ) + .await + }, None | Some(_) => Ok(()), }, Services::Polygon(_, e) => match e.event { @@ -831,6 +846,43 @@ impl Processor { Ok(()) } + + async fn switch_collection_submitted( + &self, + id: String, + payload: SwitchCollectionResult, + ) -> Result<()> { + let history_id = Uuid::from_str(&id)?; + let history = SwitchCollectionHistories::find_by_id(history_id) + .one(self.db.get()) + .await? + .context("Switch collection history record not found")?; + + let mint = CollectionMints::find_by_id(history.collection_mint_id) + .one(self.db.get()) + .await? + .context("Mint record not found")?; + + let mut history_am: switch_collection_histories::ActiveModel = history.clone().into(); + + if let SwitchCollectionResult::Success(signature) = payload { + let deduction_id = history.credit_deduction_id; + history_am.signature = Set(Some(signature)); + + let mut mint_am: collection_mints::ActiveModel = mint.into(); + mint_am.collection_id = Set(history.collection_id); + mint_am.update(self.db.get()).await?; + + self.credits + .confirm_deduction(TransactionId(deduction_id)) + .await?; + } else { + history_am.status = Set(CreationStatus::Failed); + } + + history_am.update(self.db.get()).await?; + Ok(()) + } } impl TryFrom for Blockchain { diff --git a/api/src/mutations/collection.rs b/api/src/mutations/collection.rs index f39979e..3f3494a 100644 --- a/api/src/mutations/collection.rs +++ b/api/src/mutations/collection.rs @@ -2,6 +2,7 @@ use std::str::FromStr; use async_graphql::{Context, Error, InputObject, Object, Result, SimpleObject}; use hub_core::{ + chrono::Utc, credits::{CreditsClient, TransactionId}, producer::Producer, }; @@ -17,7 +18,8 @@ use crate::{ collection_creators, collection_mints, collections, metadata_jsons, prelude::{CollectionCreators, CollectionMints, Collections, MetadataJsons}, project_wallets, - sea_orm_active_enums::{Blockchain as BlockchainEnum, CreationStatus}, + sea_orm_active_enums::{Blockchain, Blockchain as BlockchainEnum, CreationStatus}, + switch_collection_histories, }, metadata_json::MetadataJson, objects::{Collection as CollectionObject, Creator, MetadataJsonInput}, @@ -459,6 +461,93 @@ impl Mutation { collection: collection.into(), }) } + + pub async fn switch_collection( + &self, + ctx: &Context<'_>, + input: SwitchCollectionInput, + ) -> Result { + let SwitchCollectionInput { + mint, + collection_address, + } = input; + + let AppContext { + db, + user_id, + organization_id, + balance, + .. + } = ctx.data::()?; + let credits = ctx.data::>()?; + let solana = ctx.data::()?; + + let user_id = user_id.0.ok_or(Error::new("X-USER-ID header not found"))?; + let org_id = organization_id + .0 + .ok_or(Error::new("X-ORG-ID header not found"))?; + let balance = balance.0.ok_or(Error::new("X-BALANCE header not found"))?; + let (mint, collection) = CollectionMints::find_by_id_with_collection(mint) + .one(db.get()) + .await? + .ok_or(Error::new("Mint not found"))?; + + let collection = collection.ok_or(Error::new("Collection not found"))?; + + let new_collection = Collections::find() + .filter(collections::Column::Address.eq(collection_address.to_string())) + .one(db.get()) + .await? + .ok_or(Error::new("Collection not found"))?; + + let TransactionId(deduction_id) = credits + .submit_pending_deduction( + org_id, + user_id, + Actions::UpdateMint, + collection.blockchain.into(), + balance, + ) + .await?; + + match collection.blockchain { + Blockchain::Solana => { + validate_solana_address(&collection_address)?; + let history_am = switch_collection_histories::ActiveModel { + collection_mint_id: Set(mint.id), + collection_id: Set(new_collection.id), + credit_deduction_id: Set(deduction_id), + signature: Set(None), + status: Set(CreationStatus::Pending), + initiated_by: Set(user_id), + created_at: Set(Utc::now().naive_utc()), + ..Default::default() + }; + + let history = history_am.insert(db.get()).await?; + + solana + .event() + .switch_collection( + NftEventKey { + id: history.id.to_string(), + project_id: collection.project_id.to_string(), + user_id: user_id.to_string(), + }, + crate::proto::SwitchCollectionPayload { + mint_id: mint.id.to_string(), + collection_id: new_collection.id.to_string(), + }, + ) + .await?; + + Ok(SwitchCollectionPayload { + collection_mint: mint.into(), + }) + }, + _ => Err(Error::new("Blockchain not supported")), + } + } } pub async fn fetch_owner( @@ -688,3 +777,14 @@ pub struct ImportCollectionPayload { /// The status of the collection import. status: CreationStatus, } + +#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] +pub struct SwitchCollectionInput { + mint: Uuid, + collection_address: String, +} + +#[derive(Debug, Clone, SimpleObject)] +pub struct SwitchCollectionPayload { + collection_mint: collection_mints::CollectionMint, +} diff --git a/migration/src/lib.rs b/migration/src/lib.rs index 8d41c10..5ec6e56 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -52,6 +52,7 @@ mod m20230725_135946_rename_purchases_to_mint_histories; mod m20230725_144506_drop_solana_collections_table; mod m20230807_090847_create_histories_table; mod m20230818_163948_downcase_polygon_addresses; +mod m20230821_131630_create_switch_collection_histories_table; pub struct Migrator; @@ -111,6 +112,7 @@ impl MigratorTrait for Migrator { Box::new(m20230725_144506_drop_solana_collections_table::Migration), Box::new(m20230807_090847_create_histories_table::Migration), Box::new(m20230818_163948_downcase_polygon_addresses::Migration), + Box::new(m20230821_131630_create_switch_collection_histories_table::Migration), ] } } diff --git a/migration/src/m20230821_131630_create_switch_collection_histories_table.rs b/migration/src/m20230821_131630_create_switch_collection_histories_table.rs new file mode 100644 index 0000000..901a96a --- /dev/null +++ b/migration/src/m20230821_131630_create_switch_collection_histories_table.rs @@ -0,0 +1,119 @@ +use sea_orm_migration::prelude::*; + +use crate::m20230214_212301_create_collections_table::CreationStatus; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(SwitchCollectionHistories::Table) + .if_not_exists() + .col( + ColumnDef::new(SwitchCollectionHistories::Id) + .uuid() + .not_null() + .primary_key() + .extra("default gen_random_uuid()".to_string()), + ) + .col( + ColumnDef::new(SwitchCollectionHistories::CollectionMintId) + .uuid() + .not_null(), + ) + .col( + ColumnDef::new(SwitchCollectionHistories::CreditDeductionId) + .uuid() + .not_null(), + ) + .col( + ColumnDef::new(SwitchCollectionHistories::CollectionId) + .uuid() + .not_null(), + ) + .col( + ColumnDef::new(SwitchCollectionHistories::Signature) + .text() + .null(), + ) + .col( + ColumnDef::new(SwitchCollectionHistories::Status) + .custom(CreationStatus::Type) + .not_null(), + ) + .col( + ColumnDef::new(SwitchCollectionHistories::InitiatedBy) + .uuid() + .not_null(), + ) + .col( + ColumnDef::new(SwitchCollectionHistories::CreatedAt) + .timestamp() + .not_null() + .extra("default now()".to_string()), + ) + .to_owned(), + ) + .await?; + + manager + .create_index( + IndexCreateStatement::new() + .name("switch_collection_histories_collection_mint_id_idx") + .table(SwitchCollectionHistories::Table) + .col(SwitchCollectionHistories::CollectionMintId) + .index_type(IndexType::BTree) + .to_owned(), + ) + .await?; + + manager + .create_index( + IndexCreateStatement::new() + .name("switch_collection_histories_credit_deduction_id_idx") + .table(SwitchCollectionHistories::Table) + .col(SwitchCollectionHistories::CreditDeductionId) + .index_type(IndexType::BTree) + .to_owned(), + ) + .await?; + + manager + .create_index( + IndexCreateStatement::new() + .name("switch_collection_histories_status_idx") + .table(SwitchCollectionHistories::Table) + .col(SwitchCollectionHistories::Status) + .index_type(IndexType::BTree) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table( + Table::drop() + .table(SwitchCollectionHistories::Table) + .to_owned(), + ) + .await + } +} + +#[derive(Iden)] +pub enum SwitchCollectionHistories { + Table, + Id, + CollectionMintId, + CollectionId, + CreditDeductionId, + Signature, + Status, + InitiatedBy, + CreatedAt, +} From 71420512e883c0ca7f19db12b3569961a9f61e4c Mon Sep 17 00:00:00 2001 From: imabdulbasit Date: Thu, 24 Aug 2023 00:31:44 +0500 Subject: [PATCH 2/4] check for collection --- api/src/mutations/collection.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/src/mutations/collection.rs b/api/src/mutations/collection.rs index 3f3494a..7aeeac6 100644 --- a/api/src/mutations/collection.rs +++ b/api/src/mutations/collection.rs @@ -500,6 +500,10 @@ impl Mutation { .await? .ok_or(Error::new("Collection not found"))?; + if collection.id == new_collection.id { + return Err(Error::new("Collection already switched")); + } + let TransactionId(deduction_id) = credits .submit_pending_deduction( org_id, From 0bfcfc2773be0a4da86e01444a44b929175ec7e9 Mon Sep 17 00:00:00 2001 From: imabdulbasit Date: Mon, 28 Aug 2023 19:06:24 +0500 Subject: [PATCH 3/4] Switch collection history field in mint object --- api/src/dataloaders/collection_mints.rs | 8 +- api/src/dataloaders/mod.rs | 2 + .../switch_collection_histories.rs | 50 ++++++ api/src/entities/collection_mints.rs | 154 +--------------- api/src/entities/mint_histories.rs | 4 +- .../entities/switch_collection_histories.rs | 5 +- api/src/events.rs | 16 +- api/src/lib.rs | 8 +- api/src/mutations/collection.rs | 12 +- api/src/mutations/mint.rs | 12 +- api/src/mutations/transfer.rs | 3 +- api/src/objects/collection.rs | 9 +- api/src/objects/collection_mint.rs | 168 ++++++++++++++++++ api/src/objects/customer.rs | 11 +- api/src/objects/mod.rs | 2 + api/src/objects/wallet.rs | 8 +- api/src/queries/mint.rs | 2 +- 17 files changed, 278 insertions(+), 196 deletions(-) create mode 100644 api/src/dataloaders/switch_collection_histories.rs create mode 100644 api/src/objects/collection_mint.rs diff --git a/api/src/dataloaders/collection_mints.rs b/api/src/dataloaders/collection_mints.rs index c456f78..1e37b6f 100644 --- a/api/src/dataloaders/collection_mints.rs +++ b/api/src/dataloaders/collection_mints.rs @@ -4,7 +4,7 @@ use async_graphql::{dataloader::Loader as DataLoader, FieldError, Result}; use poem::async_trait; use sea_orm::prelude::*; -use crate::{db::Connection, entities::collection_mints}; +use crate::{db::Connection, entities::collection_mints, objects::CollectionMint}; #[derive(Debug, Clone)] pub struct Loader { @@ -21,7 +21,7 @@ impl Loader { #[async_trait] impl DataLoader for Loader { type Error = FieldError; - type Value = Vec; + type Value = Vec; async fn load(&self, keys: &[Uuid]) -> Result, Self::Error> { let collection_mints = collection_mints::Entity::find() @@ -60,7 +60,7 @@ impl OwnerLoader { #[async_trait] impl DataLoader for OwnerLoader { type Error = FieldError; - type Value = Vec; + type Value = Vec; async fn load(&self, keys: &[String]) -> Result, Self::Error> { let collection_mints = collection_mints::Entity::find() @@ -100,7 +100,7 @@ impl CollectionMintLoader { #[async_trait] impl DataLoader for CollectionMintLoader { type Error = FieldError; - type Value = collection_mints::CollectionMint; + type Value = CollectionMint; async fn load(&self, keys: &[Uuid]) -> Result, Self::Error> { let collection_mints = collection_mints::Entity::find() diff --git a/api/src/dataloaders/mod.rs b/api/src/dataloaders/mod.rs index 2bf0b1e..b2244cb 100644 --- a/api/src/dataloaders/mod.rs +++ b/api/src/dataloaders/mod.rs @@ -11,6 +11,7 @@ mod mint_histories; mod nft_transfers; mod project_collection; mod project_collections; +mod switch_collection_histories; mod update_histories; pub use collection::Loader as CollectionLoader; @@ -34,4 +35,5 @@ pub use mint_histories::{ pub use nft_transfers::CollectionMintTransfersLoader; pub use project_collection::ProjectCollectionLoader; pub use project_collections::ProjectCollectionsLoader; +pub use switch_collection_histories::SwitchCollectionHistoryLoader; pub use update_histories::UpdateMintHistoryLoader; diff --git a/api/src/dataloaders/switch_collection_histories.rs b/api/src/dataloaders/switch_collection_histories.rs new file mode 100644 index 0000000..c1653bf --- /dev/null +++ b/api/src/dataloaders/switch_collection_histories.rs @@ -0,0 +1,50 @@ +use std::collections::HashMap; + +use async_graphql::{dataloader::Loader as DataLoader, FieldError, Result}; +use poem::async_trait; +use sea_orm::{prelude::*, Order, QueryOrder}; + +use crate::{db::Connection, entities::switch_collection_histories}; + +#[derive(Debug, Clone)] +pub struct SwitchCollectionHistoryLoader { + pub db: Connection, +} + +impl SwitchCollectionHistoryLoader { + #[must_use] + pub fn new(db: Connection) -> Self { + Self { db } + } +} + +#[async_trait] +impl DataLoader for SwitchCollectionHistoryLoader { + type Error = FieldError; + type Value = Vec; + + async fn load(&self, keys: &[Uuid]) -> Result, Self::Error> { + let conn = self.db.get(); + let switch_histories = switch_collection_histories::Entity::find() + .filter( + switch_collection_histories::Column::CollectionMintId + .is_in(keys.iter().map(ToOwned::to_owned)), + ) + .order_by(switch_collection_histories::Column::CreatedAt, Order::Desc) + .all(conn) + .await?; + + Ok(switch_histories.into_iter().fold( + HashMap::new(), + |mut acc: HashMap>, switch_history| { + acc.entry(switch_history.collection_mint_id.clone()) + .or_insert_with(Vec::new); + + acc.entry(switch_history.collection_mint_id.clone()) + .and_modify(|switch_histories| switch_histories.push(switch_history.into())); + + acc + }, + )) + } +} diff --git a/api/src/entities/collection_mints.rs b/api/src/entities/collection_mints.rs index f10d6e8..940ab90 100644 --- a/api/src/entities/collection_mints.rs +++ b/api/src/entities/collection_mints.rs @@ -1,17 +1,9 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.0 -use async_graphql::{ComplexObject, Context, Error, Result, SimpleObject}; +use async_graphql::Result; use sea_orm::{entity::prelude::*, JoinType, QuerySelect, SelectTwo}; -use super::{ - collections, mint_creators, mint_histories, nft_transfers, - sea_orm_active_enums::{Blockchain, CreationStatus}, - update_histories, -}; -use crate::{ - objects::{Collection, MetadataJson}, - AppContext, -}; +use super::{collections, sea_orm_active_enums::CreationStatus}; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "collection_mints")] @@ -34,148 +26,6 @@ pub struct Model { pub compressed: bool, } -/// Represents a single NFT minted from a collection. -#[derive(Clone, Debug, PartialEq, Eq, SimpleObject)] -#[graphql(complex)] -pub struct CollectionMint { - /// The unique ID of the minted NFT. - pub id: Uuid, - /// The ID of the collection the NFT was minted from. - pub collection_id: Uuid, - /// The address of the NFT - /// On Solana this is the mint address. - /// On EVM chains it is the concatenation of the contract address and the token id `{contractAddress}:{tokenId}`. - pub address: Option, - /// The wallet address of the owner of the NFT. - pub owner: String, - /// The status of the NFT creation. - pub creation_status: CreationStatus, - /// The unique ID of the creator of the NFT. - pub created_by: Uuid, - /// The date and time when the NFT was created. - pub created_at: DateTimeWithTimeZone, - /// The transaction signature associated with the NFT. - pub signature: Option, - /// The unique edition number of the NFT. - pub edition: i64, - /// The seller fee basis points (ie royalties) for the NFT. - pub seller_fee_basis_points: i16, - /// credits deduction id - pub credits_deduction_id: Option, - /// Indicates if the NFT is compressed. Compression is only supported on Solana. - pub compressed: bool, -} - -#[ComplexObject] -impl CollectionMint { - /// The collection the NFT was minted from. - async fn collection(&self, ctx: &Context<'_>) -> Result> { - let AppContext { - collection_loader, .. - } = ctx.data::()?; - - collection_loader.load_one(self.collection_id).await - } - - /// The metadata json associated to the collection. - /// [Metaplex v1.1.0 Standard](https://docs.metaplex.com/programs/token-metadata/token-standard) - async fn metadata_json(&self, ctx: &Context<'_>) -> Result> { - let AppContext { - metadata_json_loader, - .. - } = ctx.data::()?; - let collection = self.collection(ctx).await?.ok_or(Error::new(format!( - "Collection not found for collection mint {:?}", - &self.id - )))?; - - match collection.blockchain { - Blockchain::Solana => metadata_json_loader.load_one(self.id).await, - Blockchain::Polygon => metadata_json_loader.load_one(self.collection_id).await, - Blockchain::Ethereum => Err(Error::new("Ethereum not supported")), - } - } - - /// The update history of the mint. - async fn update_histories( - &self, - ctx: &Context<'_>, - ) -> Result>> { - let AppContext { - update_mint_history_loader, - .. - } = ctx.data::()?; - - update_mint_history_loader.load_one(self.id).await - } - /// The creators of the mint. Includes the creator addresses and their shares. - async fn creators(&self, ctx: &Context<'_>) -> Result>> { - let AppContext { - mint_creators_loader, - .. - } = ctx.data::()?; - - mint_creators_loader.load_one(self.id).await - } - - /// The record of the original mint. - async fn mint_history(&self, ctx: &Context<'_>) -> Result> { - let AppContext { - collection_mint_mint_history_loader, - .. - } = ctx.data::()?; - - collection_mint_mint_history_loader.load_one(self.id).await - } - - /// The history of transfers for the mint. - async fn transfer_histories( - &self, - ctx: &Context<'_>, - ) -> Result>> { - let AppContext { - collection_mint_transfers_loader, - .. - } = ctx.data::()?; - - collection_mint_transfers_loader.load_one(self.id).await - } -} - -impl From for CollectionMint { - fn from( - Model { - id, - collection_id, - address, - owner, - creation_status, - created_by, - created_at, - signature, - edition, - seller_fee_basis_points, - credits_deduction_id, - compressed, - }: Model, - ) -> Self { - Self { - id, - collection_id, - address, - owner, - creation_status, - created_by, - created_at, - signature, - edition, - seller_fee_basis_points, - credits_deduction_id, - compressed, - } - } -} - #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { #[sea_orm( diff --git a/api/src/entities/mint_histories.rs b/api/src/entities/mint_histories.rs index 435a05a..9cd5799 100644 --- a/api/src/entities/mint_histories.rs +++ b/api/src/entities/mint_histories.rs @@ -3,8 +3,8 @@ use async_graphql::{ComplexObject, Context, Result, SimpleObject}; use sea_orm::entity::prelude::*; -use super::{collection_mints::CollectionMint, sea_orm_active_enums::CreationStatus}; -use crate::AppContext; +use super::sea_orm_active_enums::CreationStatus; +use crate::{objects::CollectionMint, AppContext}; /// A record of a minted NFT. #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, SimpleObject)] diff --git a/api/src/entities/switch_collection_histories.rs b/api/src/entities/switch_collection_histories.rs index 2907648..35733f7 100644 --- a/api/src/entities/switch_collection_histories.rs +++ b/api/src/entities/switch_collection_histories.rs @@ -1,11 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3 - +use async_graphql::SimpleObject; use sea_orm::entity::prelude::*; use super::sea_orm_active_enums::CreationStatus; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, SimpleObject)] #[sea_orm(table_name = "switch_collection_histories")] +#[graphql(concrete(name = "SwitchCollectionHistory", params()))] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: Uuid, diff --git a/api/src/events.rs b/api/src/events.rs index b4ab669..695b042 100644 --- a/api/src/events.rs +++ b/api/src/events.rs @@ -66,6 +66,8 @@ pub enum ProcessorErrorKind { DbMissingTransferCharge, #[error("No associated update history found in database")] DbMissingUpdateHistory, + #[error("No associated switch collection history found in database")] + DbMissingSwitchCollectionHistory, #[error("Database record contains no deduction ID")] RecordMissingDeductionId, @@ -251,6 +253,10 @@ impl Processor { SolanaNftsEvent::UpdateCollectionMintFailed(_) | SolanaNftsEvent::RetryUpdateMintFailed(_), ) => self.mint_updated(id, UpdateResult::Failure).await, + Some(SolanaNftsEvent::SwitchMintCollectionFailed(_)) => { + self.switch_collection_submitted(id, SwitchCollectionResult::Failure) + .await + }, Some(SolanaNftsEvent::UpdateMintOwner(e)) => self.update_mint_owner(id, e).await, Some(SolanaNftsEvent::ImportedExternalCollection(e)) => { self.index_collection(id, project_id, user_id, e).await @@ -304,10 +310,10 @@ impl Processor { } = metadata.ok_or(ProcessorErrorKind::MissingCollectionMetadata)?; let collection_am = collections::ActiveModel { - id: Set(id.parse()?), + id: Set(Uuid::from_str(&id)?), blockchain: Set(Blockchain::Solana), supply: Set(supply.map(Into::into)), - project_id: Set(project_id.parse()?), + project_id: Set(Uuid::from_str(&project_id)?), credits_deduction_id: Set(None), creation_status: Set(CreationStatus::Created), total_mints: Set(0), @@ -851,17 +857,17 @@ impl Processor { &self, id: String, payload: SwitchCollectionResult, - ) -> Result<()> { + ) -> ProcessResult<()> { let history_id = Uuid::from_str(&id)?; let history = SwitchCollectionHistories::find_by_id(history_id) .one(self.db.get()) .await? - .context("Switch collection history record not found")?; + .ok_or(ProcessorErrorKind::DbMissingSwitchCollectionHistory)?; let mint = CollectionMints::find_by_id(history.collection_mint_id) .one(self.db.get()) .await? - .context("Mint record not found")?; + .ok_or(ProcessorErrorKind::DbMissingCollectionMint)?; let mut history_am: switch_collection_histories::ActiveModel = history.clone().into(); diff --git a/api/src/lib.rs b/api/src/lib.rs index 3aa4bfd..7254b3c 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -26,7 +26,8 @@ use dataloaders::{ CollectionMintMintHistoryLoader, CollectionMintTransfersLoader, CollectionMintsLoader, CollectionMintsOwnerLoader, CreatorsLoader, DropLoader, DropMintHistoryLoader, HoldersLoader, MetadataJsonAttributesLoader, MetadataJsonLoader, MintCreatorsLoader, MinterMintHistoryLoader, - ProjectCollectionLoader, ProjectCollectionsLoader, ProjectDropsLoader, UpdateMintHistoryLoader, + ProjectCollectionLoader, ProjectCollectionsLoader, ProjectDropsLoader, + SwitchCollectionHistoryLoader, UpdateMintHistoryLoader, }; use db::Connection; use hub_core::{ @@ -286,6 +287,7 @@ pub struct AppContext { mint_creators_loader: DataLoader, collection_mint_mint_history_loader: DataLoader, collection_mint_transfers_loader: DataLoader, + switch_collection_history_loader: DataLoader, } impl AppContext { @@ -334,7 +336,8 @@ impl AppContext { ); let collection_mint_transfers_loader = DataLoader::new(CollectionMintTransfersLoader::new(db.clone()), tokio::spawn); - + let switch_collection_history_loader = + DataLoader::new(SwitchCollectionHistoryLoader::new(db.clone()), tokio::spawn); Self { db, user_id, @@ -360,6 +363,7 @@ impl AppContext { mint_creators_loader, collection_mint_mint_history_loader, collection_mint_transfers_loader, + switch_collection_history_loader, } } } diff --git a/api/src/mutations/collection.rs b/api/src/mutations/collection.rs index 7aeeac6..d3ea2ab 100644 --- a/api/src/mutations/collection.rs +++ b/api/src/mutations/collection.rs @@ -22,7 +22,7 @@ use crate::{ switch_collection_histories, }, metadata_json::MetadataJson, - objects::{Collection as CollectionObject, Creator, MetadataJsonInput}, + objects::{Collection as CollectionObject, CollectionMint, Creator, MetadataJsonInput}, proto::{ nft_events::Event as NftEvent, CollectionCreation, CollectionImport, CreationStatus as NftCreationStatus, Creator as ProtoCreator, MasterEdition, @@ -462,6 +462,10 @@ impl Mutation { }) } + /// This mutation allows you to change the collection to which a mint belongs. + /// For Solana, the mint specified by `input` must already belong to a Metaplex Certified Collection. + /// The collection you are aiming to switch to must also be Metaplex Certified Collection. + pub async fn switch_collection( &self, ctx: &Context<'_>, @@ -576,11 +580,13 @@ pub async fn fetch_owner( Ok(owner) } +/// Result of a successful create collection mutation. #[derive(Debug, Clone, SimpleObject)] pub struct CreateCollectionPayload { collection: CollectionObject, } +/// Input object for creating a collection. #[derive(Debug, Clone, Serialize, Deserialize, InputObject)] pub struct CreateCollectionInput { pub project: Uuid, @@ -782,13 +788,15 @@ pub struct ImportCollectionPayload { status: CreationStatus, } +/// Input object for switching a mint's collection. #[derive(Debug, Clone, Serialize, Deserialize, InputObject)] pub struct SwitchCollectionInput { mint: Uuid, collection_address: String, } +/// Represents the result of a successful switch collection mutation. #[derive(Debug, Clone, SimpleObject)] pub struct SwitchCollectionPayload { - collection_mint: collection_mints::CollectionMint, + collection_mint: CollectionMint, } diff --git a/api/src/mutations/mint.rs b/api/src/mutations/mint.rs index db02144..263bd3a 100644 --- a/api/src/mutations/mint.rs +++ b/api/src/mutations/mint.rs @@ -21,7 +21,7 @@ use crate::{ update_histories, }, metadata_json::MetadataJson, - objects::{Creator, MetadataJsonInput}, + objects::{CollectionMint, Creator, MetadataJsonInput}, proto::{ self, nft_events::Event as NftEvent, CreationStatus as NftCreationStatus, MetaplexMetadata, MintCollectionCreation, MintCreation, NftEventKey, NftEvents, RetryUpdateSolanaMintPayload, @@ -890,7 +890,7 @@ pub struct MintDropInput { /// Represents payload data for the `mint_edition` mutation #[derive(Debug, Clone, SimpleObject)] pub struct MintEditionPayload { - collection_mint: collection_mints::CollectionMint, + collection_mint: CollectionMint, } /// Represents input data for `retry_mint` mutation with an ID as a field of type UUID @@ -902,7 +902,7 @@ pub struct RetryMintEditionInput { /// Represents payload data for `retry_mint` mutation #[derive(Debug, Clone, SimpleObject)] pub struct RetryMintEditionPayload { - collection_mint: collection_mints::CollectionMint, + collection_mint: CollectionMint, } /// Represents input data for `mint_to_collection` mutation with a collection ID, recipient, metadata, and optional seller fee basis points as fields @@ -941,12 +941,12 @@ pub struct UpdateMintInput { #[derive(Debug, Clone, SimpleObject)] pub struct MintToCollectionPayload { /// The minted NFT - collection_mint: collection_mints::CollectionMint, + collection_mint: CollectionMint, } #[derive(Debug, Clone, SimpleObject)] pub struct UpdateMintPayload { - collection_mint: collection_mints::CollectionMint, + collection_mint: CollectionMint, } /// Represents input data for `retry_mint_to_collection` mutation with an ID as a field of type UUID @@ -960,7 +960,7 @@ pub struct RetryMintToCollectionInput { #[derive(Debug, Clone, SimpleObject)] pub struct RetryMintToCollectionPayload { /// The retried minted NFT - collection_mint: collection_mints::CollectionMint, + collection_mint: CollectionMint, } #[derive(Debug, Clone, InputObject)] diff --git a/api/src/mutations/transfer.rs b/api/src/mutations/transfer.rs index 2601bb4..98ea103 100644 --- a/api/src/mutations/transfer.rs +++ b/api/src/mutations/transfer.rs @@ -7,11 +7,12 @@ use super::collection::{validate_evm_address, validate_solana_address}; use crate::{ blockchains::{polygon::Polygon, solana::Solana, TransferEvent}, entities::{ - collection_mints::{self, CollectionMint}, + collection_mints, prelude::CustomerWallets, sea_orm_active_enums::{Blockchain, CreationStatus}, transfer_charges, }, + objects::CollectionMint, proto::{self, NftEventKey, TransferPolygonAsset}, Actions, AppContext, OrganizationId, UserID, }; diff --git a/api/src/objects/collection.rs b/api/src/objects/collection.rs index e86b2e8..e0d2c3d 100644 --- a/api/src/objects/collection.rs +++ b/api/src/objects/collection.rs @@ -1,10 +1,10 @@ use async_graphql::{Context, Object, Result}; use sea_orm::entity::prelude::*; -use super::{metadata_json::MetadataJson, Drop, Holder}; +use super::{metadata_json::MetadataJson, CollectionMint, Drop, Holder}; use crate::{ entities::{ - collection_creators, collection_mints, + collection_creators, collections::Model, mint_histories, sea_orm_active_enums::{Blockchain, CreationStatus}, @@ -119,10 +119,7 @@ impl Collection { } /// The list of minted NFTs from the collection including the NFTs address and current owner's wallet address. - async fn mints( - &self, - ctx: &Context<'_>, - ) -> Result>> { + async fn mints(&self, ctx: &Context<'_>) -> Result>> { let AppContext { collection_mints_loader, .. diff --git a/api/src/objects/collection_mint.rs b/api/src/objects/collection_mint.rs new file mode 100644 index 0000000..a7c6b21 --- /dev/null +++ b/api/src/objects/collection_mint.rs @@ -0,0 +1,168 @@ +use async_graphql::{ComplexObject, Context, Error, Result, SimpleObject}; +use sea_orm::entity::prelude::*; + +use crate::{ + entities::{ + collection_mints::Model, + mint_creators, mint_histories, nft_transfers, + sea_orm_active_enums::{Blockchain, CreationStatus}, + switch_collection_histories, update_histories, + }, + objects::{Collection, MetadataJson}, + AppContext, +}; + +/// Represents a single NFT minted from a collection. +#[derive(Clone, Debug, PartialEq, Eq, SimpleObject)] +#[graphql(complex)] +pub struct CollectionMint { + /// The unique ID of the minted NFT. + pub id: Uuid, + /// The ID of the collection the NFT was minted from. + pub collection_id: Uuid, + /// The address of the NFT + /// On Solana this is the mint address. + /// On EVM chains it is the concatenation of the contract address and the token id `{contractAddress}:{tokenId}`. + pub address: Option, + /// The wallet address of the owner of the NFT. + pub owner: String, + /// The status of the NFT creation. + pub creation_status: CreationStatus, + /// The unique ID of the creator of the NFT. + pub created_by: Uuid, + /// The date and time when the NFT was created. + pub created_at: DateTimeWithTimeZone, + /// The transaction signature associated with the NFT. + pub signature: Option, + /// The unique edition number of the NFT. + pub edition: i64, + /// The seller fee basis points (ie royalties) for the NFT. + pub seller_fee_basis_points: i16, + /// credits deduction id + pub credits_deduction_id: Option, + /// Indicates if the NFT is compressed. Compression is only supported on Solana. + pub compressed: bool, +} + +#[ComplexObject] +impl CollectionMint { + /// The collection the NFT was minted from. + async fn collection(&self, ctx: &Context<'_>) -> Result> { + let AppContext { + collection_loader, .. + } = ctx.data::()?; + + collection_loader.load_one(self.collection_id).await + } + + /// The metadata json associated to the collection. + /// [Metaplex v1.1.0 Standard](https://docs.metaplex.com/programs/token-metadata/token-standard) + async fn metadata_json(&self, ctx: &Context<'_>) -> Result> { + let AppContext { + metadata_json_loader, + .. + } = ctx.data::()?; + let collection = self.collection(ctx).await?.ok_or(Error::new(format!( + "Collection not found for collection mint {:?}", + &self.id + )))?; + + match collection.blockchain { + Blockchain::Solana => metadata_json_loader.load_one(self.id).await, + Blockchain::Polygon => metadata_json_loader.load_one(self.collection_id).await, + Blockchain::Ethereum => Err(Error::new("Ethereum not supported")), + } + } + + /// The update history of the mint. + async fn update_histories( + &self, + ctx: &Context<'_>, + ) -> Result>> { + let AppContext { + update_mint_history_loader, + .. + } = ctx.data::()?; + + update_mint_history_loader.load_one(self.id).await + } + /// The creators of the mint. Includes the creator addresses and their shares. + async fn creators(&self, ctx: &Context<'_>) -> Result>> { + let AppContext { + mint_creators_loader, + .. + } = ctx.data::()?; + + mint_creators_loader.load_one(self.id).await + } + + /// The record of the original mint. + async fn mint_history(&self, ctx: &Context<'_>) -> Result> { + let AppContext { + collection_mint_mint_history_loader, + .. + } = ctx.data::()?; + + collection_mint_mint_history_loader.load_one(self.id).await + } + + /// The history of transfers for the mint. + async fn transfer_histories( + &self, + ctx: &Context<'_>, + ) -> Result>> { + let AppContext { + collection_mint_transfers_loader, + .. + } = ctx.data::()?; + + collection_mint_transfers_loader.load_one(self.id).await + } + + /// The history of switched collections for the mint. + async fn switch_collection_histories( + &self, + ctx: &Context<'_>, + ) -> Result>> { + let AppContext { + switch_collection_history_loader, + .. + } = ctx.data::()?; + + switch_collection_history_loader.load_one(self.id).await + } +} + +impl From for CollectionMint { + fn from( + Model { + id, + collection_id, + address, + owner, + creation_status, + created_by, + created_at, + signature, + edition, + seller_fee_basis_points, + credits_deduction_id, + compressed, + }: Model, + ) -> Self { + Self { + id, + collection_id, + address, + owner, + creation_status, + created_by, + created_at, + signature, + edition, + seller_fee_basis_points, + credits_deduction_id, + compressed, + } + } +} diff --git a/api/src/objects/customer.rs b/api/src/objects/customer.rs index bf44f0c..c5a59eb 100644 --- a/api/src/objects/customer.rs +++ b/api/src/objects/customer.rs @@ -1,10 +1,8 @@ use async_graphql::{ComplexObject, Context, Result, SimpleObject}; use hub_core::uuid::Uuid; -use crate::{ - entities::{collection_mints, mint_histories}, - AppContext, -}; +use super::CollectionMint; +use crate::{entities::mint_histories, AppContext}; /// A project customer. #[derive(SimpleObject, Debug, Clone)] @@ -21,10 +19,7 @@ pub struct Customer { impl Customer { /// The NFTs owned by any of the customers' wallets. #[graphql(requires = "addresses")] - async fn mints( - &self, - ctx: &Context<'_>, - ) -> Result>> { + async fn mints(&self, ctx: &Context<'_>) -> Result>> { let AppContext { collection_mints_owner_loader, .. diff --git a/api/src/objects/mod.rs b/api/src/objects/mod.rs index 242986e..380f69b 100644 --- a/api/src/objects/mod.rs +++ b/api/src/objects/mod.rs @@ -1,6 +1,7 @@ #![allow(clippy::unused_async)] mod collection; +mod collection_mint; mod creator; mod customer; mod drop; @@ -10,6 +11,7 @@ mod project; mod wallet; pub use collection::Collection; +pub use collection_mint::CollectionMint; pub use creator::Creator; pub use customer::Customer; pub use drop::Drop; diff --git a/api/src/objects/wallet.rs b/api/src/objects/wallet.rs index f52931e..b6293a7 100644 --- a/api/src/objects/wallet.rs +++ b/api/src/objects/wallet.rs @@ -1,6 +1,7 @@ use async_graphql::{ComplexObject, Context, Result, SimpleObject}; -use crate::{entities::collection_mints, AppContext}; +use super::CollectionMint; +use crate::AppContext; /// A blockchain wallet is a digital wallet that allows users to securely store, manage, and transfer their cryptocurrencies or other digital assets on a blockchain network. #[derive(SimpleObject, Debug, Clone)] @@ -14,10 +15,7 @@ pub struct Wallet { #[ComplexObject] impl Wallet { /// The NFTs that were minted from Holaplex and are owned by the wallet's address. - async fn mints( - &self, - ctx: &Context<'_>, - ) -> Result>> { + async fn mints(&self, ctx: &Context<'_>) -> Result>> { let AppContext { collection_mints_owner_loader, .. diff --git a/api/src/queries/mint.rs b/api/src/queries/mint.rs index 43cb649..cb0ec76 100644 --- a/api/src/queries/mint.rs +++ b/api/src/queries/mint.rs @@ -1,7 +1,7 @@ use async_graphql::{Context, Object, Result}; use hub_core::uuid::Uuid; -use crate::{entities::collection_mints::CollectionMint, AppContext}; +use crate::{objects::CollectionMint, AppContext}; #[derive(Debug, Clone, Copy, Default)] pub struct Query; From eb7d8a53046e14dc59a71b397b0af37e419c41a1 Mon Sep 17 00:00:00 2001 From: mpw Date: Mon, 28 Aug 2023 11:36:34 -0300 Subject: [PATCH 4/4] use nightly for clippy --- .github/workflows/cargo.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml index b70bd85..0101f95 100644 --- a/.github/workflows/cargo.yml +++ b/.github/workflows/cargo.yml @@ -38,7 +38,7 @@ jobs: - name: Install latest nightly uses: actions-rs/toolchain@v1 with: - toolchain: 1.71.0 + toolchain: nightly-2022-12-11 override: true components: rustfmt, clippy