diff --git a/.gitignore b/.gitignore index 4a858ff704..3c93d03dd5 100644 --- a/.gitignore +++ b/.gitignore @@ -107,4 +107,6 @@ artifacts rust/cw-contracts/*/target # cypress -cypress/screenshots \ No newline at end of file +cypress/screenshots +# secret files +rust/utils/mnemonic.ts \ No newline at end of file diff --git a/api/launchpad/v1/launchpad.proto b/api/launchpad/v1/launchpad.proto new file mode 100644 index 0000000000..4902e5eced --- /dev/null +++ b/api/launchpad/v1/launchpad.proto @@ -0,0 +1,166 @@ +syntax = "proto3"; + +package launchpad.v1; +option go_package = "./launchpadpb"; + +service LaunchpadService { + rpc UploadMetadatas(UploadMetadatasRequest) returns (UploadMetadatasResponse); + rpc CalculateCollectionMerkleRoot(CalculateCollectionMerkleRootRequest) returns (CalculateCollectionMerkleRootResponse); + rpc TokenMetadata(TokenMetadataRequest) returns (TokenMetadataResponse); + rpc LaunchpadProjectsByCreator(LaunchpadProjectsByCreatorRequest) returns (LaunchpadProjectsByCreatorResponse); + rpc LaunchpadProjects(LaunchpadProjectsRequest) returns (LaunchpadProjectsResponse); + rpc LaunchpadProjectById(LaunchpadProjectByIdRequest) returns (LaunchpadProjectByIdResponse); + rpc LaunchpadProjectsCounts(LaunchpadProjectsCountsRequest) returns (LaunchpadProjectsCountsResponse); + rpc ProposeApproveProject(ProposeApproveProjectRequest) returns (ProposeApproveProjectResponse); +} + +enum Sort { + SORT_UNSPECIFIED = 0; + SORT_COLLECTION_NAME = 1; +} + +enum SortDirection { + SORT_DIRECTION_UNSPECIFIED = 0; + SORT_DIRECTION_ASCENDING = 1; + SORT_DIRECTION_DESCENDING = 2; +} + +enum Status { + STATUS_UNSPECIFIED = 0; + STATUS_INCOMPLETE = 1; + STATUS_COMPLETE = 2; + STATUS_REVIEWING = 3; + STATUS_CONFIRMED = 4; +} + +// ------------------------------- + +message LaunchpadProjectsByCreatorRequest { + string creator_id = 1; + string network_id = 2; + int32 limit = 3; + int32 offset = 4; + Sort sort = 5; + SortDirection sort_direction = 6; + optional Status status = 7; +} + +message LaunchpadProjectsByCreatorResponse { + repeated LaunchpadProject projects = 1; +} + +message LaunchpadProjectsRequest { + string network_id = 1; + int32 limit = 2; + int32 offset = 3; + Sort sort = 4; + SortDirection sort_direction = 5; + optional Status status = 6; +} + +message LaunchpadProjectsResponse { + repeated LaunchpadProject projects = 1; +} + +message LaunchpadProjectByIdRequest { + string network_id = 1; + string project_id = 2; +} + +message LaunchpadProjectByIdResponse { + LaunchpadProject project = 1; +} + +message UploadMetadatasRequest { + string sender = 1; + string network_id = 2; + string project_id = 3; + repeated Metadata metadatas = 4; + optional string pinata_jwt = 5; +} + +message UploadMetadatasResponse { + string merkle_root = 1; +} + +message CalculateCollectionMerkleRootRequest { + string sender = 1; + repeated Metadata metadatas = 2; +} + +message CalculateCollectionMerkleRootResponse { + string merkle_root = 1; +} + +message TokenMetadataRequest { + string sender = 1; + string network_id = 2; + string project_id = 3; + uint32 token_id = 4; +} + +message TokenMetadataResponse { + string merkle_root = 1; + Metadata metadata = 2; + repeated string merkle_proof = 3; +} + +message LaunchpadProjectsCountsRequest { + string network_id = 1; +} + +message LaunchpadProjectsCountsResponse { + repeated StatusCount statusCounts = 1; +} + +message ProposeApproveProjectRequest { + string sender = 1; + string network_id = 2; + string project_id = 3; + string proposal_id = 4; +} + +message ProposeApproveProjectResponse { + bool approved = 1; +} + +// ------------------------------- + +message StatusCount { + Status status = 1; + uint32 count = 2; +} + +message LaunchpadProject { + string id = 1; + string network_id = 2; + string creator_id = 3; + string collection_data = 4; + Status status = 5; + optional string proposal_id = 6; +} + +message Metadata { + optional string image = 1; + optional string image_data = 2; + optional string external_url = 3; + optional string description = 4; + optional string name = 5; + repeated Trait attributes = 6; + optional string background_color = 7; + optional string animation_url = 8; + optional string youtube_url = 9; + optional uint64 royalty_percentage = 10; + optional string royalty_payment_address = 11; +} + +message Trait { + optional string display_type = 1; + string trait_type = 2; + string value = 3; +} + + + + + diff --git a/assets/icons/cross.svg b/assets/icons/cross.svg new file mode 100644 index 0000000000..90221c8183 --- /dev/null +++ b/assets/icons/cross.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/dot-more.svg b/assets/icons/dots-six.svg similarity index 100% rename from assets/icons/dot-more.svg rename to assets/icons/dots-six.svg diff --git a/assets/icons/warning.svg b/assets/icons/warning.svg index 85797a7a30..c254cf8e5f 100644 --- a/assets/icons/warning.svg +++ b/assets/icons/warning.svg @@ -1,5 +1,5 @@ - - - - - + + + + + diff --git a/buf.gen.yaml b/buf.gen.yaml index b45c8e779b..0f40600eb1 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -2,6 +2,8 @@ version: v1 plugins: - name: go out: go/pkg + - plugin: buf.build/community/neoeinstein-prost:v0.3.1 + out: rust/apipb/src - name: go-grpc out: go/pkg - name: ts diff --git a/build_release.sh b/build_release.sh new file mode 100755 index 0000000000..26a7c22aba --- /dev/null +++ b/build_release.sh @@ -0,0 +1,10 @@ +# Optimized builds +# docker run --rm -v "$(pwd)/rust":/code \ +# --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ +# --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ +# cosmwasm/workspace-optimizer-arm64:0.15.0 + +docker run --rm -v "$(pwd)":/code \ + --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + cosmwasm/rust-optimizer-arm64:0.15.0 ./rust/cw-contracts/nft-launchpad \ No newline at end of file diff --git a/go.mod b/go.mod index 29c74a0ac7..836f7ecea6 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/go-co-op/gocron v1.18.0 github.com/gorilla/websocket v1.5.0 github.com/improbable-eng/grpc-web v0.15.0 + github.com/ipfs/boxo v0.8.0 github.com/jackc/pgx/v5 v5.3.0 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.7 @@ -168,9 +169,9 @@ require ( github.com/holiman/uint256 v1.2.2-0.20230321075855-87b91420868c // indirect github.com/huandu/skiplist v1.2.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/ipfs/boxo v0.8.0 // indirect github.com/ipfs/go-cid v0.4.1 // indirect github.com/ipfs/go-ipfs-api v0.6.0 // indirect + github.com/ipfs/go-log/v2 v2.5.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jhump/protoreflect v1.15.1 // indirect diff --git a/go.sum b/go.sum index 48f769225e..8be6cc23a7 100644 --- a/go.sum +++ b/go.sum @@ -852,6 +852,8 @@ github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= github.com/ipfs/go-ipfs-api v0.6.0 h1:JARgG0VTbjyVhO5ZfesnbXv9wTcMvoKRBLF1SzJqzmg= github.com/ipfs/go-ipfs-api v0.6.0/go.mod h1:iDC2VMwN9LUpQV/GzEeZ2zNqd8NUdRmWcFM+K/6odf0= +github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= +github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= diff --git a/go/cmd/check-pin/main.go b/go/cmd/check-pin/main.go new file mode 100644 index 0000000000..1bd43614c0 --- /dev/null +++ b/go/cmd/check-pin/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "context" + "fmt" + + remoteClient "github.com/ipfs/boxo/pinning/remote/client" +) + +func main() { + // url := "https://nft.storage" + url := "https://api.pinata.cloud/psa" + // bearerToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6ZXRocjoweDljQzdjNTI2QUMyOGQ0MDZFOTk3Zjg5RjVEODY1NDkzYzQyZjhFMzYiLCJpc3MiOiJuZnQtc3RvcmFnZSIsImlhdCI6MTcxMzg1NTY3Mjc4NiwibmFtZSI6InBpbm5pbmcifQ.yGO_AZl91Ail71u0AdNoXE7G9NmEnxQ_vjq8uh4Pxn0" + bearerToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySW5mb3JtYXRpb24iOnsiaWQiOiIwMjJlZjcyOC0xMGJjLTQ2ZmMtYjI1NC02M2Y2MWM0N2YyZjAiLCJlbWFpbCI6InRyb25naGlldS5oYUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwicGluX3BvbGljeSI6eyJyZWdpb25zIjpbeyJpZCI6IkZSQTEiLCJkZXNpcmVkUmVwbGljYXRpb25Db3VudCI6MX1dLCJ2ZXJzaW9uIjoxfSwibWZhX2VuYWJsZWQiOmZhbHNlLCJzdGF0dXMiOiJBQ1RJVkUifSwiYXV0aGVudGljYXRpb25UeXBlIjoic2NvcGVkS2V5Iiwic2NvcGVkS2V5S2V5IjoiMTgwNDY2ZTA5YjE0ZjBiODg1M2YiLCJzY29wZWRLZXlTZWNyZXQiOiI1NGEwMzRiNzJiMzk0NzM2MzQ1NTZlY2I5OGQzNmI4Y2Q2NmM2ZTM2MWNhM2I4MDVhZTMyZGJlYTQwZTRlZDY1IiwiaWF0IjoxNzEzODYzMzc5fQ.tuSSi9dSjSO2IOTlNsU6kgWxSMIG5N6mlYzC3VuSkTA" + + client := remoteClient.NewClient(url, bearerToken) + ctx := context.Background() + + data, err := client.LsSync(ctx) + + // myCid, err := cid.Decode("Qmf9eHWdSY3v2cwm4ckTcVj59pXV7QSiU6zfeUwXnNx1Me") + // fmt.Println("myCid:", myCid, " - err:", err) + + // status, err := client.Add(ctx, myCid) + // fmt.Println("status:", status, " - err:", err) + + fmt.Println(err) + fmt.Println(data) +} diff --git a/go/cmd/teritori-launchpad-backend/main.go b/go/cmd/teritori-launchpad-backend/main.go new file mode 100644 index 0000000000..f433f6a5bf --- /dev/null +++ b/go/cmd/teritori-launchpad-backend/main.go @@ -0,0 +1,146 @@ +package main + +import ( + "context" + "flag" + "fmt" + "net" + "net/http" + "os" + + "github.com/TERITORI/teritori-dapp/go/internal/indexerdb" + "github.com/TERITORI/teritori-dapp/go/pkg/launchpad" + "github.com/TERITORI/teritori-dapp/go/pkg/launchpadpb" + "github.com/TERITORI/teritori-dapp/go/pkg/networks" + "github.com/improbable-eng/grpc-web/go/grpcweb" + "github.com/peterbourgon/ff/v3" + "github.com/pkg/errors" + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +// FIXME: for now, I dont find how to add reflection on grpc wrapped by http server +// so to enable Postman reflection, I'm using this DEBUG const to switch on/off that capacity +const DEBUG = false + +func main() { + fs := flag.NewFlagSet("teritori-dapp-backend", flag.ContinueOnError) + var ( + enableTls = flag.Bool("enable_tls", false, "Use TLS - required for HTTP2.") + tlsCertFilePath = flag.String("tls_cert_file", "../../misc/localhost.crt", "Path to the CRT/PEM file.") + tlsKeyFilePath = flag.String("tls_key_file", "../../misc/localhost.key", "Path to the private key file.") + dbHost = fs.String("db-indexer-host", "", "host postgreSQL database") + dbPort = fs.String("db-indexer-port", "", "port for postgreSQL database") + dbPass = fs.String("postgres-password", "", "password for postgreSQL database") + dbName = fs.String("database-name", "", "database name for postgreSQL") + dbUser = fs.String("postgres-user", "", "username for postgreSQL") + networksFile = fs.String("networks-file", "networks.json", "path to networks config file") + pinataJWT = fs.String("pinata-jwt", "", "Pinata admin JWT token") + ) + if err := ff.Parse(fs, os.Args[1:], + ff.WithEnvVars(), + ff.WithIgnoreUndefined(true), + ff.WithConfigFile(".env"), + ff.WithConfigFileParser(ff.EnvParser), + ff.WithAllowMissingConfigFile(true), + ); err != nil { + panic(errors.Wrap(err, "failed to parse flags")) + } + + logger, err := zap.NewDevelopment() + if err != nil { + panic(errors.Wrap(err, "failed to create logger")) + } + + if *pinataJWT == "" { + logger.Warn("missing PINATA_JWT, feed pinning will be disabled") + } + + // load networks + networksBytes, err := os.ReadFile(*networksFile) + if err != nil { + panic(errors.Wrap(err, "failed to read networks config file")) + } + netstore, err := networks.UnmarshalNetworkStore(networksBytes) + if err != nil { + panic(errors.Wrap(err, "failed to unmarshal networks config")) + } + + var launchpadModels = []interface{}{ + // users + &indexerdb.User{}, + + // launchpad + &LaunchpadProject{}, + &LaunchpadToken{}, + } + + dataConnexion := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s", + *dbHost, *dbUser, *dbPass, *dbName, *dbPort) + launchpadDB, err := indexerdb.NewPostgresDB(dataConnexion) + + if err != nil { + panic(errors.Wrap(err, "failed to access db")) + } + launchpadDB.AutoMigrate(launchpadModels...) + + port := 9080 + if *enableTls { + port = 9081 + } + + launchpadSvc := launchpad.NewLaunchpadService(context.Background(), &launchpad.Config{ + Logger: logger, + IndexerDB: launchpadDB, + PinataJWT: *pinataJWT, + NetworkStore: netstore, + }) + + server := grpc.NewServer() + launchpadpb.RegisterLaunchpadServiceServer(server, launchpadSvc) + + if DEBUG { + lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + if err != nil { + panic(errors.Wrapf(err, "[DEBUG] failed to listen on port %d", port)) + } + + reflection.Register(server) + + logger.Info(fmt.Sprintf("[DEBUG] gRPC server listening at: %s", lis.Addr().String())) + if err := server.Serve(lis); err != nil { + panic(errors.Errorf("failed to serve: %v", err)) + } + } + + wrappedServer := grpcweb.WrapServer(server, + grpcweb.WithWebsockets(true), + grpcweb.WithWebsocketOriginFunc(func(*http.Request) bool { return true })) + + handler := func(resp http.ResponseWriter, req *http.Request) { + resp.Header().Set("Access-Control-Allow-Origin", "*") + resp.Header().Set("Access-Control-Allow-Headers", "*") + logger.Debug(fmt.Sprintf("Request: %v", req)) + wrappedServer.ServeHTTP(resp, req) + } + + httpServer := http.Server{ + Addr: fmt.Sprintf(":%d", port), + Handler: http.HandlerFunc(handler), + } + + reflection.Register(server) + + logger.Info(fmt.Sprintf("Starting server. http port: %d, with TLS: %v", port, *enableTls)) + + if *enableTls { + if err := httpServer.ListenAndServeTLS(*tlsCertFilePath, *tlsKeyFilePath); err != nil { + panic(fmt.Errorf("failed starting http2 server: %v", err)) + } + } else { + if err := httpServer.ListenAndServe(); err != nil { + panic(fmt.Errorf("failed starting http server: %v", err)) + } + } +} diff --git a/go/cmd/teritori-launchpad-backend/models.go b/go/cmd/teritori-launchpad-backend/models.go new file mode 100644 index 0000000000..e825afe634 --- /dev/null +++ b/go/cmd/teritori-launchpad-backend/models.go @@ -0,0 +1,22 @@ +package main + +import ( + "github.com/TERITORI/teritori-dapp/go/pkg/launchpadpb" + "gorm.io/datatypes" +) + +type LaunchpadProject struct { + NetworkID string `gorm:"primaryKey"` + ProjectID uint32 `gorm:"primaryKey"` + + Status launchpadpb.Status + ProposalId string +} + +type LaunchpadToken struct { + NetworkID string `gorm:"primaryKey"` + ProjectID uint32 `gorm:"primaryKey"` + TokenID uint32 `gorm:"primaryKey"` + + Metadata datatypes.JSON +} diff --git a/go/internal/indexerdb/db.go b/go/internal/indexerdb/db.go index ef006a9ecd..cf7a5f9691 100644 --- a/go/internal/indexerdb/db.go +++ b/go/internal/indexerdb/db.go @@ -90,6 +90,10 @@ var allModels = []interface{}{ // names &Name{}, + + // launchpad + &LaunchpadProject{}, + &LaunchpadToken{}, } func NewSQLiteDB(path string) (*gorm.DB, error) { diff --git a/go/internal/indexerdb/launchpad.go b/go/internal/indexerdb/launchpad.go new file mode 100644 index 0000000000..e49e9ba5db --- /dev/null +++ b/go/internal/indexerdb/launchpad.go @@ -0,0 +1,26 @@ +package indexerdb + +import ( + "github.com/TERITORI/teritori-dapp/go/pkg/launchpadpb" + "github.com/TERITORI/teritori-dapp/go/pkg/networks" + "gorm.io/datatypes" +) + +type LaunchpadProject struct { + NetworkID string `gorm:"primaryKey"` + ProjectID string `gorm:"primaryKey"` + CreatorID networks.UserID `gorm:"index"` + + Status launchpadpb.Status + ProposalId string + + CollectionData datatypes.JSON +} + +type LaunchpadToken struct { + NetworkID string `gorm:"primaryKey"` + ProjectID string `gorm:"primaryKey"` + TokenID uint32 `gorm:"primaryKey"` + + Metadata datatypes.JSON +} diff --git a/go/internal/indexerhandler/dao.go b/go/internal/indexerhandler/dao.go index 7a9fedbcc9..f790a1e0fc 100644 --- a/go/internal/indexerhandler/dao.go +++ b/go/internal/indexerhandler/dao.go @@ -268,6 +268,8 @@ func (h *Handler) handleExecuteDAOExecute(e *Message, execMsg *wasmtypes.MsgExec return h.handleExecuteUpdateTNSMetadata(e, syntheticExecMsg) case "create_post": return h.handleExecuteCreatePost(e, syntheticExecMsg) + case "deploy_collection": + return h.handleExecuteDeployCollection(e, syntheticExecMsg) } h.logger.Debug("ignored dao execute sub message with unknown action", zap.String("action", action), zap.String("payload", string(string(subExecMsg.Execute.Msg))), zap.String("tx", e.TxHash), zap.String("dao", dao.ContractAddress), zap.Uint64("proposal-id", daoExecuteMsg.Execute.ProposalID)) diff --git a/go/internal/indexerhandler/handle.go b/go/internal/indexerhandler/handle.go index 69bb7129b9..0394e436c1 100644 --- a/go/internal/indexerhandler/handle.go +++ b/go/internal/indexerhandler/handle.go @@ -335,9 +335,21 @@ func (h *Handler) handleExecute(e *Message) error { if err := h.handleExecutePremiumFeedSubscribe(e, &executeMsg); err != nil { return errors.Wrap(err, "failed to handle premium feed subscribe") } + // Launchpad + case "submit_collection": + if err := h.handleExecuteSubmitCollection(e, &executeMsg); err != nil { + return errors.Wrap(err, "failed to handle submit collection") + } + case "update_merkle_root": + if err := h.handleExecuteUpdateMerkleRoot(e, &executeMsg); err != nil { + return errors.Wrap(err, "failed to handle update merkle root") + } + case "deploy_collection": + if err := h.handleExecuteDeployCollection(e, &executeMsg); err != nil { + return errors.Wrap(err, "failed to handle deploy collection") + } } } - return nil } @@ -378,3 +390,4 @@ func (h *Handler) handleExecuteMint(e *Message, execMsg *wasmtypes.MsgExecuteCon return nil } + diff --git a/go/internal/indexerhandler/launchpad.go b/go/internal/indexerhandler/launchpad.go new file mode 100644 index 0000000000..4c789ffb1b --- /dev/null +++ b/go/internal/indexerhandler/launchpad.go @@ -0,0 +1,197 @@ +package indexerhandler + +import ( + "encoding/json" + + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + "github.com/TERITORI/teritori-dapp/go/internal/indexerdb" + "github.com/TERITORI/teritori-dapp/go/pkg/launchpadpb" + "github.com/friendsofgo/errors" + "go.uber.org/zap" +) + +type SubmitCollectionMsg struct { + SubmitCollection struct { + Collection struct { + Name string `json:"name"` + Desc string `json:"desc"` + } `json:"collection"` + } `json:"submit_collection"` +} + +func (h *Handler) handleExecuteSubmitCollection(e *Message, execMsg *wasmtypes.MsgExecuteContract) error { + var jsonData map[string]map[string]interface{} + if err := json.Unmarshal(execMsg.Msg.Bytes(), &jsonData); err != nil { + return errors.Wrap(err, "failed to unmarshal json") + } + + collectionData := jsonData["submit_collection"]["collection"] + if collectionData == nil { + return errors.New("failed to get collection data") + } + + collectionId := e.Events["wasm.collection_id"][0] + if collectionId == "" { + return errors.New("failed to get collection id") + } + + collectionDataJson, err := json.Marshal(collectionData) + if err != nil { + return errors.Wrap(err, "failed to marshal collection data") + } + + project := indexerdb.LaunchpadProject{ + NetworkID: h.config.Network.ID, + ProjectID: collectionId, + CollectionData: collectionDataJson, + CreatorID: h.config.Network.UserID(execMsg.Sender), + Status: launchpadpb.Status_STATUS_INCOMPLETE, + } + if err := h.db.Create(project).Error; err != nil { + return errors.Wrap(err, "failed to create project") + } + + h.logger.Info("submited project", zap.Any("symbol", collectionId)) + + return nil +} + +func (h *Handler) handleExecuteUpdateMerkleRoot(e *Message, execMsg *wasmtypes.MsgExecuteContract) error { + var jsonData map[string]map[string]interface{} + if err := json.Unmarshal(execMsg.Msg.Bytes(), &jsonData); err != nil { + return errors.Wrap(err, "failed to unmarshal json") + } + + collectionId := jsonData["update_merkle_root"]["collection_id"] + if collectionId == "" { + return errors.New("failed to get collection id") + } + + merkleRoot := jsonData["update_merkle_root"]["merkle_root"] + if merkleRoot == "" { + return errors.New("failed to get merkle root") + } + + // Update collection_data + if err := h.db.Exec(` + UPDATE launchpad_projects + SET collection_data = jsonb_set(collection_data, '{metadatas_merkle_root}', to_jsonb(?::text)) + WHERE project_id = ? AND network_id = ?`, + merkleRoot, collectionId, h.config.Network.ID, + ).Error; err != nil { + return errors.Wrap(err, "failed to update deployed address in collection_data") + } + + // Update status + if err := + h.db. + Model(&indexerdb.LaunchpadProject{}). + Where("project_id = ?", collectionId). + Where("network_id = ?", h.config.Network.ID). + UpdateColumn("status", launchpadpb.Status_STATUS_COMPLETE). + Error; err != nil { + return errors.Wrap(err, "failed to update project status to COMPLETE") + } + + h.logger.Info("updated merkle root", zap.Any("merkleRoot", merkleRoot)) + + return nil +} + +func (h *Handler) handleExecuteDeployCollection(e *Message, execMsg *wasmtypes.MsgExecuteContract) error { + var jsonData map[string]map[string]interface{} + if err := json.Unmarshal(execMsg.Msg.Bytes(), &jsonData); err != nil { + return errors.Wrap(err, "failed to unmarshal json") + } + collectionId, ok := jsonData["deploy_collection"]["collection_id"].(string) + if !ok || collectionId == "" { + return errors.New("failed to get collection id or collection id is not a string") + } + + deployedAddress := e.Events["instantiate._contract_address"][0] + if deployedAddress == "" { + return errors.New("failed to get deployed address from reply") + } + + // Update collection_data + if err := h.db.Exec(` + UPDATE launchpad_projects + SET collection_data = jsonb_set(collection_data, '{deployed_address}', to_jsonb(?::text)) + WHERE project_id = ? AND network_id = ?`, + deployedAddress, collectionId, h.config.Network.ID, + ).Error; err != nil { + return errors.Wrap(err, "failed to update deployed address in collection_data") + } + + // Update status + if err := h.db. + Model(&indexerdb.LaunchpadProject{}). + Where("project_id = ?", collectionId). + Where("network_id = ?", h.config.Network.ID). + UpdateColumn("status", launchpadpb.Status_STATUS_CONFIRMED). + Error; err != nil { + return errors.Wrap(err, "failed to update project status to CONFIRMED") + } + + var project indexerdb.LaunchpadProject + if err := h.db.Model(&indexerdb.LaunchpadProject{}). + Where("project_id = ?", collectionId). + Where("network_id = ?", h.config.Network.ID). + Scan(&project).Error; err != nil { + return errors.Wrap(err, "failed to get launchpad_project from database") + } + collectionData := project.CollectionData + + // Create usable collection + // TODO: Use a defined shape for collection_data ? + var collectionDataJSON map[string]json.RawMessage + if err := json.Unmarshal(collectionData, &collectionDataJSON); err != nil { + return errors.Wrap(err, "failed to unmarhsal collection_data") + } + var name string + if raw, ok := collectionDataJSON["name"]; ok { + if err := json.Unmarshal(raw, &name); err != nil { + return errors.Wrap(err, "failed to unmarshal name") + } + } + var coverImgUri string + if raw, ok := collectionDataJSON["cover_img_uri"]; ok { + if err := json.Unmarshal(raw, &coverImgUri); err != nil { + return errors.Wrap(err, "failed to unmarshal cover_img_uri") + } + } + var tokensCount uint64 + if raw, ok := collectionDataJSON["tokens_count"]; ok { + if err := json.Unmarshal(raw, &tokensCount); err != nil { + return errors.Wrap(err, "failed to unmarshal tokens_count") + } + } + + blockTime, err := e.GetBlockTime() + if err != nil { + return errors.Wrap(err, "failed to get block time") + } + + networkCollectionId := h.config.Network.CollectionID(deployedAddress) + if err := h.db.Create(&indexerdb.Collection{ + ID: networkCollectionId, + NetworkID: h.config.Network.ID, + Name: name, + ImageURI: coverImgUri, + MaxSupply: int(tokensCount), + SecondaryDuringMint: true, + Time: blockTime, + TeritoriCollection: &indexerdb.TeritoriCollection{ + NetworkID: h.config.Network.ID, + MintContractAddress: deployedAddress, + NFTContractAddress: deployedAddress, + CreatorAddress: execMsg.Sender, + }, + }).Error; err != nil { + return errors.Wrap(err, "failed to create teritori collection") + } + + h.logger.Info("created teritori collection", zap.String("id", string(networkCollectionId))) + + return nil +} diff --git a/go/pkg/launchpad/helpers.go b/go/pkg/launchpad/helpers.go new file mode 100644 index 0000000000..504985fe6b --- /dev/null +++ b/go/pkg/launchpad/helpers.go @@ -0,0 +1,54 @@ +package launchpad + +import ( + "github.com/TERITORI/teritori-dapp/go/pkg/launchpadpb" + "github.com/TERITORI/teritori-dapp/go/pkg/merkletree" + "github.com/pkg/errors" +) + +func (s *Launchpad) getCollectionMerkleTree(collectionID string) (*merkletree.MerkleTree, error) { + return nil, nil +} + +func (s *Launchpad) buildCollectionMerkleTree(metadatas []*launchpadpb.Metadata) (*merkletree.MerkleTree, error) { + // Create merkle tree + leaves := make([]merkletree.Content, len(metadatas)) + + i := 0 + for _, data := range metadatas { + leaves[i] = NewMetadataFromPb(data) + i++ + } + + // NOTE: Don't sort leaves to keep the same order of uploaded file + tree, err := merkletree.New(leaves) + if err != nil { + return nil, errors.Wrap(err, "failed to created merkle tree") + } + + return tree, nil +} + +func (s *Launchpad) buildWhitelistMerkleTree(addresses []string) (*merkletree.MerkleTree, error) { + // Create merkle tree + leaves := make([]merkletree.Content, len(addresses)) + + i := 0 + for _, addr := range addresses { + leaves[i] = NewWhitelistAddress(addr) + i++ + } + + // NOTE: Don't sort leaves to keep the same order of uploaded file + tree, err := merkletree.New(leaves) + if err != nil { + return nil, errors.Wrap(err, "failed to created merkle tree") + } + + return tree, nil +} + +// Verify if sender is allowed to get Token metadata +func (s *Launchpad) verifySender(sender string) error { + return nil +} diff --git a/go/pkg/launchpad/ipfs.go b/go/pkg/launchpad/ipfs.go new file mode 100644 index 0000000000..c152d91b43 --- /dev/null +++ b/go/pkg/launchpad/ipfs.go @@ -0,0 +1,94 @@ +package launchpad + +import ( + "context" + "fmt" + "strings" + + "github.com/pkg/errors" + "golang.org/x/exp/slices" + + remoteClient "github.com/ipfs/boxo/pinning/remote/client" + "github.com/ipfs/go-cid" +) + +type Provider string + +const ( + Pinata Provider = "pinata" +) + +type PinningService struct { + url string + bearerToken string + client *remoteClient.Client +} + +func NewPinningService(provider Provider, bearerToken string) (*PinningService, error) { + var url string + + switch provider { + case Pinata: + url = "https://api.pinata.cloud/psa" + default: + return nil, errors.New(fmt.Sprintf("unknown provider: %s", provider)) + } + + client := remoteClient.NewClient(url, bearerToken) + + return &PinningService{ + url, + bearerToken, + client, + }, nil +} + +func (ps *PinningService) VerifyPinned(cidStrs ...string) (bool, error) { + ctx := context.Background() + cids := make([]cid.Cid, len(cidStrs)) + for idx, cidStr := range cidStrs { + cidObj, err := cid.Decode(cidStr) + if err != nil { + return false, errors.Wrap(err, fmt.Sprintf("fail to parse cid: %s", cidStr)) + } + // LsSync doesn't work with CIDv1, so we convert to CIDv0 to use LsSync + cidv0 := cid.NewCidV0(cidObj.Hash()) + cids[idx] = cidv0 + } + + pinnedItems, err := ps.client.LsSync( + ctx, + remoteClient.PinOpts.FilterCIDs(cids...), + remoteClient.PinOpts.FilterStatus(remoteClient.StatusPinned), + ) + + if err != nil { + return false, errors.Wrap(err, "error while fetching pinned items") + } + if len(pinnedItems) == 0 { + return false, errors.New("no pinned items fetched") + } + + pinnedStrs := make([]string, len(pinnedItems)) + for idx, item := range pinnedItems { + if string(item.GetStatus()) == remoteClient.StatusPinned.String() { + // We convert back to CIDv1 + cidv1 := cid.NewCidV1(0x70, item.GetPin().GetCid().Hash()) + pinnedStrs[idx] = cidv1.String() + } + } + var unpinnedCids []string + + // Veryfing given CIDs and pinned ones + for _, cidStr := range cidStrs { + if !slices.Contains(pinnedStrs, cidStr) { + unpinnedCids = append(unpinnedCids, cidStr) + } + } + + if len(unpinnedCids) != 0 { + return false, errors.New(fmt.Sprintf("there exists some unpinned items: %s", strings.Join(unpinnedCids, ","))) + } + + return true, nil +} diff --git a/go/pkg/launchpad/service.go b/go/pkg/launchpad/service.go new file mode 100644 index 0000000000..96b1ee1e73 --- /dev/null +++ b/go/pkg/launchpad/service.go @@ -0,0 +1,454 @@ +package launchpad + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + + "github.com/TERITORI/teritori-dapp/go/internal/indexerdb" + "github.com/TERITORI/teritori-dapp/go/pkg/launchpadpb" + "github.com/TERITORI/teritori-dapp/go/pkg/networks" + "github.com/pkg/errors" + "go.uber.org/zap" + "golang.org/x/exp/slices" + "gorm.io/datatypes" + "gorm.io/gorm" +) + +type Launchpad struct { + launchpadpb.UnimplementedLaunchpadServiceServer + conf *Config +} + +type Config struct { + Logger *zap.Logger + IndexerDB *gorm.DB + PinataJWT string + NetworkStore networks.NetworkStore +} + +func NewLaunchpadService(ctx context.Context, conf *Config) launchpadpb.LaunchpadServiceServer { + // FIXME: validate config + return &Launchpad{ + conf: conf, + } +} + +// ================================ + +// IMPORTANT !!! TODO !!! +// For now, for simplicity, we upload images to ipfs from client side then this backend will +// only check if images have been pinnned correctly. +// +// This approche has 1 downside: +// User can query ipfs node to get all existing images of collection (AI to detect similar images for example) +// then guest what are the remainning NFT then decide if it's interesting to mint. +// +// For now it's not a big problem but in the future, the more secure solution will be handling image upload by backend. +// We dont do it for now yet because grpc-web does not support bidi stream so handle client send image => backend => ipfs properly can take time + +// Upload collection metadatas and generate corresponding merkle root +// This will delete all existing tokens metadatas and replace by new ones +func (s *Launchpad) UploadMetadatas(ctx context.Context, req *launchpadpb.UploadMetadatasRequest) (*launchpadpb.UploadMetadatasResponse, error) { + if err := s.verifySender(req.Sender); err != nil { + return nil, errors.Wrap(err, "failed to verify sender") + } + + // Check if client sent pinata jwt along with the payload + pinataJwt := req.GetPinataJwt() + if pinataJwt == "" { + // If pinataJwt is not sent then try to use the system key + pinataJwt = s.conf.PinataJWT + + // If system does not have JWT then throw error + if pinataJwt == "" { + return nil, errors.New("JWT key is required for this endpoint") + } + } + + pinnedCIDs := []string{} + for _, metadataItem := range req.Metadatas { + pinnedCIDs = append(pinnedCIDs, extractCID(*metadataItem.Image)) + } + + // Check pinning + // pinningSrv, err := NewPinningService(Pinata, pinataJwt) + // if err != nil { + // return nil, errors.Wrap(err, "failed to get pinning service") + // } + + // if _, err := pinningSrv.VerifyPinned(pinnedCIDs...); err != nil { + // return nil, errors.Wrap(err, "failed to verify pinned images") + // } + + // Check if all files have been pinned correctly + for _, metadataItem := range req.Metadatas { + if !slices.Contains(pinnedCIDs, extractCID(*metadataItem.Image)) { + return nil, errors.New(fmt.Sprintf("image %s has not been pinned correctly", *metadataItem.Image)) + } + } + + // We search for the LaunchpadProject + // It has been created by indexer when collection has been submitted on-chain + project := indexerdb.LaunchpadProject{ + ProjectID: req.ProjectId, + NetworkID: req.NetworkId, + } + + if err := s.conf.IndexerDB.First(&project).Error; err != nil { + return nil, errors.Wrap(err, "failed to get the requested project") + } + + var hex_root string + + if err := s.conf.IndexerDB.Transaction(func(tx *gorm.DB) error { + // Delete all existing tokens metadatas + if err := s.conf.IndexerDB.Delete(&indexerdb.LaunchpadToken{}, "network_id = ? AND project_id = ?", req.NetworkId, req.ProjectId).Error; err != nil { + return errors.Wrap(err, "failed to flush existing metatadas") + } + + tokenMetadatas := []indexerdb.LaunchpadToken{} + + for idx, metadata := range req.Metadatas { + metadataJson, err := json.Marshal(metadata) + if err != nil { + return errors.Wrap(err, "failed to marshal metadata to json") + } + + tokenMetadatas = append(tokenMetadatas, indexerdb.LaunchpadToken{ + ProjectID: req.ProjectId, + NetworkID: req.NetworkId, + TokenID: uint32(idx) + 1, + Metadata: datatypes.JSON([]byte(metadataJson)), + }) + } + + if err := s.conf.IndexerDB.Create(tokenMetadatas).Error; err != nil { + return errors.Wrap(err, "failed to save token metadatas") + } + + tree, err := s.buildCollectionMerkleTree(req.Metadatas) + if err != nil { + return errors.Wrap(err, "failed to calculate merkle root") + } + hex_root = tree.GetHexRootWithoutPrefix() + + if err := s.conf.IndexerDB.Save(&project).Error; err != nil { + return errors.Wrap(err, "failed to update project merkle root") + } + + return nil + }); err != nil { + return nil, errors.Wrap(err, "fail to process") + } + + return &launchpadpb.UploadMetadatasResponse{MerkleRoot: hex_root}, nil +} + +// Store the proposal made by the admin DAO at the first "Approve" +func (s *Launchpad) ProposeApproveProject(ctx context.Context, req *launchpadpb.ProposeApproveProjectRequest) (*launchpadpb.ProposeApproveProjectResponse, error) { + if err := s.verifySender(req.Sender); err != nil { + return nil, errors.Wrap(err, "failed to verify sender") + } + + updates := map[string]interface{}{ + "proposal_id": req.ProposalId, + "status": launchpadpb.Status_STATUS_REVIEWING, + } + + if err := + s.conf.IndexerDB. + Model(&indexerdb.LaunchpadProject{}). + Where("project_id = ?", req.ProjectId). + Where("network_id = ?", req.NetworkId). + UpdateColumns(updates). + Error; err != nil { + return nil, errors.Wrap(err, "failed to update propsal id and project status to REVIEWING") + } + + return &launchpadpb.ProposeApproveProjectResponse{ + Approved: true, + }, nil +} + +// ================================ + +// Calculate collection merkle root +func (s *Launchpad) CalculateCollectionMerkleRoot(ctx context.Context, req *launchpadpb.CalculateCollectionMerkleRootRequest) (*launchpadpb.CalculateCollectionMerkleRootResponse, error) { + if err := s.verifySender(req.Sender); err != nil { + return nil, errors.Wrap(err, "failed to verify sender") + } + + tree, err := s.buildCollectionMerkleTree(req.Metadatas) + if err != nil { + return nil, errors.Wrap(err, "failed to calculate merkle root") + } + // Remove 0x at first position because rust does not have that 0x + hex_root := tree.GetHexRootWithoutPrefix() + + return &launchpadpb.CalculateCollectionMerkleRootResponse{MerkleRoot: hex_root}, nil +} + +// Get token metadata, merkle root, merke proof to be used for claiming the on-chain token +func (s *Launchpad) TokenMetadata(ctx context.Context, req *launchpadpb.TokenMetadataRequest) (*launchpadpb.TokenMetadataResponse, error) { + if err := s.verifySender(req.Sender); err != nil { + return nil, errors.Wrap(err, "failed to verify sender") + } + + var tokens []indexerdb.LaunchpadToken + if err := s.conf.IndexerDB.Find(&tokens, "network_id = ? AND project_id = ?", req.NetworkId, req.ProjectId).Error; err != nil { + return nil, errors.Wrap(err, "failed to collection tokens to build merkle tree") + } + + var metadatas []*launchpadpb.Metadata + var tokenMetadata *launchpadpb.Metadata + + for id, token := range tokens { + var metadata launchpadpb.Metadata + if err := json.Unmarshal(token.Metadata, &metadata); err != nil { + return nil, errors.Wrap(err, "failed to parse metadata") + } + metadatas = append(metadatas, &metadata) + + if id == int(req.TokenId) { + tokenMetadata = &metadata + } + } + + if tokenMetadata.Name == nil { + return nil, errors.New("failed to get metadata for given token") + } + + tree, err := s.buildCollectionMerkleTree(metadatas) + if err != nil { + return nil, errors.Wrap(err, "failed to build merke tree") + } + + proof, err := tree.GetHexProofWithoutPrefix(NewMetadataFromPb(tokenMetadata)) + if err != nil { + return nil, errors.Wrap(err, "failed to get proof") + } + + return &launchpadpb.TokenMetadataResponse{ + Metadata: tokenMetadata, + MerkleRoot: tree.GetHexRootWithoutPrefix(), + MerkleProof: proof, + }, nil +} + +// Get launchpad projects by creator_id +func (s *Launchpad) LaunchpadProjectsByCreator(ctx context.Context, req *launchpadpb.LaunchpadProjectsByCreatorRequest) (*launchpadpb.LaunchpadProjectsByCreatorResponse, error) { + limit := req.GetLimit() + if limit <= 0 { + return nil, errors.New("limit must be a positive number") + } + + // TODO: Handle offset for pagination + offset := req.GetOffset() + if offset < 0 { + return nil, errors.New("offset must be greater or equal to 0") + } + + networkID := req.GetNetworkId() + if networkID == "" { + return nil, errors.New("missing network id") + } + _, err := s.conf.NetworkStore.GetNetwork(networkID) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("unknown network id '%s'", networkID)) + } + + creatorID := req.GetCreatorId() + if creatorID == "" { + return nil, errors.New("creatorID is mandatory") + } + + status := req.GetStatus() + if status < 0 { + return nil, errors.New("invalid status") + } + + var projects []indexerdb.LaunchpadProject + + orderDirection := "" + switch req.GetSortDirection() { + case launchpadpb.SortDirection_SORT_DIRECTION_UNSPECIFIED: + orderDirection = "" + case launchpadpb.SortDirection_SORT_DIRECTION_ASCENDING: + orderDirection = " ASC " + case launchpadpb.SortDirection_SORT_DIRECTION_DESCENDING: + orderDirection = " DESC " + } + orderSQL := "" + switch req.GetSort() { + case launchpadpb.Sort_SORT_COLLECTION_NAME: + orderSQL = " ORDER BY lp.collection_data->>'name'" + orderDirection + case launchpadpb.Sort_SORT_UNSPECIFIED: + orderSQL = "" + } + + statusFilterSQL := "AND lp.status = " + strconv.FormatInt(int64(status.Number()), 10) + if status == launchpadpb.Status_STATUS_UNSPECIFIED { + statusFilterSQL = "" + } + + err = s.conf.IndexerDB.Raw(fmt.Sprintf(` + SELECT * FROM launchpad_projects AS lp WHERE lp.creator_id = ? AND lp.network_id = ? %s %s LIMIT ? + `, + statusFilterSQL, orderSQL), creatorID, networkID, limit).Scan(&projects).Error + if err != nil { + return nil, errors.Wrap(err, "failed to query database") + } + + result := make([]*launchpadpb.LaunchpadProject, len(projects)) + for idx, dbProject := range projects { + result[idx] = &launchpadpb.LaunchpadProject{ + Id: dbProject.ProjectID, + NetworkId: dbProject.NetworkID, + CreatorId: string(dbProject.CreatorID), + CollectionData: string(dbProject.CollectionData), + Status: dbProject.Status, + ProposalId: &dbProject.ProposalId, + } + } + + return &launchpadpb.LaunchpadProjectsByCreatorResponse{ + Projects: result, + }, nil +} + +// Get all launchpad projects +func (s *Launchpad) LaunchpadProjects(ctx context.Context, req *launchpadpb.LaunchpadProjectsRequest) (*launchpadpb.LaunchpadProjectsResponse, error) { + limit := req.GetLimit() + if limit <= 0 { + return nil, errors.New("limit must be a positive number") + } + + // TODO: Handle offset for pagination + offset := req.GetOffset() + if offset < 0 { + return nil, errors.New("offset must be greater or equal to 0") + } + + networkID := req.GetNetworkId() + if networkID == "" { + return nil, errors.New("missing network id") + } + _, err := s.conf.NetworkStore.GetNetwork(networkID) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("unknown network id '%s'", networkID)) + } + + status := req.GetStatus() + if status < 0 { + return nil, errors.New("invalid status") + } + + var projects []indexerdb.LaunchpadProject + + orderDirection := "" + switch req.GetSortDirection() { + case launchpadpb.SortDirection_SORT_DIRECTION_UNSPECIFIED: + orderDirection = "" + case launchpadpb.SortDirection_SORT_DIRECTION_ASCENDING: + orderDirection = " ASC " + case launchpadpb.SortDirection_SORT_DIRECTION_DESCENDING: + orderDirection = " DESC " + } + orderSQL := "" + switch req.GetSort() { + case launchpadpb.Sort_SORT_COLLECTION_NAME: + orderSQL = "ORDER BY lp.collection_data->>'name'" + orderDirection + case launchpadpb.Sort_SORT_UNSPECIFIED: + orderSQL = "" + } + + statusFilterSQL := "AND lp.status = " + strconv.FormatInt(int64(status.Number()), 10) + if status == launchpadpb.Status_STATUS_UNSPECIFIED { + statusFilterSQL = "" + } + + err = s.conf.IndexerDB.Raw(fmt.Sprintf(` + SELECT * FROM launchpad_projects AS lp WHERE lp.network_id = ? %s %s LIMIT ? + `, + statusFilterSQL, orderSQL), networkID, limit).Scan(&projects).Error + if err != nil { + return nil, errors.Wrap(err, "failed to query database") + } + + result := make([]*launchpadpb.LaunchpadProject, len(projects)) + for idx, dbProject := range projects { + result[idx] = &launchpadpb.LaunchpadProject{ + Id: dbProject.ProjectID, + NetworkId: dbProject.NetworkID, + CreatorId: string(dbProject.CreatorID), + CollectionData: string(dbProject.CollectionData), + Status: dbProject.Status, + ProposalId: &dbProject.ProposalId, + } + } + + return &launchpadpb.LaunchpadProjectsResponse{ + Projects: result, + }, nil +} + +// Get launchpad project by project_id +func (s *Launchpad) LaunchpadProjectById(ctx context.Context, req *launchpadpb.LaunchpadProjectByIdRequest) (*launchpadpb.LaunchpadProjectByIdResponse, error) { + networkID := req.GetNetworkId() + if networkID == "" { + return nil, errors.New("missing network id") + } + _, err := s.conf.NetworkStore.GetNetwork(networkID) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("unknown network id '%s'", networkID)) + } + + projectID := req.GetProjectId() + if projectID == "" { + return nil, errors.New("missing project id") + } + + var project *indexerdb.LaunchpadProject + + err = s.conf.IndexerDB.Raw(`SELECT * FROM launchpad_projects AS lp WHERE lp.project_id = ? AND lp.network_id = ?`, projectID, networkID).Scan(&project).Error + if err != nil { + return nil, errors.Wrap(err, "failed to query database") + } + + return &launchpadpb.LaunchpadProjectByIdResponse{ + Project: &launchpadpb.LaunchpadProject{ + Id: project.ProjectID, + NetworkId: project.NetworkID, + CreatorId: string(project.CreatorID), + CollectionData: string(project.CollectionData), + Status: project.Status, + ProposalId: &project.ProposalId, + }, + }, nil +} + +// Get all launchpad projects counts +func (s *Launchpad) LaunchpadProjectsCounts(ctx context.Context, req *launchpadpb.LaunchpadProjectsCountsRequest) (*launchpadpb.LaunchpadProjectsCountsResponse, error) { + networkID := req.GetNetworkId() + if networkID == "" { + return nil, errors.New("missing network id") + } + _, err := s.conf.NetworkStore.GetNetwork(networkID) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("unknown network id '%s'", networkID)) + } + + var statusCounts []*launchpadpb.StatusCount + if err := s.conf.IndexerDB.Model(&indexerdb.LaunchpadProject{}). + Select("status, COUNT(*) as count"). + Group("status"). + Scan(&statusCounts).Error; err != nil { + return nil, errors.Wrap(err, "failed to query database") + } + + return &launchpadpb.LaunchpadProjectsCountsResponse{ + StatusCounts: statusCounts, + }, nil +} diff --git a/go/pkg/launchpad/utils.go b/go/pkg/launchpad/utils.go new file mode 100644 index 0000000000..19ae9e88a5 --- /dev/null +++ b/go/pkg/launchpad/utils.go @@ -0,0 +1,153 @@ +package launchpad + +import ( + "bytes" + "fmt" + + "github.com/TERITORI/teritori-dapp/go/pkg/launchpadpb" + "github.com/TERITORI/teritori-dapp/go/pkg/merkletree" + "github.com/cosmos/gogoproto/proto" + "github.com/ethereum/go-ethereum/crypto" + "github.com/pkg/errors" +) + +type WhitelistAddress struct { + value string +} + +func NewWhitelistAddress(addr string) *WhitelistAddress { + return &WhitelistAddress{value: addr} +} + +func (addr *WhitelistAddress) toStr() string { + return fmt.Sprintf("%s", addr.value) +} + +func (addr *WhitelistAddress) toBytes() []byte { + return []byte(addr.toStr()) +} + +func (addr *WhitelistAddress) GetID() string { + return addr.value +} + +func (addr *WhitelistAddress) CalculateHash() ([]byte, error) { + res := crypto.Keccak256(addr.toBytes()) + return res, nil +} + +func (addr *WhitelistAddress) Equals(other merkletree.Content) (bool, error) { + otherVal := other.(*WhitelistAddress).toStr() + return otherVal == addr.toStr(), nil +} + +func (addr *WhitelistAddress) ToJSONB() interface{} { + return map[string]interface{}{} +} + +type Metadata struct { + launchpadpb.Metadata +} + +func NewMetadataFromPb(data *launchpadpb.Metadata) *Metadata { + return &Metadata{ + Metadata: launchpadpb.Metadata{ + Image: data.Image, + ImageData: data.ImageData, + ExternalUrl: data.ExternalUrl, + Description: data.Description, + Name: data.Name, + Attributes: data.Attributes, + BackgroundColor: data.BackgroundColor, + AnimationUrl: data.AnimationUrl, + YoutubeUrl: data.YoutubeUrl, + RoyaltyPercentage: data.RoyaltyPercentage, + RoyaltyPaymentAddress: data.RoyaltyPaymentAddress, + }, + } +} + +func (m *Metadata) proto_encode() ([]byte, error) { + bytes, err := proto.Marshal(&m.Metadata) + if err != nil { + return nil, errors.Wrap(err, "failed to proto_encode metadata to proto") + } + + return bytes, nil +} + +// Converts the Metadata data into one string +func (m *Metadata) GetID() string { + id := "" + for _, attribute := range m.Attributes { + if attribute.DisplayType != nil { + id += *attribute.DisplayType + } + id += attribute.TraitType + id += attribute.Value + } + id += *m.Image + id += *m.Name + if m.ImageData != nil { + id += *m.ImageData + } + if m.ExternalUrl != nil { + id += *m.ExternalUrl + } + if m.Description != nil { + id += *m.Description + } + if m.BackgroundColor != nil { + id += *m.BackgroundColor + } + if m.AnimationUrl != nil { + id += *m.AnimationUrl + } + if m.YoutubeUrl != nil { + id += *m.YoutubeUrl + } + if m.RoyaltyPercentage != nil { + id += fmt.Sprintf(":%d", *m.RoyaltyPercentage) + } + if m.RoyaltyPaymentAddress != nil { + id += *m.RoyaltyPaymentAddress + } + return id +} + +// CalculateHash hashes the values of a Metadata +func (m *Metadata) CalculateHash() ([]byte, error) { + bytes, err := m.proto_encode() + if err != nil { + return nil, errors.Wrap(err, "failed to calculate hash") + } + + res := crypto.Keccak256(bytes) + return res, nil +} + +// Equals tests for equality of two Metadata +func (m *Metadata) Equals(other merkletree.Content) (bool, error) { + thisBytes, err := m.proto_encode() + if err != nil { + return false, errors.Wrap(err, "failed to proto_encode current metadata to bytes") + } + + otherBytes, err := other.(*Metadata).proto_encode() + if err != nil { + return false, errors.Wrap(err, "failed to proto_encode other metadata to bytes") + } + + return bytes.Equal(thisBytes, otherBytes), nil +} + +func (m *Metadata) ToJSONB() interface{} { + return map[string]interface{}{} +} + +func extractCID(imageURL string) string { + if len(imageURL) >= 7 && imageURL[:7] == "ipfs://" { + return imageURL[7:] + } + return imageURL +} diff --git a/go/pkg/launchpadpb/launchpad.pb.go b/go/pkg/launchpadpb/launchpad.pb.go new file mode 100644 index 0000000000..734674550a --- /dev/null +++ b/go/pkg/launchpadpb/launchpad.pb.go @@ -0,0 +1,2092 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc (unknown) +// source: launchpad/v1/launchpad.proto + +package launchpadpb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Sort int32 + +const ( + Sort_SORT_UNSPECIFIED Sort = 0 + Sort_SORT_COLLECTION_NAME Sort = 1 +) + +// Enum value maps for Sort. +var ( + Sort_name = map[int32]string{ + 0: "SORT_UNSPECIFIED", + 1: "SORT_COLLECTION_NAME", + } + Sort_value = map[string]int32{ + "SORT_UNSPECIFIED": 0, + "SORT_COLLECTION_NAME": 1, + } +) + +func (x Sort) Enum() *Sort { + p := new(Sort) + *p = x + return p +} + +func (x Sort) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Sort) Descriptor() protoreflect.EnumDescriptor { + return file_launchpad_v1_launchpad_proto_enumTypes[0].Descriptor() +} + +func (Sort) Type() protoreflect.EnumType { + return &file_launchpad_v1_launchpad_proto_enumTypes[0] +} + +func (x Sort) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Sort.Descriptor instead. +func (Sort) EnumDescriptor() ([]byte, []int) { + return file_launchpad_v1_launchpad_proto_rawDescGZIP(), []int{0} +} + +type SortDirection int32 + +const ( + SortDirection_SORT_DIRECTION_UNSPECIFIED SortDirection = 0 + SortDirection_SORT_DIRECTION_ASCENDING SortDirection = 1 + SortDirection_SORT_DIRECTION_DESCENDING SortDirection = 2 +) + +// Enum value maps for SortDirection. +var ( + SortDirection_name = map[int32]string{ + 0: "SORT_DIRECTION_UNSPECIFIED", + 1: "SORT_DIRECTION_ASCENDING", + 2: "SORT_DIRECTION_DESCENDING", + } + SortDirection_value = map[string]int32{ + "SORT_DIRECTION_UNSPECIFIED": 0, + "SORT_DIRECTION_ASCENDING": 1, + "SORT_DIRECTION_DESCENDING": 2, + } +) + +func (x SortDirection) Enum() *SortDirection { + p := new(SortDirection) + *p = x + return p +} + +func (x SortDirection) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SortDirection) Descriptor() protoreflect.EnumDescriptor { + return file_launchpad_v1_launchpad_proto_enumTypes[1].Descriptor() +} + +func (SortDirection) Type() protoreflect.EnumType { + return &file_launchpad_v1_launchpad_proto_enumTypes[1] +} + +func (x SortDirection) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SortDirection.Descriptor instead. +func (SortDirection) EnumDescriptor() ([]byte, []int) { + return file_launchpad_v1_launchpad_proto_rawDescGZIP(), []int{1} +} + +type Status int32 + +const ( + Status_STATUS_UNSPECIFIED Status = 0 + Status_STATUS_INCOMPLETE Status = 1 + Status_STATUS_COMPLETE Status = 2 + Status_STATUS_REVIEWING Status = 3 + Status_STATUS_CONFIRMED Status = 4 +) + +// Enum value maps for Status. +var ( + Status_name = map[int32]string{ + 0: "STATUS_UNSPECIFIED", + 1: "STATUS_INCOMPLETE", + 2: "STATUS_COMPLETE", + 3: "STATUS_REVIEWING", + 4: "STATUS_CONFIRMED", + } + Status_value = map[string]int32{ + "STATUS_UNSPECIFIED": 0, + "STATUS_INCOMPLETE": 1, + "STATUS_COMPLETE": 2, + "STATUS_REVIEWING": 3, + "STATUS_CONFIRMED": 4, + } +) + +func (x Status) Enum() *Status { + p := new(Status) + *p = x + return p +} + +func (x Status) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Status) Descriptor() protoreflect.EnumDescriptor { + return file_launchpad_v1_launchpad_proto_enumTypes[2].Descriptor() +} + +func (Status) Type() protoreflect.EnumType { + return &file_launchpad_v1_launchpad_proto_enumTypes[2] +} + +func (x Status) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Status.Descriptor instead. +func (Status) EnumDescriptor() ([]byte, []int) { + return file_launchpad_v1_launchpad_proto_rawDescGZIP(), []int{2} +} + +type LaunchpadProjectsByCreatorRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CreatorId string `protobuf:"bytes,1,opt,name=creator_id,json=creatorId,proto3" json:"creator_id,omitempty"` + NetworkId string `protobuf:"bytes,2,opt,name=network_id,json=networkId,proto3" json:"network_id,omitempty"` + Limit int32 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"` + Offset int32 `protobuf:"varint,4,opt,name=offset,proto3" json:"offset,omitempty"` + Sort Sort `protobuf:"varint,5,opt,name=sort,proto3,enum=launchpad.v1.Sort" json:"sort,omitempty"` + SortDirection SortDirection `protobuf:"varint,6,opt,name=sort_direction,json=sortDirection,proto3,enum=launchpad.v1.SortDirection" json:"sort_direction,omitempty"` + Status *Status `protobuf:"varint,7,opt,name=status,proto3,enum=launchpad.v1.Status,oneof" json:"status,omitempty"` +} + +func (x *LaunchpadProjectsByCreatorRequest) Reset() { + *x = LaunchpadProjectsByCreatorRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LaunchpadProjectsByCreatorRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LaunchpadProjectsByCreatorRequest) ProtoMessage() {} + +func (x *LaunchpadProjectsByCreatorRequest) ProtoReflect() protoreflect.Message { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LaunchpadProjectsByCreatorRequest.ProtoReflect.Descriptor instead. +func (*LaunchpadProjectsByCreatorRequest) Descriptor() ([]byte, []int) { + return file_launchpad_v1_launchpad_proto_rawDescGZIP(), []int{0} +} + +func (x *LaunchpadProjectsByCreatorRequest) GetCreatorId() string { + if x != nil { + return x.CreatorId + } + return "" +} + +func (x *LaunchpadProjectsByCreatorRequest) GetNetworkId() string { + if x != nil { + return x.NetworkId + } + return "" +} + +func (x *LaunchpadProjectsByCreatorRequest) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *LaunchpadProjectsByCreatorRequest) GetOffset() int32 { + if x != nil { + return x.Offset + } + return 0 +} + +func (x *LaunchpadProjectsByCreatorRequest) GetSort() Sort { + if x != nil { + return x.Sort + } + return Sort_SORT_UNSPECIFIED +} + +func (x *LaunchpadProjectsByCreatorRequest) GetSortDirection() SortDirection { + if x != nil { + return x.SortDirection + } + return SortDirection_SORT_DIRECTION_UNSPECIFIED +} + +func (x *LaunchpadProjectsByCreatorRequest) GetStatus() Status { + if x != nil && x.Status != nil { + return *x.Status + } + return Status_STATUS_UNSPECIFIED +} + +type LaunchpadProjectsByCreatorResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Projects []*LaunchpadProject `protobuf:"bytes,1,rep,name=projects,proto3" json:"projects,omitempty"` +} + +func (x *LaunchpadProjectsByCreatorResponse) Reset() { + *x = LaunchpadProjectsByCreatorResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LaunchpadProjectsByCreatorResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LaunchpadProjectsByCreatorResponse) ProtoMessage() {} + +func (x *LaunchpadProjectsByCreatorResponse) ProtoReflect() protoreflect.Message { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LaunchpadProjectsByCreatorResponse.ProtoReflect.Descriptor instead. +func (*LaunchpadProjectsByCreatorResponse) Descriptor() ([]byte, []int) { + return file_launchpad_v1_launchpad_proto_rawDescGZIP(), []int{1} +} + +func (x *LaunchpadProjectsByCreatorResponse) GetProjects() []*LaunchpadProject { + if x != nil { + return x.Projects + } + return nil +} + +type LaunchpadProjectsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + NetworkId string `protobuf:"bytes,1,opt,name=network_id,json=networkId,proto3" json:"network_id,omitempty"` + Limit int32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` + Offset int32 `protobuf:"varint,3,opt,name=offset,proto3" json:"offset,omitempty"` + Sort Sort `protobuf:"varint,4,opt,name=sort,proto3,enum=launchpad.v1.Sort" json:"sort,omitempty"` + SortDirection SortDirection `protobuf:"varint,5,opt,name=sort_direction,json=sortDirection,proto3,enum=launchpad.v1.SortDirection" json:"sort_direction,omitempty"` + Status *Status `protobuf:"varint,6,opt,name=status,proto3,enum=launchpad.v1.Status,oneof" json:"status,omitempty"` +} + +func (x *LaunchpadProjectsRequest) Reset() { + *x = LaunchpadProjectsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LaunchpadProjectsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LaunchpadProjectsRequest) ProtoMessage() {} + +func (x *LaunchpadProjectsRequest) ProtoReflect() protoreflect.Message { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LaunchpadProjectsRequest.ProtoReflect.Descriptor instead. +func (*LaunchpadProjectsRequest) Descriptor() ([]byte, []int) { + return file_launchpad_v1_launchpad_proto_rawDescGZIP(), []int{2} +} + +func (x *LaunchpadProjectsRequest) GetNetworkId() string { + if x != nil { + return x.NetworkId + } + return "" +} + +func (x *LaunchpadProjectsRequest) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *LaunchpadProjectsRequest) GetOffset() int32 { + if x != nil { + return x.Offset + } + return 0 +} + +func (x *LaunchpadProjectsRequest) GetSort() Sort { + if x != nil { + return x.Sort + } + return Sort_SORT_UNSPECIFIED +} + +func (x *LaunchpadProjectsRequest) GetSortDirection() SortDirection { + if x != nil { + return x.SortDirection + } + return SortDirection_SORT_DIRECTION_UNSPECIFIED +} + +func (x *LaunchpadProjectsRequest) GetStatus() Status { + if x != nil && x.Status != nil { + return *x.Status + } + return Status_STATUS_UNSPECIFIED +} + +type LaunchpadProjectsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Projects []*LaunchpadProject `protobuf:"bytes,1,rep,name=projects,proto3" json:"projects,omitempty"` +} + +func (x *LaunchpadProjectsResponse) Reset() { + *x = LaunchpadProjectsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LaunchpadProjectsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LaunchpadProjectsResponse) ProtoMessage() {} + +func (x *LaunchpadProjectsResponse) ProtoReflect() protoreflect.Message { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LaunchpadProjectsResponse.ProtoReflect.Descriptor instead. +func (*LaunchpadProjectsResponse) Descriptor() ([]byte, []int) { + return file_launchpad_v1_launchpad_proto_rawDescGZIP(), []int{3} +} + +func (x *LaunchpadProjectsResponse) GetProjects() []*LaunchpadProject { + if x != nil { + return x.Projects + } + return nil +} + +type LaunchpadProjectByIdRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + NetworkId string `protobuf:"bytes,1,opt,name=network_id,json=networkId,proto3" json:"network_id,omitempty"` + ProjectId string `protobuf:"bytes,2,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` +} + +func (x *LaunchpadProjectByIdRequest) Reset() { + *x = LaunchpadProjectByIdRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LaunchpadProjectByIdRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LaunchpadProjectByIdRequest) ProtoMessage() {} + +func (x *LaunchpadProjectByIdRequest) ProtoReflect() protoreflect.Message { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LaunchpadProjectByIdRequest.ProtoReflect.Descriptor instead. +func (*LaunchpadProjectByIdRequest) Descriptor() ([]byte, []int) { + return file_launchpad_v1_launchpad_proto_rawDescGZIP(), []int{4} +} + +func (x *LaunchpadProjectByIdRequest) GetNetworkId() string { + if x != nil { + return x.NetworkId + } + return "" +} + +func (x *LaunchpadProjectByIdRequest) GetProjectId() string { + if x != nil { + return x.ProjectId + } + return "" +} + +type LaunchpadProjectByIdResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Project *LaunchpadProject `protobuf:"bytes,1,opt,name=project,proto3" json:"project,omitempty"` +} + +func (x *LaunchpadProjectByIdResponse) Reset() { + *x = LaunchpadProjectByIdResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LaunchpadProjectByIdResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LaunchpadProjectByIdResponse) ProtoMessage() {} + +func (x *LaunchpadProjectByIdResponse) ProtoReflect() protoreflect.Message { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LaunchpadProjectByIdResponse.ProtoReflect.Descriptor instead. +func (*LaunchpadProjectByIdResponse) Descriptor() ([]byte, []int) { + return file_launchpad_v1_launchpad_proto_rawDescGZIP(), []int{5} +} + +func (x *LaunchpadProjectByIdResponse) GetProject() *LaunchpadProject { + if x != nil { + return x.Project + } + return nil +} + +type UploadMetadatasRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Sender string `protobuf:"bytes,1,opt,name=sender,proto3" json:"sender,omitempty"` + NetworkId string `protobuf:"bytes,2,opt,name=network_id,json=networkId,proto3" json:"network_id,omitempty"` + ProjectId string `protobuf:"bytes,3,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + Metadatas []*Metadata `protobuf:"bytes,4,rep,name=metadatas,proto3" json:"metadatas,omitempty"` + PinataJwt *string `protobuf:"bytes,5,opt,name=pinata_jwt,json=pinataJwt,proto3,oneof" json:"pinata_jwt,omitempty"` +} + +func (x *UploadMetadatasRequest) Reset() { + *x = UploadMetadatasRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UploadMetadatasRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UploadMetadatasRequest) ProtoMessage() {} + +func (x *UploadMetadatasRequest) ProtoReflect() protoreflect.Message { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UploadMetadatasRequest.ProtoReflect.Descriptor instead. +func (*UploadMetadatasRequest) Descriptor() ([]byte, []int) { + return file_launchpad_v1_launchpad_proto_rawDescGZIP(), []int{6} +} + +func (x *UploadMetadatasRequest) GetSender() string { + if x != nil { + return x.Sender + } + return "" +} + +func (x *UploadMetadatasRequest) GetNetworkId() string { + if x != nil { + return x.NetworkId + } + return "" +} + +func (x *UploadMetadatasRequest) GetProjectId() string { + if x != nil { + return x.ProjectId + } + return "" +} + +func (x *UploadMetadatasRequest) GetMetadatas() []*Metadata { + if x != nil { + return x.Metadatas + } + return nil +} + +func (x *UploadMetadatasRequest) GetPinataJwt() string { + if x != nil && x.PinataJwt != nil { + return *x.PinataJwt + } + return "" +} + +type UploadMetadatasResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + MerkleRoot string `protobuf:"bytes,1,opt,name=merkle_root,json=merkleRoot,proto3" json:"merkle_root,omitempty"` +} + +func (x *UploadMetadatasResponse) Reset() { + *x = UploadMetadatasResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UploadMetadatasResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UploadMetadatasResponse) ProtoMessage() {} + +func (x *UploadMetadatasResponse) ProtoReflect() protoreflect.Message { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UploadMetadatasResponse.ProtoReflect.Descriptor instead. +func (*UploadMetadatasResponse) Descriptor() ([]byte, []int) { + return file_launchpad_v1_launchpad_proto_rawDescGZIP(), []int{7} +} + +func (x *UploadMetadatasResponse) GetMerkleRoot() string { + if x != nil { + return x.MerkleRoot + } + return "" +} + +type CalculateCollectionMerkleRootRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Sender string `protobuf:"bytes,1,opt,name=sender,proto3" json:"sender,omitempty"` + Metadatas []*Metadata `protobuf:"bytes,2,rep,name=metadatas,proto3" json:"metadatas,omitempty"` +} + +func (x *CalculateCollectionMerkleRootRequest) Reset() { + *x = CalculateCollectionMerkleRootRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CalculateCollectionMerkleRootRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CalculateCollectionMerkleRootRequest) ProtoMessage() {} + +func (x *CalculateCollectionMerkleRootRequest) ProtoReflect() protoreflect.Message { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CalculateCollectionMerkleRootRequest.ProtoReflect.Descriptor instead. +func (*CalculateCollectionMerkleRootRequest) Descriptor() ([]byte, []int) { + return file_launchpad_v1_launchpad_proto_rawDescGZIP(), []int{8} +} + +func (x *CalculateCollectionMerkleRootRequest) GetSender() string { + if x != nil { + return x.Sender + } + return "" +} + +func (x *CalculateCollectionMerkleRootRequest) GetMetadatas() []*Metadata { + if x != nil { + return x.Metadatas + } + return nil +} + +type CalculateCollectionMerkleRootResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + MerkleRoot string `protobuf:"bytes,1,opt,name=merkle_root,json=merkleRoot,proto3" json:"merkle_root,omitempty"` +} + +func (x *CalculateCollectionMerkleRootResponse) Reset() { + *x = CalculateCollectionMerkleRootResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CalculateCollectionMerkleRootResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CalculateCollectionMerkleRootResponse) ProtoMessage() {} + +func (x *CalculateCollectionMerkleRootResponse) ProtoReflect() protoreflect.Message { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CalculateCollectionMerkleRootResponse.ProtoReflect.Descriptor instead. +func (*CalculateCollectionMerkleRootResponse) Descriptor() ([]byte, []int) { + return file_launchpad_v1_launchpad_proto_rawDescGZIP(), []int{9} +} + +func (x *CalculateCollectionMerkleRootResponse) GetMerkleRoot() string { + if x != nil { + return x.MerkleRoot + } + return "" +} + +type TokenMetadataRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Sender string `protobuf:"bytes,1,opt,name=sender,proto3" json:"sender,omitempty"` + NetworkId string `protobuf:"bytes,2,opt,name=network_id,json=networkId,proto3" json:"network_id,omitempty"` + ProjectId string `protobuf:"bytes,3,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + TokenId uint32 `protobuf:"varint,4,opt,name=token_id,json=tokenId,proto3" json:"token_id,omitempty"` +} + +func (x *TokenMetadataRequest) Reset() { + *x = TokenMetadataRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TokenMetadataRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TokenMetadataRequest) ProtoMessage() {} + +func (x *TokenMetadataRequest) ProtoReflect() protoreflect.Message { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TokenMetadataRequest.ProtoReflect.Descriptor instead. +func (*TokenMetadataRequest) Descriptor() ([]byte, []int) { + return file_launchpad_v1_launchpad_proto_rawDescGZIP(), []int{10} +} + +func (x *TokenMetadataRequest) GetSender() string { + if x != nil { + return x.Sender + } + return "" +} + +func (x *TokenMetadataRequest) GetNetworkId() string { + if x != nil { + return x.NetworkId + } + return "" +} + +func (x *TokenMetadataRequest) GetProjectId() string { + if x != nil { + return x.ProjectId + } + return "" +} + +func (x *TokenMetadataRequest) GetTokenId() uint32 { + if x != nil { + return x.TokenId + } + return 0 +} + +type TokenMetadataResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + MerkleRoot string `protobuf:"bytes,1,opt,name=merkle_root,json=merkleRoot,proto3" json:"merkle_root,omitempty"` + Metadata *Metadata `protobuf:"bytes,2,opt,name=metadata,proto3" json:"metadata,omitempty"` + MerkleProof []string `protobuf:"bytes,3,rep,name=merkle_proof,json=merkleProof,proto3" json:"merkle_proof,omitempty"` +} + +func (x *TokenMetadataResponse) Reset() { + *x = TokenMetadataResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TokenMetadataResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TokenMetadataResponse) ProtoMessage() {} + +func (x *TokenMetadataResponse) ProtoReflect() protoreflect.Message { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[11] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TokenMetadataResponse.ProtoReflect.Descriptor instead. +func (*TokenMetadataResponse) Descriptor() ([]byte, []int) { + return file_launchpad_v1_launchpad_proto_rawDescGZIP(), []int{11} +} + +func (x *TokenMetadataResponse) GetMerkleRoot() string { + if x != nil { + return x.MerkleRoot + } + return "" +} + +func (x *TokenMetadataResponse) GetMetadata() *Metadata { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *TokenMetadataResponse) GetMerkleProof() []string { + if x != nil { + return x.MerkleProof + } + return nil +} + +type LaunchpadProjectsCountsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + NetworkId string `protobuf:"bytes,1,opt,name=network_id,json=networkId,proto3" json:"network_id,omitempty"` +} + +func (x *LaunchpadProjectsCountsRequest) Reset() { + *x = LaunchpadProjectsCountsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LaunchpadProjectsCountsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LaunchpadProjectsCountsRequest) ProtoMessage() {} + +func (x *LaunchpadProjectsCountsRequest) ProtoReflect() protoreflect.Message { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LaunchpadProjectsCountsRequest.ProtoReflect.Descriptor instead. +func (*LaunchpadProjectsCountsRequest) Descriptor() ([]byte, []int) { + return file_launchpad_v1_launchpad_proto_rawDescGZIP(), []int{12} +} + +func (x *LaunchpadProjectsCountsRequest) GetNetworkId() string { + if x != nil { + return x.NetworkId + } + return "" +} + +type LaunchpadProjectsCountsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StatusCounts []*StatusCount `protobuf:"bytes,1,rep,name=statusCounts,proto3" json:"statusCounts,omitempty"` +} + +func (x *LaunchpadProjectsCountsResponse) Reset() { + *x = LaunchpadProjectsCountsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LaunchpadProjectsCountsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LaunchpadProjectsCountsResponse) ProtoMessage() {} + +func (x *LaunchpadProjectsCountsResponse) ProtoReflect() protoreflect.Message { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[13] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LaunchpadProjectsCountsResponse.ProtoReflect.Descriptor instead. +func (*LaunchpadProjectsCountsResponse) Descriptor() ([]byte, []int) { + return file_launchpad_v1_launchpad_proto_rawDescGZIP(), []int{13} +} + +func (x *LaunchpadProjectsCountsResponse) GetStatusCounts() []*StatusCount { + if x != nil { + return x.StatusCounts + } + return nil +} + +type ProposeApproveProjectRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Sender string `protobuf:"bytes,1,opt,name=sender,proto3" json:"sender,omitempty"` + NetworkId string `protobuf:"bytes,2,opt,name=network_id,json=networkId,proto3" json:"network_id,omitempty"` + ProjectId string `protobuf:"bytes,3,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + ProposalId string `protobuf:"bytes,4,opt,name=proposal_id,json=proposalId,proto3" json:"proposal_id,omitempty"` +} + +func (x *ProposeApproveProjectRequest) Reset() { + *x = ProposeApproveProjectRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProposeApproveProjectRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProposeApproveProjectRequest) ProtoMessage() {} + +func (x *ProposeApproveProjectRequest) ProtoReflect() protoreflect.Message { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[14] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProposeApproveProjectRequest.ProtoReflect.Descriptor instead. +func (*ProposeApproveProjectRequest) Descriptor() ([]byte, []int) { + return file_launchpad_v1_launchpad_proto_rawDescGZIP(), []int{14} +} + +func (x *ProposeApproveProjectRequest) GetSender() string { + if x != nil { + return x.Sender + } + return "" +} + +func (x *ProposeApproveProjectRequest) GetNetworkId() string { + if x != nil { + return x.NetworkId + } + return "" +} + +func (x *ProposeApproveProjectRequest) GetProjectId() string { + if x != nil { + return x.ProjectId + } + return "" +} + +func (x *ProposeApproveProjectRequest) GetProposalId() string { + if x != nil { + return x.ProposalId + } + return "" +} + +type ProposeApproveProjectResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Approved bool `protobuf:"varint,1,opt,name=approved,proto3" json:"approved,omitempty"` +} + +func (x *ProposeApproveProjectResponse) Reset() { + *x = ProposeApproveProjectResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProposeApproveProjectResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProposeApproveProjectResponse) ProtoMessage() {} + +func (x *ProposeApproveProjectResponse) ProtoReflect() protoreflect.Message { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[15] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProposeApproveProjectResponse.ProtoReflect.Descriptor instead. +func (*ProposeApproveProjectResponse) Descriptor() ([]byte, []int) { + return file_launchpad_v1_launchpad_proto_rawDescGZIP(), []int{15} +} + +func (x *ProposeApproveProjectResponse) GetApproved() bool { + if x != nil { + return x.Approved + } + return false +} + +type StatusCount struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Status Status `protobuf:"varint,1,opt,name=status,proto3,enum=launchpad.v1.Status" json:"status,omitempty"` + Count uint32 `protobuf:"varint,2,opt,name=count,proto3" json:"count,omitempty"` +} + +func (x *StatusCount) Reset() { + *x = StatusCount{} + if protoimpl.UnsafeEnabled { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StatusCount) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StatusCount) ProtoMessage() {} + +func (x *StatusCount) ProtoReflect() protoreflect.Message { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[16] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StatusCount.ProtoReflect.Descriptor instead. +func (*StatusCount) Descriptor() ([]byte, []int) { + return file_launchpad_v1_launchpad_proto_rawDescGZIP(), []int{16} +} + +func (x *StatusCount) GetStatus() Status { + if x != nil { + return x.Status + } + return Status_STATUS_UNSPECIFIED +} + +func (x *StatusCount) GetCount() uint32 { + if x != nil { + return x.Count + } + return 0 +} + +type LaunchpadProject struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + NetworkId string `protobuf:"bytes,2,opt,name=network_id,json=networkId,proto3" json:"network_id,omitempty"` + CreatorId string `protobuf:"bytes,3,opt,name=creator_id,json=creatorId,proto3" json:"creator_id,omitempty"` + CollectionData string `protobuf:"bytes,4,opt,name=collection_data,json=collectionData,proto3" json:"collection_data,omitempty"` + Status Status `protobuf:"varint,5,opt,name=status,proto3,enum=launchpad.v1.Status" json:"status,omitempty"` + ProposalId *string `protobuf:"bytes,6,opt,name=proposal_id,json=proposalId,proto3,oneof" json:"proposal_id,omitempty"` +} + +func (x *LaunchpadProject) Reset() { + *x = LaunchpadProject{} + if protoimpl.UnsafeEnabled { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LaunchpadProject) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LaunchpadProject) ProtoMessage() {} + +func (x *LaunchpadProject) ProtoReflect() protoreflect.Message { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[17] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LaunchpadProject.ProtoReflect.Descriptor instead. +func (*LaunchpadProject) Descriptor() ([]byte, []int) { + return file_launchpad_v1_launchpad_proto_rawDescGZIP(), []int{17} +} + +func (x *LaunchpadProject) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *LaunchpadProject) GetNetworkId() string { + if x != nil { + return x.NetworkId + } + return "" +} + +func (x *LaunchpadProject) GetCreatorId() string { + if x != nil { + return x.CreatorId + } + return "" +} + +func (x *LaunchpadProject) GetCollectionData() string { + if x != nil { + return x.CollectionData + } + return "" +} + +func (x *LaunchpadProject) GetStatus() Status { + if x != nil { + return x.Status + } + return Status_STATUS_UNSPECIFIED +} + +func (x *LaunchpadProject) GetProposalId() string { + if x != nil && x.ProposalId != nil { + return *x.ProposalId + } + return "" +} + +type Metadata struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Image *string `protobuf:"bytes,1,opt,name=image,proto3,oneof" json:"image,omitempty"` + ImageData *string `protobuf:"bytes,2,opt,name=image_data,json=imageData,proto3,oneof" json:"image_data,omitempty"` + ExternalUrl *string `protobuf:"bytes,3,opt,name=external_url,json=externalUrl,proto3,oneof" json:"external_url,omitempty"` + Description *string `protobuf:"bytes,4,opt,name=description,proto3,oneof" json:"description,omitempty"` + Name *string `protobuf:"bytes,5,opt,name=name,proto3,oneof" json:"name,omitempty"` + Attributes []*Trait `protobuf:"bytes,6,rep,name=attributes,proto3" json:"attributes,omitempty"` + BackgroundColor *string `protobuf:"bytes,7,opt,name=background_color,json=backgroundColor,proto3,oneof" json:"background_color,omitempty"` + AnimationUrl *string `protobuf:"bytes,8,opt,name=animation_url,json=animationUrl,proto3,oneof" json:"animation_url,omitempty"` + YoutubeUrl *string `protobuf:"bytes,9,opt,name=youtube_url,json=youtubeUrl,proto3,oneof" json:"youtube_url,omitempty"` + RoyaltyPercentage *uint64 `protobuf:"varint,10,opt,name=royalty_percentage,json=royaltyPercentage,proto3,oneof" json:"royalty_percentage,omitempty"` + RoyaltyPaymentAddress *string `protobuf:"bytes,11,opt,name=royalty_payment_address,json=royaltyPaymentAddress,proto3,oneof" json:"royalty_payment_address,omitempty"` +} + +func (x *Metadata) Reset() { + *x = Metadata{} + if protoimpl.UnsafeEnabled { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Metadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Metadata) ProtoMessage() {} + +func (x *Metadata) ProtoReflect() protoreflect.Message { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[18] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Metadata.ProtoReflect.Descriptor instead. +func (*Metadata) Descriptor() ([]byte, []int) { + return file_launchpad_v1_launchpad_proto_rawDescGZIP(), []int{18} +} + +func (x *Metadata) GetImage() string { + if x != nil && x.Image != nil { + return *x.Image + } + return "" +} + +func (x *Metadata) GetImageData() string { + if x != nil && x.ImageData != nil { + return *x.ImageData + } + return "" +} + +func (x *Metadata) GetExternalUrl() string { + if x != nil && x.ExternalUrl != nil { + return *x.ExternalUrl + } + return "" +} + +func (x *Metadata) GetDescription() string { + if x != nil && x.Description != nil { + return *x.Description + } + return "" +} + +func (x *Metadata) GetName() string { + if x != nil && x.Name != nil { + return *x.Name + } + return "" +} + +func (x *Metadata) GetAttributes() []*Trait { + if x != nil { + return x.Attributes + } + return nil +} + +func (x *Metadata) GetBackgroundColor() string { + if x != nil && x.BackgroundColor != nil { + return *x.BackgroundColor + } + return "" +} + +func (x *Metadata) GetAnimationUrl() string { + if x != nil && x.AnimationUrl != nil { + return *x.AnimationUrl + } + return "" +} + +func (x *Metadata) GetYoutubeUrl() string { + if x != nil && x.YoutubeUrl != nil { + return *x.YoutubeUrl + } + return "" +} + +func (x *Metadata) GetRoyaltyPercentage() uint64 { + if x != nil && x.RoyaltyPercentage != nil { + return *x.RoyaltyPercentage + } + return 0 +} + +func (x *Metadata) GetRoyaltyPaymentAddress() string { + if x != nil && x.RoyaltyPaymentAddress != nil { + return *x.RoyaltyPaymentAddress + } + return "" +} + +type Trait struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DisplayType *string `protobuf:"bytes,1,opt,name=display_type,json=displayType,proto3,oneof" json:"display_type,omitempty"` + TraitType string `protobuf:"bytes,2,opt,name=trait_type,json=traitType,proto3" json:"trait_type,omitempty"` + Value string `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *Trait) Reset() { + *x = Trait{} + if protoimpl.UnsafeEnabled { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Trait) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Trait) ProtoMessage() {} + +func (x *Trait) ProtoReflect() protoreflect.Message { + mi := &file_launchpad_v1_launchpad_proto_msgTypes[19] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Trait.ProtoReflect.Descriptor instead. +func (*Trait) Descriptor() ([]byte, []int) { + return file_launchpad_v1_launchpad_proto_rawDescGZIP(), []int{19} +} + +func (x *Trait) GetDisplayType() string { + if x != nil && x.DisplayType != nil { + return *x.DisplayType + } + return "" +} + +func (x *Trait) GetTraitType() string { + if x != nil { + return x.TraitType + } + return "" +} + +func (x *Trait) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +var File_launchpad_v1_launchpad_proto protoreflect.FileDescriptor + +var file_launchpad_v1_launchpad_proto_rawDesc = []byte{ + 0x0a, 0x1c, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2f, 0x76, 0x31, 0x2f, 0x6c, + 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, + 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x76, 0x31, 0x22, 0xb9, 0x02, 0x0a, + 0x21, 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x73, 0x42, 0x79, 0x43, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x49, + 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x69, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x64, + 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x26, + 0x0a, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x6c, + 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x6f, 0x72, 0x74, + 0x52, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x12, 0x42, 0x0a, 0x0e, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x64, + 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, + 0x2e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x6f, + 0x72, 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0d, 0x73, 0x6f, 0x72, + 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x31, 0x0a, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x6c, 0x61, 0x75, + 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x48, 0x00, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, + 0x07, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x60, 0x0a, 0x22, 0x4c, 0x61, 0x75, 0x6e, + 0x63, 0x68, 0x70, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x42, 0x79, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, + 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1e, 0x2e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x76, 0x31, 0x2e, + 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, + 0x52, 0x08, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x22, 0x91, 0x02, 0x0a, 0x18, 0x4c, + 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x16, 0x0a, 0x06, + 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x6f, 0x66, + 0x66, 0x73, 0x65, 0x74, 0x12, 0x26, 0x0a, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x76, + 0x31, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x52, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x12, 0x42, 0x0a, 0x0e, + 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, + 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x0d, 0x73, 0x6f, 0x72, 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x31, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x14, 0x2e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x76, 0x31, 0x2e, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x48, 0x00, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x57, + 0x0a, 0x19, 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x6a, 0x65, + 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x08, 0x70, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, + 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x61, 0x75, + 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x08, 0x70, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x22, 0x5b, 0x0a, 0x1b, 0x4c, 0x61, 0x75, 0x6e, 0x63, + 0x68, 0x70, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x42, 0x79, 0x49, 0x64, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, + 0x63, 0x74, 0x49, 0x64, 0x22, 0x58, 0x0a, 0x1c, 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, + 0x64, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x42, 0x79, 0x49, 0x64, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, + 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x50, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x22, 0xd7, + 0x01, 0x0a, 0x16, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x6e, + 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x6e, 0x64, 0x65, + 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x69, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x64, + 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, + 0x34, 0x0a, 0x09, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x73, 0x18, 0x04, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x76, + 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x09, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x73, 0x12, 0x22, 0x0a, 0x0a, 0x70, 0x69, 0x6e, 0x61, 0x74, 0x61, 0x5f, + 0x6a, 0x77, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x09, 0x70, 0x69, 0x6e, + 0x61, 0x74, 0x61, 0x4a, 0x77, 0x74, 0x88, 0x01, 0x01, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x70, 0x69, + 0x6e, 0x61, 0x74, 0x61, 0x5f, 0x6a, 0x77, 0x74, 0x22, 0x3a, 0x0a, 0x17, 0x55, 0x70, 0x6c, 0x6f, + 0x61, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x65, 0x72, 0x6b, 0x6c, 0x65, 0x5f, 0x72, 0x6f, + 0x6f, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x65, 0x72, 0x6b, 0x6c, 0x65, + 0x52, 0x6f, 0x6f, 0x74, 0x22, 0x74, 0x0a, 0x24, 0x43, 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, + 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x72, 0x6b, 0x6c, + 0x65, 0x52, 0x6f, 0x6f, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, + 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, + 0x6e, 0x64, 0x65, 0x72, 0x12, 0x34, 0x0a, 0x09, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, + 0x70, 0x61, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, + 0x09, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x73, 0x22, 0x48, 0x0a, 0x25, 0x43, 0x61, + 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x4d, 0x65, 0x72, 0x6b, 0x6c, 0x65, 0x52, 0x6f, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x65, 0x72, 0x6b, 0x6c, 0x65, 0x5f, 0x72, 0x6f, + 0x6f, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x65, 0x72, 0x6b, 0x6c, 0x65, + 0x52, 0x6f, 0x6f, 0x74, 0x22, 0x87, 0x01, 0x0a, 0x14, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, + 0x06, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, + 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x49, 0x64, 0x22, 0x8f, + 0x01, 0x0a, 0x15, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x65, 0x72, 0x6b, + 0x6c, 0x65, 0x5f, 0x72, 0x6f, 0x6f, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, + 0x65, 0x72, 0x6b, 0x6c, 0x65, 0x52, 0x6f, 0x6f, 0x74, 0x12, 0x32, 0x0a, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6c, 0x61, + 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x21, 0x0a, + 0x0c, 0x6d, 0x65, 0x72, 0x6b, 0x6c, 0x65, 0x5f, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x65, 0x72, 0x6b, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x6f, 0x66, + 0x22, 0x3f, 0x0a, 0x1e, 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x50, 0x72, 0x6f, + 0x6a, 0x65, 0x63, 0x74, 0x73, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, + 0x64, 0x22, 0x60, 0x0a, 0x1f, 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x50, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, + 0x75, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6c, 0x61, 0x75, + 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x0c, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, 0x75, + 0x6e, 0x74, 0x73, 0x22, 0x95, 0x01, 0x0a, 0x1c, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x41, + 0x70, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a, + 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, + 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x49, 0x64, 0x22, 0x3b, 0x0a, 0x1d, 0x50, + 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x41, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x50, 0x72, 0x6f, + 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, + 0x61, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, + 0x61, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x64, 0x22, 0x51, 0x0a, 0x0b, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2c, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, + 0x70, 0x61, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xed, 0x01, 0x0a, 0x10, + 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x1d, 0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x64, 0x12, + 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x12, 0x27, + 0x0a, 0x0f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x12, 0x2c, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, + 0x70, 0x61, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0b, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, + 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0a, 0x70, 0x72, + 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x49, 0x64, 0x88, 0x01, 0x01, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, + 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x22, 0x84, 0x05, 0x0a, 0x08, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x19, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, + 0x88, 0x01, 0x01, 0x12, 0x22, 0x0a, 0x0a, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x5f, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x09, 0x69, 0x6d, 0x61, 0x67, 0x65, + 0x44, 0x61, 0x74, 0x61, 0x88, 0x01, 0x01, 0x12, 0x26, 0x0a, 0x0c, 0x65, 0x78, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, + 0x0b, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x55, 0x72, 0x6c, 0x88, 0x01, 0x01, 0x12, + 0x25, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x48, 0x03, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x17, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x48, 0x04, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, + 0x33, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x06, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, + 0x76, 0x31, 0x2e, 0x54, 0x72, 0x61, 0x69, 0x74, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x10, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, + 0x6e, 0x64, 0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x48, 0x05, + 0x52, 0x0f, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x43, 0x6f, 0x6c, 0x6f, + 0x72, 0x88, 0x01, 0x01, 0x12, 0x28, 0x0a, 0x0d, 0x61, 0x6e, 0x69, 0x6d, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x48, 0x06, 0x52, 0x0c, 0x61, + 0x6e, 0x69, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x72, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x24, + 0x0a, 0x0b, 0x79, 0x6f, 0x75, 0x74, 0x75, 0x62, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x09, 0x48, 0x07, 0x52, 0x0a, 0x79, 0x6f, 0x75, 0x74, 0x75, 0x62, 0x65, 0x55, 0x72, + 0x6c, 0x88, 0x01, 0x01, 0x12, 0x32, 0x0a, 0x12, 0x72, 0x6f, 0x79, 0x61, 0x6c, 0x74, 0x79, 0x5f, + 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x61, 0x67, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, + 0x48, 0x08, 0x52, 0x11, 0x72, 0x6f, 0x79, 0x61, 0x6c, 0x74, 0x79, 0x50, 0x65, 0x72, 0x63, 0x65, + 0x6e, 0x74, 0x61, 0x67, 0x65, 0x88, 0x01, 0x01, 0x12, 0x3b, 0x0a, 0x17, 0x72, 0x6f, 0x79, 0x61, + 0x6c, 0x74, 0x79, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x48, 0x09, 0x52, 0x15, 0x72, 0x6f, 0x79, + 0x61, 0x6c, 0x74, 0x79, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x88, 0x01, 0x01, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x42, + 0x0d, 0x0a, 0x0b, 0x5f, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x42, 0x0f, + 0x0a, 0x0d, 0x5f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x75, 0x72, 0x6c, 0x42, + 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x42, + 0x07, 0x0a, 0x05, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x62, 0x61, 0x63, + 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x42, 0x10, 0x0a, + 0x0e, 0x5f, 0x61, 0x6e, 0x69, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x42, + 0x0e, 0x0a, 0x0c, 0x5f, 0x79, 0x6f, 0x75, 0x74, 0x75, 0x62, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x42, + 0x15, 0x0a, 0x13, 0x5f, 0x72, 0x6f, 0x79, 0x61, 0x6c, 0x74, 0x79, 0x5f, 0x70, 0x65, 0x72, 0x63, + 0x65, 0x6e, 0x74, 0x61, 0x67, 0x65, 0x42, 0x1a, 0x0a, 0x18, 0x5f, 0x72, 0x6f, 0x79, 0x61, 0x6c, + 0x74, 0x79, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x22, 0x75, 0x0a, 0x05, 0x54, 0x72, 0x61, 0x69, 0x74, 0x12, 0x26, 0x0a, 0x0c, 0x64, + 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x48, 0x00, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x54, 0x79, 0x70, 0x65, + 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x72, 0x61, 0x69, 0x74, 0x5f, 0x74, 0x79, 0x70, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x72, 0x61, 0x69, 0x74, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x64, 0x69, 0x73, + 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x36, 0x0a, 0x04, 0x53, 0x6f, 0x72, + 0x74, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, + 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x18, 0x0a, 0x14, 0x53, 0x4f, 0x52, 0x54, 0x5f, + 0x43, 0x4f, 0x4c, 0x4c, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x4e, 0x41, 0x4d, 0x45, 0x10, + 0x01, 0x2a, 0x6c, 0x0a, 0x0d, 0x53, 0x6f, 0x72, 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x1a, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x44, 0x49, 0x52, 0x45, 0x43, + 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, + 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x44, 0x49, 0x52, 0x45, 0x43, + 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x41, 0x53, 0x43, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, + 0x12, 0x1d, 0x0a, 0x19, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x49, + 0x4f, 0x4e, 0x5f, 0x44, 0x45, 0x53, 0x43, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x2a, + 0x78, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x12, 0x53, 0x54, 0x41, + 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, + 0x00, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x49, 0x4e, 0x43, 0x4f, + 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x54, 0x41, 0x54, + 0x55, 0x53, 0x5f, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x02, 0x12, 0x14, 0x0a, + 0x10, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x52, 0x45, 0x56, 0x49, 0x45, 0x57, 0x49, 0x4e, + 0x47, 0x10, 0x03, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x4f, + 0x4e, 0x46, 0x49, 0x52, 0x4d, 0x45, 0x44, 0x10, 0x04, 0x32, 0x97, 0x07, 0x0a, 0x10, 0x4c, 0x61, + 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5e, + 0x0a, 0x0f, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x73, 0x12, 0x24, 0x2e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x76, 0x31, + 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, + 0x70, 0x61, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x88, + 0x01, 0x0a, 0x1d, 0x43, 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x72, 0x6b, 0x6c, 0x65, 0x52, 0x6f, 0x6f, 0x74, + 0x12, 0x32, 0x2e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x76, 0x31, 0x2e, + 0x43, 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x72, 0x6b, 0x6c, 0x65, 0x52, 0x6f, 0x6f, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, + 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x72, 0x6b, 0x6c, 0x65, 0x52, 0x6f, 0x6f, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x22, 0x2e, 0x6c, 0x61, 0x75, + 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, + 0x2e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x7f, 0x0a, 0x1a, 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, + 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x42, 0x79, 0x43, 0x72, 0x65, 0x61, 0x74, 0x6f, + 0x72, 0x12, 0x2f, 0x2e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x76, 0x31, + 0x2e, 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x73, 0x42, 0x79, 0x43, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x76, + 0x31, 0x2e, 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x6a, 0x65, + 0x63, 0x74, 0x73, 0x42, 0x79, 0x43, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x64, 0x0a, 0x11, 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, + 0x64, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x26, 0x2e, 0x6c, 0x61, 0x75, 0x6e, + 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, + 0x61, 0x64, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x27, 0x2e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x76, 0x31, + 0x2e, 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6d, 0x0a, 0x14, 0x4c, 0x61, + 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x42, 0x79, + 0x49, 0x64, 0x12, 0x29, 0x2e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x76, + 0x31, 0x2e, 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x6a, 0x65, + 0x63, 0x74, 0x42, 0x79, 0x49, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, + 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x61, 0x75, + 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x42, 0x79, 0x49, + 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x76, 0x0a, 0x17, 0x4c, 0x61, 0x75, + 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x43, 0x6f, + 0x75, 0x6e, 0x74, 0x73, 0x12, 0x2c, 0x2e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, + 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x50, 0x72, 0x6f, + 0x6a, 0x65, 0x63, 0x74, 0x73, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x76, + 0x31, 0x2e, 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x6a, 0x65, + 0x63, 0x74, 0x73, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x70, 0x0a, 0x15, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x41, 0x70, 0x70, 0x72, + 0x6f, 0x76, 0x65, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x2a, 0x2e, 0x6c, 0x61, 0x75, + 0x6e, 0x63, 0x68, 0x70, 0x61, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, + 0x65, 0x41, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, + 0x61, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x41, 0x70, 0x70, + 0x72, 0x6f, 0x76, 0x65, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x42, 0x0f, 0x5a, 0x0d, 0x2e, 0x2f, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x70, + 0x61, 0x64, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_launchpad_v1_launchpad_proto_rawDescOnce sync.Once + file_launchpad_v1_launchpad_proto_rawDescData = file_launchpad_v1_launchpad_proto_rawDesc +) + +func file_launchpad_v1_launchpad_proto_rawDescGZIP() []byte { + file_launchpad_v1_launchpad_proto_rawDescOnce.Do(func() { + file_launchpad_v1_launchpad_proto_rawDescData = protoimpl.X.CompressGZIP(file_launchpad_v1_launchpad_proto_rawDescData) + }) + return file_launchpad_v1_launchpad_proto_rawDescData +} + +var file_launchpad_v1_launchpad_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_launchpad_v1_launchpad_proto_msgTypes = make([]protoimpl.MessageInfo, 20) +var file_launchpad_v1_launchpad_proto_goTypes = []interface{}{ + (Sort)(0), // 0: launchpad.v1.Sort + (SortDirection)(0), // 1: launchpad.v1.SortDirection + (Status)(0), // 2: launchpad.v1.Status + (*LaunchpadProjectsByCreatorRequest)(nil), // 3: launchpad.v1.LaunchpadProjectsByCreatorRequest + (*LaunchpadProjectsByCreatorResponse)(nil), // 4: launchpad.v1.LaunchpadProjectsByCreatorResponse + (*LaunchpadProjectsRequest)(nil), // 5: launchpad.v1.LaunchpadProjectsRequest + (*LaunchpadProjectsResponse)(nil), // 6: launchpad.v1.LaunchpadProjectsResponse + (*LaunchpadProjectByIdRequest)(nil), // 7: launchpad.v1.LaunchpadProjectByIdRequest + (*LaunchpadProjectByIdResponse)(nil), // 8: launchpad.v1.LaunchpadProjectByIdResponse + (*UploadMetadatasRequest)(nil), // 9: launchpad.v1.UploadMetadatasRequest + (*UploadMetadatasResponse)(nil), // 10: launchpad.v1.UploadMetadatasResponse + (*CalculateCollectionMerkleRootRequest)(nil), // 11: launchpad.v1.CalculateCollectionMerkleRootRequest + (*CalculateCollectionMerkleRootResponse)(nil), // 12: launchpad.v1.CalculateCollectionMerkleRootResponse + (*TokenMetadataRequest)(nil), // 13: launchpad.v1.TokenMetadataRequest + (*TokenMetadataResponse)(nil), // 14: launchpad.v1.TokenMetadataResponse + (*LaunchpadProjectsCountsRequest)(nil), // 15: launchpad.v1.LaunchpadProjectsCountsRequest + (*LaunchpadProjectsCountsResponse)(nil), // 16: launchpad.v1.LaunchpadProjectsCountsResponse + (*ProposeApproveProjectRequest)(nil), // 17: launchpad.v1.ProposeApproveProjectRequest + (*ProposeApproveProjectResponse)(nil), // 18: launchpad.v1.ProposeApproveProjectResponse + (*StatusCount)(nil), // 19: launchpad.v1.StatusCount + (*LaunchpadProject)(nil), // 20: launchpad.v1.LaunchpadProject + (*Metadata)(nil), // 21: launchpad.v1.Metadata + (*Trait)(nil), // 22: launchpad.v1.Trait +} +var file_launchpad_v1_launchpad_proto_depIdxs = []int32{ + 0, // 0: launchpad.v1.LaunchpadProjectsByCreatorRequest.sort:type_name -> launchpad.v1.Sort + 1, // 1: launchpad.v1.LaunchpadProjectsByCreatorRequest.sort_direction:type_name -> launchpad.v1.SortDirection + 2, // 2: launchpad.v1.LaunchpadProjectsByCreatorRequest.status:type_name -> launchpad.v1.Status + 20, // 3: launchpad.v1.LaunchpadProjectsByCreatorResponse.projects:type_name -> launchpad.v1.LaunchpadProject + 0, // 4: launchpad.v1.LaunchpadProjectsRequest.sort:type_name -> launchpad.v1.Sort + 1, // 5: launchpad.v1.LaunchpadProjectsRequest.sort_direction:type_name -> launchpad.v1.SortDirection + 2, // 6: launchpad.v1.LaunchpadProjectsRequest.status:type_name -> launchpad.v1.Status + 20, // 7: launchpad.v1.LaunchpadProjectsResponse.projects:type_name -> launchpad.v1.LaunchpadProject + 20, // 8: launchpad.v1.LaunchpadProjectByIdResponse.project:type_name -> launchpad.v1.LaunchpadProject + 21, // 9: launchpad.v1.UploadMetadatasRequest.metadatas:type_name -> launchpad.v1.Metadata + 21, // 10: launchpad.v1.CalculateCollectionMerkleRootRequest.metadatas:type_name -> launchpad.v1.Metadata + 21, // 11: launchpad.v1.TokenMetadataResponse.metadata:type_name -> launchpad.v1.Metadata + 19, // 12: launchpad.v1.LaunchpadProjectsCountsResponse.statusCounts:type_name -> launchpad.v1.StatusCount + 2, // 13: launchpad.v1.StatusCount.status:type_name -> launchpad.v1.Status + 2, // 14: launchpad.v1.LaunchpadProject.status:type_name -> launchpad.v1.Status + 22, // 15: launchpad.v1.Metadata.attributes:type_name -> launchpad.v1.Trait + 9, // 16: launchpad.v1.LaunchpadService.UploadMetadatas:input_type -> launchpad.v1.UploadMetadatasRequest + 11, // 17: launchpad.v1.LaunchpadService.CalculateCollectionMerkleRoot:input_type -> launchpad.v1.CalculateCollectionMerkleRootRequest + 13, // 18: launchpad.v1.LaunchpadService.TokenMetadata:input_type -> launchpad.v1.TokenMetadataRequest + 3, // 19: launchpad.v1.LaunchpadService.LaunchpadProjectsByCreator:input_type -> launchpad.v1.LaunchpadProjectsByCreatorRequest + 5, // 20: launchpad.v1.LaunchpadService.LaunchpadProjects:input_type -> launchpad.v1.LaunchpadProjectsRequest + 7, // 21: launchpad.v1.LaunchpadService.LaunchpadProjectById:input_type -> launchpad.v1.LaunchpadProjectByIdRequest + 15, // 22: launchpad.v1.LaunchpadService.LaunchpadProjectsCounts:input_type -> launchpad.v1.LaunchpadProjectsCountsRequest + 17, // 23: launchpad.v1.LaunchpadService.ProposeApproveProject:input_type -> launchpad.v1.ProposeApproveProjectRequest + 10, // 24: launchpad.v1.LaunchpadService.UploadMetadatas:output_type -> launchpad.v1.UploadMetadatasResponse + 12, // 25: launchpad.v1.LaunchpadService.CalculateCollectionMerkleRoot:output_type -> launchpad.v1.CalculateCollectionMerkleRootResponse + 14, // 26: launchpad.v1.LaunchpadService.TokenMetadata:output_type -> launchpad.v1.TokenMetadataResponse + 4, // 27: launchpad.v1.LaunchpadService.LaunchpadProjectsByCreator:output_type -> launchpad.v1.LaunchpadProjectsByCreatorResponse + 6, // 28: launchpad.v1.LaunchpadService.LaunchpadProjects:output_type -> launchpad.v1.LaunchpadProjectsResponse + 8, // 29: launchpad.v1.LaunchpadService.LaunchpadProjectById:output_type -> launchpad.v1.LaunchpadProjectByIdResponse + 16, // 30: launchpad.v1.LaunchpadService.LaunchpadProjectsCounts:output_type -> launchpad.v1.LaunchpadProjectsCountsResponse + 18, // 31: launchpad.v1.LaunchpadService.ProposeApproveProject:output_type -> launchpad.v1.ProposeApproveProjectResponse + 24, // [24:32] is the sub-list for method output_type + 16, // [16:24] is the sub-list for method input_type + 16, // [16:16] is the sub-list for extension type_name + 16, // [16:16] is the sub-list for extension extendee + 0, // [0:16] is the sub-list for field type_name +} + +func init() { file_launchpad_v1_launchpad_proto_init() } +func file_launchpad_v1_launchpad_proto_init() { + if File_launchpad_v1_launchpad_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_launchpad_v1_launchpad_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LaunchpadProjectsByCreatorRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_launchpad_v1_launchpad_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LaunchpadProjectsByCreatorResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_launchpad_v1_launchpad_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LaunchpadProjectsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_launchpad_v1_launchpad_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LaunchpadProjectsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_launchpad_v1_launchpad_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LaunchpadProjectByIdRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_launchpad_v1_launchpad_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LaunchpadProjectByIdResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_launchpad_v1_launchpad_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UploadMetadatasRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_launchpad_v1_launchpad_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UploadMetadatasResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_launchpad_v1_launchpad_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CalculateCollectionMerkleRootRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_launchpad_v1_launchpad_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CalculateCollectionMerkleRootResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_launchpad_v1_launchpad_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TokenMetadataRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_launchpad_v1_launchpad_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TokenMetadataResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_launchpad_v1_launchpad_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LaunchpadProjectsCountsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_launchpad_v1_launchpad_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LaunchpadProjectsCountsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_launchpad_v1_launchpad_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProposeApproveProjectRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_launchpad_v1_launchpad_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProposeApproveProjectResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_launchpad_v1_launchpad_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StatusCount); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_launchpad_v1_launchpad_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LaunchpadProject); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_launchpad_v1_launchpad_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Metadata); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_launchpad_v1_launchpad_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Trait); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_launchpad_v1_launchpad_proto_msgTypes[0].OneofWrappers = []interface{}{} + file_launchpad_v1_launchpad_proto_msgTypes[2].OneofWrappers = []interface{}{} + file_launchpad_v1_launchpad_proto_msgTypes[6].OneofWrappers = []interface{}{} + file_launchpad_v1_launchpad_proto_msgTypes[17].OneofWrappers = []interface{}{} + file_launchpad_v1_launchpad_proto_msgTypes[18].OneofWrappers = []interface{}{} + file_launchpad_v1_launchpad_proto_msgTypes[19].OneofWrappers = []interface{}{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_launchpad_v1_launchpad_proto_rawDesc, + NumEnums: 3, + NumMessages: 20, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_launchpad_v1_launchpad_proto_goTypes, + DependencyIndexes: file_launchpad_v1_launchpad_proto_depIdxs, + EnumInfos: file_launchpad_v1_launchpad_proto_enumTypes, + MessageInfos: file_launchpad_v1_launchpad_proto_msgTypes, + }.Build() + File_launchpad_v1_launchpad_proto = out.File + file_launchpad_v1_launchpad_proto_rawDesc = nil + file_launchpad_v1_launchpad_proto_goTypes = nil + file_launchpad_v1_launchpad_proto_depIdxs = nil +} diff --git a/go/pkg/launchpadpb/launchpad_grpc.pb.go b/go/pkg/launchpadpb/launchpad_grpc.pb.go new file mode 100644 index 0000000000..5ac12da7a1 --- /dev/null +++ b/go/pkg/launchpadpb/launchpad_grpc.pb.go @@ -0,0 +1,357 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc (unknown) +// source: launchpad/v1/launchpad.proto + +package launchpadpb + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// LaunchpadServiceClient is the client API for LaunchpadService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type LaunchpadServiceClient interface { + UploadMetadatas(ctx context.Context, in *UploadMetadatasRequest, opts ...grpc.CallOption) (*UploadMetadatasResponse, error) + CalculateCollectionMerkleRoot(ctx context.Context, in *CalculateCollectionMerkleRootRequest, opts ...grpc.CallOption) (*CalculateCollectionMerkleRootResponse, error) + TokenMetadata(ctx context.Context, in *TokenMetadataRequest, opts ...grpc.CallOption) (*TokenMetadataResponse, error) + LaunchpadProjectsByCreator(ctx context.Context, in *LaunchpadProjectsByCreatorRequest, opts ...grpc.CallOption) (*LaunchpadProjectsByCreatorResponse, error) + LaunchpadProjects(ctx context.Context, in *LaunchpadProjectsRequest, opts ...grpc.CallOption) (*LaunchpadProjectsResponse, error) + LaunchpadProjectById(ctx context.Context, in *LaunchpadProjectByIdRequest, opts ...grpc.CallOption) (*LaunchpadProjectByIdResponse, error) + LaunchpadProjectsCounts(ctx context.Context, in *LaunchpadProjectsCountsRequest, opts ...grpc.CallOption) (*LaunchpadProjectsCountsResponse, error) + ProposeApproveProject(ctx context.Context, in *ProposeApproveProjectRequest, opts ...grpc.CallOption) (*ProposeApproveProjectResponse, error) +} + +type launchpadServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewLaunchpadServiceClient(cc grpc.ClientConnInterface) LaunchpadServiceClient { + return &launchpadServiceClient{cc} +} + +func (c *launchpadServiceClient) UploadMetadatas(ctx context.Context, in *UploadMetadatasRequest, opts ...grpc.CallOption) (*UploadMetadatasResponse, error) { + out := new(UploadMetadatasResponse) + err := c.cc.Invoke(ctx, "/launchpad.v1.LaunchpadService/UploadMetadatas", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *launchpadServiceClient) CalculateCollectionMerkleRoot(ctx context.Context, in *CalculateCollectionMerkleRootRequest, opts ...grpc.CallOption) (*CalculateCollectionMerkleRootResponse, error) { + out := new(CalculateCollectionMerkleRootResponse) + err := c.cc.Invoke(ctx, "/launchpad.v1.LaunchpadService/CalculateCollectionMerkleRoot", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *launchpadServiceClient) TokenMetadata(ctx context.Context, in *TokenMetadataRequest, opts ...grpc.CallOption) (*TokenMetadataResponse, error) { + out := new(TokenMetadataResponse) + err := c.cc.Invoke(ctx, "/launchpad.v1.LaunchpadService/TokenMetadata", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *launchpadServiceClient) LaunchpadProjectsByCreator(ctx context.Context, in *LaunchpadProjectsByCreatorRequest, opts ...grpc.CallOption) (*LaunchpadProjectsByCreatorResponse, error) { + out := new(LaunchpadProjectsByCreatorResponse) + err := c.cc.Invoke(ctx, "/launchpad.v1.LaunchpadService/LaunchpadProjectsByCreator", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *launchpadServiceClient) LaunchpadProjects(ctx context.Context, in *LaunchpadProjectsRequest, opts ...grpc.CallOption) (*LaunchpadProjectsResponse, error) { + out := new(LaunchpadProjectsResponse) + err := c.cc.Invoke(ctx, "/launchpad.v1.LaunchpadService/LaunchpadProjects", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *launchpadServiceClient) LaunchpadProjectById(ctx context.Context, in *LaunchpadProjectByIdRequest, opts ...grpc.CallOption) (*LaunchpadProjectByIdResponse, error) { + out := new(LaunchpadProjectByIdResponse) + err := c.cc.Invoke(ctx, "/launchpad.v1.LaunchpadService/LaunchpadProjectById", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *launchpadServiceClient) LaunchpadProjectsCounts(ctx context.Context, in *LaunchpadProjectsCountsRequest, opts ...grpc.CallOption) (*LaunchpadProjectsCountsResponse, error) { + out := new(LaunchpadProjectsCountsResponse) + err := c.cc.Invoke(ctx, "/launchpad.v1.LaunchpadService/LaunchpadProjectsCounts", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *launchpadServiceClient) ProposeApproveProject(ctx context.Context, in *ProposeApproveProjectRequest, opts ...grpc.CallOption) (*ProposeApproveProjectResponse, error) { + out := new(ProposeApproveProjectResponse) + err := c.cc.Invoke(ctx, "/launchpad.v1.LaunchpadService/ProposeApproveProject", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// LaunchpadServiceServer is the server API for LaunchpadService service. +// All implementations must embed UnimplementedLaunchpadServiceServer +// for forward compatibility +type LaunchpadServiceServer interface { + UploadMetadatas(context.Context, *UploadMetadatasRequest) (*UploadMetadatasResponse, error) + CalculateCollectionMerkleRoot(context.Context, *CalculateCollectionMerkleRootRequest) (*CalculateCollectionMerkleRootResponse, error) + TokenMetadata(context.Context, *TokenMetadataRequest) (*TokenMetadataResponse, error) + LaunchpadProjectsByCreator(context.Context, *LaunchpadProjectsByCreatorRequest) (*LaunchpadProjectsByCreatorResponse, error) + LaunchpadProjects(context.Context, *LaunchpadProjectsRequest) (*LaunchpadProjectsResponse, error) + LaunchpadProjectById(context.Context, *LaunchpadProjectByIdRequest) (*LaunchpadProjectByIdResponse, error) + LaunchpadProjectsCounts(context.Context, *LaunchpadProjectsCountsRequest) (*LaunchpadProjectsCountsResponse, error) + ProposeApproveProject(context.Context, *ProposeApproveProjectRequest) (*ProposeApproveProjectResponse, error) + mustEmbedUnimplementedLaunchpadServiceServer() +} + +// UnimplementedLaunchpadServiceServer must be embedded to have forward compatible implementations. +type UnimplementedLaunchpadServiceServer struct { +} + +func (UnimplementedLaunchpadServiceServer) UploadMetadatas(context.Context, *UploadMetadatasRequest) (*UploadMetadatasResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UploadMetadatas not implemented") +} +func (UnimplementedLaunchpadServiceServer) CalculateCollectionMerkleRoot(context.Context, *CalculateCollectionMerkleRootRequest) (*CalculateCollectionMerkleRootResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CalculateCollectionMerkleRoot not implemented") +} +func (UnimplementedLaunchpadServiceServer) TokenMetadata(context.Context, *TokenMetadataRequest) (*TokenMetadataResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method TokenMetadata not implemented") +} +func (UnimplementedLaunchpadServiceServer) LaunchpadProjectsByCreator(context.Context, *LaunchpadProjectsByCreatorRequest) (*LaunchpadProjectsByCreatorResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method LaunchpadProjectsByCreator not implemented") +} +func (UnimplementedLaunchpadServiceServer) LaunchpadProjects(context.Context, *LaunchpadProjectsRequest) (*LaunchpadProjectsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method LaunchpadProjects not implemented") +} +func (UnimplementedLaunchpadServiceServer) LaunchpadProjectById(context.Context, *LaunchpadProjectByIdRequest) (*LaunchpadProjectByIdResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method LaunchpadProjectById not implemented") +} +func (UnimplementedLaunchpadServiceServer) LaunchpadProjectsCounts(context.Context, *LaunchpadProjectsCountsRequest) (*LaunchpadProjectsCountsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method LaunchpadProjectsCounts not implemented") +} +func (UnimplementedLaunchpadServiceServer) ProposeApproveProject(context.Context, *ProposeApproveProjectRequest) (*ProposeApproveProjectResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ProposeApproveProject not implemented") +} +func (UnimplementedLaunchpadServiceServer) mustEmbedUnimplementedLaunchpadServiceServer() {} + +// UnsafeLaunchpadServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to LaunchpadServiceServer will +// result in compilation errors. +type UnsafeLaunchpadServiceServer interface { + mustEmbedUnimplementedLaunchpadServiceServer() +} + +func RegisterLaunchpadServiceServer(s grpc.ServiceRegistrar, srv LaunchpadServiceServer) { + s.RegisterService(&LaunchpadService_ServiceDesc, srv) +} + +func _LaunchpadService_UploadMetadatas_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UploadMetadatasRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LaunchpadServiceServer).UploadMetadatas(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/launchpad.v1.LaunchpadService/UploadMetadatas", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LaunchpadServiceServer).UploadMetadatas(ctx, req.(*UploadMetadatasRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _LaunchpadService_CalculateCollectionMerkleRoot_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CalculateCollectionMerkleRootRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LaunchpadServiceServer).CalculateCollectionMerkleRoot(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/launchpad.v1.LaunchpadService/CalculateCollectionMerkleRoot", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LaunchpadServiceServer).CalculateCollectionMerkleRoot(ctx, req.(*CalculateCollectionMerkleRootRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _LaunchpadService_TokenMetadata_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TokenMetadataRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LaunchpadServiceServer).TokenMetadata(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/launchpad.v1.LaunchpadService/TokenMetadata", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LaunchpadServiceServer).TokenMetadata(ctx, req.(*TokenMetadataRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _LaunchpadService_LaunchpadProjectsByCreator_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LaunchpadProjectsByCreatorRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LaunchpadServiceServer).LaunchpadProjectsByCreator(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/launchpad.v1.LaunchpadService/LaunchpadProjectsByCreator", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LaunchpadServiceServer).LaunchpadProjectsByCreator(ctx, req.(*LaunchpadProjectsByCreatorRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _LaunchpadService_LaunchpadProjects_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LaunchpadProjectsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LaunchpadServiceServer).LaunchpadProjects(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/launchpad.v1.LaunchpadService/LaunchpadProjects", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LaunchpadServiceServer).LaunchpadProjects(ctx, req.(*LaunchpadProjectsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _LaunchpadService_LaunchpadProjectById_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LaunchpadProjectByIdRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LaunchpadServiceServer).LaunchpadProjectById(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/launchpad.v1.LaunchpadService/LaunchpadProjectById", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LaunchpadServiceServer).LaunchpadProjectById(ctx, req.(*LaunchpadProjectByIdRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _LaunchpadService_LaunchpadProjectsCounts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LaunchpadProjectsCountsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LaunchpadServiceServer).LaunchpadProjectsCounts(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/launchpad.v1.LaunchpadService/LaunchpadProjectsCounts", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LaunchpadServiceServer).LaunchpadProjectsCounts(ctx, req.(*LaunchpadProjectsCountsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _LaunchpadService_ProposeApproveProject_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ProposeApproveProjectRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LaunchpadServiceServer).ProposeApproveProject(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/launchpad.v1.LaunchpadService/ProposeApproveProject", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LaunchpadServiceServer).ProposeApproveProject(ctx, req.(*ProposeApproveProjectRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// LaunchpadService_ServiceDesc is the grpc.ServiceDesc for LaunchpadService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var LaunchpadService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "launchpad.v1.LaunchpadService", + HandlerType: (*LaunchpadServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "UploadMetadatas", + Handler: _LaunchpadService_UploadMetadatas_Handler, + }, + { + MethodName: "CalculateCollectionMerkleRoot", + Handler: _LaunchpadService_CalculateCollectionMerkleRoot_Handler, + }, + { + MethodName: "TokenMetadata", + Handler: _LaunchpadService_TokenMetadata_Handler, + }, + { + MethodName: "LaunchpadProjectsByCreator", + Handler: _LaunchpadService_LaunchpadProjectsByCreator_Handler, + }, + { + MethodName: "LaunchpadProjects", + Handler: _LaunchpadService_LaunchpadProjects_Handler, + }, + { + MethodName: "LaunchpadProjectById", + Handler: _LaunchpadService_LaunchpadProjectById_Handler, + }, + { + MethodName: "LaunchpadProjectsCounts", + Handler: _LaunchpadService_LaunchpadProjectsCounts_Handler, + }, + { + MethodName: "ProposeApproveProject", + Handler: _LaunchpadService_ProposeApproveProject_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "launchpad/v1/launchpad.proto", +} diff --git a/go/pkg/merkletree/merkletree.go b/go/pkg/merkletree/merkletree.go index 261f13940a..3357e35fd3 100644 --- a/go/pkg/merkletree/merkletree.go +++ b/go/pkg/merkletree/merkletree.go @@ -1,195 +1,211 @@ -package merkletree - -import ( - "bytes" - "encoding/hex" - "errors" - "fmt" - "hash" - "math/big" - "sort" - - "golang.org/x/crypto/sha3" -) - -// @yo1110: -// Teritori Version of Merkle tree. Support: -// - keccak256 -// - sort pairs -// - do not duplicate impair node - -type Content interface { - CalculateHash() ([]byte, error) - Equals(other Content) (bool, error) - ToJSONB() interface{} - GetID() string -} - -type MerkleTree struct { - Root *Node - merkleRoot []byte - Leafs []*Node - hashStrategy func() hash.Hash - sort bool -} - -type Node struct { - Tree *MerkleTree - Parent *Node - Left *Node - Right *Node - leaf bool - Hash []byte - C Content -} - -// sortAppend sort and append the nodes to be compatible with OpenZepplin libraries -// https://github.com/OpenZeppelin/openzeppelin-contracts-ethereum-package/blob/master/contracts/cryptography/MerkleProof.sol -func sortAppend(sort bool, a, b []byte) []byte { - if !sort { - return append(a, b...) - } - var aBig, bBig big.Int - aBig.SetBytes(a) - bBig.SetBytes(b) - if aBig.Cmp(&bBig) == -1 { - return append(a, b...) - } - return append(b, a...) -} - -func toHex(bytesValue []byte) string { - return fmt.Sprintf("0x%s", hex.EncodeToString(bytesValue)) -} - -func New(cs []Content) (*MerkleTree, error) { - t := &MerkleTree{ - hashStrategy: sha3.NewLegacyKeccak256, - sort: true, - } - - // Sort Nodes to ensure that we have always the same tree from a given set of data - sort.Slice(cs, func(i, j int) bool { - return cs[i].GetID() < cs[j].GetID() - }) - - root, leafs, err := buildWithContent(cs, t) - if err != nil { - return nil, err - } - t.Root = root - t.Leafs = leafs - t.merkleRoot = root.Hash - return t, nil -} - -func (m *MerkleTree) ToArrayJSONB() []interface{} { - result := make([]interface{}, len(m.Leafs)) - - for idx, leaf := range m.Leafs { - result[idx] = leaf.C.ToJSONB() - } - - return result -} - -func (m *MerkleTree) GetHexProof(content Content) ([]string, error) { - for _, current := range m.Leafs { - ok, err := current.C.Equals(content) - if err != nil { - return nil, err - } - - if ok { - currentParent := current.Parent - var proof []string - for currentParent != nil { - if bytes.Equal(currentParent.Left.Hash, current.Hash) { - rightHash := currentParent.Right.Hash - if rightHash != nil { - proof = append(proof, toHex(rightHash)) - } - } else { - proof = append(proof, toHex(currentParent.Left.Hash)) - } - current = currentParent - currentParent = currentParent.Parent - } - return proof, nil - } - } - return nil, nil -} - -func buildWithContent(cs []Content, t *MerkleTree) (*Node, []*Node, error) { - if len(cs) == 0 { - return nil, nil, errors.New("error: cannot construct tree with no content") - } - var leafs []*Node - - for _, c := range cs { - hash, err := c.CalculateHash() - if err != nil { - return nil, nil, err - } - - leafs = append(leafs, &Node{ - Hash: hash, - C: c, - leaf: true, - Tree: t, - }) - } - - root, err := buildIntermediate(leafs, t) - if err != nil { - return nil, nil, err - } - - return root, leafs, nil -} - -func buildIntermediate(nl []*Node, t *MerkleTree) (*Node, error) { - var nodes []*Node - - for i := 0; i < len(nl); i += 2 { - h := t.hashStrategy() - - leftNode := nl[i] - rightNode := &Node{} - mergedHash := leftNode.Hash - - if i+1 < len(nl) { - rightNode = nl[i+1] - chash := sortAppend(t.sort, leftNode.Hash, rightNode.Hash) - if _, err := h.Write(chash); err != nil { - return nil, err - } - mergedHash = h.Sum(nil) - } - - n := &Node{ - Left: leftNode, - Right: rightNode, - Hash: mergedHash, - Tree: t, - } - nodes = append(nodes, n) - leftNode.Parent = n - rightNode.Parent = n - - if len(nl) == 2 || len(nl) == 1 { - return n, nil - } - } - return buildIntermediate(nodes, t) -} - -// GetRoot returns the unverified Merkle Root (hash of the root node) of the tree. -func (m *MerkleTree) GetRoot() []byte { - return m.merkleRoot -} - -func (m *MerkleTree) GetHexRoot() string { - return toHex(m.GetRoot()) -} +package merkletree + +import ( + "bytes" + "encoding/hex" + "fmt" + "hash" + "math/big" + "sort" + + "github.com/pkg/errors" + + "golang.org/x/crypto/sha3" +) + +// @yo1110: +// Teritori Version of Merkle tree. Support: +// - keccak256 +// - sort pairs +// - do not duplicate impair node + +type Content interface { + CalculateHash() ([]byte, error) + Equals(other Content) (bool, error) + ToJSONB() interface{} + GetID() string +} + +type MerkleTree struct { + Root *Node + merkleRoot []byte + Leafs []*Node + hashStrategy func() hash.Hash + sort bool +} + +type Node struct { + Tree *MerkleTree + Parent *Node + Left *Node + Right *Node + leaf bool + Hash []byte + C Content +} + +// sortAppend sort and append the nodes to be compatible with OpenZepplin libraries +// https://github.com/OpenZeppelin/openzeppelin-contracts-ethereum-package/blob/master/contracts/cryptography/MerkleProof.sol +func sortAppend(sort bool, a, b []byte) []byte { + if !sort { + return append(a, b...) + } + var aBig, bBig big.Int + aBig.SetBytes(a) + bBig.SetBytes(b) + if aBig.Cmp(&bBig) == -1 { + return append(a, b...) + } + return append(b, a...) +} + +func toHex(bytesValue []byte) string { + return fmt.Sprintf("0x%s", hex.EncodeToString(bytesValue)) +} + +func New(cs []Content) (*MerkleTree, error) { + t := &MerkleTree{ + hashStrategy: sha3.NewLegacyKeccak256, + sort: true, + } + + // Sort Nodes to ensure that we have always the same tree from a given set of data + sort.Slice(cs, func(i, j int) bool { + return cs[i].GetID() < cs[j].GetID() + }) + + root, leafs, err := buildWithContent(cs, t) + if err != nil { + return nil, err + } + t.Root = root + t.Leafs = leafs + t.merkleRoot = root.Hash + return t, nil +} + +func (m *MerkleTree) ToArrayJSONB() []interface{} { + result := make([]interface{}, len(m.Leafs)) + + for idx, leaf := range m.Leafs { + result[idx] = leaf.C.ToJSONB() + } + + return result +} + +func (m *MerkleTree) GetHexProof(content Content) ([]string, error) { + for _, current := range m.Leafs { + ok, err := current.C.Equals(content) + if err != nil { + return nil, err + } + + if ok { + currentParent := current.Parent + var proof []string + for currentParent != nil { + if bytes.Equal(currentParent.Left.Hash, current.Hash) { + rightHash := currentParent.Right.Hash + if rightHash != nil { + proof = append(proof, toHex(rightHash)) + } + } else { + proof = append(proof, toHex(currentParent.Left.Hash)) + } + current = currentParent + currentParent = currentParent.Parent + } + return proof, nil + } + } + return nil, nil +} + +func buildWithContent(cs []Content, t *MerkleTree) (*Node, []*Node, error) { + if len(cs) == 0 { + return nil, nil, errors.New("error: cannot construct tree with no content") + } + var leafs []*Node + + for _, c := range cs { + hash, err := c.CalculateHash() + if err != nil { + return nil, nil, err + } + + leafs = append(leafs, &Node{ + Hash: hash, + C: c, + leaf: true, + Tree: t, + }) + } + + root, err := buildIntermediate(leafs, t) + if err != nil { + return nil, nil, err + } + + return root, leafs, nil +} + +func buildIntermediate(nl []*Node, t *MerkleTree) (*Node, error) { + var nodes []*Node + + for i := 0; i < len(nl); i += 2 { + h := t.hashStrategy() + + leftNode := nl[i] + rightNode := &Node{} + mergedHash := leftNode.Hash + + if i+1 < len(nl) { + rightNode = nl[i+1] + chash := sortAppend(t.sort, leftNode.Hash, rightNode.Hash) + if _, err := h.Write(chash); err != nil { + return nil, err + } + mergedHash = h.Sum(nil) + } + + n := &Node{ + Left: leftNode, + Right: rightNode, + Hash: mergedHash, + Tree: t, + } + nodes = append(nodes, n) + leftNode.Parent = n + rightNode.Parent = n + + if len(nl) == 2 || len(nl) == 1 { + return n, nil + } + } + return buildIntermediate(nodes, t) +} + +// GetRoot returns the unverified Merkle Root (hash of the root node) of the tree. +func (m *MerkleTree) GetRoot() []byte { + return m.merkleRoot +} + +func (m *MerkleTree) GetHexRoot() string { + return toHex(m.GetRoot()) +} + +func (m *MerkleTree) GetHexRootWithoutPrefix() string { + return m.GetHexRoot()[2:] +} +func (m *MerkleTree) GetHexProofWithoutPrefix(content Content) ([]string, error) { + proof, err := m.GetHexProof(content) + if err != nil { + return nil, errors.Wrap(err, "failed to get prefixed hex proof") + } + var proofWithoutPrefix []string + for _, item := range proof { + proofWithoutPrefix = append(proofWithoutPrefix, item[2:]) + } + return proofWithoutPrefix, nil +} diff --git a/go/pkg/networks/features.gen.go b/go/pkg/networks/features.gen.go index 07bcc8646a..b48ee855c2 100644 --- a/go/pkg/networks/features.gen.go +++ b/go/pkg/networks/features.gen.go @@ -2,6 +2,7 @@ package networks import ( "encoding/json" + "github.com/pkg/errors" ) diff --git a/launchpad.db b/launchpad.db new file mode 100644 index 0000000000..5ae14e2d28 Binary files /dev/null and b/launchpad.db differ diff --git a/networks.json b/networks.json index 2b6da64e2b..19652a0aea 100644 --- a/networks.json +++ b/networks.json @@ -11617,6 +11617,14 @@ "type": "CosmWasmNFTsBurner", "burnerContractAddress": "tori1qyl0j7a24amk8k8gcmvv07y2zjx7nkcwpk73js24euh64hkja6esd2p2xp" }, + { + "type": "NFTLaunchpad", + "launchpadContractAddress": "tori1te7ku4de0k4rl8lhyuvpmhygaq7k7vy5zpsjq9y638efxsnx3aaqck2utf", + "launchpadEndpoint": "https://dapp-backend.testnet.teritori.com", + "nftTr721CodeId": 60, + "codeId": 71, + "defaultMintDenom": "utori" + }, { "type": "NFTMarketplace", "cwAddressListCodeId": 63, @@ -11679,7 +11687,7 @@ "nameServiceDefaultImage": "ipfs://bafkreieqcwmjcb64r42ygs6a4dswz63djzgayjn3rhzjber3e42cknawlm", "nameServiceTLD": ".tori", "vaultContractAddress": "tori1wn625s4jcmvk0szpl85rj5azkfc6suyvf75q6vrddscjdphtve8s2f76jm", - "riotContractAddressGen0": "tori1r8raaqul4j05qtn0t05603mgquxfl8e9p7kcf7smwzcv2hc5rrlq0vket0", + "riotContractAddressGen0": "tori1hzz0s0ucrhdp6tue2lxk3c03nj6f60qy463we7lgx0wudd72ctmstg4wkc", "riotContractAddressGen1": "", "riotSquadStakingContractAddressV1": "", "riotSquadStakingContractAddressV2": "", @@ -11687,7 +11695,7 @@ "secondaryDuringMintList": [ "", "tori1436kxs0w2es6xlqpp9rd35e3d0cjnw4sv8j3a7483sgks29jqwgsjscd88", - "tori1r8raaqul4j05qtn0t05603mgquxfl8e9p7kcf7smwzcv2hc5rrlq0vket0", + "tori1hzz0s0ucrhdp6tue2lxk3c03nj6f60qy463we7lgx0wudd72ctmstg4wkc", "" ], "excludeFromLaunchpadList": [ diff --git a/package.json b/package.json index e8ed206254..a83a7b7e41 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,7 @@ "graphql-request": "^5", "html-to-draftjs": "^1.5.0", "immutable": "^4.0.0", + "keccak256": "^1.0.6", "kubernetes-models": "^4.3.1", "leaflet": "^1.9.4", "leaflet.markercluster": "^1.5.3", diff --git a/packages/api/launchpad/v1/launchpad.ts b/packages/api/launchpad/v1/launchpad.ts new file mode 100644 index 0000000000..b120bc9fee --- /dev/null +++ b/packages/api/launchpad/v1/launchpad.ts @@ -0,0 +1,2535 @@ +/* eslint-disable */ +import { grpc } from "@improbable-eng/grpc-web"; +import { BrowserHeaders } from "browser-headers"; +import Long from "long"; +import _m0 from "protobufjs/minimal"; + +export const protobufPackage = "launchpad.v1"; + +export enum Sort { + SORT_UNSPECIFIED = 0, + SORT_COLLECTION_NAME = 1, + UNRECOGNIZED = -1, +} + +export function sortFromJSON(object: any): Sort { + switch (object) { + case 0: + case "SORT_UNSPECIFIED": + return Sort.SORT_UNSPECIFIED; + case 1: + case "SORT_COLLECTION_NAME": + return Sort.SORT_COLLECTION_NAME; + case -1: + case "UNRECOGNIZED": + default: + return Sort.UNRECOGNIZED; + } +} + +export function sortToJSON(object: Sort): string { + switch (object) { + case Sort.SORT_UNSPECIFIED: + return "SORT_UNSPECIFIED"; + case Sort.SORT_COLLECTION_NAME: + return "SORT_COLLECTION_NAME"; + case Sort.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + +export enum SortDirection { + SORT_DIRECTION_UNSPECIFIED = 0, + SORT_DIRECTION_ASCENDING = 1, + SORT_DIRECTION_DESCENDING = 2, + UNRECOGNIZED = -1, +} + +export function sortDirectionFromJSON(object: any): SortDirection { + switch (object) { + case 0: + case "SORT_DIRECTION_UNSPECIFIED": + return SortDirection.SORT_DIRECTION_UNSPECIFIED; + case 1: + case "SORT_DIRECTION_ASCENDING": + return SortDirection.SORT_DIRECTION_ASCENDING; + case 2: + case "SORT_DIRECTION_DESCENDING": + return SortDirection.SORT_DIRECTION_DESCENDING; + case -1: + case "UNRECOGNIZED": + default: + return SortDirection.UNRECOGNIZED; + } +} + +export function sortDirectionToJSON(object: SortDirection): string { + switch (object) { + case SortDirection.SORT_DIRECTION_UNSPECIFIED: + return "SORT_DIRECTION_UNSPECIFIED"; + case SortDirection.SORT_DIRECTION_ASCENDING: + return "SORT_DIRECTION_ASCENDING"; + case SortDirection.SORT_DIRECTION_DESCENDING: + return "SORT_DIRECTION_DESCENDING"; + case SortDirection.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + +export enum Status { + STATUS_UNSPECIFIED = 0, + STATUS_INCOMPLETE = 1, + STATUS_COMPLETE = 2, + STATUS_REVIEWING = 3, + STATUS_CONFIRMED = 4, + UNRECOGNIZED = -1, +} + +export function statusFromJSON(object: any): Status { + switch (object) { + case 0: + case "STATUS_UNSPECIFIED": + return Status.STATUS_UNSPECIFIED; + case 1: + case "STATUS_INCOMPLETE": + return Status.STATUS_INCOMPLETE; + case 2: + case "STATUS_COMPLETE": + return Status.STATUS_COMPLETE; + case 3: + case "STATUS_REVIEWING": + return Status.STATUS_REVIEWING; + case 4: + case "STATUS_CONFIRMED": + return Status.STATUS_CONFIRMED; + case -1: + case "UNRECOGNIZED": + default: + return Status.UNRECOGNIZED; + } +} + +export function statusToJSON(object: Status): string { + switch (object) { + case Status.STATUS_UNSPECIFIED: + return "STATUS_UNSPECIFIED"; + case Status.STATUS_INCOMPLETE: + return "STATUS_INCOMPLETE"; + case Status.STATUS_COMPLETE: + return "STATUS_COMPLETE"; + case Status.STATUS_REVIEWING: + return "STATUS_REVIEWING"; + case Status.STATUS_CONFIRMED: + return "STATUS_CONFIRMED"; + case Status.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + +export interface LaunchpadProjectsByCreatorRequest { + creatorId: string; + networkId: string; + limit: number; + offset: number; + sort: Sort; + sortDirection: SortDirection; + status?: Status | undefined; +} + +export interface LaunchpadProjectsByCreatorResponse { + projects: LaunchpadProject[]; +} + +export interface LaunchpadProjectsRequest { + networkId: string; + limit: number; + offset: number; + sort: Sort; + sortDirection: SortDirection; + status?: Status | undefined; +} + +export interface LaunchpadProjectsResponse { + projects: LaunchpadProject[]; +} + +export interface LaunchpadProjectByIdRequest { + networkId: string; + projectId: string; +} + +export interface LaunchpadProjectByIdResponse { + project: LaunchpadProject | undefined; +} + +export interface UploadMetadatasRequest { + sender: string; + networkId: string; + projectId: string; + metadatas: Metadata[]; + pinataJwt?: string | undefined; +} + +export interface UploadMetadatasResponse { + merkleRoot: string; +} + +export interface CalculateCollectionMerkleRootRequest { + sender: string; + metadatas: Metadata[]; +} + +export interface CalculateCollectionMerkleRootResponse { + merkleRoot: string; +} + +export interface TokenMetadataRequest { + sender: string; + networkId: string; + projectId: string; + tokenId: number; +} + +export interface TokenMetadataResponse { + merkleRoot: string; + metadata: Metadata | undefined; + merkleProof: string[]; +} + +export interface LaunchpadProjectsCountsRequest { + networkId: string; +} + +export interface LaunchpadProjectsCountsResponse { + statusCounts: StatusCount[]; +} + +export interface ProposeApproveProjectRequest { + sender: string; + networkId: string; + projectId: string; + proposalId: string; +} + +export interface ProposeApproveProjectResponse { + approved: boolean; +} + +export interface StatusCount { + status: Status; + count: number; +} + +export interface LaunchpadProject { + id: string; + networkId: string; + creatorId: string; + collectionData: string; + status: Status; + proposalId?: string | undefined; +} + +export interface Metadata { + image?: string | undefined; + imageData?: string | undefined; + externalUrl?: string | undefined; + description?: string | undefined; + name?: string | undefined; + attributes: Trait[]; + backgroundColor?: string | undefined; + animationUrl?: string | undefined; + youtubeUrl?: string | undefined; + royaltyPercentage?: number | undefined; + royaltyPaymentAddress?: string | undefined; +} + +export interface Trait { + displayType?: string | undefined; + traitType: string; + value: string; +} + +function createBaseLaunchpadProjectsByCreatorRequest(): LaunchpadProjectsByCreatorRequest { + return { creatorId: "", networkId: "", limit: 0, offset: 0, sort: 0, sortDirection: 0, status: undefined }; +} + +export const LaunchpadProjectsByCreatorRequest = { + encode(message: LaunchpadProjectsByCreatorRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.creatorId !== "") { + writer.uint32(10).string(message.creatorId); + } + if (message.networkId !== "") { + writer.uint32(18).string(message.networkId); + } + if (message.limit !== 0) { + writer.uint32(24).int32(message.limit); + } + if (message.offset !== 0) { + writer.uint32(32).int32(message.offset); + } + if (message.sort !== 0) { + writer.uint32(40).int32(message.sort); + } + if (message.sortDirection !== 0) { + writer.uint32(48).int32(message.sortDirection); + } + if (message.status !== undefined) { + writer.uint32(56).int32(message.status); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): LaunchpadProjectsByCreatorRequest { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseLaunchpadProjectsByCreatorRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.creatorId = reader.string(); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.networkId = reader.string(); + continue; + case 3: + if (tag !== 24) { + break; + } + + message.limit = reader.int32(); + continue; + case 4: + if (tag !== 32) { + break; + } + + message.offset = reader.int32(); + continue; + case 5: + if (tag !== 40) { + break; + } + + message.sort = reader.int32() as any; + continue; + case 6: + if (tag !== 48) { + break; + } + + message.sortDirection = reader.int32() as any; + continue; + case 7: + if (tag !== 56) { + break; + } + + message.status = reader.int32() as any; + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): LaunchpadProjectsByCreatorRequest { + return { + creatorId: isSet(object.creatorId) ? globalThis.String(object.creatorId) : "", + networkId: isSet(object.networkId) ? globalThis.String(object.networkId) : "", + limit: isSet(object.limit) ? globalThis.Number(object.limit) : 0, + offset: isSet(object.offset) ? globalThis.Number(object.offset) : 0, + sort: isSet(object.sort) ? sortFromJSON(object.sort) : 0, + sortDirection: isSet(object.sortDirection) ? sortDirectionFromJSON(object.sortDirection) : 0, + status: isSet(object.status) ? statusFromJSON(object.status) : undefined, + }; + }, + + toJSON(message: LaunchpadProjectsByCreatorRequest): unknown { + const obj: any = {}; + if (message.creatorId !== "") { + obj.creatorId = message.creatorId; + } + if (message.networkId !== "") { + obj.networkId = message.networkId; + } + if (message.limit !== 0) { + obj.limit = Math.round(message.limit); + } + if (message.offset !== 0) { + obj.offset = Math.round(message.offset); + } + if (message.sort !== 0) { + obj.sort = sortToJSON(message.sort); + } + if (message.sortDirection !== 0) { + obj.sortDirection = sortDirectionToJSON(message.sortDirection); + } + if (message.status !== undefined) { + obj.status = statusToJSON(message.status); + } + return obj; + }, + + create, I>>( + base?: I, + ): LaunchpadProjectsByCreatorRequest { + return LaunchpadProjectsByCreatorRequest.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>( + object: I, + ): LaunchpadProjectsByCreatorRequest { + const message = createBaseLaunchpadProjectsByCreatorRequest(); + message.creatorId = object.creatorId ?? ""; + message.networkId = object.networkId ?? ""; + message.limit = object.limit ?? 0; + message.offset = object.offset ?? 0; + message.sort = object.sort ?? 0; + message.sortDirection = object.sortDirection ?? 0; + message.status = object.status ?? undefined; + return message; + }, +}; + +function createBaseLaunchpadProjectsByCreatorResponse(): LaunchpadProjectsByCreatorResponse { + return { projects: [] }; +} + +export const LaunchpadProjectsByCreatorResponse = { + encode(message: LaunchpadProjectsByCreatorResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + for (const v of message.projects) { + LaunchpadProject.encode(v!, writer.uint32(10).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): LaunchpadProjectsByCreatorResponse { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseLaunchpadProjectsByCreatorResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.projects.push(LaunchpadProject.decode(reader, reader.uint32())); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): LaunchpadProjectsByCreatorResponse { + return { + projects: globalThis.Array.isArray(object?.projects) + ? object.projects.map((e: any) => LaunchpadProject.fromJSON(e)) + : [], + }; + }, + + toJSON(message: LaunchpadProjectsByCreatorResponse): unknown { + const obj: any = {}; + if (message.projects?.length) { + obj.projects = message.projects.map((e) => LaunchpadProject.toJSON(e)); + } + return obj; + }, + + create, I>>( + base?: I, + ): LaunchpadProjectsByCreatorResponse { + return LaunchpadProjectsByCreatorResponse.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>( + object: I, + ): LaunchpadProjectsByCreatorResponse { + const message = createBaseLaunchpadProjectsByCreatorResponse(); + message.projects = object.projects?.map((e) => LaunchpadProject.fromPartial(e)) || []; + return message; + }, +}; + +function createBaseLaunchpadProjectsRequest(): LaunchpadProjectsRequest { + return { networkId: "", limit: 0, offset: 0, sort: 0, sortDirection: 0, status: undefined }; +} + +export const LaunchpadProjectsRequest = { + encode(message: LaunchpadProjectsRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.networkId !== "") { + writer.uint32(10).string(message.networkId); + } + if (message.limit !== 0) { + writer.uint32(16).int32(message.limit); + } + if (message.offset !== 0) { + writer.uint32(24).int32(message.offset); + } + if (message.sort !== 0) { + writer.uint32(32).int32(message.sort); + } + if (message.sortDirection !== 0) { + writer.uint32(40).int32(message.sortDirection); + } + if (message.status !== undefined) { + writer.uint32(48).int32(message.status); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): LaunchpadProjectsRequest { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseLaunchpadProjectsRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.networkId = reader.string(); + continue; + case 2: + if (tag !== 16) { + break; + } + + message.limit = reader.int32(); + continue; + case 3: + if (tag !== 24) { + break; + } + + message.offset = reader.int32(); + continue; + case 4: + if (tag !== 32) { + break; + } + + message.sort = reader.int32() as any; + continue; + case 5: + if (tag !== 40) { + break; + } + + message.sortDirection = reader.int32() as any; + continue; + case 6: + if (tag !== 48) { + break; + } + + message.status = reader.int32() as any; + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): LaunchpadProjectsRequest { + return { + networkId: isSet(object.networkId) ? globalThis.String(object.networkId) : "", + limit: isSet(object.limit) ? globalThis.Number(object.limit) : 0, + offset: isSet(object.offset) ? globalThis.Number(object.offset) : 0, + sort: isSet(object.sort) ? sortFromJSON(object.sort) : 0, + sortDirection: isSet(object.sortDirection) ? sortDirectionFromJSON(object.sortDirection) : 0, + status: isSet(object.status) ? statusFromJSON(object.status) : undefined, + }; + }, + + toJSON(message: LaunchpadProjectsRequest): unknown { + const obj: any = {}; + if (message.networkId !== "") { + obj.networkId = message.networkId; + } + if (message.limit !== 0) { + obj.limit = Math.round(message.limit); + } + if (message.offset !== 0) { + obj.offset = Math.round(message.offset); + } + if (message.sort !== 0) { + obj.sort = sortToJSON(message.sort); + } + if (message.sortDirection !== 0) { + obj.sortDirection = sortDirectionToJSON(message.sortDirection); + } + if (message.status !== undefined) { + obj.status = statusToJSON(message.status); + } + return obj; + }, + + create, I>>(base?: I): LaunchpadProjectsRequest { + return LaunchpadProjectsRequest.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): LaunchpadProjectsRequest { + const message = createBaseLaunchpadProjectsRequest(); + message.networkId = object.networkId ?? ""; + message.limit = object.limit ?? 0; + message.offset = object.offset ?? 0; + message.sort = object.sort ?? 0; + message.sortDirection = object.sortDirection ?? 0; + message.status = object.status ?? undefined; + return message; + }, +}; + +function createBaseLaunchpadProjectsResponse(): LaunchpadProjectsResponse { + return { projects: [] }; +} + +export const LaunchpadProjectsResponse = { + encode(message: LaunchpadProjectsResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + for (const v of message.projects) { + LaunchpadProject.encode(v!, writer.uint32(10).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): LaunchpadProjectsResponse { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseLaunchpadProjectsResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.projects.push(LaunchpadProject.decode(reader, reader.uint32())); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): LaunchpadProjectsResponse { + return { + projects: globalThis.Array.isArray(object?.projects) + ? object.projects.map((e: any) => LaunchpadProject.fromJSON(e)) + : [], + }; + }, + + toJSON(message: LaunchpadProjectsResponse): unknown { + const obj: any = {}; + if (message.projects?.length) { + obj.projects = message.projects.map((e) => LaunchpadProject.toJSON(e)); + } + return obj; + }, + + create, I>>(base?: I): LaunchpadProjectsResponse { + return LaunchpadProjectsResponse.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): LaunchpadProjectsResponse { + const message = createBaseLaunchpadProjectsResponse(); + message.projects = object.projects?.map((e) => LaunchpadProject.fromPartial(e)) || []; + return message; + }, +}; + +function createBaseLaunchpadProjectByIdRequest(): LaunchpadProjectByIdRequest { + return { networkId: "", projectId: "" }; +} + +export const LaunchpadProjectByIdRequest = { + encode(message: LaunchpadProjectByIdRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.networkId !== "") { + writer.uint32(10).string(message.networkId); + } + if (message.projectId !== "") { + writer.uint32(18).string(message.projectId); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): LaunchpadProjectByIdRequest { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseLaunchpadProjectByIdRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.networkId = reader.string(); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.projectId = reader.string(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): LaunchpadProjectByIdRequest { + return { + networkId: isSet(object.networkId) ? globalThis.String(object.networkId) : "", + projectId: isSet(object.projectId) ? globalThis.String(object.projectId) : "", + }; + }, + + toJSON(message: LaunchpadProjectByIdRequest): unknown { + const obj: any = {}; + if (message.networkId !== "") { + obj.networkId = message.networkId; + } + if (message.projectId !== "") { + obj.projectId = message.projectId; + } + return obj; + }, + + create, I>>(base?: I): LaunchpadProjectByIdRequest { + return LaunchpadProjectByIdRequest.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): LaunchpadProjectByIdRequest { + const message = createBaseLaunchpadProjectByIdRequest(); + message.networkId = object.networkId ?? ""; + message.projectId = object.projectId ?? ""; + return message; + }, +}; + +function createBaseLaunchpadProjectByIdResponse(): LaunchpadProjectByIdResponse { + return { project: undefined }; +} + +export const LaunchpadProjectByIdResponse = { + encode(message: LaunchpadProjectByIdResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.project !== undefined) { + LaunchpadProject.encode(message.project, writer.uint32(10).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): LaunchpadProjectByIdResponse { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseLaunchpadProjectByIdResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.project = LaunchpadProject.decode(reader, reader.uint32()); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): LaunchpadProjectByIdResponse { + return { project: isSet(object.project) ? LaunchpadProject.fromJSON(object.project) : undefined }; + }, + + toJSON(message: LaunchpadProjectByIdResponse): unknown { + const obj: any = {}; + if (message.project !== undefined) { + obj.project = LaunchpadProject.toJSON(message.project); + } + return obj; + }, + + create, I>>(base?: I): LaunchpadProjectByIdResponse { + return LaunchpadProjectByIdResponse.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): LaunchpadProjectByIdResponse { + const message = createBaseLaunchpadProjectByIdResponse(); + message.project = (object.project !== undefined && object.project !== null) + ? LaunchpadProject.fromPartial(object.project) + : undefined; + return message; + }, +}; + +function createBaseUploadMetadatasRequest(): UploadMetadatasRequest { + return { sender: "", networkId: "", projectId: "", metadatas: [], pinataJwt: undefined }; +} + +export const UploadMetadatasRequest = { + encode(message: UploadMetadatasRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.sender !== "") { + writer.uint32(10).string(message.sender); + } + if (message.networkId !== "") { + writer.uint32(18).string(message.networkId); + } + if (message.projectId !== "") { + writer.uint32(26).string(message.projectId); + } + for (const v of message.metadatas) { + Metadata.encode(v!, writer.uint32(34).fork()).ldelim(); + } + if (message.pinataJwt !== undefined) { + writer.uint32(42).string(message.pinataJwt); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): UploadMetadatasRequest { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseUploadMetadatasRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.sender = reader.string(); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.networkId = reader.string(); + continue; + case 3: + if (tag !== 26) { + break; + } + + message.projectId = reader.string(); + continue; + case 4: + if (tag !== 34) { + break; + } + + message.metadatas.push(Metadata.decode(reader, reader.uint32())); + continue; + case 5: + if (tag !== 42) { + break; + } + + message.pinataJwt = reader.string(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): UploadMetadatasRequest { + return { + sender: isSet(object.sender) ? globalThis.String(object.sender) : "", + networkId: isSet(object.networkId) ? globalThis.String(object.networkId) : "", + projectId: isSet(object.projectId) ? globalThis.String(object.projectId) : "", + metadatas: globalThis.Array.isArray(object?.metadatas) + ? object.metadatas.map((e: any) => Metadata.fromJSON(e)) + : [], + pinataJwt: isSet(object.pinataJwt) ? globalThis.String(object.pinataJwt) : undefined, + }; + }, + + toJSON(message: UploadMetadatasRequest): unknown { + const obj: any = {}; + if (message.sender !== "") { + obj.sender = message.sender; + } + if (message.networkId !== "") { + obj.networkId = message.networkId; + } + if (message.projectId !== "") { + obj.projectId = message.projectId; + } + if (message.metadatas?.length) { + obj.metadatas = message.metadatas.map((e) => Metadata.toJSON(e)); + } + if (message.pinataJwt !== undefined) { + obj.pinataJwt = message.pinataJwt; + } + return obj; + }, + + create, I>>(base?: I): UploadMetadatasRequest { + return UploadMetadatasRequest.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): UploadMetadatasRequest { + const message = createBaseUploadMetadatasRequest(); + message.sender = object.sender ?? ""; + message.networkId = object.networkId ?? ""; + message.projectId = object.projectId ?? ""; + message.metadatas = object.metadatas?.map((e) => Metadata.fromPartial(e)) || []; + message.pinataJwt = object.pinataJwt ?? undefined; + return message; + }, +}; + +function createBaseUploadMetadatasResponse(): UploadMetadatasResponse { + return { merkleRoot: "" }; +} + +export const UploadMetadatasResponse = { + encode(message: UploadMetadatasResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.merkleRoot !== "") { + writer.uint32(10).string(message.merkleRoot); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): UploadMetadatasResponse { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseUploadMetadatasResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.merkleRoot = reader.string(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): UploadMetadatasResponse { + return { merkleRoot: isSet(object.merkleRoot) ? globalThis.String(object.merkleRoot) : "" }; + }, + + toJSON(message: UploadMetadatasResponse): unknown { + const obj: any = {}; + if (message.merkleRoot !== "") { + obj.merkleRoot = message.merkleRoot; + } + return obj; + }, + + create, I>>(base?: I): UploadMetadatasResponse { + return UploadMetadatasResponse.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): UploadMetadatasResponse { + const message = createBaseUploadMetadatasResponse(); + message.merkleRoot = object.merkleRoot ?? ""; + return message; + }, +}; + +function createBaseCalculateCollectionMerkleRootRequest(): CalculateCollectionMerkleRootRequest { + return { sender: "", metadatas: [] }; +} + +export const CalculateCollectionMerkleRootRequest = { + encode(message: CalculateCollectionMerkleRootRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.sender !== "") { + writer.uint32(10).string(message.sender); + } + for (const v of message.metadatas) { + Metadata.encode(v!, writer.uint32(18).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): CalculateCollectionMerkleRootRequest { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseCalculateCollectionMerkleRootRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.sender = reader.string(); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.metadatas.push(Metadata.decode(reader, reader.uint32())); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): CalculateCollectionMerkleRootRequest { + return { + sender: isSet(object.sender) ? globalThis.String(object.sender) : "", + metadatas: globalThis.Array.isArray(object?.metadatas) + ? object.metadatas.map((e: any) => Metadata.fromJSON(e)) + : [], + }; + }, + + toJSON(message: CalculateCollectionMerkleRootRequest): unknown { + const obj: any = {}; + if (message.sender !== "") { + obj.sender = message.sender; + } + if (message.metadatas?.length) { + obj.metadatas = message.metadatas.map((e) => Metadata.toJSON(e)); + } + return obj; + }, + + create, I>>( + base?: I, + ): CalculateCollectionMerkleRootRequest { + return CalculateCollectionMerkleRootRequest.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>( + object: I, + ): CalculateCollectionMerkleRootRequest { + const message = createBaseCalculateCollectionMerkleRootRequest(); + message.sender = object.sender ?? ""; + message.metadatas = object.metadatas?.map((e) => Metadata.fromPartial(e)) || []; + return message; + }, +}; + +function createBaseCalculateCollectionMerkleRootResponse(): CalculateCollectionMerkleRootResponse { + return { merkleRoot: "" }; +} + +export const CalculateCollectionMerkleRootResponse = { + encode(message: CalculateCollectionMerkleRootResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.merkleRoot !== "") { + writer.uint32(10).string(message.merkleRoot); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): CalculateCollectionMerkleRootResponse { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseCalculateCollectionMerkleRootResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.merkleRoot = reader.string(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): CalculateCollectionMerkleRootResponse { + return { merkleRoot: isSet(object.merkleRoot) ? globalThis.String(object.merkleRoot) : "" }; + }, + + toJSON(message: CalculateCollectionMerkleRootResponse): unknown { + const obj: any = {}; + if (message.merkleRoot !== "") { + obj.merkleRoot = message.merkleRoot; + } + return obj; + }, + + create, I>>( + base?: I, + ): CalculateCollectionMerkleRootResponse { + return CalculateCollectionMerkleRootResponse.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>( + object: I, + ): CalculateCollectionMerkleRootResponse { + const message = createBaseCalculateCollectionMerkleRootResponse(); + message.merkleRoot = object.merkleRoot ?? ""; + return message; + }, +}; + +function createBaseTokenMetadataRequest(): TokenMetadataRequest { + return { sender: "", networkId: "", projectId: "", tokenId: 0 }; +} + +export const TokenMetadataRequest = { + encode(message: TokenMetadataRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.sender !== "") { + writer.uint32(10).string(message.sender); + } + if (message.networkId !== "") { + writer.uint32(18).string(message.networkId); + } + if (message.projectId !== "") { + writer.uint32(26).string(message.projectId); + } + if (message.tokenId !== 0) { + writer.uint32(32).uint32(message.tokenId); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): TokenMetadataRequest { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseTokenMetadataRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.sender = reader.string(); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.networkId = reader.string(); + continue; + case 3: + if (tag !== 26) { + break; + } + + message.projectId = reader.string(); + continue; + case 4: + if (tag !== 32) { + break; + } + + message.tokenId = reader.uint32(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): TokenMetadataRequest { + return { + sender: isSet(object.sender) ? globalThis.String(object.sender) : "", + networkId: isSet(object.networkId) ? globalThis.String(object.networkId) : "", + projectId: isSet(object.projectId) ? globalThis.String(object.projectId) : "", + tokenId: isSet(object.tokenId) ? globalThis.Number(object.tokenId) : 0, + }; + }, + + toJSON(message: TokenMetadataRequest): unknown { + const obj: any = {}; + if (message.sender !== "") { + obj.sender = message.sender; + } + if (message.networkId !== "") { + obj.networkId = message.networkId; + } + if (message.projectId !== "") { + obj.projectId = message.projectId; + } + if (message.tokenId !== 0) { + obj.tokenId = Math.round(message.tokenId); + } + return obj; + }, + + create, I>>(base?: I): TokenMetadataRequest { + return TokenMetadataRequest.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): TokenMetadataRequest { + const message = createBaseTokenMetadataRequest(); + message.sender = object.sender ?? ""; + message.networkId = object.networkId ?? ""; + message.projectId = object.projectId ?? ""; + message.tokenId = object.tokenId ?? 0; + return message; + }, +}; + +function createBaseTokenMetadataResponse(): TokenMetadataResponse { + return { merkleRoot: "", metadata: undefined, merkleProof: [] }; +} + +export const TokenMetadataResponse = { + encode(message: TokenMetadataResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.merkleRoot !== "") { + writer.uint32(10).string(message.merkleRoot); + } + if (message.metadata !== undefined) { + Metadata.encode(message.metadata, writer.uint32(18).fork()).ldelim(); + } + for (const v of message.merkleProof) { + writer.uint32(26).string(v!); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): TokenMetadataResponse { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseTokenMetadataResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.merkleRoot = reader.string(); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.metadata = Metadata.decode(reader, reader.uint32()); + continue; + case 3: + if (tag !== 26) { + break; + } + + message.merkleProof.push(reader.string()); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): TokenMetadataResponse { + return { + merkleRoot: isSet(object.merkleRoot) ? globalThis.String(object.merkleRoot) : "", + metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined, + merkleProof: globalThis.Array.isArray(object?.merkleProof) + ? object.merkleProof.map((e: any) => globalThis.String(e)) + : [], + }; + }, + + toJSON(message: TokenMetadataResponse): unknown { + const obj: any = {}; + if (message.merkleRoot !== "") { + obj.merkleRoot = message.merkleRoot; + } + if (message.metadata !== undefined) { + obj.metadata = Metadata.toJSON(message.metadata); + } + if (message.merkleProof?.length) { + obj.merkleProof = message.merkleProof; + } + return obj; + }, + + create, I>>(base?: I): TokenMetadataResponse { + return TokenMetadataResponse.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): TokenMetadataResponse { + const message = createBaseTokenMetadataResponse(); + message.merkleRoot = object.merkleRoot ?? ""; + message.metadata = (object.metadata !== undefined && object.metadata !== null) + ? Metadata.fromPartial(object.metadata) + : undefined; + message.merkleProof = object.merkleProof?.map((e) => e) || []; + return message; + }, +}; + +function createBaseLaunchpadProjectsCountsRequest(): LaunchpadProjectsCountsRequest { + return { networkId: "" }; +} + +export const LaunchpadProjectsCountsRequest = { + encode(message: LaunchpadProjectsCountsRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.networkId !== "") { + writer.uint32(10).string(message.networkId); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): LaunchpadProjectsCountsRequest { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseLaunchpadProjectsCountsRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.networkId = reader.string(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): LaunchpadProjectsCountsRequest { + return { networkId: isSet(object.networkId) ? globalThis.String(object.networkId) : "" }; + }, + + toJSON(message: LaunchpadProjectsCountsRequest): unknown { + const obj: any = {}; + if (message.networkId !== "") { + obj.networkId = message.networkId; + } + return obj; + }, + + create, I>>(base?: I): LaunchpadProjectsCountsRequest { + return LaunchpadProjectsCountsRequest.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>( + object: I, + ): LaunchpadProjectsCountsRequest { + const message = createBaseLaunchpadProjectsCountsRequest(); + message.networkId = object.networkId ?? ""; + return message; + }, +}; + +function createBaseLaunchpadProjectsCountsResponse(): LaunchpadProjectsCountsResponse { + return { statusCounts: [] }; +} + +export const LaunchpadProjectsCountsResponse = { + encode(message: LaunchpadProjectsCountsResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + for (const v of message.statusCounts) { + StatusCount.encode(v!, writer.uint32(10).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): LaunchpadProjectsCountsResponse { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseLaunchpadProjectsCountsResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.statusCounts.push(StatusCount.decode(reader, reader.uint32())); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): LaunchpadProjectsCountsResponse { + return { + statusCounts: globalThis.Array.isArray(object?.statusCounts) + ? object.statusCounts.map((e: any) => StatusCount.fromJSON(e)) + : [], + }; + }, + + toJSON(message: LaunchpadProjectsCountsResponse): unknown { + const obj: any = {}; + if (message.statusCounts?.length) { + obj.statusCounts = message.statusCounts.map((e) => StatusCount.toJSON(e)); + } + return obj; + }, + + create, I>>(base?: I): LaunchpadProjectsCountsResponse { + return LaunchpadProjectsCountsResponse.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>( + object: I, + ): LaunchpadProjectsCountsResponse { + const message = createBaseLaunchpadProjectsCountsResponse(); + message.statusCounts = object.statusCounts?.map((e) => StatusCount.fromPartial(e)) || []; + return message; + }, +}; + +function createBaseProposeApproveProjectRequest(): ProposeApproveProjectRequest { + return { sender: "", networkId: "", projectId: "", proposalId: "" }; +} + +export const ProposeApproveProjectRequest = { + encode(message: ProposeApproveProjectRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.sender !== "") { + writer.uint32(10).string(message.sender); + } + if (message.networkId !== "") { + writer.uint32(18).string(message.networkId); + } + if (message.projectId !== "") { + writer.uint32(26).string(message.projectId); + } + if (message.proposalId !== "") { + writer.uint32(34).string(message.proposalId); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): ProposeApproveProjectRequest { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseProposeApproveProjectRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.sender = reader.string(); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.networkId = reader.string(); + continue; + case 3: + if (tag !== 26) { + break; + } + + message.projectId = reader.string(); + continue; + case 4: + if (tag !== 34) { + break; + } + + message.proposalId = reader.string(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): ProposeApproveProjectRequest { + return { + sender: isSet(object.sender) ? globalThis.String(object.sender) : "", + networkId: isSet(object.networkId) ? globalThis.String(object.networkId) : "", + projectId: isSet(object.projectId) ? globalThis.String(object.projectId) : "", + proposalId: isSet(object.proposalId) ? globalThis.String(object.proposalId) : "", + }; + }, + + toJSON(message: ProposeApproveProjectRequest): unknown { + const obj: any = {}; + if (message.sender !== "") { + obj.sender = message.sender; + } + if (message.networkId !== "") { + obj.networkId = message.networkId; + } + if (message.projectId !== "") { + obj.projectId = message.projectId; + } + if (message.proposalId !== "") { + obj.proposalId = message.proposalId; + } + return obj; + }, + + create, I>>(base?: I): ProposeApproveProjectRequest { + return ProposeApproveProjectRequest.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): ProposeApproveProjectRequest { + const message = createBaseProposeApproveProjectRequest(); + message.sender = object.sender ?? ""; + message.networkId = object.networkId ?? ""; + message.projectId = object.projectId ?? ""; + message.proposalId = object.proposalId ?? ""; + return message; + }, +}; + +function createBaseProposeApproveProjectResponse(): ProposeApproveProjectResponse { + return { approved: false }; +} + +export const ProposeApproveProjectResponse = { + encode(message: ProposeApproveProjectResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.approved === true) { + writer.uint32(8).bool(message.approved); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): ProposeApproveProjectResponse { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseProposeApproveProjectResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 8) { + break; + } + + message.approved = reader.bool(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): ProposeApproveProjectResponse { + return { approved: isSet(object.approved) ? globalThis.Boolean(object.approved) : false }; + }, + + toJSON(message: ProposeApproveProjectResponse): unknown { + const obj: any = {}; + if (message.approved === true) { + obj.approved = message.approved; + } + return obj; + }, + + create, I>>(base?: I): ProposeApproveProjectResponse { + return ProposeApproveProjectResponse.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>( + object: I, + ): ProposeApproveProjectResponse { + const message = createBaseProposeApproveProjectResponse(); + message.approved = object.approved ?? false; + return message; + }, +}; + +function createBaseStatusCount(): StatusCount { + return { status: 0, count: 0 }; +} + +export const StatusCount = { + encode(message: StatusCount, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.status !== 0) { + writer.uint32(8).int32(message.status); + } + if (message.count !== 0) { + writer.uint32(16).uint32(message.count); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): StatusCount { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseStatusCount(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 8) { + break; + } + + message.status = reader.int32() as any; + continue; + case 2: + if (tag !== 16) { + break; + } + + message.count = reader.uint32(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): StatusCount { + return { + status: isSet(object.status) ? statusFromJSON(object.status) : 0, + count: isSet(object.count) ? globalThis.Number(object.count) : 0, + }; + }, + + toJSON(message: StatusCount): unknown { + const obj: any = {}; + if (message.status !== 0) { + obj.status = statusToJSON(message.status); + } + if (message.count !== 0) { + obj.count = Math.round(message.count); + } + return obj; + }, + + create, I>>(base?: I): StatusCount { + return StatusCount.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): StatusCount { + const message = createBaseStatusCount(); + message.status = object.status ?? 0; + message.count = object.count ?? 0; + return message; + }, +}; + +function createBaseLaunchpadProject(): LaunchpadProject { + return { id: "", networkId: "", creatorId: "", collectionData: "", status: 0, proposalId: undefined }; +} + +export const LaunchpadProject = { + encode(message: LaunchpadProject, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.networkId !== "") { + writer.uint32(18).string(message.networkId); + } + if (message.creatorId !== "") { + writer.uint32(26).string(message.creatorId); + } + if (message.collectionData !== "") { + writer.uint32(34).string(message.collectionData); + } + if (message.status !== 0) { + writer.uint32(40).int32(message.status); + } + if (message.proposalId !== undefined) { + writer.uint32(50).string(message.proposalId); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): LaunchpadProject { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseLaunchpadProject(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.networkId = reader.string(); + continue; + case 3: + if (tag !== 26) { + break; + } + + message.creatorId = reader.string(); + continue; + case 4: + if (tag !== 34) { + break; + } + + message.collectionData = reader.string(); + continue; + case 5: + if (tag !== 40) { + break; + } + + message.status = reader.int32() as any; + continue; + case 6: + if (tag !== 50) { + break; + } + + message.proposalId = reader.string(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): LaunchpadProject { + return { + id: isSet(object.id) ? globalThis.String(object.id) : "", + networkId: isSet(object.networkId) ? globalThis.String(object.networkId) : "", + creatorId: isSet(object.creatorId) ? globalThis.String(object.creatorId) : "", + collectionData: isSet(object.collectionData) ? globalThis.String(object.collectionData) : "", + status: isSet(object.status) ? statusFromJSON(object.status) : 0, + proposalId: isSet(object.proposalId) ? globalThis.String(object.proposalId) : undefined, + }; + }, + + toJSON(message: LaunchpadProject): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + if (message.networkId !== "") { + obj.networkId = message.networkId; + } + if (message.creatorId !== "") { + obj.creatorId = message.creatorId; + } + if (message.collectionData !== "") { + obj.collectionData = message.collectionData; + } + if (message.status !== 0) { + obj.status = statusToJSON(message.status); + } + if (message.proposalId !== undefined) { + obj.proposalId = message.proposalId; + } + return obj; + }, + + create, I>>(base?: I): LaunchpadProject { + return LaunchpadProject.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): LaunchpadProject { + const message = createBaseLaunchpadProject(); + message.id = object.id ?? ""; + message.networkId = object.networkId ?? ""; + message.creatorId = object.creatorId ?? ""; + message.collectionData = object.collectionData ?? ""; + message.status = object.status ?? 0; + message.proposalId = object.proposalId ?? undefined; + return message; + }, +}; + +function createBaseMetadata(): Metadata { + return { + image: undefined, + imageData: undefined, + externalUrl: undefined, + description: undefined, + name: undefined, + attributes: [], + backgroundColor: undefined, + animationUrl: undefined, + youtubeUrl: undefined, + royaltyPercentage: undefined, + royaltyPaymentAddress: undefined, + }; +} + +export const Metadata = { + encode(message: Metadata, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.image !== undefined) { + writer.uint32(10).string(message.image); + } + if (message.imageData !== undefined) { + writer.uint32(18).string(message.imageData); + } + if (message.externalUrl !== undefined) { + writer.uint32(26).string(message.externalUrl); + } + if (message.description !== undefined) { + writer.uint32(34).string(message.description); + } + if (message.name !== undefined) { + writer.uint32(42).string(message.name); + } + for (const v of message.attributes) { + Trait.encode(v!, writer.uint32(50).fork()).ldelim(); + } + if (message.backgroundColor !== undefined) { + writer.uint32(58).string(message.backgroundColor); + } + if (message.animationUrl !== undefined) { + writer.uint32(66).string(message.animationUrl); + } + if (message.youtubeUrl !== undefined) { + writer.uint32(74).string(message.youtubeUrl); + } + if (message.royaltyPercentage !== undefined) { + writer.uint32(80).uint64(message.royaltyPercentage); + } + if (message.royaltyPaymentAddress !== undefined) { + writer.uint32(90).string(message.royaltyPaymentAddress); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): Metadata { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseMetadata(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.image = reader.string(); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.imageData = reader.string(); + continue; + case 3: + if (tag !== 26) { + break; + } + + message.externalUrl = reader.string(); + continue; + case 4: + if (tag !== 34) { + break; + } + + message.description = reader.string(); + continue; + case 5: + if (tag !== 42) { + break; + } + + message.name = reader.string(); + continue; + case 6: + if (tag !== 50) { + break; + } + + message.attributes.push(Trait.decode(reader, reader.uint32())); + continue; + case 7: + if (tag !== 58) { + break; + } + + message.backgroundColor = reader.string(); + continue; + case 8: + if (tag !== 66) { + break; + } + + message.animationUrl = reader.string(); + continue; + case 9: + if (tag !== 74) { + break; + } + + message.youtubeUrl = reader.string(); + continue; + case 10: + if (tag !== 80) { + break; + } + + message.royaltyPercentage = longToNumber(reader.uint64() as Long); + continue; + case 11: + if (tag !== 90) { + break; + } + + message.royaltyPaymentAddress = reader.string(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): Metadata { + return { + image: isSet(object.image) ? globalThis.String(object.image) : undefined, + imageData: isSet(object.imageData) ? globalThis.String(object.imageData) : undefined, + externalUrl: isSet(object.externalUrl) ? globalThis.String(object.externalUrl) : undefined, + description: isSet(object.description) ? globalThis.String(object.description) : undefined, + name: isSet(object.name) ? globalThis.String(object.name) : undefined, + attributes: globalThis.Array.isArray(object?.attributes) + ? object.attributes.map((e: any) => Trait.fromJSON(e)) + : [], + backgroundColor: isSet(object.backgroundColor) ? globalThis.String(object.backgroundColor) : undefined, + animationUrl: isSet(object.animationUrl) ? globalThis.String(object.animationUrl) : undefined, + youtubeUrl: isSet(object.youtubeUrl) ? globalThis.String(object.youtubeUrl) : undefined, + royaltyPercentage: isSet(object.royaltyPercentage) ? globalThis.Number(object.royaltyPercentage) : undefined, + royaltyPaymentAddress: isSet(object.royaltyPaymentAddress) + ? globalThis.String(object.royaltyPaymentAddress) + : undefined, + }; + }, + + toJSON(message: Metadata): unknown { + const obj: any = {}; + if (message.image !== undefined) { + obj.image = message.image; + } + if (message.imageData !== undefined) { + obj.imageData = message.imageData; + } + if (message.externalUrl !== undefined) { + obj.externalUrl = message.externalUrl; + } + if (message.description !== undefined) { + obj.description = message.description; + } + if (message.name !== undefined) { + obj.name = message.name; + } + if (message.attributes?.length) { + obj.attributes = message.attributes.map((e) => Trait.toJSON(e)); + } + if (message.backgroundColor !== undefined) { + obj.backgroundColor = message.backgroundColor; + } + if (message.animationUrl !== undefined) { + obj.animationUrl = message.animationUrl; + } + if (message.youtubeUrl !== undefined) { + obj.youtubeUrl = message.youtubeUrl; + } + if (message.royaltyPercentage !== undefined) { + obj.royaltyPercentage = Math.round(message.royaltyPercentage); + } + if (message.royaltyPaymentAddress !== undefined) { + obj.royaltyPaymentAddress = message.royaltyPaymentAddress; + } + return obj; + }, + + create, I>>(base?: I): Metadata { + return Metadata.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): Metadata { + const message = createBaseMetadata(); + message.image = object.image ?? undefined; + message.imageData = object.imageData ?? undefined; + message.externalUrl = object.externalUrl ?? undefined; + message.description = object.description ?? undefined; + message.name = object.name ?? undefined; + message.attributes = object.attributes?.map((e) => Trait.fromPartial(e)) || []; + message.backgroundColor = object.backgroundColor ?? undefined; + message.animationUrl = object.animationUrl ?? undefined; + message.youtubeUrl = object.youtubeUrl ?? undefined; + message.royaltyPercentage = object.royaltyPercentage ?? undefined; + message.royaltyPaymentAddress = object.royaltyPaymentAddress ?? undefined; + return message; + }, +}; + +function createBaseTrait(): Trait { + return { displayType: undefined, traitType: "", value: "" }; +} + +export const Trait = { + encode(message: Trait, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.displayType !== undefined) { + writer.uint32(10).string(message.displayType); + } + if (message.traitType !== "") { + writer.uint32(18).string(message.traitType); + } + if (message.value !== "") { + writer.uint32(26).string(message.value); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): Trait { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseTrait(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.displayType = reader.string(); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.traitType = reader.string(); + continue; + case 3: + if (tag !== 26) { + break; + } + + message.value = reader.string(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): Trait { + return { + displayType: isSet(object.displayType) ? globalThis.String(object.displayType) : undefined, + traitType: isSet(object.traitType) ? globalThis.String(object.traitType) : "", + value: isSet(object.value) ? globalThis.String(object.value) : "", + }; + }, + + toJSON(message: Trait): unknown { + const obj: any = {}; + if (message.displayType !== undefined) { + obj.displayType = message.displayType; + } + if (message.traitType !== "") { + obj.traitType = message.traitType; + } + if (message.value !== "") { + obj.value = message.value; + } + return obj; + }, + + create, I>>(base?: I): Trait { + return Trait.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): Trait { + const message = createBaseTrait(); + message.displayType = object.displayType ?? undefined; + message.traitType = object.traitType ?? ""; + message.value = object.value ?? ""; + return message; + }, +}; + +export interface LaunchpadService { + UploadMetadatas( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise; + CalculateCollectionMerkleRoot( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise; + TokenMetadata(request: DeepPartial, metadata?: grpc.Metadata): Promise; + LaunchpadProjectsByCreator( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise; + LaunchpadProjects( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise; + LaunchpadProjectById( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise; + LaunchpadProjectsCounts( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise; + ProposeApproveProject( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise; +} + +export class LaunchpadServiceClientImpl implements LaunchpadService { + private readonly rpc: Rpc; + + constructor(rpc: Rpc) { + this.rpc = rpc; + this.UploadMetadatas = this.UploadMetadatas.bind(this); + this.CalculateCollectionMerkleRoot = this.CalculateCollectionMerkleRoot.bind(this); + this.TokenMetadata = this.TokenMetadata.bind(this); + this.LaunchpadProjectsByCreator = this.LaunchpadProjectsByCreator.bind(this); + this.LaunchpadProjects = this.LaunchpadProjects.bind(this); + this.LaunchpadProjectById = this.LaunchpadProjectById.bind(this); + this.LaunchpadProjectsCounts = this.LaunchpadProjectsCounts.bind(this); + this.ProposeApproveProject = this.ProposeApproveProject.bind(this); + } + + UploadMetadatas( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise { + return this.rpc.unary(LaunchpadServiceUploadMetadatasDesc, UploadMetadatasRequest.fromPartial(request), metadata); + } + + CalculateCollectionMerkleRoot( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise { + return this.rpc.unary( + LaunchpadServiceCalculateCollectionMerkleRootDesc, + CalculateCollectionMerkleRootRequest.fromPartial(request), + metadata, + ); + } + + TokenMetadata(request: DeepPartial, metadata?: grpc.Metadata): Promise { + return this.rpc.unary(LaunchpadServiceTokenMetadataDesc, TokenMetadataRequest.fromPartial(request), metadata); + } + + LaunchpadProjectsByCreator( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise { + return this.rpc.unary( + LaunchpadServiceLaunchpadProjectsByCreatorDesc, + LaunchpadProjectsByCreatorRequest.fromPartial(request), + metadata, + ); + } + + LaunchpadProjects( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise { + return this.rpc.unary( + LaunchpadServiceLaunchpadProjectsDesc, + LaunchpadProjectsRequest.fromPartial(request), + metadata, + ); + } + + LaunchpadProjectById( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise { + return this.rpc.unary( + LaunchpadServiceLaunchpadProjectByIdDesc, + LaunchpadProjectByIdRequest.fromPartial(request), + metadata, + ); + } + + LaunchpadProjectsCounts( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise { + return this.rpc.unary( + LaunchpadServiceLaunchpadProjectsCountsDesc, + LaunchpadProjectsCountsRequest.fromPartial(request), + metadata, + ); + } + + ProposeApproveProject( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise { + return this.rpc.unary( + LaunchpadServiceProposeApproveProjectDesc, + ProposeApproveProjectRequest.fromPartial(request), + metadata, + ); + } +} + +export const LaunchpadServiceDesc = { serviceName: "launchpad.v1.LaunchpadService" }; + +export const LaunchpadServiceUploadMetadatasDesc: UnaryMethodDefinitionish = { + methodName: "UploadMetadatas", + service: LaunchpadServiceDesc, + requestStream: false, + responseStream: false, + requestType: { + serializeBinary() { + return UploadMetadatasRequest.encode(this).finish(); + }, + } as any, + responseType: { + deserializeBinary(data: Uint8Array) { + const value = UploadMetadatasResponse.decode(data); + return { + ...value, + toObject() { + return value; + }, + }; + }, + } as any, +}; + +export const LaunchpadServiceCalculateCollectionMerkleRootDesc: UnaryMethodDefinitionish = { + methodName: "CalculateCollectionMerkleRoot", + service: LaunchpadServiceDesc, + requestStream: false, + responseStream: false, + requestType: { + serializeBinary() { + return CalculateCollectionMerkleRootRequest.encode(this).finish(); + }, + } as any, + responseType: { + deserializeBinary(data: Uint8Array) { + const value = CalculateCollectionMerkleRootResponse.decode(data); + return { + ...value, + toObject() { + return value; + }, + }; + }, + } as any, +}; + +export const LaunchpadServiceTokenMetadataDesc: UnaryMethodDefinitionish = { + methodName: "TokenMetadata", + service: LaunchpadServiceDesc, + requestStream: false, + responseStream: false, + requestType: { + serializeBinary() { + return TokenMetadataRequest.encode(this).finish(); + }, + } as any, + responseType: { + deserializeBinary(data: Uint8Array) { + const value = TokenMetadataResponse.decode(data); + return { + ...value, + toObject() { + return value; + }, + }; + }, + } as any, +}; + +export const LaunchpadServiceLaunchpadProjectsByCreatorDesc: UnaryMethodDefinitionish = { + methodName: "LaunchpadProjectsByCreator", + service: LaunchpadServiceDesc, + requestStream: false, + responseStream: false, + requestType: { + serializeBinary() { + return LaunchpadProjectsByCreatorRequest.encode(this).finish(); + }, + } as any, + responseType: { + deserializeBinary(data: Uint8Array) { + const value = LaunchpadProjectsByCreatorResponse.decode(data); + return { + ...value, + toObject() { + return value; + }, + }; + }, + } as any, +}; + +export const LaunchpadServiceLaunchpadProjectsDesc: UnaryMethodDefinitionish = { + methodName: "LaunchpadProjects", + service: LaunchpadServiceDesc, + requestStream: false, + responseStream: false, + requestType: { + serializeBinary() { + return LaunchpadProjectsRequest.encode(this).finish(); + }, + } as any, + responseType: { + deserializeBinary(data: Uint8Array) { + const value = LaunchpadProjectsResponse.decode(data); + return { + ...value, + toObject() { + return value; + }, + }; + }, + } as any, +}; + +export const LaunchpadServiceLaunchpadProjectByIdDesc: UnaryMethodDefinitionish = { + methodName: "LaunchpadProjectById", + service: LaunchpadServiceDesc, + requestStream: false, + responseStream: false, + requestType: { + serializeBinary() { + return LaunchpadProjectByIdRequest.encode(this).finish(); + }, + } as any, + responseType: { + deserializeBinary(data: Uint8Array) { + const value = LaunchpadProjectByIdResponse.decode(data); + return { + ...value, + toObject() { + return value; + }, + }; + }, + } as any, +}; + +export const LaunchpadServiceLaunchpadProjectsCountsDesc: UnaryMethodDefinitionish = { + methodName: "LaunchpadProjectsCounts", + service: LaunchpadServiceDesc, + requestStream: false, + responseStream: false, + requestType: { + serializeBinary() { + return LaunchpadProjectsCountsRequest.encode(this).finish(); + }, + } as any, + responseType: { + deserializeBinary(data: Uint8Array) { + const value = LaunchpadProjectsCountsResponse.decode(data); + return { + ...value, + toObject() { + return value; + }, + }; + }, + } as any, +}; + +export const LaunchpadServiceProposeApproveProjectDesc: UnaryMethodDefinitionish = { + methodName: "ProposeApproveProject", + service: LaunchpadServiceDesc, + requestStream: false, + responseStream: false, + requestType: { + serializeBinary() { + return ProposeApproveProjectRequest.encode(this).finish(); + }, + } as any, + responseType: { + deserializeBinary(data: Uint8Array) { + const value = ProposeApproveProjectResponse.decode(data); + return { + ...value, + toObject() { + return value; + }, + }; + }, + } as any, +}; + +interface UnaryMethodDefinitionishR extends grpc.UnaryMethodDefinition { + requestStream: any; + responseStream: any; +} + +type UnaryMethodDefinitionish = UnaryMethodDefinitionishR; + +interface Rpc { + unary( + methodDesc: T, + request: any, + metadata: grpc.Metadata | undefined, + ): Promise; +} + +export class GrpcWebImpl { + private host: string; + private options: { + transport?: grpc.TransportFactory; + + debug?: boolean; + metadata?: grpc.Metadata; + upStreamRetryCodes?: number[]; + }; + + constructor( + host: string, + options: { + transport?: grpc.TransportFactory; + + debug?: boolean; + metadata?: grpc.Metadata; + upStreamRetryCodes?: number[]; + }, + ) { + this.host = host; + this.options = options; + } + + unary( + methodDesc: T, + _request: any, + metadata: grpc.Metadata | undefined, + ): Promise { + const request = { ..._request, ...methodDesc.requestType }; + const maybeCombinedMetadata = metadata && this.options.metadata + ? new BrowserHeaders({ ...this.options?.metadata.headersMap, ...metadata?.headersMap }) + : metadata ?? this.options.metadata; + return new Promise((resolve, reject) => { + grpc.unary(methodDesc, { + request, + host: this.host, + metadata: maybeCombinedMetadata ?? {}, + ...(this.options.transport !== undefined ? { transport: this.options.transport } : {}), + debug: this.options.debug ?? false, + onEnd: function (response) { + if (response.status === grpc.Code.OK) { + resolve(response.message!.toObject()); + } else { + const err = new GrpcWebError(response.statusMessage, response.status, response.trailers); + reject(err); + } + }, + }); + }); + } +} + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +type KeysOfUnion = T extends T ? keyof T : never; +export type Exact = P extends Builtin ? P + : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; + +function longToNumber(long: Long): number { + if (long.gt(globalThis.Number.MAX_SAFE_INTEGER)) { + throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); + } + return long.toNumber(); +} + +if (_m0.util.Long !== Long) { + _m0.util.Long = Long as any; + _m0.configure(); +} + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} + +export class GrpcWebError extends globalThis.Error { + constructor(message: string, public code: grpc.Code, public metadata: grpc.Metadata) { + super(message); + } +} diff --git a/packages/components/FilePreview/SelectedFilesPreview/ItemView.tsx b/packages/components/FilePreview/SelectedFilesPreview/ItemView.tsx new file mode 100644 index 0000000000..57ba7f483b --- /dev/null +++ b/packages/components/FilePreview/SelectedFilesPreview/ItemView.tsx @@ -0,0 +1,79 @@ +import React, { FC } from "react"; +import { TouchableOpacity, Image, StyleProp, ViewStyle } from "react-native"; + +import { neutral77, secondaryColor } from "../../../utils/style/colors"; +import { fontSemibold11, fontSemibold13 } from "../../../utils/style/fonts"; +import { layout } from "../../../utils/style/layout"; +import { BrandText } from "../../BrandText"; +import { PrimaryBox } from "../../boxes/PrimaryBox"; + +export const itemSize = 120; +export const ItemView: FC<{ + label: string; + subLabel?: string; + uri: string; + onPress: () => void; + style?: StyleProp; +}> = ({ label, subLabel, uri, onPress, style }) => { + return ( + + + + + + + + {label} + + {subLabel && ( + + {subLabel} + + )} + + + ); +}; diff --git a/packages/components/FilePreview/SelectedFilesPreview/SelectedFilesPreview.tsx b/packages/components/FilePreview/SelectedFilesPreview/SelectedFilesPreview.tsx new file mode 100644 index 0000000000..97b9859393 --- /dev/null +++ b/packages/components/FilePreview/SelectedFilesPreview/SelectedFilesPreview.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { View } from "react-native"; + +import { itemSize, ItemView } from "./ItemView"; +import { neutral33, neutral55 } from "../../../utils/style/colors"; +import { fontSemibold20 } from "../../../utils/style/fonts"; +import { LocalFileData } from "../../../utils/types/files"; +import { BrandText } from "../../BrandText"; +import { PrimaryBox } from "../../boxes/PrimaryBox"; + +import { DeleteButton } from "@/components/FilePreview/DeleteButton"; +import { GridList } from "@/components/layout/GridList"; +import { layout } from "@/utils/style/layout"; + +export const SelectedFilesPreview: React.FC<{ + files: LocalFileData[]; + onPressItem: (file: LocalFileData, itemIndex: number) => void; + onPressDeleteItem: (itemIndex: number) => void; +}> = ({ files, onPressItem, onPressDeleteItem }) => { + return ( + + {files.length ? ( + + data={files} + keyExtractor={(item) => item.url} + renderItem={({ item, index }, elemWidth) => ( + + onPressDeleteItem(index)} + style={{ top: 0, right: 0 }} + /> + { + onPressItem(item, index); + }} + style={{ width: elemWidth }} + /> + + )} + minElemWidth={itemSize} + gap={layout.spacing_x2_5} + noFixedHeight + /> + ) : ( + + + Selected files preview + + + )} + + ); +}; diff --git a/packages/components/NetworkSelector/NetworkSelectorWithLabel.tsx b/packages/components/NetworkSelector/NetworkSelectorWithLabel.tsx new file mode 100644 index 0000000000..0cef927569 --- /dev/null +++ b/packages/components/NetworkSelector/NetworkSelectorWithLabel.tsx @@ -0,0 +1,127 @@ +import React, { useState } from "react"; +import { StyleProp, View, ViewStyle } from "react-native"; + +import { NetworkSelectorMenu } from "./NetworkSelectorMenu"; +import chevronDownSVG from "../../../assets/icons/chevron-down.svg"; +import chevronUpSVG from "../../../assets/icons/chevron-up.svg"; +import { useDropdowns } from "../../hooks/useDropdowns"; +import { useSelectedNetworkInfo } from "../../hooks/useSelectedNetwork"; +import { NetworkFeature, NetworkKind } from "../../networks"; +import { neutral17, neutral77, secondaryColor } from "../../utils/style/colors"; +import { fontSemibold14 } from "../../utils/style/fonts"; +import { layout } from "../../utils/style/layout"; +import { BrandText } from "../BrandText"; +import { SVG } from "../SVG"; +import { TertiaryBox } from "../boxes/TertiaryBox"; +import { Label } from "../inputs/TextInputCustom"; + +import { NetworkIcon } from "@/components/NetworkIcon"; +import { CustomPressable } from "@/components/buttons/CustomPressable"; +import { SpacerRow } from "@/components/spacer"; + +export const NetworkSelectorWithLabel: React.FC<{ + label: string; + style?: StyleProp; + forceNetworkId?: string; + forceNetworkKind?: NetworkKind; + forceNetworkFeatures?: NetworkFeature[]; +}> = ({ + style, + forceNetworkId, + forceNetworkKind, + forceNetworkFeatures, + label, +}) => { + const [isDropdownOpen, setDropdownState, ref] = useDropdowns(); + const [hovered, setHovered] = useState(false); + const selectedNetworkInfo = useSelectedNetworkInfo(); + + return ( + + setHovered(true)} + onHoverOut={() => setHovered(false)} + onPress={() => setDropdownState(!isDropdownOpen)} + > + + + + + + + + + + {selectedNetworkInfo?.displayName} + + + + + + + + {isDropdownOpen && ( + {}} + optionsMenuwidth={416} + style={{ width: "100%", marginTop: layout.spacing_x0_75 }} + forceNetworkId={forceNetworkId} + forceNetworkKind={forceNetworkKind} + forceNetworkFeatures={forceNetworkFeatures} + /> + )} + + + ); +}; diff --git a/packages/components/dao/DAOProposals.tsx b/packages/components/dao/DAOProposals.tsx index 5df9bfe14d..eadb50de3c 100644 --- a/packages/components/dao/DAOProposals.tsx +++ b/packages/components/dao/DAOProposals.tsx @@ -46,10 +46,11 @@ export const DAOProposals: React.FC<{ // TODO: double check we properly use threshold and quorum // TODO: use correct threshold, quorum and total power for passed/executed proposals -const ProposalRow: React.FC<{ +export const ProposalRow: React.FC<{ daoId: string | undefined; proposal: AppProposalResponse; -}> = ({ daoId, proposal }) => { + style?: StyleProp; +}> = ({ daoId, proposal, style }) => { const [network] = parseUserId(daoId); const halfGap = 24; @@ -109,13 +110,16 @@ const ProposalRow: React.FC<{ return ( { component={LaunchpadApplyScreen} options={{ header: () => null, - title: screenTitle("Launchpad (Apply)"), + title: screenTitle("Apply to Launchpad"), + }} + /> + null, + title: screenTitle("Create Collection"), + }} + /> + null, + title: screenTitle("Complete Collection"), + }} + /> + null, + title: screenTitle("My Collections"), }} /> - { title: screenTitle("Launchpad ERC20 Create Sale"), }} /> + null, + title: screenTitle("Launchpad Administration Dashboard"), + }} + /> + null, + title: screenTitle("Launchpad Applications"), + }} + /> + null, + title: screenTitle("Launchpad Ready Applications"), + }} + /> + null, + title: screenTitle("Launchpad Application Review"), + }} + /> {/* ==== Multisig */} { const isSidebarExpanded = useSelector(selectSidebarExpanded); const selectedApps = useSelector(selectCheckedApps); const availableApps = useSelector(selectAvailableApps); + const userId = useSelectedWallet()?.userId; + const { isUserLaunchpadAdmin } = useIsUserLaunchpadAdmin(userId); + const [developerMode] = useDeveloperMode(); const dispatch = useAppDispatch(); // on mobile sidebar is not expanded on load @@ -50,6 +56,7 @@ export const useSidebar = () => { ); } }, [availableApps, dispatch, selectedApps.length]); + const dynamicSidebar = useMemo(() => { const dynamicAppsSelection = [] as { [key: string]: any; @@ -91,21 +98,42 @@ export const useSidebar = () => { return; } - dynamicAppsSelection[element] = SIDEBAR_LIST[option.id] - ? SIDEBAR_LIST[option.id] - : { - id: option.id, - title: option.title, - route: option.route, - url: option.url, - icon: option.icon, - }; + if (SIDEBAR_LIST[option.id]) { + const newOption = cloneDeep(SIDEBAR_LIST[option.id]); + + // Sidebar restriction (Hide items or nested items): + // Launchpad Admin + if ( + !isUserLaunchpadAdmin && + newOption.id === "Launchpad" && + newOption.nested && + has(newOption, "nested.admin") + ) { + delete newOption.nested.admin; + } + + dynamicAppsSelection[element] = newOption; + } else { + dynamicAppsSelection[element] = { + id: option.id, + title: option.title, + route: option.route, + url: option.url, + icon: option.icon, + }; + } }); dynamicAppsSelection["dappstore"] = SIDEBAR_LIST["DAppsStore"]; return dynamicAppsSelection; - }, [forceDAppsList, availableApps, selectedApps, developerMode]); + }, [ + availableApps, + selectedApps, + developerMode, + isUserLaunchpadAdmin, + forceDAppsList, + ]); const toggleSidebar = () => { dispatch(setSidebarExpanded(!isSidebarExpanded)); diff --git a/packages/hooks/launchpad/useCompleteCollection.ts b/packages/hooks/launchpad/useCompleteCollection.ts new file mode 100644 index 0000000000..ad252a07a5 --- /dev/null +++ b/packages/hooks/launchpad/useCompleteCollection.ts @@ -0,0 +1,181 @@ +import { useCallback } from "react"; +import { useSelector } from "react-redux"; + +import { Metadata, Trait } from "@/api/launchpad/v1/launchpad"; +import { useFeedbacks } from "@/context/FeedbacksProvider"; +import { NftLaunchpadClient } from "@/contracts-clients/nft-launchpad"; +import { useIpfs } from "@/hooks/useIpfs"; +import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork"; +import useSelectedWallet from "@/hooks/useSelectedWallet"; +import { getNetworkFeature, NetworkFeature } from "@/networks"; +import { getKeplrSigningCosmWasmClient } from "@/networks/signer"; +import { selectNFTStorageAPI } from "@/store/slices/settings"; +import { mustGetLaunchpadClient } from "@/utils/backend"; +import { generateIpfsKey, isIpfsPathValid } from "@/utils/ipfs"; +import { LocalFileData, RemoteFileData } from "@/utils/types/files"; +import { CollectionAssetsMetadatasFormValues } from "@/utils/types/launchpad"; + +export const useCompleteCollection = () => { + const selectedNetworkId = useSelectedNetworkId(); + const selectedWallet = useSelectedWallet(); + const { setToast } = useFeedbacks(); + const userIPFSKey = useSelector(selectNFTStorageAPI); + const { uploadFilesToPinata } = useIpfs(); + + const completeCollection = useCallback( + async ( + collectionId: string, + assetsMetadatasFormsValues: CollectionAssetsMetadatasFormValues, + ) => { + if (!selectedWallet) return false; + const userId = selectedWallet.userId; + const walletAddress = selectedWallet.address; + const networkId = selectedWallet.networkId; + + const signingComswasmClient = + await getKeplrSigningCosmWasmClient(selectedNetworkId); + const cosmwasmNftLaunchpadFeature = getNetworkFeature( + selectedNetworkId, + NetworkFeature.CosmWasmNFTLaunchpad, + ); + if (!cosmwasmNftLaunchpadFeature) return false; + + const launchpadBackendClient = mustGetLaunchpadClient(networkId); + + const nftLaunchpadContractClient = new NftLaunchpadClient( + signingComswasmClient, + walletAddress, + cosmwasmNftLaunchpadFeature.launchpadContractAddress, + ); + const pinataJWTKey = + assetsMetadatasFormsValues.nftApiKey || + userIPFSKey || + (await generateIpfsKey(selectedNetworkId, userId)); + if (!pinataJWTKey) { + console.error("upload file err : No Pinata JWT"); + setToast({ + mode: "normal", + type: "error", + title: "Files upload failed", + }); + return false; + } + + try { + const metadatas: Metadata[] = []; + if (!assetsMetadatasFormsValues.assetsMetadatas?.length) return false; + + // IMPORTANT TODO: + // For now, for simplicity, we upload images to ipfs from client side then this backend will + // only check if images have been pinnned correctly. + // ===> Please, see go/pkg/launchpad/service.go + const assetsMetadataImages: LocalFileData[] = + assetsMetadatasFormsValues.assetsMetadatas.map( + (assetMetadata) => assetMetadata.image, + ); + const remoteAssetsMetadataImages: RemoteFileData[] = + await uploadFilesToPinata({ + files: assetsMetadataImages, + pinataJWTKey, + }); + + if (!assetsMetadataImages?.length) { + console.error("Error: Seems to be no image uploaded to IPFS"); + setToast({ + title: "Seems to be no image uploaded to IPFS", + message: "Please try again", + type: "error", + mode: "normal", + }); + return false; + } + + assetsMetadatasFormsValues.assetsMetadatas.forEach( + (assetMetadata, index) => { + const image = remoteAssetsMetadataImages[index]; + if (!isIpfsPathValid(image.url)) { + setToast({ + title: + "At least one uploaded image has an invalid IPFS hash and has beed ignored", + message: "Please try again", + type: "error", + mode: "normal", + }); + return; + } + if (!assetMetadata.attributes.length) return; + const attributes: Trait[] = assetMetadata.attributes.map( + (attribute) => { + return { + value: attribute.value, + traitType: attribute.type, + }; + }, + ); + + // Tr721Metadata + metadatas.push({ + image: image.url, + externalUrl: assetMetadata.externalUrl, + description: assetMetadata.description, + name: assetMetadata.name, + youtubeUrl: assetMetadata.youtubeUrl, + attributes, + // TODO: Hanlde these ? + // imageData: "", + // backgroundColor: "", + // animationUrl: "", + // youtubeUrl: "", + // royaltyPercentage: 5, + // royaltyPaymentAddress: "", + }); + }, + ); + + // ========== Send Metadata of this collection to the backend + const { merkleRoot } = await launchpadBackendClient.UploadMetadatas({ + sender: walletAddress, + projectId: collectionId, + pinataJwt: pinataJWTKey, + networkId: selectedNetworkId, + metadatas, + }); + + // ========== Provide the merkle root through the contract + await nftLaunchpadContractClient.updateMerkleRoot({ + collectionId, + merkleRoot, + }); + + setToast({ + mode: "normal", + type: "success", + title: "Collection completed", + }); + + return true; + } catch (e: any) { + console.error( + "Error completing a NFT Collection in the Launchpad: ", + e, + ); + setToast({ + mode: "normal", + type: "error", + title: "Error completing a NFT Collection in the Launchpad", + message: e.message, + }); + return false; + } + }, + [ + selectedNetworkId, + selectedWallet, + setToast, + userIPFSKey, + uploadFilesToPinata, + ], + ); + + return { completeCollection }; +}; diff --git a/packages/hooks/launchpad/useCreateCollection.ts b/packages/hooks/launchpad/useCreateCollection.ts new file mode 100644 index 0000000000..b631c04cee --- /dev/null +++ b/packages/hooks/launchpad/useCreateCollection.ts @@ -0,0 +1,241 @@ +import keccak256 from "keccak256"; // Tested and this lib is compatible with merkle tree libs from Rust and Go +import { MerkleTree } from "merkletreejs"; +import { useCallback } from "react"; +import { useSelector } from "react-redux"; + +import { Coin } from "./../../contracts-clients/nft-launchpad/NftLaunchpad.types"; + +import { useFeedbacks } from "@/context/FeedbacksProvider"; +import { + MintPeriod, + NftLaunchpadClient, + WhitelistInfo, +} from "@/contracts-clients/nft-launchpad"; +import { useCompleteCollection } from "@/hooks/launchpad/useCompleteCollection"; +import { PinataFileProps, useIpfs } from "@/hooks/useIpfs"; +import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork"; +import useSelectedWallet from "@/hooks/useSelectedWallet"; +import { getNetworkFeature, NetworkFeature } from "@/networks"; +import { getKeplrSigningCosmWasmClient } from "@/networks/signer"; +import { selectNFTStorageAPI } from "@/store/slices/settings"; +import { generateIpfsKey } from "@/utils/ipfs"; +import { LocalFileData } from "@/utils/types/files"; +import { + CollectionAssetsMetadatasFormValues, + CollectionFormValues, + CollectionMintPeriodFormValues, + CollectionToSubmit, +} from "@/utils/types/launchpad"; + +export const useCreateCollection = () => { + // Since the Collection network is the selected network, we use useSelectedNetworkId (See LaunchpadBasic.tsx) + const selectedNetworkId = useSelectedNetworkId(); + const selectedWallet = useSelectedWallet(); + const { setToast } = useFeedbacks(); + const userIPFSKey = useSelector(selectNFTStorageAPI); + const { pinataPinFileToIPFS, uploadFilesToPinata } = useIpfs(); + const { completeCollection } = useCompleteCollection(); + + const createCollection = useCallback( + async (collectionFormValues: CollectionFormValues) => { + if (!selectedWallet) return false; + const userId = selectedWallet.userId; + const walletAddress = selectedWallet.address; + + const signingComswasmClient = + await getKeplrSigningCosmWasmClient(selectedNetworkId); + const cosmwasmNftLaunchpadFeature = getNetworkFeature( + selectedNetworkId, + NetworkFeature.CosmWasmNFTLaunchpad, + ); + if (!cosmwasmNftLaunchpadFeature) return false; + + const nftLaunchpadContractClient = new NftLaunchpadClient( + signingComswasmClient, + walletAddress, + cosmwasmNftLaunchpadFeature.launchpadContractAddress, + ); + const pinataJWTKey = + collectionFormValues.assetsMetadatas?.nftApiKey || + userIPFSKey || + (await generateIpfsKey(selectedNetworkId, userId)); + if (!pinataJWTKey) { + console.error("Project creation error: No Pinata JWT"); + setToast({ + mode: "normal", + type: "error", + title: "Project creation error: No Pinata JWT", + }); + return false; + } + + try { + // ========== Cover image + const fileIpfsHash = await pinataPinFileToIPFS({ + pinataJWTKey, + file: collectionFormValues.coverImage, + } as PinataFileProps); + if (!fileIpfsHash) { + console.error("Project creation error: Pin to Pinata failed"); + setToast({ + mode: "normal", + type: "error", + title: "Project creation error: Pin to Pinata failed", + }); + return false; + } + + // ========== Whitelists + const whitelistAddressesFilesToUpload: LocalFileData[] = []; + collectionFormValues.mintPeriods.forEach((mintPeriod) => { + if (mintPeriod.whitelistAddressesFile) + whitelistAddressesFilesToUpload.push( + mintPeriod.whitelistAddressesFile, + ); + }); + const remoteWhitelistAddressesFiles = await uploadFilesToPinata({ + pinataJWTKey, + files: whitelistAddressesFilesToUpload, + }); + const mint_periods: MintPeriod[] = collectionFormValues.mintPeriods.map( + (mintPeriod: CollectionMintPeriodFormValues, index) => { + let whitelist_info: WhitelistInfo | null = null; + if ( + mintPeriod.whitelistAddresses?.length && + remoteWhitelistAddressesFiles[index].url + ) { + const addresses: string[] = mintPeriod.whitelistAddresses; + const leaves = addresses.map(keccak256); + const tree = new MerkleTree(leaves, keccak256); + const merkleRoot = tree.getRoot().toString("hex"); + whitelist_info = { + addresses_count: addresses.length, + addresses_ipfs: remoteWhitelistAddressesFiles[index].url, + addresses_merkle_root: merkleRoot, + }; + } + const price: Coin | null = mintPeriod.price + ? { + amount: mintPeriod.price.amount || "0", + denom: mintPeriod.price.denom, + } + : null; + return { + price, + end_time: mintPeriod.endTime, + max_tokens: mintPeriod.maxTokens + ? parseInt(mintPeriod.maxTokens, 10) + : 0, + limit_per_address: mintPeriod.perAddressLimit + ? parseInt(mintPeriod.perAddressLimit, 10) + : 0, + start_time: mintPeriod.startTime, + whitelist_info, + }; + }, + ); + + const assetsMetadataFormsValues: + | CollectionAssetsMetadatasFormValues + | undefined + | null = collectionFormValues.assetsMetadatas; + + // ========== Final collection + const collection: CollectionToSubmit = { + name: collectionFormValues.name, + desc: collectionFormValues.description, + symbol: collectionFormValues.symbol, + website_link: collectionFormValues.websiteLink, + contact_email: collectionFormValues.email, + project_type: collectionFormValues.projectTypes.join(), + tokens_count: assetsMetadataFormsValues?.assetsMetadatas?.length || 0, + reveal_time: collectionFormValues.revealTime, + team_desc: collectionFormValues.teamDescription, + partners: collectionFormValues.partnersDescription, + investment_desc: collectionFormValues.investDescription, + investment_link: collectionFormValues.investLink, + artwork_desc: collectionFormValues.artworkDescription, + cover_img_uri: "ipfs://" + fileIpfsHash, + is_applied_previously: collectionFormValues.isPreviouslyApplied, + is_project_derivative: collectionFormValues.isDerivativeProject, + is_ready_for_mint: collectionFormValues.isReadyForMint, + is_dox: collectionFormValues.isDox, + escrow_mint_proceeds_period: parseInt( + collectionFormValues.escrowMintProceedsPeriod, + 10, + ), + dao_whitelist_count: parseInt( + collectionFormValues.daoWhitelistCount, + 10, + ), + mint_periods, + royalty_address: collectionFormValues.royaltyAddress, + royalty_percentage: collectionFormValues.royaltyPercentage + ? parseInt(collectionFormValues.royaltyPercentage, 10) + : null, + target_network: selectedNetworkId, + }; + const collectionId = collectionFormValues.symbol; + + // ========== Submit the collection through the contract + await nftLaunchpadContractClient.submitCollection({ + collection, + }); + + // ========== Handle assets metadata + if (!assetsMetadataFormsValues?.assetsMetadatas?.length) { + setToast({ + mode: "normal", + type: "success", + title: "Project submitted (Incomplete)", + message: "You will need to Complete the Project", + }); + } else { + const isCompleteSuccess = await completeCollection( + collectionId, + assetsMetadataFormsValues, + ); + + if (!isCompleteSuccess) { + setToast({ + mode: "normal", + type: "warning", + title: "Project submitted (Incomplete)", + message: + "Error during uploading the Assets.\nYou will need to Complete the Project", + }); + } else { + setToast({ + mode: "normal", + type: "success", + title: "Project submitted", + }); + } + } + + return true; + } catch (e: any) { + console.error("Error creating a NFT Collection in the Launchpad: ", e); + setToast({ + mode: "normal", + type: "error", + title: "Error creating a NFT Collection in the Launchpad", + message: e.message, + }); + } + }, + [ + pinataPinFileToIPFS, + selectedWallet, + userIPFSKey, + uploadFilesToPinata, + selectedNetworkId, + setToast, + completeCollection, + ], + ); + + return { + createCollection, + }; +}; diff --git a/packages/hooks/launchpad/useGetLaunchpadAdmin.ts b/packages/hooks/launchpad/useGetLaunchpadAdmin.ts new file mode 100644 index 0000000000..ba35aac90b --- /dev/null +++ b/packages/hooks/launchpad/useGetLaunchpadAdmin.ts @@ -0,0 +1,69 @@ +import { useQuery } from "@tanstack/react-query"; + +import { useSelectedNetworkId } from "./../useSelectedNetwork"; +import useSelectedWallet from "./../useSelectedWallet"; + +import { useFeedbacks } from "@/context/FeedbacksProvider"; +import { NftLaunchpadClient } from "@/contracts-clients/nft-launchpad"; +import { getNetworkFeature, NetworkFeature, getUserId } from "@/networks"; +import { getKeplrSigningCosmWasmClient } from "@/networks/signer"; + +export const useGetLaunchpadAdmin = () => { + const selectedWallet = useSelectedWallet(); + const userAddress = selectedWallet?.address; + const selectedNetworkId = useSelectedNetworkId(); + const { setToast } = useFeedbacks(); + + const { data, ...other } = useQuery( + ["getLaunchpadAdmin", userAddress, selectedNetworkId], + async () => { + try { + if (!userAddress) { + throw Error("No user address"); + } + const signingComswasmClient = + await getKeplrSigningCosmWasmClient(selectedNetworkId); + const cosmwasmNftLaunchpadFeature = getNetworkFeature( + selectedNetworkId, + NetworkFeature.CosmWasmNFTLaunchpad, + ); + + if (!cosmwasmNftLaunchpadFeature) { + throw Error("No Launchpad feature"); + } + + const nftLaunchpadContractClient = new NftLaunchpadClient( + signingComswasmClient, + userAddress, + cosmwasmNftLaunchpadFeature.launchpadContractAddress, + ); + + if (!nftLaunchpadContractClient) { + throw Error("Launchpad contract client not found"); + } + + // The Launchapd Admin DAO is the deployer set in the config of the nft-launchpad contract + const config = await nftLaunchpadContractClient.getConfig(); + if (!config.launchpad_admin) { + throw Error("No Launchpad admin set"); + } + + const adminDaoId = getUserId(selectedNetworkId, config.launchpad_admin); + + return adminDaoId; + } catch (e: any) { + console.error("Error getting Launchpad admin: ", e); + setToast({ + mode: "normal", + type: "error", + title: "Error getting Launchpad admin", + message: e.message, + }); + } + }, + { + enabled: !!userAddress, + }, + ); + return { launchpadAdminId: data, ...other }; +}; diff --git a/packages/hooks/launchpad/useIsUserLaunchpadAdmin.ts b/packages/hooks/launchpad/useIsUserLaunchpadAdmin.ts new file mode 100644 index 0000000000..0598831e33 --- /dev/null +++ b/packages/hooks/launchpad/useIsUserLaunchpadAdmin.ts @@ -0,0 +1,84 @@ +import { useQuery } from "@tanstack/react-query"; + +import { useFeedbacks } from "@/context/FeedbacksProvider"; +import { NftLaunchpadClient } from "@/contracts-clients/nft-launchpad"; +import { + getNetworkFeature, + parseUserId, + NetworkFeature, + getUserId, +} from "@/networks"; +import { getKeplrSigningCosmWasmClient } from "@/networks/signer"; +import { mustGetDAOClient } from "@/utils/backend"; + +export const useIsUserLaunchpadAdmin = (userId?: string) => { + const { setToast } = useFeedbacks(); + + const { data, ...other } = useQuery( + ["isUserLaunchpadAdmin", userId], + async () => { + try { + const [network, userAddress] = parseUserId(userId); + if (!userId) { + throw new Error("Invalid sender"); + } + if (!network) { + throw new Error("Invalid network"); + } + const networkId = network.id; + + const signingComswasmClient = + await getKeplrSigningCosmWasmClient(networkId); + const cosmwasmNftLaunchpadFeature = getNetworkFeature( + networkId, + NetworkFeature.CosmWasmNFTLaunchpad, + ); + + if (!cosmwasmNftLaunchpadFeature) { + throw new Error("No Launchpad feature"); + } + + const daoClient = mustGetDAOClient(networkId); + const nftLaunchpadContractClient = new NftLaunchpadClient( + signingComswasmClient, + userAddress, + cosmwasmNftLaunchpadFeature.launchpadContractAddress, + ); + + if (!daoClient) { + throw new Error("DAO client not found"); + } + if (!nftLaunchpadContractClient) { + throw new Error("Launchpad contract client not found"); + } + + // The Launchapd Admin DAO is the deployer set in the config of the nft-launchpad contract + const config = await nftLaunchpadContractClient.getConfig(); + if (!config.launchpad_admin) { + throw new Error("No Launchpad admin set"); + } + + const adminDaoId = getUserId(networkId, config.launchpad_admin); + const { isMember } = await daoClient.IsUserDAOMember({ + userId, + daoId: adminDaoId, + }); + + return isMember; + } catch (e: any) { + console.error("Error veryfing Launchpad admin: ", e); + setToast({ + mode: "normal", + type: "error", + title: "Error veryfing Launchpad admin", + message: e.message, + }); + return false; + } + }, + { + enabled: !!userId, + }, + ); + return { isUserLaunchpadAdmin: data, ...other }; +}; diff --git a/packages/hooks/launchpad/useLaunchpadProjectById.ts b/packages/hooks/launchpad/useLaunchpadProjectById.ts new file mode 100644 index 0000000000..9313df1f1f --- /dev/null +++ b/packages/hooks/launchpad/useLaunchpadProjectById.ts @@ -0,0 +1,49 @@ +import { useQuery } from "@tanstack/react-query"; + +import { + LaunchpadProject, + LaunchpadProjectByIdRequest, + LaunchpadProjectByIdResponse, +} from "@/api/launchpad/v1/launchpad"; +import { useFeedbacks } from "@/context/FeedbacksProvider"; +import { mustGetLaunchpadClient } from "@/utils/backend"; + +export const useLaunchpadProjectById = (req: LaunchpadProjectByIdRequest) => { + const { setToast } = useFeedbacks(); + const networkId = req.networkId; + // const userAddress = req.userAddress; + const projectId = req.projectId; + + const { data, ...other } = useQuery( + [ + "launchpadProjectById", + projectId, + networkId, + // , userAddress + ], + async () => { + try { + const client = mustGetLaunchpadClient(networkId); + if ( + !client + // || !userAddress + ) { + return null; + } + const response: LaunchpadProjectByIdResponse = + await client.LaunchpadProjectById(req); + return response.project || null; + } catch (e: any) { + console.error("Error getting launchpad project: ", e); + setToast({ + mode: "normal", + type: "error", + title: "Error getting launchpad project", + message: e.message, + }); + return null; + } + }, + ); + return { launchpadProject: data, ...other }; +}; diff --git a/packages/hooks/launchpad/useLaunchpadProjects.ts b/packages/hooks/launchpad/useLaunchpadProjects.ts new file mode 100644 index 0000000000..dea7a68e82 --- /dev/null +++ b/packages/hooks/launchpad/useLaunchpadProjects.ts @@ -0,0 +1,56 @@ +import { useQuery } from "@tanstack/react-query"; + +import { + LaunchpadProject, + LaunchpadProjectsRequest, + LaunchpadProjectsResponse, +} from "@/api/launchpad/v1/launchpad"; +import { useFeedbacks } from "@/context/FeedbacksProvider"; +import { mustGetLaunchpadClient } from "@/utils/backend"; + +export const useLaunchpadProjects = (req: LaunchpadProjectsRequest) => { + const { setToast } = useFeedbacks(); + const networkId = req.networkId; + // const userAddress = req.userAddress; + + const { data, ...other } = useQuery( + [ + "collectionsByCreator", + networkId, + // , userAddress + ], + async () => { + const launchpadProjects: LaunchpadProject[] = []; + + try { + const client = mustGetLaunchpadClient(networkId); + + if ( + !client + // || !userAddress + ) { + return []; + } + const response: LaunchpadProjectsResponse = + await client.LaunchpadProjects(req); + response.projects.forEach((data) => { + if (!data) { + return; + } + launchpadProjects.push(data); + }); + } catch (e: any) { + console.error("Error getting launchpad projects: ", e); + setToast({ + mode: "normal", + type: "error", + title: "Error getting launchpad projects", + message: e.message, + }); + } + + return launchpadProjects; + }, + ); + return { launchpadProjects: data, ...other }; +}; diff --git a/packages/hooks/launchpad/useLaunchpadProjectsByCreator.ts b/packages/hooks/launchpad/useLaunchpadProjectsByCreator.ts new file mode 100644 index 0000000000..3dcb508e18 --- /dev/null +++ b/packages/hooks/launchpad/useLaunchpadProjectsByCreator.ts @@ -0,0 +1,60 @@ +import { useQuery } from "@tanstack/react-query"; + +import { + LaunchpadProjectsByCreatorRequest, + LaunchpadProjectsByCreatorResponse, + LaunchpadProject, +} from "@/api/launchpad/v1/launchpad"; +import { useFeedbacks } from "@/context/FeedbacksProvider"; +import { mustGetLaunchpadClient } from "@/utils/backend"; + +export const useLaunchpadProjectsByCreator = ( + req: LaunchpadProjectsByCreatorRequest, +) => { + const { setToast } = useFeedbacks(); + const networkId = req.networkId; + const creatorId = req.creatorId; + // const userAddress = req.userAddress; + + const { data, ...other } = useQuery( + [ + "launchpadProjectsByCreator", + networkId, + creatorId, + // , userAddress + ], + async () => { + const launchpadProjects: LaunchpadProject[] = []; + + try { + const client = mustGetLaunchpadClient(networkId); + + if ( + !client || + !creatorId + // !userAddress + ) { + return []; + } + const response: LaunchpadProjectsByCreatorResponse = + await client.LaunchpadProjectsByCreator(req); + response.projects.forEach((data) => { + if (!data) { + return; + } + launchpadProjects.push(data); + }); + } catch (e: any) { + console.error("Error getting launchpad projects: ", e); + setToast({ + mode: "normal", + type: "error", + title: "Error getting launchpad projects", + message: e.message, + }); + } + return launchpadProjects; + }, + ); + return { launchpadProjects: data, ...other }; +}; diff --git a/packages/hooks/launchpad/useLaunchpadProjectsCounts.ts b/packages/hooks/launchpad/useLaunchpadProjectsCounts.ts new file mode 100644 index 0000000000..f000798b67 --- /dev/null +++ b/packages/hooks/launchpad/useLaunchpadProjectsCounts.ts @@ -0,0 +1,41 @@ +import { useQuery } from "@tanstack/react-query"; + +import { LaunchpadProjectsCountsRequest } from "@/api/launchpad/v1/launchpad"; +import { useFeedbacks } from "@/context/FeedbacksProvider"; +import { mustGetLaunchpadClient } from "@/utils/backend"; + +export const useLaunchpadProjectsCounts = ( + req: LaunchpadProjectsCountsRequest, +) => { + const { setToast } = useFeedbacks(); + const networkId = req.networkId; + + const { data, ...other } = useQuery( + ["launchpadProjectsCounts", networkId], + async () => { + try { + const client = mustGetLaunchpadClient(networkId); + if (!client) { + throw new Error("Missing client"); + } + + const { statusCounts } = await client.LaunchpadProjectsCounts(req); + return statusCounts; + } catch (err: any) { + const title = "Error getting launchpad projects counts"; + const message = err instanceof Error ? err.message : `${err}`; + console.error(title, message); + setToast({ + mode: "normal", + type: "error", + title, + message, + }); + } + }, + { + enabled: !!networkId, + }, + ); + return { statusCounts: data, ...other }; +}; diff --git a/packages/hooks/launchpad/useProposeApproveProject.ts b/packages/hooks/launchpad/useProposeApproveProject.ts new file mode 100644 index 0000000000..752927af51 --- /dev/null +++ b/packages/hooks/launchpad/useProposeApproveProject.ts @@ -0,0 +1,148 @@ +import { useCallback } from "react"; + +import { useGetLaunchpadAdmin } from "./useGetLaunchpadAdmin"; +import { useIsUserLaunchpadAdmin } from "./useIsUserLaunchpadAdmin"; +import { DaoProposalSingleClient } from "../../contracts-clients/dao-proposal-single/DaoProposalSingle.client"; +import { useDAOMakeProposal } from "../dao/useDAOMakeProposal"; +import { useDAOFirstProposalModule } from "../dao/useDAOProposalModules"; + +import { useFeedbacks } from "@/context/FeedbacksProvider"; +import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork"; +import useSelectedWallet from "@/hooks/useSelectedWallet"; +import { getNetworkFeature, NetworkFeature } from "@/networks"; +import { getKeplrSigningCosmWasmClient } from "@/networks/signer"; +import { mustGetLaunchpadClient } from "@/utils/backend"; + +export const useProposeApproveProject = () => { + const { launchpadAdminId } = useGetLaunchpadAdmin(); // It's a DAO + const makeProposal = useDAOMakeProposal(launchpadAdminId); + const selectedNetworkId = useSelectedNetworkId(); + const selectedWallet = useSelectedWallet(); + const userAddress = selectedWallet?.address; + const { isUserLaunchpadAdmin } = useIsUserLaunchpadAdmin( + selectedWallet?.userId, + ); + const { setToast } = useFeedbacks(); + const { daoFirstProposalModule } = + useDAOFirstProposalModule(launchpadAdminId); + + // Make a proposal deploy_collection and approve it + const proposeApproveProject = useCallback( + async (projectId: string) => { + try { + if (!isUserLaunchpadAdmin) { + throw new Error("Unauthorized"); + } + if (!userAddress) { + throw new Error("Invalid sender"); + } + if (!daoFirstProposalModule?.address) { + throw new Error("Invalid DAO Proposal module"); + } + + const cosmwasmNftLaunchpadFeature = getNetworkFeature( + selectedNetworkId, + NetworkFeature.CosmWasmNFTLaunchpad, + ); + + if (!cosmwasmNftLaunchpadFeature) { + throw new Error("No Launchpad feature"); + } + + // ---- Make the proposal + const makeProposalRes = await makeProposal(userAddress, { + title: "Approve Project " + projectId, + description: "", + msgs: [ + { + wasm: { + execute: { + contract_addr: + cosmwasmNftLaunchpadFeature.launchpadContractAddress, + msg: Buffer.from( + JSON.stringify({ + deploy_collection: { + collection_id: projectId, + }, + }), + ).toString("base64"), + funds: [], + }, + }, + }, + ], + }); + + // ---- Get the freshly made proposal id + const event = makeProposalRes.events.find((ev) => + ev.attributes.some((attr) => attr.key === "proposal_id"), + ); + const proposalId = event?.attributes.find( + (attribute) => attribute.key === "proposal_id", + )?.value; + if (!proposalId) { + throw new Error("Failed to retreive the proposal"); + } + + const signingCosmWasmClient = + await getKeplrSigningCosmWasmClient(selectedNetworkId); + const daoProposalClient = new DaoProposalSingleClient( + signingCosmWasmClient, + userAddress, + daoFirstProposalModule?.address, + ); + + // ---- Approve the proposal + await daoProposalClient.vote( + { proposalId: parseInt(proposalId, 10), vote: "yes" }, + "auto", + ); + + // ---- Update the DB by adding proposalId to the project + // The proposal has always at least a vote "yes". So this project is considered as + const launchpadBackendClient = + mustGetLaunchpadClient(selectedNetworkId); + const { approved } = await launchpadBackendClient.ProposeApproveProject( + { + sender: userAddress, + projectId, + networkId: selectedNetworkId, + proposalId, + }, + ); + + if (!approved) { + throw new Error( + "Failed to update the project status after first approbation", + ); + } + + setToast({ + mode: "normal", + type: "success", + title: "Successfully approved the Launchpad Collection", + }); + } catch (err: unknown) { + console.error("Error approving the Launchpad Collection: ", err); + if (err instanceof Error) { + setToast({ + mode: "normal", + type: "error", + title: "Error approving the Launchpad Collection", + message: err.message, + }); + } + } + }, + [ + selectedNetworkId, + userAddress, + setToast, + isUserLaunchpadAdmin, + daoFirstProposalModule, + makeProposal, + ], + ); + + return { proposeApproveProject, launchpadAdminId }; +}; diff --git a/packages/networks/teritori-testnet/index.ts b/packages/networks/teritori-testnet/index.ts index 667f98dae3..12c8bc36a0 100644 --- a/packages/networks/teritori-testnet/index.ts +++ b/packages/networks/teritori-testnet/index.ts @@ -41,7 +41,7 @@ const cosmwasmNftLaunchpadFeature: CosmWasmNFTLaunchpad = { }; const riotContractAddressGen0 = - "tori1r8raaqul4j05qtn0t05603mgquxfl8e9p7kcf7smwzcv2hc5rrlq0vket0"; + "tori1hzz0s0ucrhdp6tue2lxk3c03nj6f60qy463we7lgx0wudd72ctmstg4wkc"; const riotContractAddressGen1 = ""; export const teritoriTestnetNetwork: CosmosNetworkInfo = { diff --git a/packages/screens/FeedNewArticle/FeedNewArticleScreen.tsx b/packages/screens/FeedNewArticle/FeedNewArticleScreen.tsx index 2b82e3faa4..8c24348429 100644 --- a/packages/screens/FeedNewArticle/FeedNewArticleScreen.tsx +++ b/packages/screens/FeedNewArticle/FeedNewArticleScreen.tsx @@ -4,9 +4,7 @@ import { Controller, useForm } from "react-hook-form"; import { ScrollView, View } from "react-native"; import { useSelector } from "react-redux"; -import priceSVG from "../../../assets/icons/price.svg"; -import useSelectedWallet from "../../hooks/useSelectedWallet"; - +import priceSVG from "@/assets/icons/price.svg"; import { BrandText } from "@/components/BrandText"; import { SVG } from "@/components/SVG"; import { ScreenContainer } from "@/components/ScreenContainer"; @@ -25,6 +23,7 @@ import { useFeedPosting } from "@/hooks/feed/useFeedPosting"; import { useIpfs } from "@/hooks/useIpfs"; import { useIsMobile } from "@/hooks/useIsMobile"; import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork"; +import useSelectedWallet from "@/hooks/useSelectedWallet"; import { NetworkFeature } from "@/networks"; import { selectNFTStorageAPI } from "@/store/slices/settings"; import { feedPostingStep, FeedPostingStepId } from "@/utils/feed/posting"; diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadAdministrationOverview/LaunchpadAdministrationOverviewScreen.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadAdministrationOverview/LaunchpadAdministrationOverviewScreen.tsx new file mode 100644 index 0000000000..e43fd0c181 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadAdministrationOverview/LaunchpadAdministrationOverviewScreen.tsx @@ -0,0 +1,192 @@ +import React, { useState } from "react"; +import { useWindowDimensions, View } from "react-native"; + +import { ConfirmedsTable } from "./../components/ConfirmedsTable"; +import { ReviewingsTable } from "./../components/ReviewingsTable"; +import { ApplicationStatusCard } from "./components/ApplicationStatusCard"; +import { CompletesTable } from "../components/CompletesTable"; +import { IncompletesTable } from "../components/IncompletesTable"; + +import { StatusCount } from "@/api/launchpad/v1/launchpad"; +import { BrandText } from "@/components/BrandText"; +import { ScreenContainer } from "@/components/ScreenContainer"; +import { PrimaryButtonOutline } from "@/components/buttons/PrimaryButtonOutline"; +import { SpacerColumn } from "@/components/spacer"; +import { Tabs } from "@/components/tabs/Tabs"; +import { useIsUserLaunchpadAdmin } from "@/hooks/launchpad/useIsUserLaunchpadAdmin"; +import { useLaunchpadProjectsCounts } from "@/hooks/launchpad/useLaunchpadProjectsCounts"; +import { useAppNavigation } from "@/hooks/navigation/useAppNavigation"; +import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork"; +import useSelectedWallet from "@/hooks/useSelectedWallet"; +import { NetworkFeature } from "@/networks"; +import { statusToCount } from "@/utils/launchpad"; +import { errorColor } from "@/utils/style/colors"; +import { fontSemibold20, fontSemibold28 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; + +const MD_BREAKPOINT = 820; +export type LaunchpadAdminDashboardTabsListType = + | "INCOMPLETE" + | "COMPLETE" + | "REVIEWING" + | "CONFIRMED"; + +export const launchpadAdminTabs = (statusCounts?: StatusCount[]) => { + return { + INCOMPLETE: { + name: "INCOMPLETE", + badgeCount: statusToCount("INCOMPLETE", statusCounts), + }, + COMPLETE: { + name: "COMPLETE", + badgeCount: statusToCount("COMPLETE", statusCounts), + }, + REVIEWING: { + name: "REVIEWING", + badgeCount: statusToCount("REVIEWING", statusCounts), + }, + CONFIRMED: { + name: "CONFIRMED", + badgeCount: statusToCount("CONFIRMED", statusCounts), + }, + }; +}; + +export const LaunchpadAdministrationOverviewScreen: React.FC = () => { + const navigation = useAppNavigation(); + const selectedNetworkId = useSelectedNetworkId(); + const userId = useSelectedWallet()?.userId; + const { isUserLaunchpadAdmin, isLoading: isUserAdminLoading } = + useIsUserLaunchpadAdmin(userId); + + const { width } = useWindowDimensions(); + const { statusCounts } = useLaunchpadProjectsCounts({ + networkId: selectedNetworkId, + }); + + const [selectedTab, setSelectedTab] = + useState("INCOMPLETE"); + + if (!isUserLaunchpadAdmin) + return ( + } + headerChildren={ + + {isUserAdminLoading ? "Loading..." : "Unauthorized"} + + } + responsive + forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]} + > + + {isUserAdminLoading ? "Loading..." : "Unauthorized"} + + + ); + + return ( + } + headerChildren={ + Administration Dashboard + } + responsive + forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]} + > + + + + Launchpad Administration Overview + + + + = MD_BREAKPOINT ? "row" : "column", + justifyContent: "center", + }} + > + + = MD_BREAKPOINT ? layout.spacing_x1_5 : 0, + marginVertical: width >= MD_BREAKPOINT ? 0 : layout.spacing_x1_5, + }} + /> + navigation.navigate("LaunchpadReadyApplications") + // : undefined + // } + isReady + /> + + + + + + {selectedTab === "INCOMPLETE" ? ( + + ) : selectedTab === "COMPLETE" ? ( + + ) : selectedTab === "REVIEWING" ? ( + + ) : selectedTab === "CONFIRMED" ? ( + + ) : ( + <> + )} + + + + + navigation.navigate("LaunchpadApplications")} + style={{ alignSelf: "center" }} + /> + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadAdministrationOverview/components/ApplicationStatusCard.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadAdministrationOverview/components/ApplicationStatusCard.tsx new file mode 100644 index 0000000000..03369a6d1f --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadAdministrationOverview/components/ApplicationStatusCard.tsx @@ -0,0 +1,72 @@ +import React from "react"; +import { View, StyleProp, TouchableOpacity } from "react-native"; + +import chevronRightSVG from "@/assets/icons/chevron-right.svg"; +import { BrandText } from "@/components/BrandText"; +import { SVG } from "@/components/SVG"; +import { BoxStyle } from "@/components/boxes/Box"; +import { PrimaryBox } from "@/components/boxes/PrimaryBox"; +import { + neutral00, + neutral11, + neutral30, + neutral44, +} from "@/utils/style/colors"; +import { fontSemibold16, fontSemibold24 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; + +export const ApplicationStatusCard: React.FC<{ + label: string; + count: number; + style?: StyleProp; + onPress?: () => void; + isReady?: boolean; +}> = ({ label, style, count, isReady, onPress }) => { + return ( + + + {label} + + {count} + + {onPress && !!count && ( + + + + )} + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/LaunchpadApplicationReviewScreen.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/LaunchpadApplicationReviewScreen.tsx new file mode 100644 index 0000000000..5165727f75 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/LaunchpadApplicationReviewScreen.tsx @@ -0,0 +1,216 @@ +import React, { useMemo, useState } from "react"; +import { View } from "react-native"; + +import { Separator } from "./../../../../components/separators/Separator"; +import { ApplicationDetail } from "./components/ApplicationDetail"; +import { CreatorInformation } from "./components/CreatorInformation"; +import { InvestmentInformation } from "./components/InvestmentInformation"; +import { MintingInformation } from "./components/MintingInformation"; +import { ProjectInformation } from "./components/ProjectInformation"; +import { TeamInformation } from "./components/TeamInformation"; +import { Status } from "../../../../api/launchpad/v1/launchpad"; +import { PrimaryButton } from "../../../../components/buttons/PrimaryButton"; +import { ProposalRow } from "../../../../components/dao/DAOProposals"; +import { useDAOProposalById } from "../../../../hooks/dao/useDAOProposalById"; +import { useProposeApproveProject } from "../../../../hooks/launchpad/useProposeApproveProject"; + +import { BrandText } from "@/components/BrandText"; +import { NotFound } from "@/components/NotFound"; +import { ScreenContainer } from "@/components/ScreenContainer"; +import { SpacerColumn } from "@/components/spacer"; +import { useInvalidateDAOProposals } from "@/hooks/dao/useDAOProposals"; +import { useIsUserLaunchpadAdmin } from "@/hooks/launchpad/useIsUserLaunchpadAdmin"; +import { useLaunchpadProjectById } from "@/hooks/launchpad/useLaunchpadProjectById"; +import { useAppNavigation } from "@/hooks/navigation/useAppNavigation"; +import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork"; +import useSelectedWallet from "@/hooks/useSelectedWallet"; +import { NetworkFeature } from "@/networks"; +import { parseCollectionData } from "@/utils/launchpad"; +import { ScreenFC } from "@/utils/navigation"; +import { errorColor } from "@/utils/style/colors"; +import { fontSemibold20 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; + +// =====> TODO: SHOW ALL DATA, MINT PERIODS, ASSETS, ETC + +export const launchpadReviewBreakpointM = 800; +export const launchpadReviewBreakpointS = 600; +export const launchpadReviewBreakpointSM = 400; + +export const LaunchpadApplicationReviewScreen: ScreenFC< + "LaunchpadApplicationReview" +> = ({ route }) => { + const { id: projectId } = route.params; + const navigation = useAppNavigation(); + const selectedNetworkId = useSelectedNetworkId(); + const [isApproveLoading, setApproveLoading] = useState(false); + const userId = useSelectedWallet()?.userId; + const { isUserLaunchpadAdmin, isLoading: isUserAdminLoading } = + useIsUserLaunchpadAdmin(userId); + const { proposeApproveProject, launchpadAdminId } = + useProposeApproveProject(); + const invalidateDAOProposals = useInvalidateDAOProposals(launchpadAdminId); + const { launchpadProject, isLoading: isProjectsLoading } = + useLaunchpadProjectById({ + projectId, + networkId: selectedNetworkId, + }); + const collectionData = + launchpadProject && parseCollectionData(launchpadProject); + const { daoProposal } = useDAOProposalById( + launchpadAdminId, + launchpadProject?.proposalId + ? parseInt(launchpadProject.proposalId, 10) + : undefined, + ); + const isLoading = useMemo( + () => isUserAdminLoading || isProjectsLoading, + [isUserAdminLoading, isProjectsLoading], + ); + const isUserOwner = + launchpadProject?.creatorId && launchpadProject.creatorId === userId; + + const onPressApprove = async () => { + setApproveLoading(true); + try { + await proposeApproveProject(projectId); + } catch (e) { + console.error("Error approving the collection", e); + setApproveLoading(false); + } finally { + invalidateDAOProposals(); + } + setTimeout(() => { + setApproveLoading(false); + }, 1000); + }; + + const onBackPress = () => { + const routes = navigation.getState().routes; + const previousScreen = routes[routes.length - 2]; + if ( + previousScreen && + previousScreen.name !== "LaunchpadComplete" && + previousScreen.name !== "LaunchpadCreate" && + navigation.canGoBack() + ) { + navigation.goBack(); + } else if (isUserLaunchpadAdmin) { + navigation.navigate("LaunchpadAdministrationOverview"); + } else { + navigation.navigate("LaunchpadMyCollections"); + } + }; + + if (!isUserLaunchpadAdmin || !isUserOwner) { + return ( + } + headerChildren={ + + {isLoading ? "Loading..." : "Unauthorized"} + + } + responsive + onBackPress={onBackPress} + forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]} + > + + {isLoading ? "Loading..." : "Unauthorized"} + + + ); + } + + if (!launchpadProject || !collectionData) { + return ( + } + headerChildren={ + + {isLoading ? "Loading..." : "Application not found"} + + } + responsive + onBackPress={onBackPress} + forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]} + > + {isLoading ? ( + + Loading... + + ) : ( + + )} + + ); + } + + return ( + } + headerChildren={ + Application Review + } + responsive + onBackPress={onBackPress} + forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]} + > + {selectedNetworkId !== launchpadProject.networkId ? ( + Wrong network + ) : ( + + + + + {daoProposal && + isUserLaunchpadAdmin && + launchpadProject.status !== Status.STATUS_INCOMPLETE ? ( + <> + + + + ) : launchpadProject.status !== Status.STATUS_INCOMPLETE && + isUserLaunchpadAdmin ? ( + <> + + + + + + ) : null} + + + + + + + + + + )} + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/ApplicationCard.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/ApplicationCard.tsx new file mode 100644 index 0000000000..7ac87ef8d6 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/ApplicationCard.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { StyleProp, useWindowDimensions } from "react-native"; + +import { launchpadReviewBreakpointSM } from "../LaunchpadApplicationReviewScreen"; + +import { BrandText } from "@/components/BrandText"; +import { BoxStyle } from "@/components/boxes/Box"; +import { TertiaryBox } from "@/components/boxes/TertiaryBox"; +import { neutral77 } from "@/utils/style/colors"; +import { fontMedium14, fontSemibold12 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; + +export const ApplicationCard: React.FC<{ + title: string; + value: string; + style?: StyleProp; +}> = ({ title, value, style }) => { + const { width } = useWindowDimensions(); + + return ( + = launchpadReviewBreakpointSM && { flex: 1 }, + style, + ]} + > + + {title} + + {value} + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/ApplicationDetail.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/ApplicationDetail.tsx new file mode 100644 index 0000000000..5a2cd31799 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/ApplicationDetail.tsx @@ -0,0 +1,157 @@ +import React from "react"; +import { View, Linking } from "react-native"; + +import { PrimaryButton } from "./../../../../../components/buttons/PrimaryButton"; +import { useAppNavigation } from "./../../../../../hooks/navigation/useAppNavigation"; +import { getCollectionId } from "./../../../../../networks/index"; +import { launchpadProjectStatus } from "./../../../../../utils/launchpad"; +import { ApplicationCard } from "./ApplicationCard"; +import { Status } from "../../../../../api/launchpad/v1/launchpad"; +import { StatusBadge } from "../../../components/StatusBadge"; + +import launchpadApplySVG from "@/assets/icons/launchpad-apply.svg"; +import websiteSVG from "@/assets/icons/website.svg"; +import { BrandText } from "@/components/BrandText"; +import { OptimizedImage } from "@/components/OptimizedImage"; +import { BoxStyle } from "@/components/boxes/Box"; +import { SocialButton } from "@/components/buttons/SocialButton"; +import { SpacerColumn, SpacerRow } from "@/components/spacer"; +import { useMaxResolution } from "@/hooks/useMaxResolution"; +import { neutralFF } from "@/utils/style/colors"; +import { fontSemibold14, fontSemibold28 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { CollectionDataResult } from "@/utils/types/launchpad"; + +const breakpointM = 900; + +export const ApplicationDetail: React.FC<{ + collectionData: CollectionDataResult; + projectStatus: Status; +}> = ({ collectionData, projectStatus }) => { + const { width } = useMaxResolution(); + const navigation = useAppNavigation(); + + const onPressDeployedCollectionButton = () => { + if (collectionData.deployed_address) { + const id = getCollectionId( + collectionData.target_network, + collectionData.deployed_address, + ); + navigation.navigate("MintCollection", { id }); + } + }; + + return ( + = breakpointM ? "row" : "column-reverse", + alignItems: width >= breakpointM ? "flex-start" : "center", + justifyContent: "center", + }} + > + {/* ===== Left container */} + + + + {launchpadProjectStatus(projectStatus) === "INCOMPLETE" && ( + <> + + + navigation.navigate("LaunchpadComplete", { + id: collectionData.symbol, + }) + } + /> + + )} + + + {collectionData.name} + + + + + + + + {collectionData.desc} + + + Linking.openURL(collectionData.website_link)} + /> + + {!!collectionData.deployed_address && ( + + )} + + + + {width >= breakpointM ? ( + + ) : ( + + )} + + {/* ===== Right container */} + = breakpointM ? 534 : 380} + width={width >= breakpointM ? 534 : 380} + style={[ + { + height: width >= breakpointM ? 534 : 380, + width: width >= breakpointM ? 534 : 380, + }, + width >= breakpointM && { flex: 1 }, + ]} + sourceURI={collectionData.cover_img_uri} + /> + + ); +}; + +const applicationCardCStyle: BoxStyle = { width: 100, maxWidth: 140 }; diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/CreatorInformation.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/CreatorInformation.tsx new file mode 100644 index 0000000000..9821319bc2 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/CreatorInformation.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import { View, useWindowDimensions } from "react-native"; + +import { ApplicationCard } from "./ApplicationCard"; + +import { BrandText } from "@/components/BrandText"; +import { useNSUserInfo } from "@/hooks/useNSUserInfo"; +import { parseUserId } from "@/networks"; +import { launchpadReviewBreakpointM } from "@/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/LaunchpadApplicationReviewScreen"; +import { fontSemibold20 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { tinyAddress } from "@/utils/text"; +import { CollectionDataResult } from "@/utils/types/launchpad"; + +export const CreatorInformation: React.FC<{ + creatorId: string; + collectionData: CollectionDataResult; +}> = ({ creatorId, collectionData }) => { + const { width } = useWindowDimensions(); + const creatorNSInfo = useNSUserInfo(creatorId); + const [, creatorAddress] = parseUserId(creatorId); + const creatorDisplayName = + creatorNSInfo?.metadata?.tokenId || tinyAddress(creatorAddress); + + return ( + + Creator information + = launchpadReviewBreakpointM ? "row" : "column", + marginTop: layout.spacing_x2, + gap: layout.spacing_x1_5, + flexWrap: "wrap", + }} + > + + + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/InvestmentInformation.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/InvestmentInformation.tsx new file mode 100644 index 0000000000..e163d476d5 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/InvestmentInformation.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { View, useWindowDimensions } from "react-native"; + +import { ApplicationCard } from "./ApplicationCard"; +import { launchpadReviewBreakpointSM } from "../LaunchpadApplicationReviewScreen"; + +import { BrandText } from "@/components/BrandText"; +import { launchpadReviewBreakpointM } from "@/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/LaunchpadApplicationReviewScreen"; +import { fontSemibold20 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { CollectionDataResult } from "@/utils/types/launchpad"; + +export const InvestmentInformation: React.FC<{ + collectionData: CollectionDataResult; +}> = ({ collectionData }) => { + const { width } = useWindowDimensions(); + + return ( + + Investment information + = launchpadReviewBreakpointM ? "row" : "column", + marginTop: layout.spacing_x2, + gap: layout.spacing_x1_5, + flexWrap: "wrap", + }} + > + + + + = launchpadReviewBreakpointSM ? "row" : "column", + marginVertical: layout.spacing_x2, + gap: layout.spacing_x1_5, + flexWrap: "wrap", + }} + > + + + + + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/LinkCard.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/LinkCard.tsx new file mode 100644 index 0000000000..3be8c8d046 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/LinkCard.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import { FlatList, StyleProp, View } from "react-native"; + +import { BrandText } from "@/components/BrandText"; +import { BoxStyle } from "@/components/boxes/Box"; +import { TertiaryBox } from "@/components/boxes/TertiaryBox"; +import { neutral77 } from "@/utils/style/colors"; +import { fontSemibold12, fontSemibold14 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; + +export const LinkCard: React.FC<{ + title: string; + linksData: { title: string; link: string }[]; + style?: StyleProp; +}> = ({ title, linksData, style }) => { + return ( + + + {title} + + ( + + + {item.title} + + {/* Linking.openURL(item.link)} style={{flex: 1}}>*/} + + {item.link} + + {/**/} + + )} + /> + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/MintingInformation.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/MintingInformation.tsx new file mode 100644 index 0000000000..963e4d6000 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/MintingInformation.tsx @@ -0,0 +1,156 @@ +import moment from "moment"; +import React, { Fragment } from "react"; +import { useWindowDimensions, View } from "react-native"; + +import { ApplicationCard } from "./ApplicationCard"; +import { launchpadReviewBreakpointSM } from "../LaunchpadApplicationReviewScreen"; + +import { BrandText } from "@/components/BrandText"; +import { SpacerColumn } from "@/components/spacer"; +import { launchpadReviewBreakpointM } from "@/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/LaunchpadApplicationReviewScreen"; +import { fontSemibold20 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { + CollectionDataResult, + MintPeriodDataResult, +} from "@/utils/types/launchpad"; + +export const MintingInformation: React.FC<{ + collectionData: CollectionDataResult; +}> = ({ collectionData }) => { + const { width } = useWindowDimensions(); + + return ( + + + Minting Information + + + + + {!!( + collectionData.royalty_address && collectionData.royalty_percentage + ) && ( + = launchpadReviewBreakpointM ? "row" : "column", + marginTop: layout.spacing_x2, + flexWrap: "wrap", + gap: layout.spacing_x1_5, + }} + > + {!!collectionData.royalty_address && ( + + )} + {!!collectionData.royalty_percentage && ( + + )} + + )} + + {collectionData.mint_periods.map((mintPeriod, index) => ( + + + + + ))} + + ); +}; + +const MintPeriod: React.FC<{ + mintPeriod: MintPeriodDataResult; + index: number; +}> = ({ mintPeriod, index }) => { + const { width } = useWindowDimensions(); + + return ( + <> + {`Minting Period #${index + 1}`} + + {!!mintPeriod.price && ( + = launchpadReviewBreakpointSM ? "row" : "column", + marginTop: layout.spacing_x2, + flexWrap: "wrap", + gap: layout.spacing_x1_5, + }} + > + + + + )} + + {!!(mintPeriod.max_tokens && mintPeriod.limit_per_address) && ( + = launchpadReviewBreakpointSM ? "row" : "column", + marginTop: layout.spacing_x2, + flexWrap: "wrap", + gap: layout.spacing_x1_5, + }} + > + {!!mintPeriod.max_tokens && ( + + )} + {!!mintPeriod.limit_per_address && ( + + )} + + )} + + = launchpadReviewBreakpointSM ? "row" : "column", + marginTop: layout.spacing_x2, + flexWrap: "wrap", + gap: layout.spacing_x1_5, + }} + > + + {!!mintPeriod.end_time && ( + + )} + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/ProjectInformation.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/ProjectInformation.tsx new file mode 100644 index 0000000000..c23a5aab79 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/ProjectInformation.tsx @@ -0,0 +1,110 @@ +import React from "react"; +import { useWindowDimensions, View } from "react-native"; + +import { ApplicationCard } from "./ApplicationCard"; +import { + launchpadReviewBreakpointS, + launchpadReviewBreakpointSM, +} from "../LaunchpadApplicationReviewScreen"; + +import { BrandText } from "@/components/BrandText"; +import { fontSemibold20 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { CollectionDataResult } from "@/utils/types/launchpad"; + +export const ProjectInformation: React.FC<{ + collectionData: CollectionDataResult; +}> = ({ collectionData }) => { + const { width } = useWindowDimensions(); + + return ( + + + Project information + + + + + + = launchpadReviewBreakpointS ? "row" : "column", + marginVertical: layout.spacing_x2, + flexWrap: "wrap", + gap: layout.spacing_x1_5, + }} + > + = launchpadReviewBreakpointSM ? "row" : "column", + flexWrap: "wrap", + flex: 1, + gap: layout.spacing_x1_5, + }} + > + + + + + = launchpadReviewBreakpointSM ? "row" : "column", + flexWrap: "wrap", + flex: 1, + gap: layout.spacing_x1_5, + }} + > + + + + + + = launchpadReviewBreakpointS ? "row" : "column", + // marginTop: layout.spacing_x2, + flexWrap: "wrap", + gap: layout.spacing_x1_5, + }} + > + + + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/TeamInformation.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/TeamInformation.tsx new file mode 100644 index 0000000000..4ef85ef14b --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/TeamInformation.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { useWindowDimensions, View } from "react-native"; + +import { ApplicationCard } from "./ApplicationCard"; + +import { BrandText } from "@/components/BrandText"; +import { launchpadReviewBreakpointM } from "@/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/LaunchpadApplicationReviewScreen"; +import { fontSemibold20 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { CollectionDataResult } from "@/utils/types/launchpad"; + +export const TeamInformation: React.FC<{ + collectionData: CollectionDataResult; +}> = ({ collectionData }) => { + const { width } = useWindowDimensions(); + + return ( + + Team information + = launchpadReviewBreakpointM ? "row" : "column", + marginTop: layout.spacing_x2, + gap: layout.spacing_x1_5, + }} + > + + + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplications/LaunchpadApplicationsScreen.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplications/LaunchpadApplicationsScreen.tsx new file mode 100644 index 0000000000..4e97dc864c --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplications/LaunchpadApplicationsScreen.tsx @@ -0,0 +1,136 @@ +import React, { useState } from "react"; +import { View } from "react-native"; + +import { CompletesTable } from "./../components/CompletesTable"; +import { IncompletesTable } from "./../components/IncompletesTable"; +import { + LaunchpadAdminDashboardTabsListType, + launchpadAdminTabs, +} from "../LaunchpadAdministrationOverview/LaunchpadAdministrationOverviewScreen"; +import { ConfirmedsTable } from "../components/ConfirmedsTable"; +import { ReviewingsTable } from "../components/ReviewingsTable"; + +import { BrandText } from "@/components/BrandText"; +import { ScreenContainer } from "@/components/ScreenContainer"; +import { SpacerColumn } from "@/components/spacer"; +import { Tabs } from "@/components/tabs/Tabs"; +import { useIsUserLaunchpadAdmin } from "@/hooks/launchpad/useIsUserLaunchpadAdmin"; +import { useLaunchpadProjectsCounts } from "@/hooks/launchpad/useLaunchpadProjectsCounts"; +import { useAppNavigation } from "@/hooks/navigation/useAppNavigation"; +import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork"; +import useSelectedWallet from "@/hooks/useSelectedWallet"; +import { NetworkFeature } from "@/networks"; +import { errorColor, neutral33 } from "@/utils/style/colors"; +import { fontSemibold20, fontSemibold28 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; + +export const LaunchpadApplicationsScreen: React.FC = () => { + const navigation = useAppNavigation(); + const selectedNetworkId = useSelectedNetworkId(); + const userId = useSelectedWallet()?.userId; + const { isUserLaunchpadAdmin, isLoading: isUserAdminLoading } = + useIsUserLaunchpadAdmin(userId); + const { statusCounts } = useLaunchpadProjectsCounts({ + networkId: selectedNetworkId, + }); + + const [selectedTab, setSelectedTab] = + useState("INCOMPLETE"); + + if (!isUserLaunchpadAdmin) { + return ( + } + headerChildren={ + + {isUserAdminLoading ? "Loading..." : "Unauthorized"} + + } + responsive + onBackPress={() => + navigation.navigate("LaunchpadAdministrationOverview") + } + forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]} + > + + {isUserAdminLoading ? "Loading..." : "Unauthorized"} + + + ); + } + + return ( + } + headerChildren={ + Administration Dashboard + } + responsive + onBackPress={() => navigation.navigate("LaunchpadAdministrationOverview")} + forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]} + > + + + Launchpad Applications + + + + + + + {selectedTab === "INCOMPLETE" ? ( + + ) : selectedTab === "COMPLETE" ? ( + + ) : selectedTab === "REVIEWING" ? ( + + ) : selectedTab === "CONFIRMED" ? ( + + ) : ( + <> + )} + + + + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadReadyApplications/LaunchpadReadyApplicationsScreen.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadReadyApplications/LaunchpadReadyApplicationsScreen.tsx new file mode 100644 index 0000000000..7503411397 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadReadyApplications/LaunchpadReadyApplicationsScreen.tsx @@ -0,0 +1,116 @@ +import React from "react"; +import { View } from "react-native"; + +import { LaunchpadReadyApplicationsTable } from "./components/LaunchpadReadyApplicationsTable"; + +import { Sort, SortDirection, Status } from "@/api/launchpad/v1/launchpad"; +import { BrandText } from "@/components/BrandText"; +import { ScreenContainer } from "@/components/ScreenContainer"; +import { SpacerColumn } from "@/components/spacer"; +import { useIsUserLaunchpadAdmin } from "@/hooks/launchpad/useIsUserLaunchpadAdmin"; +import { useLaunchpadProjects } from "@/hooks/launchpad/useLaunchpadProjects"; +import { useAppNavigation } from "@/hooks/navigation/useAppNavigation"; +import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork"; +import useSelectedWallet from "@/hooks/useSelectedWallet"; +import { NetworkFeature } from "@/networks"; +import { errorColor } from "@/utils/style/colors"; +import { + fontSemibold13, + fontSemibold20, + fontSemibold28, +} from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; + +export const LaunchpadReadyApplicationsScreen: React.FC = () => { + const selectedNetworkId = useSelectedNetworkId(); + const navigation = useAppNavigation(); + const userId = useSelectedWallet()?.userId; + const { isUserLaunchpadAdmin, isLoading: isUserAdminLoading } = + useIsUserLaunchpadAdmin(userId); + const { launchpadProjects = [] } = useLaunchpadProjects({ + networkId: selectedNetworkId, + offset: 0, + limit: 100, // TODO: Pagination + sort: Sort.SORT_UNSPECIFIED, + sortDirection: SortDirection.SORT_DIRECTION_UNSPECIFIED, + status: Status.STATUS_COMPLETE, // TODO: Or STATUS_CONFIRMED ? + }); + + if (!isUserLaunchpadAdmin) { + return ( + } + headerChildren={ + + {isUserAdminLoading ? "Loading..." : "Unauthorized"} + + } + responsive + onBackPress={() => + navigation.navigate("LaunchpadAdministrationOverview") + } + forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]} + > + + {isUserAdminLoading ? "Loading..." : "Unauthorized"} + + + ); + } + + return ( + } + headerChildren={ + Administration Dashboard + } + responsive + onBackPress={() => navigation.navigate("LaunchpadAdministrationOverview")} + forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]} + > + + + Ready to Launch + + + + {launchpadProjects?.length ? ( + + ) : ( + + There is no application ready to launch + + )} + + + + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadReadyApplications/components/LaunchpadReadyApplicationsTable.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadReadyApplications/components/LaunchpadReadyApplicationsTable.tsx new file mode 100644 index 0000000000..2fd8bfabe5 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadReadyApplications/components/LaunchpadReadyApplicationsTable.tsx @@ -0,0 +1,122 @@ +import React from "react"; +import { FlatList, View } from "react-native"; + +import { + commonColumns, + LaunchpadTablesCommonColumns, +} from "../../../LaunchpadApply/LaunchpadCreate/components/LaunchpadTablesCommonColumns"; +import { StatusBadge } from "../../../components/StatusBadge"; + +import { LaunchpadProject } from "@/api/launchpad/v1/launchpad"; +import { OmniLink } from "@/components/OmniLink"; +import { TableCell } from "@/components/table/TableCell"; +import { TableHeader } from "@/components/table/TableHeader"; +import { TableRow } from "@/components/table/TableRow"; +import { CellBrandText } from "@/components/table/TableTextCell"; +import { TableWrapper } from "@/components/table/TableWrapper"; +import { TableColumns } from "@/components/table/utils"; +import { parseCollectionData } from "@/utils/launchpad"; +import { screenContentMaxWidthLarge } from "@/utils/style/layout"; + +const columns: TableColumns = { + ...commonColumns, + projectReadinessForMint: { + label: "Project Readiness for Mint", + minWidth: 200, + flex: 2, + }, + whitelistQuantity: { + label: "Whitelist quantity", + minWidth: 100, + flex: 1, + }, + premiumMarketingPackage: { + label: "Premium marketing package", + minWidth: 160, + flex: 1.8, + }, + basicMarketingPackage: { + label: "Basic marketing package", + minWidth: 140, + flex: 1.2, + }, +}; + +const breakpointM = 1120; + +export const LaunchpadReadyApplicationsTable: React.FC<{ + rows: LaunchpadProject[]; +}> = ({ rows }) => { + const renderItem = ({ + item, + index, + }: { + item: LaunchpadProject; + index: number; + }) => { + const collectionData = parseCollectionData(item); + return ( + + ); + }; + + return ( + + + + item.symbol} + /> + + + ); +}; + +const LaunchpadReadyApplicationsTableRow: React.FC<{ + launchpadProject: LaunchpadProject; + index: number; +}> = ({ launchpadProject, index }) => { + const collectionData = parseCollectionData(launchpadProject); + if (!collectionData) return null; + return ( + + + + + TODO + + + + + + TODO + + TODO + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadAdmin/components/CompletesTable.tsx b/packages/screens/Launchpad/LaunchpadAdmin/components/CompletesTable.tsx new file mode 100644 index 0000000000..526b26f1af --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadAdmin/components/CompletesTable.tsx @@ -0,0 +1,26 @@ +import React, { FC } from "react"; + +import { LaunchpadCollectionsTable } from "./LaunchpadCollectionsTable"; +import { + Sort, + SortDirection, + Status, +} from "../../../../api/launchpad/v1/launchpad"; +import { useSelectedNetworkId } from "../../../../hooks/useSelectedNetwork"; + +import { useLaunchpadProjects } from "@/hooks/launchpad/useLaunchpadProjects"; + +export const CompletesTable: FC<{ + limit: number; +}> = ({ limit }) => { + const selectedNetworkId = useSelectedNetworkId(); + const { launchpadProjects = [] } = useLaunchpadProjects({ + networkId: selectedNetworkId, + offset: 0, + limit, + sort: Sort.SORT_UNSPECIFIED, + sortDirection: SortDirection.SORT_DIRECTION_UNSPECIFIED, + status: Status.STATUS_COMPLETE, + }); + return ; +}; diff --git a/packages/screens/Launchpad/LaunchpadAdmin/components/ConfirmedsTable.tsx b/packages/screens/Launchpad/LaunchpadAdmin/components/ConfirmedsTable.tsx new file mode 100644 index 0000000000..c89124b166 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadAdmin/components/ConfirmedsTable.tsx @@ -0,0 +1,26 @@ +import React, { FC } from "react"; + +import { LaunchpadCollectionsTable } from "./LaunchpadCollectionsTable"; +import { + Sort, + SortDirection, + Status, +} from "../../../../api/launchpad/v1/launchpad"; +import { useSelectedNetworkId } from "../../../../hooks/useSelectedNetwork"; + +import { useLaunchpadProjects } from "@/hooks/launchpad/useLaunchpadProjects"; + +export const ConfirmedsTable: FC<{ + limit: number; +}> = ({ limit }) => { + const selectedNetworkId = useSelectedNetworkId(); + const { launchpadProjects = [] } = useLaunchpadProjects({ + networkId: selectedNetworkId, + offset: 0, + limit, + sort: Sort.SORT_UNSPECIFIED, + sortDirection: SortDirection.SORT_DIRECTION_UNSPECIFIED, + status: Status.STATUS_CONFIRMED, + }); + return ; +}; diff --git a/packages/screens/Launchpad/LaunchpadAdmin/components/IncompletesTable.tsx b/packages/screens/Launchpad/LaunchpadAdmin/components/IncompletesTable.tsx new file mode 100644 index 0000000000..35fc1407ec --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadAdmin/components/IncompletesTable.tsx @@ -0,0 +1,26 @@ +import React, { FC } from "react"; + +import { LaunchpadCollectionsTable } from "./LaunchpadCollectionsTable"; +import { + Sort, + SortDirection, + Status, +} from "../../../../api/launchpad/v1/launchpad"; +import { useSelectedNetworkId } from "../../../../hooks/useSelectedNetwork"; + +import { useLaunchpadProjects } from "@/hooks/launchpad/useLaunchpadProjects"; + +export const IncompletesTable: FC<{ + limit: number; +}> = ({ limit }) => { + const selectedNetworkId = useSelectedNetworkId(); + const { launchpadProjects = [] } = useLaunchpadProjects({ + networkId: selectedNetworkId, + offset: 0, + limit, + sort: Sort.SORT_UNSPECIFIED, + sortDirection: SortDirection.SORT_DIRECTION_UNSPECIFIED, + status: Status.STATUS_INCOMPLETE, + }); + return ; +}; diff --git a/packages/screens/Launchpad/LaunchpadAdmin/components/LaunchpadCollectionsTable.tsx b/packages/screens/Launchpad/LaunchpadAdmin/components/LaunchpadCollectionsTable.tsx new file mode 100644 index 0000000000..f0d2b0641a --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadAdmin/components/LaunchpadCollectionsTable.tsx @@ -0,0 +1,176 @@ +import React from "react"; +import { FlatList, View } from "react-native"; + +import { + commonColumns, + LaunchpadTablesCommonColumns, +} from "../../LaunchpadApply/LaunchpadCreate/components/LaunchpadTablesCommonColumns"; + +import { LaunchpadProject } from "@/api/launchpad/v1/launchpad"; +import externalLinkSVG from "@/assets/icons/external-link.svg"; +import { Link } from "@/components/Link"; +import { OmniLink } from "@/components/OmniLink"; +import { SVG } from "@/components/SVG"; +import { TableCell } from "@/components/table/TableCell"; +import { TableHeader } from "@/components/table/TableHeader"; +import { TableRow } from "@/components/table/TableRow"; +import { TableTextCell } from "@/components/table/TableTextCell"; +import { TableWrapper } from "@/components/table/TableWrapper"; +import { TableColumns } from "@/components/table/utils"; +import { parseCollectionData } from "@/utils/launchpad"; +import { secondaryColor } from "@/utils/style/colors"; +import { screenContentMaxWidthLarge } from "@/utils/style/layout"; + +const columns: TableColumns = { + ...commonColumns, + twitterURL: { + label: "Twitter", + minWidth: 60, + flex: 0.25, + }, + discordURL: { + label: "Discord", + minWidth: 60, + flex: 0.25, + }, + expectedTotalSupply: { + label: "Expected Total Supply", + minWidth: 140, + flex: 1.8, + }, + expectedPublicMintPrice: { + label: "Expected Public Mint Price", + minWidth: 150, + flex: 1.8, + }, + expectedMintDate: { + label: "Expected Mint Date", + minWidth: 100, + flex: 1, + }, +}; +const breakpointM = 1110; + +// Displays collection_data as CollectionDataResult[] from many launchpad_projects +export const LaunchpadCollectionsTable: React.FC<{ + rows: LaunchpadProject[]; +}> = ({ rows }) => { + const renderItem = ({ + item, + index, + }: { + item: LaunchpadProject; + index: number; + }) => { + const collectionData = parseCollectionData(item); + return ( + + ); + }; + + return ( + + + + item.symbol} + /> + + + ); +}; + +const LaunchpadCollectionsTableRow: React.FC<{ + launchpadProject: LaunchpadProject; + index: number; +}> = ({ launchpadProject, index }) => { + const collectionData = parseCollectionData(launchpadProject); + if (!collectionData) return null; + return ( + + {/* */} + + + + + {/* */} + + + + + + + {/* */} + + + + + + + {`${collectionData.tokens_count}`} + + + + TODO REMOVE + + + + TODO REMOVE + + + {/*TODO: Three dots here with possible actions on the collection ?*/} + {/**/} + + {/* */} + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadAdmin/components/ReviewingsTable.tsx b/packages/screens/Launchpad/LaunchpadAdmin/components/ReviewingsTable.tsx new file mode 100644 index 0000000000..5bb0dd3cc4 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadAdmin/components/ReviewingsTable.tsx @@ -0,0 +1,26 @@ +import React, { FC } from "react"; + +import { LaunchpadCollectionsTable } from "./LaunchpadCollectionsTable"; +import { + Sort, + SortDirection, + Status, +} from "../../../../api/launchpad/v1/launchpad"; +import { useSelectedNetworkId } from "../../../../hooks/useSelectedNetwork"; + +import { useLaunchpadProjects } from "@/hooks/launchpad/useLaunchpadProjects"; + +export const ReviewingsTable: FC<{ + limit: number; +}> = ({ limit }) => { + const selectedNetworkId = useSelectedNetworkId(); + const { launchpadProjects = [] } = useLaunchpadProjects({ + networkId: selectedNetworkId, + offset: 0, + limit, + sort: Sort.SORT_UNSPECIFIED, + sortDirection: SortDirection.SORT_DIRECTION_UNSPECIFIED, + status: Status.STATUS_REVIEWING, + }); + return ; +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadApplyScreen.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadApplyScreen.tsx index 6fd2715f5e..0644d7a9ff 100644 --- a/packages/screens/Launchpad/LaunchpadApply/LaunchpadApplyScreen.tsx +++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadApplyScreen.tsx @@ -1,18 +1,25 @@ import React from "react"; -import { StyleSheet, View } from "react-native"; +import { Linking, TextStyle, View } from "react-native"; + +import { + LargeBoxButton, + LargeBoxButtonProps, +} from "../../../components/buttons/LargeBoxButton"; import LaunchpadBannerImage from "@/assets/banners/launchpad.jpg"; import { BrandText } from "@/components/BrandText"; import { ImageBackgroundLogoText } from "@/components/ImageBackgroundLogoText"; +import { OmniLink } from "@/components/OmniLink"; import { ScreenContainer } from "@/components/ScreenContainer"; -import { - LargeBoxButton, - LargeBoxButtonProps, -} from "@/components/buttons/LargeBoxButton"; -import { SpacerColumn, SpacerRow } from "@/components/spacer"; +import { CustomPressable } from "@/components/buttons/CustomPressable"; +import { SpacerColumn } from "@/components/spacer"; +import { useMaxResolution } from "@/hooks/useMaxResolution"; import { ScreenFC } from "@/utils/navigation"; import { neutral77 } from "@/utils/style/colors"; import { fontSemibold14, fontSemibold28 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; + +const MD_BREAKPOINT = 720; const BUTTONS: LargeBoxButtonProps[] = [ { @@ -24,16 +31,17 @@ const BUTTONS: LargeBoxButtonProps[] = [ title: "Create", description: "Upload your assets, enter collection metadata and deploy your collection.", - buttonTitle: "Coming soon", + buttonTitle: "Open", }, { title: "My Collections", description: "Manage your collections with available actions and queries.", - buttonTitle: "Coming soon", + buttonTitle: "Open", }, ]; export const LaunchpadApplyScreen: ScreenFC<"LaunchpadApply"> = () => { + const { width } = useMaxResolution(); return ( = () => { Welcome - + Looking for a fast and efficient way to build an NFT collection? - + Teritori is the solution. Teritori is built to provide useful smart contract interfaces that helps you build and deploy your own NFT collections in no time. - - - - - - + + + + Linking.openURL("https://airtable.com/shr1kU7kXW0267gNV") + } + style={{ flex: 1 }} + > + + + + = MD_BREAKPOINT ? layout.spacing_x1_5 : 0, + marginVertical: width >= MD_BREAKPOINT ? 0 : layout.spacing_x1_5, + }} + > + + + + + + ); }; -// FIXME: remove StyleSheet.create -// eslint-disable-next-line no-restricted-syntax -const styles = StyleSheet.create({ - descriptionText: StyleSheet.flatten([ - fontSemibold14, - { - color: neutral77, - }, - ]), - buttonsContainer: { - flexDirection: "row", - flex: 1, - }, -}); +const descriptionTextCStyle: TextStyle = { + ...fontSemibold14, + color: neutral77, +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCompleteScreen.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCompleteScreen.tsx new file mode 100644 index 0000000000..72a96f9afb --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCompleteScreen.tsx @@ -0,0 +1,218 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import React, { useState } from "react"; +import { FieldErrors, FormProvider, useForm } from "react-hook-form"; +import { View } from "react-native"; +import { useSelector } from "react-redux"; + +import { TextInputLaunchpad } from "./components/inputs/TextInputLaunchpad"; + +import { BrandText } from "@/components/BrandText"; +import { NotFound } from "@/components/NotFound"; +import { ScreenContainer } from "@/components/ScreenContainer"; +import { PrimaryButton } from "@/components/buttons/PrimaryButton"; +import { SpacerColumn } from "@/components/spacer"; +import { useFeedbacks } from "@/context/FeedbacksProvider"; +import { useCompleteCollection } from "@/hooks/launchpad/useCompleteCollection"; +import { useLaunchpadProjectById } from "@/hooks/launchpad/useLaunchpadProjectById"; +import { useIsMobile } from "@/hooks/useIsMobile"; +import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork"; +import useSelectedWallet from "@/hooks/useSelectedWallet"; +import { NetworkFeature } from "@/networks"; +import { AssetsTab } from "@/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/AssetsTab"; +import { selectNFTStorageAPI } from "@/store/slices/settings"; +import { ScreenFC, useAppNavigation } from "@/utils/navigation"; +import { + neutral33, + neutral55, + neutral77, + primaryColor, +} from "@/utils/style/colors"; +import { + fontSemibold13, + fontSemibold14, + fontSemibold28, +} from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { + CollectionAssetsMetadatasFormValues, + CollectionFormValues, + ZodCollectionAssetsMetadatasFormValues, +} from "@/utils/types/launchpad"; + +export const LaunchpadCompleteScreen: ScreenFC<"LaunchpadComplete"> = ({ + route, +}) => { + const isMobile = useIsMobile(); + const { setToast } = useFeedbacks(); + const userIPFSKey = useSelector(selectNFTStorageAPI); + const { id: symbol } = route.params; + const selectedNetworkId = useSelectedNetworkId(); + const navigation = useAppNavigation(); + const selectedWallet = useSelectedWallet(); + const { completeCollection } = useCompleteCollection(); + const { launchpadProject } = useLaunchpadProjectById({ + networkId: selectedNetworkId, + projectId: symbol, + }); + const assetsMetadatasForm = useForm({ + mode: "all", + resolver: zodResolver(ZodCollectionAssetsMetadatasFormValues), + defaultValues: { assetsMetadatas: [], nftApiKey: userIPFSKey }, + }); + const [isLoading, setLoading] = useState(false); + const { setLoadingFullScreen } = useFeedbacks(); + const assetsMetadatas = assetsMetadatasForm.watch("assetsMetadatas"); + const nftApiKey = assetsMetadatasForm.watch("nftApiKey"); + + const onValid = async () => { + setLoading(true); + setLoadingFullScreen(true); + try { + const success = await completeCollection( + symbol, + assetsMetadatasForm.getValues(), + ); + setLoading(false); + setLoadingFullScreen(false); + if (success) navigation.navigate("LaunchpadMyCollections"); + } catch (e) { + console.error("Error completing the NFT collection", e); + } finally { + setLoading(false); + setLoadingFullScreen(false); + } + }; + + const onInvalid = (fieldsErrors: FieldErrors) => { + console.error("Fields errors: ", fieldsErrors); + setToast({ + mode: "normal", + type: "error", + title: "Unable to complete the collection", + message: + "Some fields are not correctly filled.\nPlease complete properly the mapping file.\nCheck the description for more information.", + }); + }; + + const onPressComplete = () => + assetsMetadatasForm.handleSubmit(onValid, onInvalid)(); + + return ( + } + forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]} + // TODO: Remove after tests + forceNetworkId="teritori-testnet" + headerChildren={Collection Completion} + onBackPress={() => navigation.navigate("LaunchpadMyCollections")} + > + + {!selectedWallet?.userId ? ( + + You are not connected + + ) : !launchpadProject ? ( + + ) : launchpadProject.networkId !== selectedNetworkId ? ( + Wrong network + ) : selectedWallet.userId !== launchpadProject?.creatorId ? ( + + You don't own this Collection + + ) : ( + + + + Assets & Metadata + + + + Make sure you check out{" "} + + documentation + {" "} + on how to create your collection + + + + + + + label="NFT.Storage JWT" + sublabel={ + + Used to upload the cover image and the assets to your + NFT Storage + + } + placeHolder="My Awesome Collection" + name="nftApiKey" + form={assetsMetadatasForm} + /> + + + + + + + + + + + )} + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/LaunchpadCreateScreen.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/LaunchpadCreateScreen.tsx new file mode 100644 index 0000000000..5594441f7a --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/LaunchpadCreateScreen.tsx @@ -0,0 +1,185 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import React, { useMemo, useState } from "react"; +import { FormProvider, useForm } from "react-hook-form"; +import { View } from "react-native"; +import { useSelector } from "react-redux"; + +import { BrandText } from "@/components/BrandText"; +import { ScreenContainer } from "@/components/ScreenContainer"; +import { PrimaryButton } from "@/components/buttons/PrimaryButton"; +import { SecondaryButton } from "@/components/buttons/SecondaryButton"; +import { SpacerColumn } from "@/components/spacer"; +import { useFeedbacks } from "@/context/FeedbacksProvider"; +import { useCreateCollection } from "@/hooks/launchpad/useCreateCollection"; +import { useSelectedNetworkInfo } from "@/hooks/useSelectedNetwork"; +import { NetworkFeature } from "@/networks"; +import { + LaunchpadCreateStepKey, + LaunchpadStepper, +} from "@/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/LaunchpadStepper"; +import { LaunchpadAdditional } from "@/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAdditional"; +import { LaunchpadAssetsAndMetadata } from "@/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/LaunchpadAssetsAndMetadata"; +import { LaunchpadBasic } from "@/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadBasic"; +import { LaunchpadDetails } from "@/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadDetails"; +import { LaunchpadMinting } from "@/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMinting"; +import { LaunchpadTeamAndInvestment } from "@/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadTeamAndInvestment"; +import { selectNFTStorageAPI } from "@/store/slices/settings"; +import { ScreenFC, useAppNavigation } from "@/utils/navigation"; +import { neutral33 } from "@/utils/style/colors"; +import { layout } from "@/utils/style/layout"; +import { + CollectionFormValues, + ZodCollectionFormValues, +} from "@/utils/types/launchpad"; + +export const LaunchpadCreateScreen: ScreenFC<"LaunchpadCreate"> = () => { + const navigation = useAppNavigation(); + const selectedNetwork = useSelectedNetworkInfo(); + const { setToast } = useFeedbacks(); + const userIPFSKey = useSelector(selectNFTStorageAPI); + const collectionForm = useForm({ + mode: "all", + defaultValues: { + mintPeriods: [ + { + price: { + denom: selectedNetwork?.currencies[0].denom, + }, + isOpen: true, + }, + ], + assetsMetadatas: { + nftApiKey: userIPFSKey, + }, + }, + resolver: zodResolver(ZodCollectionFormValues), + }); + const { createCollection } = useCreateCollection(); + const [selectedStepKey, setSelectedStepKey] = + useState(1); + const [isLoading, setLoading] = useState(false); + const { setLoadingFullScreen } = useFeedbacks(); + + const stepContent = useMemo(() => { + switch (selectedStepKey) { + case 1: + return ; + case 2: + return ; + case 3: + return ; + case 4: + return ; + case 5: + return ; + case 6: + return ; + default: + return ; + } + }, [selectedStepKey]); + + const onValid = async () => { + setLoading(true); + setLoadingFullScreen(true); + try { + const success = await createCollection(collectionForm.getValues()); + if (success) navigation.navigate("LaunchpadMyCollections"); + } catch (e) { + console.error("Error creating a NFT collection", e); + } finally { + setLoading(false); + setLoadingFullScreen(false); + } + }; + + const onInvalid = () => { + setToast({ + mode: "normal", + type: "error", + title: "Unable to create the collection", + message: + "Some fields are not correctly filled." + + "\nMaybe from the mapping file, please complete it properly.\nCheck the description for more information.", + }); + }; + + const onPressSubmit = () => collectionForm.handleSubmit(onValid, onInvalid)(); + + return ( + } + forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]} + // TODO: Remove after tests + forceNetworkId="teritori-testnet" + headerChildren={Apply to Launchpad} + onBackPress={() => navigation.navigate("LaunchpadApply")} + > + + + + + {stepContent} + + + + + {selectedStepKey !== 1 && ( + setSelectedStepKey(selectedStepKey - 1)} + /> + )} + + {selectedStepKey === 6 ? ( + + ) : ( + setSelectedStepKey(selectedStepKey + 1)} + /> + )} + + + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/LaunchpadStepper.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/LaunchpadStepper.tsx new file mode 100644 index 0000000000..5b3e31d907 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/LaunchpadStepper.tsx @@ -0,0 +1,247 @@ +import React, { Dispatch, FC, useRef } from "react"; +import { useFormContext } from "react-hook-form"; +import { + LayoutChangeEvent, + ScrollView, + TouchableOpacity, + useWindowDimensions, + View, +} from "react-native"; + +import ChevronRightSvg from "@/assets/icons/chevron-right.svg"; +import RejectSVG from "@/assets/icons/reject.svg"; +import { BrandText } from "@/components/BrandText"; +import { SVG } from "@/components/SVG"; +import { PrimaryBox } from "@/components/boxes/PrimaryBox"; +import { useIsMobile } from "@/hooks/useIsMobile"; +import { + neutral17, + neutral22, + neutral77, + primaryColor, + primaryTextColor, +} from "@/utils/style/colors"; +import { fontSemibold14 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { CollectionFormValues } from "@/utils/types/launchpad"; +export type LaunchpadCreateStepKey = number; + +interface LaunchpadStepperProps { + selectedStepKey: LaunchpadCreateStepKey; + setSelectedStepKey: Dispatch>; +} + +interface LaunchpadCreateStep { + key: LaunchpadCreateStepKey; + title: string; +} + +const steps: LaunchpadCreateStep[] = [ + { + key: 1, + title: "Basic", + }, + { + key: 2, + title: "Details", + }, + { + key: 3, + title: "Team & Investments", + }, + { + key: 4, + title: "Additional", + }, + { + key: 5, + title: "Minting", + }, + { + key: 6, + title: "Assets & Metadata", + }, +]; + +export const LaunchpadStepper: FC = ({ + selectedStepKey, + setSelectedStepKey, +}) => { + const { width: windowWidth } = useWindowDimensions(); + const scrollViewRef = useRef(null); + const isMobile = useIsMobile(); + const collectionForm = useFormContext(); + + const hasErrors = (stepKey: number) => { + if ( + (stepKey === 1 && + (!!collectionForm.getFieldState("name").error || + !!collectionForm.getFieldState("description").error || + !!collectionForm.getFieldState("symbol").error)) || + !!collectionForm.getFieldState("coverImage").error || + !!collectionForm.getFieldState("assetsMetadatas.nftApiKey").error + ) { + return true; + } + if ( + stepKey === 2 && + (!!collectionForm.getFieldState("websiteLink").error || + !!collectionForm.getFieldState("isDerivativeProject").error || + !!collectionForm.getFieldState("projectTypes").error || + !!collectionForm.getFieldState("isPreviouslyApplied").error || + !!collectionForm.getFieldState("email").error) + ) { + return true; + } + if ( + stepKey === 3 && + (!!collectionForm.getFieldState("teamDescription").error || + !!collectionForm.getFieldState("partnersDescription").error || + !!collectionForm.getFieldState("investDescription").error || + !!collectionForm.getFieldState("investLink").error) + ) { + return true; + } + if ( + stepKey === 4 && + (!!collectionForm.getFieldState("artworkDescription").error || + !!collectionForm.getFieldState("isReadyForMint").error || + !!collectionForm.getFieldState("isDox").error || + !!collectionForm.getFieldState("daoWhitelistCount").error || + !!collectionForm.getFieldState("escrowMintProceedsPeriod").error) + ) { + return true; + } + if ( + stepKey === 5 && + (!!collectionForm.getFieldState("mintPeriods").error || + !!collectionForm.getFieldState("royaltyAddress").error || + !!collectionForm.getFieldState("royaltyPercentage").error) + ) { + return true; + } + if ( + stepKey === 6 && + !!collectionForm.getFieldState("assetsMetadatas").error + ) { + return true; + } + }; + + const onSelectedItemLayout = (e: LayoutChangeEvent) => { + scrollViewRef.current?.scrollTo({ + x: e.nativeEvent.layout.x, + animated: false, + }); + }; + + return ( + + + = 1240 && { justifyContent: "center" }, + { + flexDirection: "row", + width: "100%", + }, + ]} + > + {steps.map((step, index) => { + const isSelected = selectedStepKey === step.key; + return ( + { + if (isSelected) onSelectedItemLayout(e); + }} + onPress={() => setSelectedStepKey(step.key)} + style={[ + { + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + paddingHorizontal: layout.spacing_x2, + paddingVertical: layout.spacing_x1, + }, + ]} + > + + {hasErrors(step.key) && ( + + )} + + {step.key} + + + + {step.title} + + {steps.length !== index + 1 && ( + + )} + + ); + })} + + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/LaunchpadTablesCommonColumns.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/LaunchpadTablesCommonColumns.tsx new file mode 100644 index 0000000000..917aafc509 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/LaunchpadTablesCommonColumns.tsx @@ -0,0 +1,108 @@ +import React from "react"; + +import { NetworkIcon } from "./../../../../../components/NetworkIcon"; + +import defaultCollectionImagePNG from "@/assets/default-images/ava.png"; +import checkBadgeSVG from "@/assets/icons/certified.svg"; +import { SVG } from "@/components/SVG"; +import { RoundedGradientImage } from "@/components/images/RoundedGradientImage"; +import { SpacerRow } from "@/components/spacer"; +import { TableCell } from "@/components/table/TableCell"; +import { CellBrandText, TableTextCell } from "@/components/table/TableTextCell"; +import { TableColumns } from "@/components/table/utils"; +import { getNetwork } from "@/networks"; +import { web3ToWeb2URI } from "@/utils/ipfs"; +import { layout } from "@/utils/style/layout"; +import { CollectionDataResult } from "@/utils/types/launchpad"; + +export const commonColumns: TableColumns = { + rank: { + label: "#", + minWidth: 20, + flex: 0.25, + }, + collectionName: { + label: "Collection Name", + minWidth: 240, + flex: 3, + }, + symbol: { + label: "Symbol", + minWidth: 80, + flex: 0.5, + }, + collectionNetwork: { + label: "Collection Network", + minWidth: 150, + flex: 1.8, + }, +}; + +export const LaunchpadTablesCommonColumns: React.FC<{ + collectionData: CollectionDataResult; + index: number; +}> = ({ collectionData, index }) => { + const network = getNetwork(collectionData.target_network); + return ( + <> + + {`${index + 1}`} + + + + + {collectionData.name} + + + + + + {collectionData.symbol} + + + + + + + {network?.displayName || "UNKNOWN NETWORK"} + + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAdditional.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAdditional.tsx new file mode 100644 index 0000000000..f6f42dee0c --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAdditional.tsx @@ -0,0 +1,143 @@ +import React, { FC } from "react"; +import { Controller, useFormContext } from "react-hook-form"; +import { View } from "react-native"; + +import { SelectInputLaunchpad } from "../../../components/inputs/selectInputs/SelectInputLaunchpad"; + +import { BrandText } from "@/components/BrandText"; +import { ErrorText } from "@/components/ErrorText"; +import { SpacerColumn } from "@/components/spacer"; +import { TextInputLaunchpad } from "@/screens/Launchpad/LaunchpadApply/components/inputs/TextInputLaunchpad"; +import { neutral55, neutral77 } from "@/utils/style/colors"; +import { + fontSemibold13, + fontSemibold14, + fontSemibold20, +} from "@/utils/style/fonts"; +import { CollectionFormValues } from "@/utils/types/launchpad"; + +export const LaunchpadAdditional: FC = () => { + const collectionForm = useFormContext(); + const escrowMintProceedsPeriod = collectionForm.watch( + "escrowMintProceedsPeriod", + ); + const isReadyForMint = collectionForm.watch("isReadyForMint"); + const isDox = collectionForm.watch("isDox"); + return ( + + + Additional Information + + + Fill the additional information + + + + + label="Please describe your artwork: " + sublabel={ + + + 1. Is it completely original? + + + 2. Who is the artist? + + + 3. How did your team meet the artist? + + + } + placeHolder="Describe here..." + name="artworkDescription" + form={collectionForm} + /> + + + name="isReadyForMint" + control={collectionForm.control} + render={({ field: { onChange } }) => ( + <> + { + onChange(item === "Yes"); + }} + label="Is your collection ready for the mint?" + style={{ zIndex: 3 }} + /> + + {collectionForm.getFieldState("isReadyForMint").error?.message} + + + )} + /> + + + + name="escrowMintProceedsPeriod" + control={collectionForm.control} + render={({ field: { onChange } }) => ( + <> + { + onChange(item); + }} + label="If selected for the launchpad, You will escrow mint proceeds for this time period:" + style={{ zIndex: 2 }} + /> + + { + collectionForm.getFieldState("escrowMintProceedsPeriod").error + ?.message + } + + + )} + /> + + + + name="isDox" + control={collectionForm.control} + render={({ field: { onChange } }) => ( + <> + { + onChange(item === "Yes"); + }} + label="Are you dox or have you planned to dox?" + style={{ zIndex: 1 }} + /> + + {collectionForm.getFieldState("isDox").error?.message} + + + )} + /> + + + + label="We'd love to offer TeritoriDAO members 10% of your whitelist supply if your project is willing. Please let us know how many whitelist spots you'd be willing to allocate our DAO: " + placeHolder="0" + name="daoWhitelistCount" + form={collectionForm} + /> + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/AssetModal.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/AssetModal.tsx new file mode 100644 index 0000000000..1f4d85dd4e --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/AssetModal.tsx @@ -0,0 +1,193 @@ +import React from "react"; +import { useFormContext } from "react-hook-form"; +import { View } from "react-native"; + +import { BrandText } from "@/components/BrandText"; +import { OptimizedImage } from "@/components/OptimizedImage"; +import { PrimaryBox } from "@/components/boxes/PrimaryBox"; +import { TertiaryBox } from "@/components/boxes/TertiaryBox"; +import { PrimaryButton } from "@/components/buttons/PrimaryButton"; +import { Label } from "@/components/inputs/TextInputCustom"; +import ModalBase from "@/components/modals/ModalBase"; +import { Separator } from "@/components/separators/Separator"; +import { TextInputLaunchpad } from "@/screens/Launchpad/LaunchpadApply/components/inputs/TextInputLaunchpad"; +import { + neutral22, + neutral33, + neutral77, + neutralFF, + secondaryColor, +} from "@/utils/style/colors"; +import { + fontSemibold14, + fontSemibold16, + fontSemibold20, +} from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { + CollectionAssetsMetadataFormValues, + CollectionAssetsMetadatasFormValues, +} from "@/utils/types/launchpad"; + +export const AssetModal: React.FC<{ + onClose: () => void; + isVisible: boolean; + elem: CollectionAssetsMetadataFormValues; + elemIndex: number; +}> = ({ onClose, isVisible, elem, elemIndex }) => { + const assetsMetadatasForm = + useFormContext(); + const namePath = `assetsMetadatas.${elemIndex}.name` as const; + const descriptionPath = `assetsMetadatas.${elemIndex}.description` as const; + const externalUrlPath = `assetsMetadatas.${elemIndex}.externalUrl` as const; + const youtubeUrlPath = `assetsMetadatas.${elemIndex}.youtubeUrl` as const; + const attributes = assetsMetadatasForm.watch( + `assetsMetadatas.${elemIndex}.attributes`, + ); + + return ( + + + {elem.image && ( + + )} + + + + Asset #{elemIndex + 1} + + + File name: {elem.image?.fileName} + + + + } + hideMainSeparator + childrenBottom={ + + + + + + + } + > + + + + + name={namePath} + label="Name" + form={assetsMetadatasForm} + placeHolder="Token name" + disabled + /> + + + name={descriptionPath} + label="Description" + form={assetsMetadatasForm} + placeHolder="Token description" + required={false} + disabled + /> + + + name={externalUrlPath} + label="External URL" + form={assetsMetadatasForm} + placeHolder="https://" + required={false} + disabled + /> + + + name={youtubeUrlPath} + label="Youtube URL" + form={assetsMetadatasForm} + placeHolder="https://" + required={false} + disabled + /> + + + + + + {attributes.map((attribute, index) => ( + + + {`${attribute.type}: ${attribute.value}`} + + + ))} + + + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/AssetsAndMetadataIssue.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/AssetsAndMetadataIssue.tsx new file mode 100644 index 0000000000..f0b4613c94 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/AssetsAndMetadataIssue.tsx @@ -0,0 +1,71 @@ +import { FC } from "react"; +import { View } from "react-native"; + +import crossSVG from "@/assets/icons/cross.svg"; +import warningTriangleSVG from "@/assets/icons/warning-triangle.svg"; +import { BrandText } from "@/components/BrandText"; +import { SVG } from "@/components/SVG"; +import { CustomPressable } from "@/components/buttons/CustomPressable"; +import { SpacerColumn, SpacerRow } from "@/components/spacer"; +import { + errorColor, + neutral17, + neutral77, + warningColor, +} from "@/utils/style/colors"; +import { fontSemibold13 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; + +export interface AssetsAndMetadataIssueObject { + title: string; + message: string; + type: "error" | "warning"; +} + +export const AssetsAndMetadataIssue: FC<{ + issue: AssetsAndMetadataIssueObject; + removeIssue: () => void; +}> = ({ issue, removeIssue }) => { + return ( + + + + + + + {issue.title} + + + + + {issue.message} + + + + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/AssetsTab.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/AssetsTab.tsx new file mode 100644 index 0000000000..01c43a0b7d --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/AssetsTab.tsx @@ -0,0 +1,750 @@ +import { parse } from "papaparse"; +import pluralize from "pluralize"; +import React, { FC, useEffect, useRef, useState } from "react"; +import { useFieldArray, useFormContext } from "react-hook-form"; +import { SafeAreaView, TouchableOpacity, View } from "react-native"; + +import { AssetModal } from "./AssetModal"; +import { AssetsAndMetadataIssue } from "./AssetsAndMetadataIssue"; + +import trashSVG from "@/assets/icons/trash.svg"; +import { BrandText } from "@/components/BrandText"; +import { SelectedFilesPreview } from "@/components/FilePreview/SelectedFilesPreview/SelectedFilesPreview"; +import { SVG } from "@/components/SVG"; +import { FileUploaderSmall } from "@/components/inputs/FileUploaderSmall"; +import { FileUploaderSmallHandle } from "@/components/inputs/FileUploaderSmall/FileUploaderSmall.type"; +import { Separator } from "@/components/separators/Separator"; +import { SpacerColumn, SpacerRow } from "@/components/spacer"; +import { useFeedbacks } from "@/context/FeedbacksProvider"; +import { useIsMobile } from "@/hooks/useIsMobile"; +import { IMAGE_MIME_TYPES, TXT_CSV_MIME_TYPES } from "@/utils/mime"; +import { + NUMBERS_COMMA_SEPARATOR_REGEXP, + NUMBERS_REGEXP, + URL_REGEX, +} from "@/utils/regex"; +import { errorColor, neutral33 } from "@/utils/style/colors"; +import { fontSemibold14 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { LocalFileData } from "@/utils/types/files"; +import { + CollectionAssetsAttributeFormValues, + CollectionAssetsMetadataFormValues, + CollectionAssetsMetadatasFormValues, +} from "@/utils/types/launchpad"; + +export const AssetsTab: React.FC = () => { + const isMobile = useIsMobile(); + const { setToast } = useFeedbacks(); + const [selectedElemIndex, setSelectedElemIndex] = useState(); + const assetsMetadatasForm = + useFormContext(); + const { fields, remove } = useFieldArray({ + control: assetsMetadatasForm.control, + name: "assetsMetadatas", + }); + const [assetsMappingDataRows, setAssetsMappingDataRows] = useState< + string[][] + >([]); + const [attributesMappingDataRows, setAttributesMappingDataRows] = useState< + string[][] + >([]); + + const attributesUploaderRef = useRef(null); + const assetsUploaderRef = useRef(null); + const imagesUploaderRef = useRef(null); + + const [assetModalVisible, setAssetModalVisible] = useState(false); + const selectedElem = fields.find((_, index) => index === selectedElemIndex); + const [attributesIssues, setAttributesIssues] = useState< + { + title: string; + message: string; + type: "error" | "warning"; + }[] + >([]); + const [assetsIssues, setAssetsIssues] = useState< + { + title: string; + message: string; + type: "error" | "warning"; + }[] + >([]); + const [imagesIssues, setImagesIssues] = useState< + { + title: string; + message: string; + type: "error" | "warning"; + }[] + >([]); + + const attributesIdsSeparator = ","; + // Assets columns + const fileNameColIndex = 0; + const nameColIndex = 1; + const descriptionColIndex = 2; + const externalURLColIndex = 3; + const youtubeURLColIndex = 4; + const attributesColIndex = 5; + // Attributes (traits) columns + const idColIndex = 0; + const typeColIndex = 1; + const valueColIndex = 2; + + const resetAllIssues = () => { + setAssetsIssues([]); + setAttributesIssues([]); + setImagesIssues([]); + }; + // We keep showing only the warnings if a image or mapping file is selected without error + const resetIssuesErrors = () => { + setAttributesIssues((issues) => + issues.filter((issue) => issue.type !== "error"), + ); + setAssetsIssues((issues) => + issues.filter((issue) => issue.type !== "error"), + ); + setImagesIssues((issues) => + issues.filter((issue) => issue.type !== "error"), + ); + }; + + const resetAll = () => { + setAssetsMappingDataRows([]); + setAttributesMappingDataRows([]); + assetsMetadatasForm.setValue("assetsMetadatas", []); + resetAllIssues(); + attributesUploaderRef.current?.resetFiles(); + assetsUploaderRef.current?.resetFiles(); + imagesUploaderRef.current?.resetFiles(); + }; + + // We ignore the first row since it's the table headings + // We ignore unwanted empty lines from the CSV + const cleanDataRows = (array: string[][]) => + array.filter( + (dataRow, dataRowIndex) => dataRow[0] !== "" && dataRowIndex > 0, + ); + // Converts attributes ids as string to array of ids + const cleanAssetAttributesIds = (ids?: string) => + ids + ?.split(attributesIdsSeparator) + .map((id) => id.trim()) + .filter((id) => NUMBERS_COMMA_SEPARATOR_REGEXP.test(id)) || []; + + // On remove image manually + const onRemoveImage = (index: number) => { + remove(index); + }; + // If all images are removed, we clear the images issues and the input file images + useEffect(() => { + if (!fields.length) { + setImagesIssues([]); + imagesUploaderRef.current?.resetFiles(); + } + }, [fields.length]); + + // On upload attributes CSV mapping file + const onUploadAttributesMapingFile = async (files: LocalFileData[]) => { + resetAllIssues(); + setAssetsMappingDataRows([]); + assetsMetadatasForm.setValue("assetsMetadatas", []); + + try { + await parse(files[0].file, { + complete: (parseResults) => { + const attributesDataRows = parseResults.data; + + // Controls CSV headings present on the first row. + if ( + attributesDataRows[0][idColIndex] !== "id" || + attributesDataRows[0][valueColIndex] !== "value" || + attributesDataRows[0][typeColIndex] !== "type" + ) { + setAttributesMappingDataRows([]); + + const title = "Invalid attributes mapping file"; + const message = + "Please verify the headings on the first row in your attributes mapping file.\nThis file is ignored.\nCheck the description for more information."; + console.error(title + ".\n" + message); + setAttributesIssues((issues) => [ + ...issues, + { + title, + message, + type: "error", + }, + ]); + return; + } + + // Verifying that all attributes rows have an id, name and value + const missingIdRows: string[][] = []; + const missingTypeRows: string[][] = []; + const missingValueRows: string[][] = []; + const wrongIdRows: string[][] = []; + const rowsIndexesToRemove: number[] = []; + + // Controling attributes + cleanDataRows(attributesDataRows).forEach((dataRow, dataRowIndex) => { + const hasNoId = !dataRow[idColIndex]?.trim(); + const hasNoValue = !dataRow[valueColIndex]?.trim(); + const hasNoType = !dataRow[typeColIndex]?.trim(); + const hasWrongId = + dataRow[idColIndex]?.trim() && + !NUMBERS_REGEXP.test(dataRow[idColIndex].trim()); + + // Warning if no id in attribute (Ignore attribute) + if (hasNoId) { + missingIdRows.push(dataRow); + } + // Warning if no value in attribute (Ignore attribute) + if (hasNoValue) { + missingValueRows.push(dataRow); + } + // Warning if no type in attribute (Ignore attribute) + if (hasNoType) { + missingTypeRows.push(dataRow); + } + // Warning if id is not a digit (Ignore attribute) + if (hasWrongId) { + wrongIdRows.push(dataRow); + } + // We get the invalidated rows to remove + if (hasNoId || hasNoValue || hasNoType || hasWrongId) { + rowsIndexesToRemove.push(dataRowIndex); + } + }); + + // We remove the wrong rows from parseResults.data (The assets rows) + const result = cleanDataRows(parseResults.data).filter( + (_, index) => !rowsIndexesToRemove.includes(index), + ); + // Soring the final result + setAttributesMappingDataRows(result); + + // Handling warnings + if (missingIdRows.length) { + const title = `Incomplete ${pluralize("attribute", missingIdRows.length)}`; + const message = `Missing "id" in ${pluralize("attribute", missingIdRows.length, true)} that ${pluralize("has", missingIdRows.length)} been ignored.\nPlease complete properly your attributes mapping file.\nCheck the description for more information.`; + console.warn(title + ".\n" + message); + setAttributesIssues((issues) => [ + ...issues, + { + title, + message, + type: "warning", + }, + ]); + } + if (missingTypeRows.length) { + const title = `Incomplete ${pluralize("attribute", missingTypeRows.length)}`; + const message = `Missing "type" in ${pluralize("attribute", missingTypeRows.length, true)} that ${pluralize("has", missingTypeRows.length)} been ignored.\nPlease complete properly your attributes mapping file.\nCheck the description for more information.`; + console.warn(title + ".\n" + message); + setAttributesIssues((issues) => [ + ...issues, + { + title, + message, + type: "warning", + }, + ]); + } + if (missingValueRows.length) { + const title = `Incomplete ${pluralize("attribute", missingValueRows.length)}`; + const message = `Missing "value" in ${pluralize("attribute", missingValueRows.length, true)} that ${pluralize("has", missingValueRows.length)} been ignored.\nPlease complete properly your attributes mapping file.\nCheck the description for more information.`; + console.warn(title + ".\n" + message); + setAttributesIssues((issues) => [ + ...issues, + { + title, + message, + type: "warning", + }, + ]); + } + if (wrongIdRows.length) { + const title = `Wrong id`; + const message = `${pluralize("attribute", wrongIdRows.length, true)} ${pluralize("has", wrongIdRows.length)} a wrong "id" value and ${pluralize("has", wrongIdRows.length)} beed ignored. Only a number is allowed.\nCheck the description for more information.`; + console.warn(title + ".\n" + message); + setAttributesIssues((issues) => [ + ...issues, + { + title, + message, + type: "warning", + }, + ]); + } + }, + }); + } catch (e) { + setAttributesMappingDataRows([]); + + console.error(`${e}`); + setToast({ + title: "Error parsing " + files[0].file.name, + message: `${e}`, + mode: "normal", + type: "error", + }); + } + }; + + // On upload assets CSV mapping file + const onUploadAssetsMappingFile = async (files: LocalFileData[]) => { + resetIssuesErrors(); + setAssetsIssues([]); + setImagesIssues([]); + assetsMetadatasForm.setValue("assetsMetadatas", []); + imagesUploaderRef.current?.resetFiles(); + + try { + await parse(files[0].file, { + complete: (parseResults) => { + const assetsDataRows = parseResults.data; + const attributesDataRows = attributesMappingDataRows; // attributesMappingDataRows is clean here + + // Controls CSV headings present on the first row. + if ( + assetsDataRows[0][fileNameColIndex] !== "file_name" || + assetsDataRows[0][nameColIndex] !== "name" || + assetsDataRows[0][descriptionColIndex] !== "description" || + assetsDataRows[0][externalURLColIndex] !== "external_url" || + assetsDataRows[0][youtubeURLColIndex] !== "youtube_url" || + assetsDataRows[0][attributesColIndex] !== "attributes" + ) { + setAssetsMappingDataRows([]); + + const title = "Invalid assets mapping file"; + const message = + "Please verify the headings on the first row in your assets mapping file.This file is ignored.\nCheck the description for more information."; + console.error(title + ".\n" + message); + setAssetsIssues((issues) => [ + ...issues, + { + title, + message, + type: "error", + }, + ]); + return; + } + + const missingNameRows: string[][] = []; + const missingAttributesRows: string[][] = []; + const unknownAttributesRowsInAssets: string[][] = []; + const wrongAttributesRowsInAssets: string[][] = []; + const wrongUrlsRowsInAssets: string[][] = []; + const rowsIndexesToRemove: number[] = []; + + // Controling assets and attributes + cleanDataRows(assetsDataRows).forEach( + (assetDataRow, assetDataRowIndex) => { + const hasNoName = !assetDataRow[nameColIndex]?.trim(); + const hasNoAttribute = !assetDataRow[attributesColIndex]?.trim(); + const hasWrongAttribute = !NUMBERS_COMMA_SEPARATOR_REGEXP.test( + assetDataRow[attributesColIndex], + ); + const hasWrongExternalUrl = + assetDataRow[externalURLColIndex]?.trim() && + !URL_REGEX.test(assetDataRow[externalURLColIndex].trim()); + const hasWrongYoutubeUrl = + assetDataRow[youtubeURLColIndex]?.trim() && + !URL_REGEX.test(assetDataRow[youtubeURLColIndex].trim()); + + // Warning if no name in asset (Ignore asset) + if (hasNoName) { + missingNameRows.push(assetDataRow); + } + // Warning if no attributes in asset (Ignore asset) + if (hasNoAttribute) { + missingAttributesRows.push(assetDataRow); + } + // Else, warning if wrong attributes ids in asset. We want numbers with comma separators (Ignore asset) + else if (hasWrongAttribute) { + wrongAttributesRowsInAssets.push(assetDataRow); + } + // We get unvalidated rows to remove + if (hasNoName || hasNoAttribute || hasWrongAttribute) { + rowsIndexesToRemove.push(assetDataRowIndex); + } + + // Warning if wrong urls in asset (No incidence) + if (hasWrongExternalUrl || hasWrongYoutubeUrl) { + wrongUrlsRowsInAssets.push(assetDataRow); + } + // Warning if unknow attributes ids in asset (No incidence) + const assetAttributesIds = cleanAssetAttributesIds( + assetDataRow[attributesColIndex], + ); + let nbIdsFound = 0; + assetAttributesIds.forEach((id) => { + attributesDataRows.forEach((attributeDataRow) => { + if (id === attributeDataRow[idColIndex]?.trim()) { + nbIdsFound++; + } + }); + }); + if (nbIdsFound < assetAttributesIds.length) { + unknownAttributesRowsInAssets.push(assetDataRow); + } + }, + ); + + // We remove the wrong rows from parseResults.data (The assets rows) + const result = cleanDataRows(assetsDataRows).filter( + (_, index) => !rowsIndexesToRemove.includes(index), + ); + // Storing the final results + setAssetsMappingDataRows(result); + + // Handling warnings + if (missingNameRows.length) { + const title = `Incomplete ${pluralize("asset", missingNameRows.length)}`; + const message = `Missing "name" in ${pluralize("asset", missingNameRows.length, true)} that ${pluralize("has", missingNameRows.length)} been ignored.\nPlease complete properly your assets mapping file.\nCheck the description for more information.`; + console.warn(title + ".\n" + message); + setAssetsIssues((issues) => [ + ...issues, + { + title, + message, + type: "warning", + }, + ]); + } + if (missingAttributesRows.length) { + const title = `Incomplete ${pluralize("asset", missingAttributesRows.length)}`; + const message = `Missing "attributes" in ${pluralize("asset", missingAttributesRows.length, true)} that ${pluralize("has", missingAttributesRows.length)} been ignored.\nPlease complete properly your assets mapping file.\nCheck the description for more information.`; + console.warn(title + ".\n" + message); + setAssetsIssues((issues) => [ + ...issues, + { + title, + message, + type: "warning", + }, + ]); + } + if (wrongAttributesRowsInAssets.length) { + const title = `Wrong attributes`; + const message = `${pluralize("asset", wrongAttributesRowsInAssets.length, true)} ${pluralize("has", wrongAttributesRowsInAssets.length)} a wrong "attributes" value and ${pluralize("has", wrongAttributesRowsInAssets.length)} been ignored. Only numbers with comma separator are allwowed.\nCheck the description for more information.`; + console.warn(title + ".\n" + message); + setAssetsIssues((issues) => [ + ...issues, + { + title, + message, + type: "warning", + }, + ]); + } + if (wrongUrlsRowsInAssets.length) { + const title = `Wrong URLs`; + const message = `${pluralize("asset", wrongUrlsRowsInAssets.length, true)} ${pluralize("has", wrongUrlsRowsInAssets.length)} a wrong "youtube_url" or "external_url" value (No incidence).\nCheck the description for more information.`; + console.warn(title + ".\n" + message); + setAssetsIssues((issues) => [ + ...issues, + { + title, + message, + type: "warning", + }, + ]); + } + if (unknownAttributesRowsInAssets.length) { + const title = `Unknown attributes`; + const message = `${pluralize("asset", unknownAttributesRowsInAssets.length, true)} ${pluralize("has", unknownAttributesRowsInAssets.length)} at least one "attributes" id that doesn't exist in your attributes mapping file. (No incidence)\nCheck the description for more information.`; + console.warn(title + ".\n" + message); + setAssetsIssues((issues) => [ + ...issues, + { + title, + message, + type: "warning", + }, + ]); + } + }, + }); + } catch (e) { + setAssetsMappingDataRows([]); + + console.error(`${e}`); + setToast({ + title: "Error parsing " + files[0].file.name, + message: `${e}`, + mode: "normal", + type: "error", + }); + } + }; + + // On upload images files + const onUploadImages = (images: LocalFileData[]) => { + if (!assetsMappingDataRows.length || !attributesMappingDataRows.length) + return; + resetIssuesErrors(); + setImagesIssues([]); + + const collectionAssetsMetadatas: CollectionAssetsMetadataFormValues[] = []; + + //The rows order in the CSV determines the assets order. + assetsMappingDataRows.forEach((assetDataRow, assetDataRowIndex) => { + images.forEach((image) => { + if (assetDataRow[fileNameColIndex] !== image.file.name) return; + // --- Mapping attributes + const mappedAttributes: CollectionAssetsAttributeFormValues[] = []; + const assetAttributesIds = [ + ...new Set(cleanAssetAttributesIds(assetDataRow[attributesColIndex])), + ]; // We ignore duplicate attributes ids from assets + assetAttributesIds.forEach((assetAttributeId) => { + attributesMappingDataRows.forEach( + (attributeDataRow, attributeDataRowIndex) => { + if (attributeDataRow[idColIndex] === assetAttributeId) { + mappedAttributes.push({ + value: attributeDataRow[valueColIndex], + type: attributeDataRow[typeColIndex], + }); + } + }, + ); + }); + + // --- Mapping assets + const mappedAssets: CollectionAssetsMetadataFormValues = { + image, + name: assetDataRow[nameColIndex], + description: assetDataRow[descriptionColIndex], + externalUrl: assetDataRow[externalURLColIndex], + youtubeUrl: assetDataRow[youtubeURLColIndex], + attributes: mappedAttributes, + }; + collectionAssetsMetadatas.push(mappedAssets); + }); + }); + assetsMetadatasForm.setValue("assetsMetadatas", collectionAssetsMetadatas); + + // Handling warnings + if (collectionAssetsMetadatas.length < images.length) { + const nbUnexpectedImages = + images.length - collectionAssetsMetadatas.length; + const title = `Unexpected ${pluralize("image", nbUnexpectedImages)}`; + const message = `${pluralize("image", nbUnexpectedImages, true)} ${pluralize("is", nbUnexpectedImages)} not expected in your assets mapping file and ${pluralize("has", nbUnexpectedImages)} been ignored.\nCheck the description for more information.`; + console.warn(title + ".\n" + message); + setImagesIssues((issues) => [ + ...issues, + { + title, + message, + type: "warning", + }, + ]); + } + + if (assetsMappingDataRows.length > collectionAssetsMetadatas.length) { + const nbMissingImages = + assetsMappingDataRows.length - collectionAssetsMetadatas.length; + const title = `Missing ${pluralize("image", nbMissingImages)}`; + const message = `${pluralize("image", nbMissingImages, true)} expected in your assets mapping file ${pluralize("is", nbMissingImages)} missing.\nCheck the description for more information.`; + console.warn(title + ".\n" + message); + setImagesIssues((issues) => [ + ...issues, + { + title, + message, + type: "warning", + }, + ]); + } + }; + + return ( + + {/* ===== Issues */} + {attributesIssues.map((issue, index) => ( + + setAttributesIssues((issues) => + issues.filter((_, i) => i !== index), + ) + } + /> + ))} + {assetsIssues.map((issue, index) => ( + + setAssetsIssues((issues) => issues.filter((_, i) => i !== index)) + } + /> + ))} + {imagesIssues.map((issue, index) => ( + + setImagesIssues((issues) => issues.filter((_, i) => i !== index)) + } + /> + ))} + + + {/* ===== Left container */} + + + + {/* Firstly: Attributes */} + + + + + {/* Secondly: Assets */} + + + + + + + {/* Thirdly: Images */} + + + {(!!fields.length || + !!assetsMappingDataRows.length || + !!attributesMappingDataRows.length) && ( + <> + + + + + + )} + + + + + {/* ---- Separator*/} + + + {/* ===== Right container */} + + metadata.image!, + )} + onPressItem={(file, itemIndex) => { + setAssetModalVisible(true); + setSelectedElemIndex(itemIndex); + }} + onPressDeleteItem={onRemoveImage} + /> + + + {selectedElem && selectedElemIndex !== undefined && ( + setAssetModalVisible(false)} + isVisible={assetModalVisible} + elem={selectedElem} + elemIndex={selectedElemIndex} + /> + )} + + + ); +}; + +const ResetAllButton: FC<{ + onPress: () => void; +}> = ({ onPress }) => { + return ( + + + + + Remove all files + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/LaunchpadAssetsAndMetadata.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/LaunchpadAssetsAndMetadata.tsx new file mode 100644 index 0000000000..f3fa04d8b0 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/LaunchpadAssetsAndMetadata.tsx @@ -0,0 +1,116 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import React, { FC, useEffect, useState } from "react"; +import { FormProvider, useForm, useFormContext } from "react-hook-form"; +import { View } from "react-native"; + +import { AssetsTab } from "./AssetsTab"; +import { UriTab } from "./UriTab"; + +import { BrandText } from "@/components/BrandText"; +import { SpacerColumn } from "@/components/spacer"; +import { Tabs } from "@/components/tabs/Tabs"; +import { useIsMobile } from "@/hooks/useIsMobile"; +import { neutral33, neutral77, primaryColor } from "@/utils/style/colors"; +import { fontSemibold14, fontSemibold28 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { + CollectionAssetsMetadatasFormValues, + CollectionFormValues, + ZodCollectionAssetsMetadatasFormValues, +} from "@/utils/types/launchpad"; + +const AssetsAndMetadataTabItems = { + assets: { + name: "Upload assets & metadata", + }, + uri: { + name: "Use an existing base URI", + }, +}; + +export const LaunchpadAssetsAndMetadata: FC = () => { + const isMobile = useIsMobile(); + const [selectedTab, setSelectedTab] = + useState("assets"); + const { watch, setValue } = useFormContext(); + const collectionAssetsMetadatas = watch("assetsMetadatas"); + const assetsMetadatasForm = useForm({ + mode: "all", + defaultValues: collectionAssetsMetadatas, // Retreive assetsMetadatas from collectionForm + resolver: zodResolver(ZodCollectionAssetsMetadatasFormValues), + }); + const assetsMetadatas = assetsMetadatasForm.watch("assetsMetadatas"); + + // Plug assetsMetadatas from assetsMetadatasForm to collectionForm + useEffect(() => { + setValue("assetsMetadatas.assetsMetadatas", assetsMetadatas); + }, [assetsMetadatas, setValue]); + + return ( + + + Assets & Metadata + + + + + Make sure you check out{" "} + + documentation + {" "} + on how to create your collection + + + + + + + {selectedTab === "assets" && ( + + + + )} + + {/*TODO: Handle this ?*/} + {selectedTab === "uri" && } + + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/UriTab.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/UriTab.tsx new file mode 100644 index 0000000000..ac97003a46 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/UriTab.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import { useFormContext } from "react-hook-form"; +import { View } from "react-native"; + +import { BrandText } from "@/components/BrandText"; +import { SpacerColumn } from "@/components/spacer"; +import { TextInputLaunchpad } from "@/screens/Launchpad/LaunchpadApply/components/inputs/TextInputLaunchpad"; +import { neutral77 } from "@/utils/style/colors"; +import { fontSemibold14 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { CollectionFormValues } from "@/utils/types/launchpad"; + +export const UriTab: React.FC = () => { + const collectionForm = useFormContext(); + + return ( + + + + + Though Teritori's tr721 contract allows for off-chain metadata + storage, it is recommended to use a decentralized storage solution, + such as IPFS. You may head over to NFT.Storage and upload your + assets & metadata manually to get a base URI for your collection. + + + + + label="Base Token URI" + placeHolder="ipfs://" + name="baseTokenUri" + form={collectionForm} + required={false} + /> + + + name="coverImageUri" + label="Cover Image URI" + placeHolder="ipfs://" + form={collectionForm} + required={false} + /> + + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadBasic.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadBasic.tsx new file mode 100644 index 0000000000..e49914fe67 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadBasic.tsx @@ -0,0 +1,145 @@ +import React, { FC } from "react"; +import { Controller, useFormContext } from "react-hook-form"; +import { View } from "react-native"; + +import { BrandText } from "@/components/BrandText"; +import { ErrorText } from "@/components/ErrorText"; +import { NetworkSelectorWithLabel } from "@/components/NetworkSelector/NetworkSelectorWithLabel"; +import { FileUploaderSmall } from "@/components/inputs/FileUploaderSmall"; +import { SpacerColumn } from "@/components/spacer"; +import { NetworkFeature } from "@/networks"; +import { TextInputLaunchpad } from "@/screens/Launchpad/LaunchpadApply/components/inputs/TextInputLaunchpad"; +import { IMAGE_MIME_TYPES } from "@/utils/mime"; +import { neutral55, neutral77, primaryColor } from "@/utils/style/colors"; +import { + fontSemibold13, + fontSemibold14, + fontSemibold28, +} from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { CollectionFormValues } from "@/utils/types/launchpad"; + +export const LaunchpadBasic: FC = () => { + const collectionForm = useFormContext(); + const coverImage = collectionForm.watch("coverImage"); + + return ( + + + Create Collection + + + + + Make sure you check out{" "} + + documentation + {" "} + on how to create your collection + + + + + + + label="Name" + placeHolder="My Awesome Collection" + name="name" + form={collectionForm} + /> + + + label="Describe your project: " + sublabel={ + + + 1. What's your concept? + + + 2. How is it different? + + + 3. What's your goal? + + + } + placeHolder="Describe here..." + name="desc" + form={collectionForm} + /> + + + label="Symbol" + placeHolder="Symbol" + name="symbol" + form={collectionForm} + valueModifier={(value) => value.toUpperCase()} + /> + + + control={collectionForm.control} + name="coverImage" + render={({ field: { onChange } }) => ( + <> + { + onChange(files[0]); + }} + filesCount={coverImage ? 1 : 0} + mimeTypes={IMAGE_MIME_TYPES} + required + imageToShow={coverImage} + onPressDelete={() => onChange(undefined)} + /> + + {collectionForm.getFieldState("coverImage").error?.message} + + + )} + /> + + + + label="NFT.Storage JWT" + sublabel={ + + Used to upload the cover image and the assets to your NFT Storage + + } + placeHolder="My Awesome Collection" + name="assetsMetadatas.nftApiKey" + form={collectionForm} + /> + + {/**/} + {/* label="External Link"*/} + {/* placeHolder="https://collection..."*/} + {/* name="externalLink"*/} + {/* form={collectionForm}*/} + {/* required={false}*/} + {/*/>*/} + + + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadDetails.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadDetails.tsx new file mode 100644 index 0000000000..8254ef351e --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadDetails.tsx @@ -0,0 +1,163 @@ +import React, { FC } from "react"; +import { Controller, useFormContext } from "react-hook-form"; +import { View } from "react-native"; + +import { MultipleSelectInput } from "../../../components/inputs/selectInputs/MultipleSelectInput"; +import { SelectInputLaunchpad } from "../../../components/inputs/selectInputs/SelectInputLaunchpad"; + +import { BrandText } from "@/components/BrandText"; +import { ErrorText } from "@/components/ErrorText"; +import { SpacerColumn } from "@/components/spacer"; +import { TextInputLaunchpad } from "@/screens/Launchpad/LaunchpadApply/components/inputs/TextInputLaunchpad"; +import { neutral55, neutral77 } from "@/utils/style/colors"; +import { + fontSemibold13, + fontSemibold14, + fontSemibold20, +} from "@/utils/style/fonts"; +import { CollectionFormValues } from "@/utils/types/launchpad"; + +export const LaunchpadDetails: FC = () => { + const collectionForm = useFormContext(); + const projectTypes = collectionForm.watch("projectTypes") || []; + const isDerivativeProject = collectionForm.watch("isDerivativeProject"); + const isPreviouslyApplied = collectionForm.watch("isPreviouslyApplied"); + + return ( + + + Collection details + + + Information about your collection + + + + + label="Website Link" + sublabel={ + + + Your project's website. It must display the project's discord + and twitter, the roadmap/whitepaper and team's information. + Please, be fully transparent to facilitate your project's review + ! + + + } + placeHolder="https://website..." + name="websiteLink" + form={collectionForm} + /> + + + label="Main contact email address: " + placeHolder="contact@email.com" + name="email" + form={collectionForm} + /> + + + name="isDerivativeProject" + control={collectionForm.control} + render={({ field: { onChange } }) => ( + <> + { + onChange(item === "Yes"); + }} + label="Is your project a derivative project?" + style={{ zIndex: 3 }} + /> + + { + collectionForm.getFieldState("isDerivativeProject").error + ?.message + } + + + )} + /> + + + + name="projectTypes" + control={collectionForm.control} + render={({ field: { onChange } }) => ( + <> + { + const selectedProjectTypes = projectTypes.includes(item) + ? projectTypes.filter((data) => data !== item) + : [...projectTypes, item]; + onChange(selectedProjectTypes); + }} + label="Project type:" + sublabel={ + + Multiple answers allowed + + } + style={{ zIndex: 2 }} + /> + + {collectionForm.getFieldState("projectTypes").error?.message} + + + )} + /> + + + + name="isPreviouslyApplied" + control={collectionForm.control} + render={({ field: { onChange } }) => ( + <> + { + onChange(item === "Yes"); + }} + label="Have you previously applied for the same project before?" + style={{ zIndex: 1 }} + /> + + { + collectionForm.getFieldState("isPreviouslyApplied").error + ?.message + } + + + )} + /> + + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/ConfigureRoyaltyDetails.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/ConfigureRoyaltyDetails.tsx new file mode 100644 index 0000000000..55c28bf557 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/ConfigureRoyaltyDetails.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import { useFormContext } from "react-hook-form"; +import { View } from "react-native"; + +import { BrandText } from "@/components/BrandText"; +import { SpacerColumn } from "@/components/spacer"; +import { TextInputLaunchpad } from "@/screens/Launchpad/LaunchpadApply/components/inputs/TextInputLaunchpad"; +import { neutral55, neutral77 } from "@/utils/style/colors"; +import { + fontSemibold13, + fontSemibold14, + fontSemibold20, +} from "@/utils/style/fonts"; +import { CollectionFormValues } from "@/utils/types/launchpad"; + +export const ConfigureRoyaltyDetails: React.FC = () => { + const collectionForm = useFormContext(); + + return ( + + + Royalty Details + + + Configure royalties + + + + + label="Payment Address " + placeHolder="teritori123456789qwertyuiopasdfghjklzxcvbnm" + name="royaltyAddress" + sublabel={ + + + Address to receive royalties + + + } + form={collectionForm} + required={false} + /> + + + label="Share Percentage " + placeHolder="8%" + name="royaltyPercentage" + sublabel={ + + + Percentage of royalties to be paid + + + } + form={collectionForm} + required={false} + /> + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriodAccordion.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriodAccordion.tsx new file mode 100644 index 0000000000..331f5bd336 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriodAccordion.tsx @@ -0,0 +1,46 @@ +import React, { FC } from "react"; +import { UseFieldArrayRemove, UseFieldArrayUpdate } from "react-hook-form"; + +import { LaunchpadMintPeriodAccordionBottom } from "./LaunchpadMintPeriodAccordionBottom"; +import { LaunchpadMintPeriodAccordionTop } from "./LaunchpadMintPeriodAccordionTop"; + +import { PrimaryBox } from "@/components/boxes/PrimaryBox"; +import { neutral00, neutral22, neutral33 } from "@/utils/style/colors"; +import { + CollectionFormValues, + CollectionMintPeriodFormValues, +} from "@/utils/types/launchpad"; + +export const LaunchpadMintPeriodAccordion: FC<{ + elem: CollectionMintPeriodFormValues; + elemIndex: number; + remove: UseFieldArrayRemove; + update: UseFieldArrayUpdate; + closeAll: () => void; +}> = ({ elem, elemIndex, remove, update, closeAll }) => { + return ( + + + + {elem.isOpen && ( + + )} + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriodAccordionBottom.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriodAccordionBottom.tsx new file mode 100644 index 0000000000..3496fd0394 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriodAccordionBottom.tsx @@ -0,0 +1,212 @@ +import React, { FC } from "react"; +import { + Controller, + UseFieldArrayRemove, + UseFieldArrayUpdate, + useFormContext, +} from "react-hook-form"; +import { View, TouchableOpacity } from "react-native"; + +import trashSVG from "@/assets/icons/trash.svg"; +import { BrandText } from "@/components/BrandText"; +import { ErrorText } from "@/components/ErrorText"; +import { SVG } from "@/components/SVG"; +import { CsvTextRowsInput } from "@/components/inputs/CsvTextRowsInput"; +import { DateTimeInput } from "@/components/inputs/DateTimeInput"; +import { Separator } from "@/components/separators/Separator"; +import { SpacerColumn, SpacerRow } from "@/components/spacer"; +import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork"; +import { getCurrency } from "@/networks"; +import { CurrencyInputLaunchpad } from "@/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/CurrencyInputLaunchpad"; +import { TextInputLaunchpad } from "@/screens/Launchpad/LaunchpadApply/components/inputs/TextInputLaunchpad"; +import { errorColor, neutral55, neutral77 } from "@/utils/style/colors"; +import { + fontSemibold13, + fontSemibold14, + fontSemibold20, +} from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { + CollectionFormValues, + CollectionMintPeriodFormValues, +} from "@/utils/types/launchpad"; + +export const LaunchpadMintPeriodAccordionBottom: FC<{ + elem: CollectionMintPeriodFormValues; + update: UseFieldArrayUpdate; + remove: UseFieldArrayRemove; + elemIndex: number; +}> = ({ elem, elemIndex, remove, update }) => { + // Since the Collection network is the selected network, we use useSelectedNetworkId (See LaunchpadBasic.tsx) + const networkId = useSelectedNetworkId(); + const collectionForm = useFormContext(); + const amountPath = `mintPeriods.${elemIndex}.price.amount` as const; + const startTimePath = `mintPeriods.${elemIndex}.startTime` as const; + const endTimePath = `mintPeriods.${elemIndex}.endTime` as const; + const maxTokensPath = `mintPeriods.${elemIndex}.maxTokens` as const; + const perAddressLimitPath = + `mintPeriods.${elemIndex}.perAddressLimit` as const; + const mintPeriods = collectionForm.watch("mintPeriods"); + const amount = collectionForm.watch(amountPath); + const startTime = collectionForm.watch(startTimePath); + const endTime = collectionForm.watch(endTimePath); + const selectedCurrency = getCurrency(networkId, elem.price.denom); + + return ( + + + name={amountPath} + control={collectionForm.control} + render={({ field: { onChange } }) => ( + <> + { + update(elemIndex, { + ...elem, + price: { ...elem.price, denom: currency.denom }, + }); + }} + onChangeAmountAtomics={(amountAtomics) => { + onChange(amountAtomics); + }} + required={false} + /> + + {collectionForm.getFieldState(amountPath).error?.message} + + + )} + /> + + + + label="Max Tokens" + placeHolder="0" + name={maxTokensPath} + sublabel={ + + + Maximum number of mintable tokens + + + } + form={collectionForm} + required={false} + /> + + + label="Per Address Limit" + placeHolder="0" + name={perAddressLimitPath} + sublabel={ + + + Maximum number of mintable tokens per address + + + } + form={collectionForm} + required={false} + /> + + + name={startTimePath} + control={collectionForm.control} + render={({ field: { onChange } }) => ( + + )} + /> + + + + name={endTimePath} + control={collectionForm.control} + render={({ field: { onChange } }) => ( + + )} + /> + + + + + Whitelist Addresses + + + Select a TXT or CSV file that contains the whitelisted addresses (One + address per line) + + + + + update(elemIndex, { + ...elem, + whitelistAddressesFile: file, + whitelistAddresses: rows, + }) + } + /> + + + { + // Can remove periods only if more than one (There will be least one period left) + (elemIndex > 0 || mintPeriods.length > 1) && ( + <> + + + + remove(elemIndex)} + > + + + + Remove Mint Period + + + + ) + } + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriodAccordionTop.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriodAccordionTop.tsx new file mode 100644 index 0000000000..91ef8392c1 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriodAccordionTop.tsx @@ -0,0 +1,95 @@ +import React, { FC } from "react"; +import { UseFieldArrayUpdate } from "react-hook-form"; +import { TouchableOpacity, View } from "react-native"; + +import chevronDownSVG from "@/assets/icons/chevron-down.svg"; +import chevronUpSVG from "@/assets/icons/chevron-up.svg"; +import { BrandText } from "@/components/BrandText"; +import { SVG } from "@/components/SVG"; +import { Separator } from "@/components/separators/Separator"; +import { SpacerColumn } from "@/components/spacer"; +import { secondaryColor } from "@/utils/style/colors"; +import { fontSemibold16 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { + CollectionFormValues, + CollectionMintPeriodFormValues, +} from "@/utils/types/launchpad"; + +export const LaunchpadMintPeriodAccordionTop: FC<{ + elem: CollectionMintPeriodFormValues; + elemIndex: number; + update: UseFieldArrayUpdate; + closeAll: () => void; +}> = ({ elem, elemIndex, update, closeAll }) => { + if (elem.isOpen) { + return ( + update(elemIndex, { ...elem, isOpen: false })} + style={{ + paddingTop: layout.spacing_x1, + paddingHorizontal: layout.spacing_x1, + }} + > + + + {`Period #${elemIndex + 1}`} + + + + + + + + + ); + } else { + return ( + { + closeAll(); + update(elemIndex, { ...elem, isOpen: true }); + }} + > + + + {`Period #${elemIndex + 1}`} + + + + + + + ); + } +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriods.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriods.tsx new file mode 100644 index 0000000000..e3637f0272 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriods.tsx @@ -0,0 +1,119 @@ +import React, { FC, Fragment, useCallback } from "react"; +import { useFieldArray, useFormContext } from "react-hook-form"; +import { TouchableOpacity, View } from "react-native"; + +import addSVG from "@/assets/icons/add-secondary.svg"; +import { BrandText } from "@/components/BrandText"; +import { SVG } from "@/components/SVG"; +import { SpacerColumn, SpacerRow } from "@/components/spacer"; +import { useSelectedNetworkInfo } from "@/hooks/useSelectedNetwork"; +import useSelectedWallet from "@/hooks/useSelectedWallet"; +import { getNetworkFeature, NetworkFeature } from "@/networks"; +import { LaunchpadMintPeriodAccordion } from "@/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriodAccordion"; +import { secondaryColor } from "@/utils/style/colors"; +import { fontSemibold14 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { CollectionFormValues } from "@/utils/types/launchpad"; + +export const LaunchpadMintPeriods: FC = () => { + const selectedWallet = useSelectedWallet(); + const networkId = selectedWallet?.networkId || ""; + const collectionForm = useFormContext(); + const selectedNetwork = useSelectedNetworkInfo(); + + const { update, append, remove } = useFieldArray({ + control: collectionForm.control, + name: "mintPeriods", + }); + const mintPeriods = collectionForm.watch("mintPeriods"); + + const closeAll = useCallback(() => { + mintPeriods.map((elem, index) => { + update(index, { ...elem, isOpen: false }); + }); + }, [mintPeriods, update]); + + const createMintPeriod = useCallback(() => { + if (!selectedNetwork) return; + closeAll(); + const feature = getNetworkFeature( + networkId, + NetworkFeature.CosmWasmNFTLaunchpad, + ); + if (!feature) { + throw new Error("This network does not support nft launchpad"); + } + append({ + price: { denom: selectedNetwork.currencies[0].denom, amount: "" }, + maxTokens: "", + perAddressLimit: "", + startTime: 0, + endTime: 0, + isOpen: true, + }); + }, [networkId, closeAll, append, selectedNetwork]); + + return ( + + + {mintPeriods.map((elem, index) => { + return ( + + + + + ); + })} + + + + + + + Add Minting Period + + + + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMinting.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMinting.tsx new file mode 100644 index 0000000000..3b8220c2bf --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMinting.tsx @@ -0,0 +1,67 @@ +import React, { FC } from "react"; +import { Controller, useFormContext } from "react-hook-form"; +import { View } from "react-native"; + +import { ConfigureRoyaltyDetails } from "./ConfigureRoyaltyDetails"; + +import { BrandText } from "@/components/BrandText"; +import { DateTimeInput } from "@/components/inputs/DateTimeInput"; +import { Separator } from "@/components/separators/Separator"; +import { SpacerColumn } from "@/components/spacer"; +import { LaunchpadMintPeriods } from "@/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriods"; +import { neutral77 } from "@/utils/style/colors"; +import { fontSemibold14, fontSemibold20 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { CollectionFormValues } from "@/utils/types/launchpad"; + +export const LaunchpadMinting: FC = () => { + const collectionForm = useFormContext(); + const revealTime = collectionForm.watch("revealTime"); + return ( + + + Minting details + + + Configure the global minting settings + + + + + name="revealTime" + control={collectionForm.control} + render={({ field: { onChange } }) => ( + + )} + /> + + + + + Minting Periods + + + Configure the minting periods, a whitelist can be applied + + + + + + + + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadTeamAndInvestment.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadTeamAndInvestment.tsx new file mode 100644 index 0000000000..229f8301d0 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadTeamAndInvestment.tsx @@ -0,0 +1,140 @@ +import React, { FC } from "react"; +import { useFormContext } from "react-hook-form"; +import { View } from "react-native"; + +import { BrandText } from "@/components/BrandText"; +import { SpacerColumn } from "@/components/spacer"; +import { TextInputLaunchpad } from "@/screens/Launchpad/LaunchpadApply/components/inputs/TextInputLaunchpad"; +import { neutral55, neutral77 } from "@/utils/style/colors"; +import { + fontSemibold13, + fontSemibold14, + fontSemibold20, +} from "@/utils/style/fonts"; +import { CollectionFormValues } from "@/utils/types/launchpad"; + +export const LaunchpadTeamAndInvestment: FC = () => { + const collectionForm = useFormContext(); + + return ( + + + Team & Investments + + + Fill the information about the team and investors + + + + + label="Describe your team: " + sublabel={ + + + 1. How many core members are you? ( Working on the project daily + ) + + + 2. Who does what in your team? + + + 3. Past accomplishments or projects? + + + 4. How did you guys meet? + + + 5. Please add Linkedin links for all your members. + + + } + placeHolder="Describe here..." + name="teamDescription" + form={collectionForm} + /> + + {/**/} + {/* label="Team links and attachments "*/} + {/* sublabel={*/} + {/* */} + {/* */} + {/* Please provide any relevant links regarding your team. You can*/} + {/* also post a google drive link.*/} + {/* */} + {/* */} + {/* }*/} + {/* placeHolder="Type here..."*/} + {/* name="teamLink"*/} + {/* form={collectionForm}*/} + {/*/>*/} + + + label="Do you have any partners on the project? " + sublabel={ + + + If yes, who are they? What do they do for you? + + + } + placeHolder="Type here..." + name="partnersDescription" + form={collectionForm} + /> + + + label="What have you invested in this project so far? " + sublabel={ + + + 1. How much upfront capital has been invested? + + + 2. Have you raised outside funding for the project? + + + 3. How long has the project been worked on? + + + 4. Is there a proof of concept or demo to show? + + + } + placeHolder="Type here..." + name="investDescription" + form={collectionForm} + /> + + + label="Investment links and attachments " + sublabel={ + + + Please provide any relevant links regarding your investment. You + can also post a google drive link. + + + } + placeHolder="Type here..." + name="investLink" + form={collectionForm} + /> + + {/**/} + {/* label="Whitepaper and roadmap: "*/} + {/* sublabel={*/} + {/* */} + {/* */} + {/* Please provide any relevant link regarding your white paper and*/} + {/* roadmap. You can also post a google drive link.*/} + {/* */} + {/* */} + {/* }*/} + {/* placeHolder="Type here..."*/} + {/* name="roadmapLink"*/} + {/* form={collectionForm}*/} + {/*/>*/} + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadMyCollections/LaunchpadMyCollectionsScreen.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadMyCollections/LaunchpadMyCollectionsScreen.tsx new file mode 100644 index 0000000000..0d49a5b928 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadMyCollections/LaunchpadMyCollectionsScreen.tsx @@ -0,0 +1,126 @@ +import React from "react"; +import { View } from "react-native"; + +import { Sort, SortDirection } from "@/api/launchpad/v1/launchpad"; +import infoSVG from "@/assets/icons/info.svg"; +import { BrandText } from "@/components/BrandText"; +import { SVG } from "@/components/SVG"; +import { ScreenContainer } from "@/components/ScreenContainer"; +import { Box } from "@/components/boxes/Box"; +import { SpacerColumn, SpacerRow } from "@/components/spacer"; +import { useLaunchpadProjectsByCreator } from "@/hooks/launchpad/useLaunchpadProjectsByCreator"; +import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork"; +import useSelectedWallet from "@/hooks/useSelectedWallet"; +import { NetworkFeature } from "@/networks"; +import { LaunchpadMyCollectionsTable } from "@/screens/Launchpad/LaunchpadApply/LaunchpadMyCollections/components/LaunchpadMyCollectionsTable"; +import { ScreenFC, useAppNavigation } from "@/utils/navigation"; +import { + neutral17, + neutral77, + primaryColor, + withAlpha, +} from "@/utils/style/colors"; +import { + fontSemibold13, + fontSemibold14, + fontSemibold28, +} from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; + +export const LaunchpadMyCollectionsScreen: ScreenFC< + "LaunchpadMyCollections" +> = () => { + const navigation = useAppNavigation(); + const selectedNetworkId = useSelectedNetworkId(); + const selectedWallet = useSelectedWallet(); + const { launchpadProjects = [] } = useLaunchpadProjectsByCreator({ + networkId: selectedNetworkId, + creatorId: selectedWallet?.userId || "", + offset: 0, + limit: 100, // TODO: Pagination + sort: Sort.SORT_UNSPECIFIED, + sortDirection: SortDirection.SORT_DIRECTION_UNSPECIFIED, + }); + + return ( + } + forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]} + headerChildren={Apply to Launchpad} + onBackPress={() => navigation.navigate("LaunchpadApply")} + > + + My collections + + + + + A list of your created collections. Learn more in the{" "} + + documentation. + + + + + + {launchpadProjects?.length ? ( + + ) : ( + + + + + + + You haven’t created any collections so far + + + )} + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadMyCollections/components/LaunchpadMyCollectionsTable.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadMyCollections/components/LaunchpadMyCollectionsTable.tsx new file mode 100644 index 0000000000..953f820f15 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadMyCollections/components/LaunchpadMyCollectionsTable.tsx @@ -0,0 +1,129 @@ +import React, { useState } from "react"; +import { FlatList, View } from "react-native"; + +import { StatusBadge } from "./../../../components/StatusBadge"; +import { CustomPressable } from "../../../../../components/buttons/CustomPressable"; +import { + commonColumns, + LaunchpadTablesCommonColumns, +} from "../../LaunchpadCreate/components/LaunchpadTablesCommonColumns"; + +import { LaunchpadProject } from "@/api/launchpad/v1/launchpad"; +import { PrimaryButton } from "@/components/buttons/PrimaryButton"; +import { TableCell } from "@/components/table/TableCell"; +import { TableHeader } from "@/components/table/TableHeader"; +import { TableRow } from "@/components/table/TableRow"; +import { TableWrapper } from "@/components/table/TableWrapper"; +import { TableColumns } from "@/components/table/utils"; +import { launchpadProjectStatus, parseCollectionData } from "@/utils/launchpad"; +import { useAppNavigation } from "@/utils/navigation"; +import { screenContentMaxWidthLarge } from "@/utils/style/layout"; + +const columns: TableColumns = { + ...commonColumns, + status: { + label: "Status", + minWidth: 200, + flex: 2, + }, + cta: { + label: "", + minWidth: 180, + flex: 2, + }, +}; + +const breakpointM = 1120; + +export const LaunchpadMyCollectionsTable: React.FC<{ + rows: LaunchpadProject[]; +}> = ({ rows }) => { + const renderItem = ({ + item, + index, + }: { + item: LaunchpadProject; + index: number; + }) => { + const collectionData = parseCollectionData(item); + return ( + + ); + }; + + return ( + + + + + + + ); +}; + +const LaunchpadReadyMyCollectionsTableRow: React.FC<{ + launchpadProject: LaunchpadProject; + index: number; +}> = ({ launchpadProject, index }) => { + const navigation = useAppNavigation(); + const collectionData = parseCollectionData(launchpadProject); + const [isHovered, setHovered] = useState(false); + + if (!collectionData) return null; + return ( + + navigation.navigate("LaunchpadApplicationReview", { + id: launchpadProject.id, + }) + } + onHoverIn={() => setHovered(true)} + onHoverOut={() => setHovered(false)} + style={isHovered && { opacity: 0.5 }} + > + + + + + + + + + {launchpadProjectStatus(launchpadProject.status) === "INCOMPLETE" && ( + + navigation.navigate("LaunchpadComplete", { + id: collectionData.symbol, + }) + } + /> + )} + + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/CurrencyInputLaunchpad.tsx b/packages/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/CurrencyInputLaunchpad.tsx new file mode 100644 index 0000000000..91e2059e9f --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/CurrencyInputLaunchpad.tsx @@ -0,0 +1,233 @@ +import { Decimal } from "@cosmjs/math"; +import React, { FC, Fragment, useRef, useState } from "react"; +import { StyleProp, TextInput, View } from "react-native"; + +import { BrandText } from "@/components/BrandText"; +import { ErrorText } from "@/components/ErrorText"; +import { Box, BoxStyle } from "@/components/boxes/Box"; +import { CustomPressable } from "@/components/buttons/CustomPressable"; +import { Label } from "@/components/inputs/TextInputCustom"; +import { SpacerColumn, SpacerRow } from "@/components/spacer"; +import { useDropdowns } from "@/hooks/useDropdowns"; +import { + allNetworks, + CurrencyInfo, + getNativeCurrency, + getNetwork, +} from "@/networks"; +import { SelectableCurrencySmall } from "@/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/SelectableCurrencySmall"; +import { SelectedCurrencySmall } from "@/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/SelectedCurrencySmall"; +import { validateFloatWithDecimals } from "@/utils/formRules"; +import { + errorColor, + neutral17, + neutral22, + neutral33, + neutral77, + secondaryColor, +} from "@/utils/style/colors"; +import { fontSemibold14 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; + +export const CurrencyInputLaunchpad: FC<{ + label?: string; + placeHolder?: string; + subtitle?: React.ReactElement; + sublabel?: React.ReactElement; + required?: boolean; + error?: string; + networkId: string; + onSelectCurrency: (currency: CurrencyInfo) => void; + onChangeAmountAtomics: (amountAtomics: string) => void; + amountAtomics?: string; + currency?: CurrencyInfo; + boxStyle?: StyleProp; +}> = ({ + label, + placeHolder = "0", + sublabel, + subtitle, + required = true, + error, + networkId, + onSelectCurrency, + onChangeAmountAtomics, + boxStyle, + amountAtomics, + currency, +}) => { + const network = getNetwork(networkId); + const currencies: CurrencyInfo[] = network?.currencies || []; + const [selectedCurrency, setSelectedCurrency] = useState( + currency || currencies[0], + ); + const selectedCurrencyNetwork = allNetworks.find( + (network) => + !!network.currencies.find( + (currency) => currency.denom === selectedCurrency?.denom, + ), + ); + const selectedNativeCurrency = getNativeCurrency( + selectedCurrencyNetwork?.id, + selectedCurrency?.denom, + ); + const [value, setValue] = useState( + selectedNativeCurrency && amountAtomics + ? Decimal.fromAtomics( + amountAtomics, + selectedNativeCurrency.decimals, + ).toString() + : "", + ); + + const inputRef = useRef(null); + const [isDropdownOpen, setDropdownState, dropdownRef] = useDropdowns(); + const [hovered, setHovered] = useState(false); + const boxHeight = 40; + + const onChangeText = (text: string) => { + if (!text) { + setValue(""); + onChangeAmountAtomics(""); + return; + } + + if ( + selectedNativeCurrency && + validateFloatWithDecimals(text, selectedNativeCurrency.decimals) + ) { + setValue(text); + onChangeAmountAtomics( + Decimal.fromUserInput( + text.endsWith(".") ? text + "0" : text, + selectedNativeCurrency.decimals, + ).atomics, + ); + } + }; + + const onPressSelectableCurrency = (currency: CurrencyInfo) => { + setValue(""); + setSelectedCurrency(currency); + onChangeAmountAtomics(""); + onSelectCurrency(currency); + setDropdownState(false); + }; + + if (!selectedCurrencyNetwork) + return Invalid network; + if (!selectedNativeCurrency) + return ( + + Invalid native currency + + ); + return ( + setHovered(true)} + onHoverOut={() => setHovered(false)} + onPress={() => inputRef?.current?.focus()} + style={{ zIndex: 1 }} + > + {/*---- Label*/} + {label && ( + <> + + + {subtitle} + + {sublabel && sublabel} + + + )} + + + {/*---- Input*/} + + + + + + {/*---- Selected currency*/} + + + {/*---- Dropdown selectable currencies */} + {currencies?.length && isDropdownOpen && ( + + {currencies?.map((currencyInfo, index) => ( + + + onPressSelectableCurrency(currencyInfo)} + /> + + ))} + + )} + + + {error} + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/SelectableCurrencySmall.tsx b/packages/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/SelectableCurrencySmall.tsx new file mode 100644 index 0000000000..95afecc580 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/SelectableCurrencySmall.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { TouchableOpacity, View } from "react-native"; + +import { BrandText } from "@/components/BrandText"; +import { CurrencyIcon } from "@/components/CurrencyIcon"; +import { CurrencyInfo, getNativeCurrency } from "@/networks"; +import { fontSemibold14 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; + +export const SelectableCurrencySmall: React.FC<{ + onPressItem: () => void; + currency: CurrencyInfo; + networkId: string; +}> = ({ onPressItem, currency, networkId }) => { + return ( + <> + + + + + + {getNativeCurrency(networkId, currency?.denom)?.displayName} + + + + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/SelectedCurrencySmall.tsx b/packages/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/SelectedCurrencySmall.tsx new file mode 100644 index 0000000000..0aaedf28a4 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/SelectedCurrencySmall.tsx @@ -0,0 +1,73 @@ +import React, { forwardRef } from "react"; +import { TouchableOpacity, View } from "react-native"; + +import chevronDownSVG from "@/assets/icons/chevron-down.svg"; +import chevronUpSVG from "@/assets/icons/chevron-up.svg"; +import { BrandText } from "@/components/BrandText"; +import { CurrencyIcon } from "@/components/CurrencyIcon"; +import { SVG } from "@/components/SVG"; +import { NativeCurrencyInfo } from "@/networks"; +import { secondaryColor } from "@/utils/style/colors"; +import { fontSemibold14 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; + +export const SelectedCurrencySmall = forwardRef< + View, + { + currency?: NativeCurrencyInfo; + selectedNetworkId: string; + isDropdownOpen: boolean; + setDropdownState: (val: boolean) => void; + disabled?: boolean; + } +>( + ( + { currency, selectedNetworkId, isDropdownOpen, setDropdownState, disabled }, + ref, + ) => { + return ( + + setDropdownState(!isDropdownOpen)} + style={{ + flexDirection: "row", + alignItems: "center", + }} + disabled={disabled} + > + + + + + {currency?.displayName || "ERROR"} + + {!disabled && ( + + )} + + + + + ); + }, +); diff --git a/packages/screens/Launchpad/LaunchpadApply/components/inputs/TextInputLaunchpad.tsx b/packages/screens/Launchpad/LaunchpadApply/components/inputs/TextInputLaunchpad.tsx new file mode 100644 index 0000000000..f4b0c5245c --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/components/inputs/TextInputLaunchpad.tsx @@ -0,0 +1,98 @@ +import React, { useRef, useState } from "react"; +import { + FieldValues, + Path, + useController, + UseFormReturn, +} from "react-hook-form"; +import { TextInput, TextInputProps, TextStyle } from "react-native"; + +import { ErrorText } from "@/components/ErrorText"; +import { TertiaryBox } from "@/components/boxes/TertiaryBox"; +import { CustomPressable } from "@/components/buttons/CustomPressable"; +import { Label } from "@/components/inputs/TextInputCustom"; +import { SpacerColumn } from "@/components/spacer"; +import { neutral22, neutral77, secondaryColor } from "@/utils/style/colors"; +import { fontSemibold14 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; + +interface TextInputLaunchpadProps + extends Omit { + label: string; + placeHolder: string; + form: UseFormReturn; + name: Path; + sublabel?: React.ReactElement; + valueModifier?: (value: string) => string; + required?: boolean; + disabled?: boolean; +} + +export const TextInputLaunchpad = ({ + form, + name, + label, + placeHolder, + sublabel, + valueModifier, + disabled, + required = true, + ...restProps +}: TextInputLaunchpadProps) => { + const inputRef = useRef(null); + const [hovered, setHovered] = useState(false); + const { fieldState, field } = useController({ + name, + control: form.control, + }); + return ( + setHovered(true)} + onHoverOut={() => setHovered(false)} + onPress={() => inputRef?.current?.focus()} + style={{ width: "100%", marginBottom: layout.spacing_x2 }} + disabled={disabled} + > + + {sublabel && sublabel} + + + + valueModifier + ? field.onChange(valueModifier(text)) + : field.onChange(text) + } + value={field.value || ""} + ref={inputRef} + editable={!disabled} + {...restProps} + /> + + + {fieldState.error?.message} + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/components/inputs/selectInputs/MultipleSelectInput.tsx b/packages/screens/Launchpad/LaunchpadApply/components/inputs/selectInputs/MultipleSelectInput.tsx new file mode 100644 index 0000000000..bb4c54d450 --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/components/inputs/selectInputs/MultipleSelectInput.tsx @@ -0,0 +1,167 @@ +import React, { FC, useState } from "react"; +import { TouchableOpacity, View, ViewStyle } from "react-native"; + +import chevronDownSVG from "@/assets/icons/chevron-down.svg"; +import chevronUpSVG from "@/assets/icons/chevron-up.svg"; +import { BrandText } from "@/components/BrandText"; +import { SVG } from "@/components/SVG"; +import { PrimaryBox } from "@/components/boxes/PrimaryBox"; +import { TertiaryBox } from "@/components/boxes/TertiaryBox"; +import { CustomPressable } from "@/components/buttons/CustomPressable"; +import { Label } from "@/components/inputs/TextInputCustom"; +import { Separator } from "@/components/separators/Separator"; +import { SpacerColumn } from "@/components/spacer"; +import { useDropdowns } from "@/hooks/useDropdowns"; +import { CheckboxDappStore } from "@/screens/DAppStore/components/CheckboxDappStore"; +import { + neutral22, + neutral44, + neutral55, + neutral77, + secondaryColor, +} from "@/utils/style/colors"; +import { fontMedium14, fontSemibold14 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; + +interface Props { + style?: ViewStyle; + onDropdownClosed?: () => void; + dropdownOptions: string[]; + placeHolder?: string; + onPressItem: (item: string) => void; + items: string[]; + label: string; + sublabel?: React.ReactElement; + required?: boolean; +} + +export const MultipleSelectInput: FC = ({ + style, + dropdownOptions, + placeHolder, + items, + label, + onPressItem, + sublabel, + required = true, +}) => { + const [isDropdownOpen, setDropdownState, ref] = useDropdowns(); + const [hovered, setHovered] = useState(false); + + return ( + + setHovered(true)} + onHoverOut={() => setHovered(false)} + onPress={() => setDropdownState(!isDropdownOpen)} + > + + {sublabel && sublabel} + + + + + + 0 ? secondaryColor : neutral77, + }, + ]} + > + {items?.length > 0 ? items.join(", ") : placeHolder} + + + + + + {isDropdownOpen && ( + + {dropdownOptions.map((item, index) => ( + { + onPressItem(item); + }} + key={index} + style={{ + paddingTop: layout.spacing_x1_5, + width: "100%", + }} + > + + + + + {item} + + + {dropdownOptions.length - 1 !== index && ( + <> + + + + )} + + ))} + + )} + + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadApply/components/inputs/selectInputs/SelectInputLaunchpad.tsx b/packages/screens/Launchpad/LaunchpadApply/components/inputs/selectInputs/SelectInputLaunchpad.tsx new file mode 100644 index 0000000000..2553de2a9f --- /dev/null +++ b/packages/screens/Launchpad/LaunchpadApply/components/inputs/selectInputs/SelectInputLaunchpad.tsx @@ -0,0 +1,160 @@ +import React, { FC, useState } from "react"; +import { TouchableOpacity, View, ViewStyle } from "react-native"; + +import chevronDownSVG from "@/assets/icons/chevron-down.svg"; +import chevronUpSVG from "@/assets/icons/chevron-up.svg"; +import { BrandText } from "@/components/BrandText"; +import { SVG } from "@/components/SVG"; +import { PrimaryBox } from "@/components/boxes/PrimaryBox"; +import { TertiaryBox } from "@/components/boxes/TertiaryBox"; +import { CustomPressable } from "@/components/buttons/CustomPressable"; +import { Label } from "@/components/inputs/TextInputCustom"; +import { Separator } from "@/components/separators/Separator"; +import { SpacerColumn } from "@/components/spacer"; +import { useDropdowns } from "@/hooks/useDropdowns"; +import { + neutral22, + neutral44, + neutral55, + neutral77, + secondaryColor, +} from "@/utils/style/colors"; +import { fontMedium14, fontSemibold14 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; + +interface Props { + style?: ViewStyle; + onDropdownClosed?: () => void; + dropdownOptions: string[]; + placeHolder?: string; + onPressItem: (item: string) => void; + item?: string; + label: string; + required?: boolean; +} + +export const SelectInputLaunchpad: FC = ({ + style, + dropdownOptions, + placeHolder, + item, + label, + onPressItem, + required = true, +}) => { + const [isDropdownOpen, setDropdownState, ref] = useDropdowns(); + const [hovered, setHovered] = useState(false); + + return ( + + setHovered(true)} + onHoverOut={() => setHovered(false)} + onPress={() => setDropdownState(!isDropdownOpen)} + > + + + + + + + {item ? item : placeHolder} + + + + + + {isDropdownOpen && ( + + {dropdownOptions.map((item, index) => ( + { + setDropdownState(false); + onPressItem(item); + }} + key={index} + style={{ + paddingTop: layout.spacing_x1_5, + width: "100%", + }} + > + + {item} + + + {dropdownOptions.length - 1 !== index && ( + <> + + + + )} + + ))} + + )} + + + + ); +}; diff --git a/packages/screens/Launchpad/LaunchpadHome/LaunchpadScreen.tsx b/packages/screens/Launchpad/LaunchpadHome/LaunchpadScreen.tsx index 6337588388..9870376656 100644 --- a/packages/screens/Launchpad/LaunchpadHome/LaunchpadScreen.tsx +++ b/packages/screens/Launchpad/LaunchpadHome/LaunchpadScreen.tsx @@ -7,12 +7,14 @@ import { Sort, SortDirection, } from "@/api/marketplace/v1/marketplace"; +import { BrandText } from "@/components/BrandText"; import { ScreenContainer } from "@/components/ScreenContainer"; import { CollectionsCarouselHeader } from "@/components/carousels/CollectionsCarouselHeader"; import { CollectionGallery } from "@/components/collections/CollectionGallery"; import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork"; import { getNetwork, NetworkFeature } from "@/networks"; import { ScreenFC } from "@/utils/navigation"; +import { fontSemibold20 } from "@/utils/style/fonts"; import { layout } from "@/utils/style/layout"; export const LaunchpadScreen: ScreenFC<"Launchpad"> = () => { @@ -21,6 +23,7 @@ export const LaunchpadScreen: ScreenFC<"Launchpad"> = () => { return ( Launchpad} > = ({ projectStatus }) => { + const textColor = + projectStatus === Status.STATUS_UNSPECIFIED || + projectStatus === Status.UNRECOGNIZED + ? neutralFF + : neutral00; + + const backgroundColor = + projectStatus === Status.STATUS_INCOMPLETE + ? "#D2DEFC" + : projectStatus === Status.STATUS_COMPLETE + ? "#9990F5" + : projectStatus === Status.STATUS_REVIEWING + ? "#9058EC" + : projectStatus === Status.STATUS_CONFIRMED + ? "#F46FBF" + : neutral33; + + return ( + + + {launchpadProjectStatus(projectStatus)} + + + ); +}; diff --git a/packages/screens/Marketplace/CollectionsTable.tsx b/packages/screens/Marketplace/CollectionsTable.tsx index 44229a0ff5..c739299a73 100644 --- a/packages/screens/Marketplace/CollectionsTable.tsx +++ b/packages/screens/Marketplace/CollectionsTable.tsx @@ -176,7 +176,7 @@ export const CollectionsTable: FC<{ renderItem={({ item, index }) => ( )} @@ -189,10 +189,10 @@ export const CollectionsTable: FC<{ const CollectionTableRow: React.FC<{ collection: PopularCollection; - rank: number; + index: number; prices: CoingeckoPrices; -}> = ({ collection, rank, prices }) => { - const rowData = getRowData(collection, rank, prices); +}> = ({ collection, index, prices }) => { + const rowData = getRowData(collection, index, prices); const target = useCollectionNavigationTarget(collection.id); const tradeDiffText = rowData["TimePeriodPercentualVolume"]; const tradeDiffColor = @@ -232,7 +232,6 @@ const CollectionTableRow: React.FC<{ size="XS" sourceURI={rowData.collectionNameData.image} style={{ - // marginRight: isMobile ? layout.spacing_x1 : layout.spacing_x1_5, marginRight: layout.spacing_x1, }} /> @@ -404,7 +403,7 @@ const getDelta = (collection: PopularCollection) => { const getRowData = ( collection: PopularCollection, - rank: number, + index: number, prices: CoingeckoPrices, ): RowData => { const [network] = parseCollectionId(collection.id); @@ -427,7 +426,7 @@ const getRowData = ( : undefined; return { id: collection.id, - rank: rank + 1, + rank: index + 1, collectionName: collection.name, collectionNameData: { collectionName: collection.name, diff --git a/packages/screens/Projects/ProjectsMakeRequestScreen/ShortPresentation.tsx b/packages/screens/Projects/ProjectsMakeRequestScreen/ShortPresentation.tsx index 635ccb565d..44fc3b2dd2 100644 --- a/packages/screens/Projects/ProjectsMakeRequestScreen/ShortPresentation.tsx +++ b/packages/screens/Projects/ProjectsMakeRequestScreen/ShortPresentation.tsx @@ -9,7 +9,6 @@ import { BrandText } from "../../../components/BrandText"; import { PrimaryButtonOutline } from "../../../components/buttons/PrimaryButtonOutline"; import { RoundedGradientImage } from "../../../components/images/RoundedGradientImage"; import { TextInputCustom } from "../../../components/inputs/TextInputCustom"; -import { FileUploader } from "../../../components/inputs/fileUploader"; import { SpacerColumn } from "../../../components/spacer"; import { useNameSearch } from "../../../hooks/search/useNameSearch"; import { useSelectedNetworkId } from "../../../hooks/useSelectedNetwork"; @@ -23,6 +22,7 @@ import { zodProjectFormData, } from "../hooks/useMakeRequestHook"; +import { FileUploader } from "@/components/inputs/fileUploader"; import { LoaderFullScreen } from "@/components/loaders/LoaderFullScreen"; import { useIpfs } from "@/hooks/useIpfs"; import { ButtonsGroup } from "@/screens/Projects/components/ButtonsGroup"; diff --git a/packages/screens/UserPublicProfile/components/modals/SubscriptionSetupModal.tsx b/packages/screens/UserPublicProfile/components/modals/SubscriptionSetupModal.tsx index 39f3e50cb9..56ac89d6ef 100644 --- a/packages/screens/UserPublicProfile/components/modals/SubscriptionSetupModal.tsx +++ b/packages/screens/UserPublicProfile/components/modals/SubscriptionSetupModal.tsx @@ -5,7 +5,6 @@ import { useFieldArray, useForm, useWatch } from "react-hook-form"; import { View, TouchableOpacity } from "react-native"; import { SubscriptionBottomComponent } from "./SubscriptionBottomComponent"; -import { AccordionComponent } from "../accordion/AccordionComponent"; import addSVG from "@/assets/icons/add-secondary.svg"; import settingsSVG from "@/assets/icons/settings-primary.svg"; @@ -24,6 +23,7 @@ import { usePremiumChannel } from "@/hooks/feed/usePremiumChannel"; import useSelectedWallet from "@/hooks/useSelectedWallet"; import { getNativeCurrency, getNetworkFeature, parseUserId } from "@/networks"; import { NetworkFeature } from "@/networks/features"; +import { AccordionComponent } from "@/screens/UserPublicProfile/components/accordion/AccordionComponent"; import { bigDaySeconds } from "@/utils/big-time"; import { mustGetCw721MembershipSigningClient } from "@/utils/feed/client"; import { mapTierToFormElement } from "@/utils/feed/premium"; diff --git a/packages/scripts/network-setup/deployLib.ts b/packages/scripts/network-setup/deployLib.ts index ee0224f8ee..fa7495702a 100644 --- a/packages/scripts/network-setup/deployLib.ts +++ b/packages/scripts/network-setup/deployLib.ts @@ -6,6 +6,7 @@ import { bech32 } from "bech32"; import _, { cloneDeep } from "lodash"; import path from "path"; +import { instantiateNftLaunchpad } from "./deployNftLaunchpad"; import { InstantiateMsg as MarketplaceVaultInstantiateMsg } from "../../contracts-clients/nft-marketplace/NftMarketplace.types"; import { ExecuteMsg as NameServiceExecuteMsg, @@ -24,7 +25,9 @@ import { cosmosNetworkGasPrice, CosmosNetworkInfo, getCosmosNetwork, + getNetworkFeature, mustGetNonSigningCosmWasmClient, + NetworkFeature, } from "@/networks"; import { zodTryParseJSON } from "@/utils/sanitize"; @@ -132,6 +135,24 @@ export const deployTeritoriEcosystem = async ( network, ); + console.log("Instantiating NFT Launchpad", network.nameServiceCodeId); + const cosmwasmNftLaunchpadFeature = cloneDeep( + getNetworkFeature(networkId, NetworkFeature.CosmWasmNFTLaunchpad), + ); + if (!cosmwasmNftLaunchpadFeature) { + console.error(`Cosmwasm Launchpad feature not found on ${networkId}`); + } else { + cosmwasmNftLaunchpadFeature.launchpadContractAddress = + await instantiateNftLaunchpad( + opts, + wallet, + walletAddr, + "TODO DAO address", + network, + cosmwasmNftLaunchpadFeature, + ); + } + if (opts.signer) { await registerTNSHandle(network, opts.signer); await testTeritoriEcosystem(network); diff --git a/packages/utils/backend.ts b/packages/utils/backend.ts index 5aa792efc1..857b8ff619 100644 --- a/packages/utils/backend.ts +++ b/packages/utils/backend.ts @@ -8,6 +8,11 @@ import { FeedServiceClientImpl, GrpcWebImpl as FeedGrpcWebImpl, } from "../api/feed/v1/feed"; +import { + LaunchpadService, + LaunchpadServiceClientImpl, + GrpcWebImpl as LaunchpadGrpcWebImpl, +} from "../api/launchpad/v1/launchpad"; import { MarketplaceServiceClientImpl, GrpcWebImpl as MarketplaceGrpcWebImpl, @@ -18,7 +23,7 @@ import { GrpcWebImpl as P2eGrpcWebImpl, P2eService, } from "../api/p2e/v1/p2e"; -import { getNetwork } from "../networks"; +import { getNetwork, getNetworkFeature, NetworkFeature } from "../networks"; const marketplaceClients: { [key: string]: MarketplaceService } = {}; @@ -107,3 +112,34 @@ export const mustGetFeedClient = (networkId: string | undefined) => { } return client; }; + +const launchpadClients: { [key: string]: LaunchpadService } = {}; + +export const getLaunchpadClient = (networkId: string | undefined) => { + const network = getNetwork(networkId); + const cosmwasmNftLaunchpadFeature = getNetworkFeature( + networkId, + NetworkFeature.CosmWasmNFTLaunchpad, + ); + if (!network || !cosmwasmNftLaunchpadFeature) { + return undefined; + } + if (!launchpadClients[network.id]) { + const rpc = new LaunchpadGrpcWebImpl( + cosmwasmNftLaunchpadFeature.launchpadEndpoint, + { + debug: false, + }, + ); + launchpadClients[network.id] = new LaunchpadServiceClientImpl(rpc); + } + return launchpadClients[network.id]; +}; + +export const mustGetLaunchpadClient = (networkId: string | undefined) => { + const client = getLaunchpadClient(networkId); + if (!client) { + throw new Error(`failed to get feed client for network '${networkId}'`); + } + return client; +}; diff --git a/packages/utils/formRules.ts b/packages/utils/formRules.ts index 0f457a873f..e299f9cc68 100644 --- a/packages/utils/formRules.ts +++ b/packages/utils/formRules.ts @@ -1,9 +1,8 @@ import { bech32 } from "bech32"; import { ValidationRule } from "react-hook-form"; -import { LETTERS_REGEXP, NUMBERS_REGEXP } from "./regex"; - import { DEFAULT_FORM_ERRORS } from "@/utils/errors"; +import { LETTERS_REGEXP, NUMBERS_REGEXP } from "@/utils/regex"; // validator should return false or string to trigger error export const validateAddress = (value: string) => { @@ -32,3 +31,10 @@ export const validateMaxNumber = (value: string, max: number) => { } return true; }; + +export const validateFloatWithDecimals = (value: string, decimals: number) => { + const regexp = new RegExp( + `^([0-9]+[.]?[0-9]{0,${decimals}}|[.][0-9]{1,${decimals}})$`, + ); + return regexp.test(value); +}; diff --git a/packages/utils/ipfs.ts b/packages/utils/ipfs.ts index d61e633e33..92218156cc 100644 --- a/packages/utils/ipfs.ts +++ b/packages/utils/ipfs.ts @@ -34,6 +34,19 @@ const ipfsPathToWeb2URL = (path: string) => { return gatewayURL; }; +export const isIpfsPathValid = (path: string) => { + try { + path = path.substring("ipfs://".length); + const separatorIndex = path.indexOf("/"); + const cidString = + separatorIndex === -1 ? path : path.substring(0, separatorIndex); + const cid = CID.parse(cidString); + return !!cid.toV1().toString(); + } catch { + return false; + } +}; + /** Get the web2 url for a web3 uri or passthrough if not a web3 uri * Only supports ipfs for now */ diff --git a/packages/utils/launchpad.ts b/packages/utils/launchpad.ts new file mode 100644 index 0000000000..a504c1033d --- /dev/null +++ b/packages/utils/launchpad.ts @@ -0,0 +1,55 @@ +import { + LaunchpadProject, + Status, + StatusCount, +} from "@/api/launchpad/v1/launchpad"; +import { zodTryParseJSON } from "@/utils/sanitize"; +import { + CollectionDataResult, + ZodCollectionDataResult, +} from "@/utils/types/launchpad"; + +export const launchpadProjectStatus = (status: Status) => { + switch (status) { + case Status.STATUS_INCOMPLETE: + return "INCOMPLETE"; + case Status.STATUS_COMPLETE: + return "COMPLETE"; + case Status.STATUS_REVIEWING: + return "REVIEWING"; + case Status.STATUS_CONFIRMED: + return "CONFIRMED"; + case Status.STATUS_UNSPECIFIED: + return "UNSPECIFIED"; + default: + return "UNSPECIFIED"; + } +}; + +export const statusToCount = (status: string, statusCounts?: StatusCount[]) => + statusCounts?.find( + (statusCount) => launchpadProjectStatus(statusCount.status) === status, + )?.count || 0; + +export const parseCollectionData = (launchpadProject: LaunchpadProject) => + zodTryParseJSON(ZodCollectionDataResult, launchpadProject.collectionData); + +export const parseMultipleCollectionsData = ( + launchpadProjects: LaunchpadProject[], +) => { + const result: CollectionDataResult[] = []; + launchpadProjects.forEach((project) => { + if (!project) { + return; + } + const collectionData: CollectionDataResult | undefined = zodTryParseJSON( + ZodCollectionDataResult, + project.collectionData, + ); + if (!collectionData) { + return; + } + result.push(collectionData); + }); + return result; +}; diff --git a/packages/utils/navigation.ts b/packages/utils/navigation.ts index b46228241b..4c1ef48d21 100644 --- a/packages/utils/navigation.ts +++ b/packages/utils/navigation.ts @@ -33,6 +33,13 @@ export type RootStackParamList = { Launchpad: undefined; LaunchpadApply: undefined; + LaunchpadCreate: undefined; + LaunchpadComplete: { id: string }; + LaunchpadMyCollections: undefined; + LaunchpadAdministrationOverview: undefined; + LaunchpadApplications: undefined; + LaunchpadApplicationReview: { id: string }; + LaunchpadReadyApplications: undefined; LaunchpadERC20: undefined; LaunchpadERC20Tokens?: { network?: string }; @@ -215,6 +222,13 @@ const getNavConfig: (homeScreen: keyof RootStackParamList) => NavConfig = ( // ==== Launchpad Launchpad: "launchpad", LaunchpadApply: "launchpad/apply", + LaunchpadCreate: "launchpad/create", + LaunchpadComplete: "launchpad/complete/:id", + LaunchpadMyCollections: "launchpad/my-collections", + LaunchpadAdministrationOverview: "launchpad/admin", + LaunchpadApplications: "launchpad/admin/applications", + LaunchpadApplicationReview: "launchpad/review/:id", + LaunchpadReadyApplications: "launchpad/admin/ready-applications", // ==== Launchpad ERC20 LaunchpadERC20: "launchpad-erc20", diff --git a/packages/utils/regex.ts b/packages/utils/regex.ts index a21ab7a143..420a51d0d0 100644 --- a/packages/utils/regex.ts +++ b/packages/utils/regex.ts @@ -1,8 +1,11 @@ export const MENTION_REGEX = /(@[\w&.-]+)/; export const URL_REGEX = /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/; +// export const IPFS_URI_REGEX = /^ipfs:\/\/.*/; export const HASHTAG_REGEX = /#\S+/; export const HTML_TAG_REGEXP = /(<([^>]+)>)/gi; export const GIF_URL_REGEX = /https?:\/\/.*\.(gif)(\?.*)?$/; export const NUMBERS_REGEXP = /^\d+$/; export const LETTERS_REGEXP = /^[A-Za-z]+$/; +export const EMAIL_REGEXP = /^[\w-]+@([\w-]+\.)+[\w-]{2,4}$/; +export const NUMBERS_COMMA_SEPARATOR_REGEXP = /^\s*\d+(\s*,\s*\d+)*\s*$/; diff --git a/packages/utils/sidebar.ts b/packages/utils/sidebar.ts index ec80f3c88b..5570367aa0 100644 --- a/packages/utils/sidebar.ts +++ b/packages/utils/sidebar.ts @@ -56,6 +56,12 @@ export const SIDEBAR_LIST: SidebarRecordType = { icon: launchpadApplySVG, route: "LaunchpadApply", }, + admin: { + title: "Admin Dashboard", + id: "admin", + icon: gridSVG, + route: "LaunchpadAdministrationOverview", + }, }, }, "multisig-wallet": { diff --git a/packages/utils/types/files.ts b/packages/utils/types/files.ts index d058a8da34..a2bd47f6cb 100644 --- a/packages/utils/types/files.ts +++ b/packages/utils/types/files.ts @@ -32,12 +32,18 @@ const ZodBaseFileData = z.object({ isThumbnailImage: z.boolean().optional(), base64Image: z.string().optional(), }); -type BaseFileData = z.infer; -export interface LocalFileData extends BaseFileData { - file: File; - thumbnailFileData?: BaseFileData & { file: File }; -} +export const ZodLocalFileData = z + .object({ + ...ZodBaseFileData.shape, + thumbnailFileData: ZodBaseFileData.extend({ + file: z.instanceof(File), + }).optional(), + }) + .extend({ + file: z.instanceof(File), + }); +export type LocalFileData = z.infer; export const ZodRemoteFileData = z.object({ ...ZodBaseFileData.shape, diff --git a/packages/utils/types/launchpad.ts b/packages/utils/types/launchpad.ts new file mode 100644 index 0000000000..110687bd1d --- /dev/null +++ b/packages/utils/types/launchpad.ts @@ -0,0 +1,232 @@ +import { z } from "zod"; + +import { Collection } from "@/contracts-clients/nft-launchpad"; +import { DEFAULT_FORM_ERRORS } from "@/utils/errors"; +import { isIpfsPathValid } from "@/utils/ipfs"; +import { + EMAIL_REGEXP, + LETTERS_REGEXP, + NUMBERS_REGEXP, + URL_REGEX, +} from "@/utils/regex"; +import { ZodLocalFileData } from "@/utils/types/files"; +const ZodCoin = z.object({ + amount: z + .string() + .trim() + .min(1, DEFAULT_FORM_ERRORS.required) + .refine( + (value) => !value || NUMBERS_REGEXP.test(value), + DEFAULT_FORM_ERRORS.onlyNumbers, + ) + .optional(), + denom: z.string().trim(), +}); + +export type Coin = z.infer; + +// ===== Shapes to build front objects +const ZodCollectionMintPeriodFormValues = z.object({ + price: ZodCoin, + maxTokens: z + .string() + .trim() + .refine( + (value) => !value || NUMBERS_REGEXP.test(value), + DEFAULT_FORM_ERRORS.onlyNumbers, + ) + .optional(), + perAddressLimit: z + .string() + .trim() + .refine( + (value) => !value || NUMBERS_REGEXP.test(value), + DEFAULT_FORM_ERRORS.onlyNumbers, + ) + .optional(), + startTime: z.number().min(1, DEFAULT_FORM_ERRORS.required), + endTime: z.number().optional(), + whitelistAddressesFile: ZodLocalFileData.optional(), + whitelistAddresses: z.array(z.string()).optional(), + isOpen: z.boolean(), +}); + +export const ZodCollectionAssetsAttributeFormValues = z.object({ + value: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required), + type: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required), +}); + +export const ZodCollectionAssetsMetadataFormValues = z.object({ + image: ZodLocalFileData, + externalUrl: z + .string() + .trim() + // We ignore the URL format control since externalUrl is optional + // .refine( + // (value) => !value || URL_REGEX.test(value), + // DEFAULT_FORM_ERRORS.onlyUrl, + // ) + .optional(), + description: z.string().trim().optional(), + name: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required), + youtubeUrl: z.string().trim().optional(), + attributes: z.array(ZodCollectionAssetsAttributeFormValues), +}); + +export const ZodCollectionAssetsMetadatasFormValues = z.object({ + assetsMetadatas: z.array(ZodCollectionAssetsMetadataFormValues).optional(), + nftApiKey: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required), +}); + +export const ZodCollectionFormValues = z.object({ + name: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required), + description: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required), + symbol: z + .string() + .trim() + .toUpperCase() + .min(1, DEFAULT_FORM_ERRORS.required) + .refine( + (value) => LETTERS_REGEXP.test(value), + DEFAULT_FORM_ERRORS.onlyLetters, + ), + websiteLink: z + .string() + .trim() + .min(1, DEFAULT_FORM_ERRORS.required) + .refine((value) => URL_REGEX.test(value), DEFAULT_FORM_ERRORS.onlyUrl), + email: z + .string() + .trim() + .min(1, DEFAULT_FORM_ERRORS.required) + .refine((value) => EMAIL_REGEXP.test(value), DEFAULT_FORM_ERRORS.onlyEmail), + projectTypes: z.array(z.string().trim()).min(1, DEFAULT_FORM_ERRORS.required), + revealTime: z.number().optional(), + teamDescription: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required), + partnersDescription: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required), + investDescription: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required), + investLink: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required), + artworkDescription: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required), + coverImage: ZodLocalFileData, + isPreviouslyApplied: z.boolean(), + isDerivativeProject: z.boolean(), + isReadyForMint: z.boolean(), + isDox: z.boolean(), + escrowMintProceedsPeriod: z + .string() + .trim() + .min(1, DEFAULT_FORM_ERRORS.required), + daoWhitelistCount: z + .string() + .trim() + .min(1, DEFAULT_FORM_ERRORS.required) + .refine( + (value) => NUMBERS_REGEXP.test(value), + DEFAULT_FORM_ERRORS.onlyNumbers, + ), + mintPeriods: z.array(ZodCollectionMintPeriodFormValues).nonempty(), + royaltyAddress: z.string().trim().optional(), + royaltyPercentage: z + .string() + .trim() + .refine( + (value) => !value || NUMBERS_REGEXP.test(value), + DEFAULT_FORM_ERRORS.onlyNumbers, + ) + .optional(), + assetsMetadatas: ZodCollectionAssetsMetadatasFormValues.optional(), + baseTokenUri: z + .string() + .trim() + .refine( + (value) => !value || isIpfsPathValid(value), + DEFAULT_FORM_ERRORS.onlyIpfsUri, + ) + .optional(), + coverImageUri: z + .string() + .trim() + .refine( + (value) => !value || isIpfsPathValid(value), + DEFAULT_FORM_ERRORS.onlyIpfsUri, + ) + .optional(), +}); + +export type CollectionFormValues = z.infer; + +export type CollectionAssetsAttributeFormValues = z.infer< + typeof ZodCollectionAssetsAttributeFormValues +>; + +export type CollectionMintPeriodFormValues = z.infer< + typeof ZodCollectionMintPeriodFormValues +>; + +export type CollectionAssetsMetadataFormValues = z.infer< + typeof ZodCollectionAssetsMetadataFormValues +>; + +export type CollectionAssetsMetadatasFormValues = z.infer< + typeof ZodCollectionAssetsMetadatasFormValues +>; + +// ===== Shapes to build objects from api +const ZodCoinDataResult = z.object({ + amount: z.string(), + denom: z.string(), +}); + +const ZodWhitelistInfoDataResult = z.object({ + addresses_count: z.number(), + addresses_ipfs: z.string(), + addresses_merkle_root: z.string(), +}); + +const ZodMintPeriodDataResult = z.object({ + end_time: z.number().nullish(), + limit_per_address: z.number().nullish(), + max_tokens: z.number().nullish(), + price: ZodCoinDataResult.nullish(), + start_time: z.number(), + whitelist_info: ZodWhitelistInfoDataResult.nullish(), +}); + +export const ZodCollectionDataResult = z.object({ + artwork_desc: z.string(), + base_token_uri: z.string().nullish(), // TODO REMOVE + contact_email: z.string(), + cover_img_uri: z.string(), + dao_whitelist_count: z.number(), + deployed_address: z.string().nullish(), + desc: z.string(), + escrow_mint_proceeds_period: z.number(), + investment_desc: z.string(), + investment_link: z.string(), + is_applied_previously: z.boolean(), + is_dox: z.boolean(), + is_project_derivative: z.boolean(), + is_ready_for_mint: z.boolean(), + metadatas_merkle_root: z.string().nullish(), + mint_periods: z.array(ZodMintPeriodDataResult), + name: z.string(), + partners: z.string(), + project_type: z.string(), + reveal_time: z.number().nullish(), + royalty_address: z.string().nullish(), + royalty_percentage: z.number().nullish(), + symbol: z.string(), + target_network: z.string(), + team_desc: z.string(), + tokens_count: z.number(), + website_link: z.string(), +}); + +export type MintPeriodDataResult = z.infer; + +export type CollectionDataResult = z.infer; + +export type CollectionToSubmit = Omit< + Collection, + "deployed_address" | "base_token_uri" | "owner" +>; diff --git a/rust/cw-contracts/nft-launchpad/Makefile b/rust/cw-contracts/nft-launchpad/Makefile index d89889f4bb..ef017aa912 100644 --- a/rust/cw-contracts/nft-launchpad/Makefile +++ b/rust/cw-contracts/nft-launchpad/Makefile @@ -51,4 +51,5 @@ deploy.mainnet: artifacts/nft_launchpad.wasm instantiate.mainnet: config-mainnet.json set -o pipefail; \ TXHASH=$$(teritorid tx wasm instantiate $(CODE_ID_MAINNET) $(CONFIG_MAINNET) --label NftsBurner --admin $(ADMIN_ADDR_MAINNET) $(TX_FLAGS_MAINNET) | jq -r .txhash); \ - while ! teritorid query tx $$TXHASH $(QUERY_FLAGS_MAINNET) 2>/dev/null | jq -r '.logs[0].events[] | select(.type=="instantiate").attributes[] | select(.key=="_contract_address").value'; do sleep 1; done \ No newline at end of file + while ! teritorid query tx $$TXHASH $(QUERY_FLAGS_MAINNET) 2>/dev/null | jq -r '.logs[0].events[] | select(.type=="instantiate").attributes[] | select(.key=="_contract_address").value'; do sleep 1; done + diff --git a/yarn.lock b/yarn.lock index 240fc5ed44..3fccee07e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14585,6 +14585,29 @@ __metadata: languageName: node linkType: hard +"keccak256@npm:^1.0.6": + version: 1.0.6 + resolution: "keccak256@npm:1.0.6" + dependencies: + bn.js: ^5.2.0 + buffer: ^6.0.3 + keccak: ^3.0.2 + checksum: decafb4b37adcfa6d06b6a5d28546d0d7a9f01ccf4b8cc8963cf8188fcc79a230d7e22988e860813623c602d764259734423e38fd7b9aadfeb409d6928a1d4cf + languageName: node + linkType: hard + +"keccak@npm:^3.0.2": + version: 3.0.4 + resolution: "keccak@npm:3.0.4" + dependencies: + node-addon-api: ^2.0.0 + node-gyp: latest + node-gyp-build: ^4.2.0 + readable-stream: ^3.6.0 + checksum: 2bf27b97b2f24225b1b44027de62be547f5c7326d87d249605665abd0c8c599d774671c35504c62c9b922cae02758504c6f76a73a84234d23af8a2211afaaa11 + languageName: node + linkType: hard + "keyv@npm:^4.0.0, keyv@npm:^4.5.3": version: 4.5.4 resolution: "keyv@npm:4.5.4" @@ -16135,6 +16158,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^2.0.0": + version: 2.0.2 + resolution: "node-addon-api@npm:2.0.2" + dependencies: + node-gyp: latest + checksum: 31fb22d674648204f8dd94167eb5aac896c841b84a9210d614bf5d97c74ef059cc6326389cf0c54d2086e35312938401d4cc82e5fcd679202503eb8ac84814f8 + languageName: node + linkType: hard + "node-dir@npm:^0.1.17": version: 0.1.17 resolution: "node-dir@npm:0.1.17" @@ -16165,7 +16197,7 @@ __metadata: languageName: node linkType: hard -"node-gyp-build@npm:^4.3.0": +"node-gyp-build@npm:^4.2.0, node-gyp-build@npm:^4.3.0": version: 4.8.0 resolution: "node-gyp-build@npm:4.8.0" bin: @@ -20232,6 +20264,7 @@ __metadata: graphql-request: ^5 html-to-draftjs: ^1.5.0 immutable: ^4.0.0 + keccak256: ^1.0.6 kubernetes-models: ^4.3.1 leaflet: ^1.9.4 leaflet.markercluster: ^1.5.3