diff --git a/internal/service/api/handlers/add_participant.go b/internal/service/api/handlers/add_participant.go new file mode 100644 index 0000000..5090d90 --- /dev/null +++ b/internal/service/api/handlers/add_participant.go @@ -0,0 +1,40 @@ +package handlers + +import ( + "errors" + "net/http" + + "gitlab.com/distributed_lab/ape" + "gitlab.com/distributed_lab/ape/problems" + + "github.com/black-pepper-team/community-indexer/internal/service/api/requests" + "github.com/black-pepper-team/community-indexer/internal/service/api/responses" + "github.com/black-pepper-team/community-indexer/internal/service/core" +) + +func AddCommunityParticipant(w http.ResponseWriter, r *http.Request) { + req, err := requests.NewAddCommunityParticipant(r) + if err != nil { + Log(r).WithField("reason", err).Debug("Bad request") + ape.RenderErr(w, problems.BadRequest(err)...) + return + } + + mintRequest, err := Core(r).AddCommunityParticipant( + req.PrivateKey, req.ParticipantAddress, req.ContractAddress, + ) + switch { + case errors.Is(err, core.ErrContractNotFound): + ape.RenderErr(w, problems.NotFound()) + Log(r).WithError(err). + Debug("Contract not found") + return + case err != nil: + Log(r).WithError(err). + Error("Failed add community participant") + ape.RenderErr(w, problems.InternalError()) + return + } + + ape.Render(w, responses.NewRegisterInCommunity(mintRequest)) +} diff --git a/internal/service/api/handlers/create_community.go b/internal/service/api/handlers/create_community.go index 2170c32..3cb0bb2 100644 --- a/internal/service/api/handlers/create_community.go +++ b/internal/service/api/handlers/create_community.go @@ -23,7 +23,7 @@ func CreateCommunity(w http.ResponseWriter, r *http.Request) { return } - newCommunity, err := Core(r).CreateCommunity(req.CollectionName, req.CollectionSymbol) + newCommunity, err := Core(r).CreateCommunity(req.CollectionName, req.CollectionSymbol, req.PrivateKey) if err != nil { Log(r).WithError(err). Error("Failed create community") diff --git a/internal/service/api/requests/add_participant.go b/internal/service/api/requests/add_participant.go new file mode 100644 index 0000000..870387e --- /dev/null +++ b/internal/service/api/requests/add_participant.go @@ -0,0 +1,62 @@ +package requests + +import ( + "crypto/ecdsa" + "encoding/json" + "net/http" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + validation "github.com/go-ozzo/ozzo-validation/v4" + "gitlab.com/distributed_lab/logan/v3/errors" +) + +type addCommunityParticipantRequest struct { + PrivateKey string `json:"private_key"` + ParticipantAddress string `json:"participant_address"` + ContractAddress string `json:"contract_address"` +} + +type AddCommunityParticipantRequest struct { + PrivateKey *ecdsa.PrivateKey + ParticipantAddress common.Address + ContractAddress common.Address +} + +func NewAddCommunityParticipant(r *http.Request) (*AddCommunityParticipantRequest, error) { + var requestBody addCommunityParticipantRequest + + if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { + return nil, errors.Wrap(err, "failed to decode json request body") + } + + if err := requestBody.validate(); err != nil { + return nil, err + } + + return requestBody.parse(), nil +} + +// nolint +func (r *addCommunityParticipantRequest) validate() error { + return validation.Errors{ + "body/private_key": validation.Validate( + r.PrivateKey, validation.Required, validation.Length(64, 64), + ), + "body/participant_address": validation.Validate( + r.ParticipantAddress, validation.Required, validation.By(MustBeValidAddress), + ), + "body/contract_address": validation.Validate( + r.ContractAddress, validation.Required, validation.By(MustBeValidAddress), + ), + }.Filter() +} + +func (r *addCommunityParticipantRequest) parse() *AddCommunityParticipantRequest { + sk, _ := crypto.HexToECDSA(r.PrivateKey) + return &AddCommunityParticipantRequest{ + PrivateKey: sk, + ParticipantAddress: common.HexToAddress(r.ParticipantAddress), + ContractAddress: common.HexToAddress(r.ContractAddress), + } +} diff --git a/internal/service/api/requests/create_community.go b/internal/service/api/requests/create_community.go index 7cd4c8e..c9b2f3a 100644 --- a/internal/service/api/requests/create_community.go +++ b/internal/service/api/requests/create_community.go @@ -1,9 +1,11 @@ package requests import ( + "crypto/ecdsa" "encoding/json" "net/http" + "github.com/ethereum/go-ethereum/crypto" validation "github.com/go-ozzo/ozzo-validation/v4" "gitlab.com/distributed_lab/logan/v3/errors" ) @@ -11,11 +13,13 @@ import ( type createCommunityRequest struct { CollectionName string `json:"collection_name"` CollectionSymbol string `json:"collection_symbol"` + PrivateKey string `json:"private_key"` } type CreateCommunityRequest struct { CollectionName string CollectionSymbol string + PrivateKey *ecdsa.PrivateKey } func NewCreateCommunity(r *http.Request) (*CreateCommunityRequest, error) { @@ -35,6 +39,9 @@ func NewCreateCommunity(r *http.Request) (*CreateCommunityRequest, error) { // nolint func (r *createCommunityRequest) validate() error { return validation.Errors{ + "body/private_key": validation.Validate( + r.PrivateKey, validation.Required, validation.Length(64, 64), + ), "body/collection_name": validation.Validate( r.CollectionName, validation.Required, ), @@ -45,7 +52,9 @@ func (r *createCommunityRequest) validate() error { } func (r *createCommunityRequest) parse() *CreateCommunityRequest { + sk, _ := crypto.HexToECDSA(r.PrivateKey) return &CreateCommunityRequest{ + PrivateKey: sk, CollectionName: r.CollectionName, CollectionSymbol: r.CollectionSymbol, } diff --git a/internal/service/api/router.go b/internal/service/api/router.go index 322e341..b6b66a5 100644 --- a/internal/service/api/router.go +++ b/internal/service/api/router.go @@ -27,6 +27,7 @@ func (s *service) router() chi.Router { r.Post("/import", handlers.ImportCommunity) r.Post("/register", handlers.RegisterInCommunity) r.Get("/register/{register-id}", handlers.GetRegisterStatus) + r.Post("/add-participant", handlers.AddCommunityParticipant) }) }) diff --git a/internal/service/core/eth_utils.go b/internal/service/core/eth_utils.go index ab98ccc..2f8fddf 100644 --- a/internal/service/core/eth_utils.go +++ b/internal/service/core/eth_utils.go @@ -3,6 +3,7 @@ package core import ( "bytes" "context" + "crypto/ecdsa" "errors" "fmt" "math/big" @@ -13,6 +14,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" ) @@ -44,9 +46,10 @@ func (c *Core) waitMined(ctx context.Context, tx *types.Transaction) (*types.Rec func (c *Core) retryChainCall( ctx context.Context, + privKey *ecdsa.PrivateKey, contractCall func(signer *bind.TransactOpts) error, ) error { - signer, err := c.newSigner(ctx) + signer, err := c.newSigner(ctx, privKey) if err != nil { return fmt.Errorf("failed to get new signer: %w", err) } @@ -65,13 +68,13 @@ func (c *Core) retryChainCall( return nil } -func (c *Core) newSigner(ctx context.Context) (*bind.TransactOpts, error) { - nonce, err := c.ethClient.PendingNonceAt(ctx, c.address) +func (c *Core) newSigner(ctx context.Context, privKey *ecdsa.PrivateKey) (*bind.TransactOpts, error) { + nonce, err := c.ethClient.PendingNonceAt(ctx, crypto.PubkeyToAddress(privKey.PublicKey)) if err != nil { return nil, fmt.Errorf("failed to get nonce: %w", err) } - auth, err := bind.NewKeyedTransactorWithChainID(c.privateKey, c.chainID) + auth, err := bind.NewKeyedTransactorWithChainID(privKey, c.chainID) if err != nil { return nil, fmt.Errorf("failed to create transaction signer: %w", err) } diff --git a/internal/service/core/handlers.go b/internal/service/core/handlers.go index 634e386..96c38cb 100644 --- a/internal/service/core/handlers.go +++ b/internal/service/core/handlers.go @@ -1,6 +1,7 @@ package core import ( + "crypto/ecdsa" "encoding/json" "errors" "fmt" @@ -11,6 +12,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/google/uuid" "github.com/iden3/go-iden3-crypto/babyjub" "github.com/iden3/go-rapidsnark/prover" @@ -53,16 +55,12 @@ func (c *Core) GetCommunityById(communityId uuid.UUID) (*data.Community, error) func (c *Core) CreateCommunity( collectionName string, collectionSymbol string, + privKey *ecdsa.PrivateKey, ) (*data.Community, error) { var err error var tx *types.Transaction var address common.Address - err = c.retryChainCall(c.ctx, func(signer *bind.TransactOpts) error { - signer, err := c.newSigner(c.ctx) - if err != nil { - return fmt.Errorf("failed to get new signer: %w", err) - } - + err = c.retryChainCall(c.ctx, privKey, func(signer *bind.TransactOpts) error { address, tx, _, err = contracts.DeployERC721Mock(signer, c.ethClient, collectionName, collectionSymbol) if err != nil { return fmt.Errorf("failed to deploy new community: %w", err) @@ -85,7 +83,7 @@ func (c *Core) CreateCommunity( Name: collectionName, Symbol: collectionSymbol, ContractAddress: address, - OwnerAddress: c.address, + OwnerAddress: crypto.PubkeyToAddress(privKey.PublicKey), } if err = c.db.New().CommunitiesQ().Insert(&newCommunity); err != nil { @@ -244,12 +242,7 @@ func (c *Core) RegisterInCommunity( } var tx *types.Transaction - err = c.retryChainCall(c.ctx, func(signer *bind.TransactOpts) error { - signer, err := c.newSigner(c.ctx) - if err != nil { - return fmt.Errorf("failed to get new signer: %w", err) - } - + err = c.retryChainCall(c.ctx, c.privateKey, func(signer *bind.TransactOpts) error { tx, err = c.authStorageContract.Register( signer, contractId, @@ -322,3 +315,65 @@ func (c *Core) GetRegister(registerRequestId uuid.UUID) (*RegisterRequest, error return registerRequest, nil } + +func (c *Core) AddCommunityParticipant( + privKey *ecdsa.PrivateKey, + participantAddress common.Address, + contractAddress common.Address, +) (*RegisterRequest, error) { + community, err := c.db.CommunitiesQ().WhereContractAddress(contractAddress).Get() + if err != nil { + return nil, fmt.Errorf("failed to get community by contract address: %w", err) + } + + if community == nil { + return nil, ErrContractNotFound + } + + collectionContract, err := contracts.NewERC721Mock(contractAddress, c.ethClient) + if err != nil { + return nil, fmt.Errorf("failed to get ERC721 contract: %w", err) + } + + var tx *types.Transaction + err = c.retryChainCall(c.ctx, privKey, func(signer *bind.TransactOpts) error { + newRandId := babyjub.NewRandPrivKey() + tx, err = collectionContract.Mint(signer, participantAddress, (newRandId).Scalar().BigInt()) + if err != nil { + return fmt.Errorf("failed to call Register: %w", err) + } + + return nil + }) + if err != nil { + return nil, fmt.Errorf("failed to call retryChainCall: %w", err) + } + + requestId := uuid.New() + mintRequest := RegisterRequest{ + Id: requestId, + Status: Processing, + } + + c.registerRequests[requestId] = &mintRequest + + go func() { + nextStatus := Registered + + if _, err := c.waitMined(c.ctx, tx); err != nil { + c.log. + WithField("tx-hash", tx.Hash().Hex()). + WithField("reason", err). + Errorf("failed to wait tx mined") + + nextStatus = FailedRegister + } + + mintRequest.Status = nextStatus + c.registerRequests[requestId] = &mintRequest + + return + }() + + return &mintRequest, nil +} diff --git a/internal/service/core/main.go b/internal/service/core/main.go index 80ee41d..6efaeae 100644 --- a/internal/service/core/main.go +++ b/internal/service/core/main.go @@ -74,6 +74,7 @@ func New(ctx context.Context, cfg config.Config) (*Core, error) { circuits: circuits, chatContract: chatContract, authStorageContract: authStorageContract, + registerRequests: make(map[uuid.UUID]*RegisterRequest), registeredUsers: make(RegisterStorage), }, nil }