diff --git a/.env-sample b/.env-sample index da62a0d..ab58060 100644 --- a/.env-sample +++ b/.env-sample @@ -12,7 +12,7 @@ DB_PORT=5432 ALLOWED_ORIGIN=* PASETO_SIGNED_BY=NetSepio APTOS_CONFIG= -APTOS_FUNCTION_ID=0x5fdf39c03b36e9c59387628ca9066c62b2ec41019355c249177a7886e663f4a1 +APTOS_FUNCTION_ID=0x75bcfe882d1a4d032ead2b47f377e4c95221594d66ab2bd09a61aded4c9d64f9 GAS_UNITS=10046 GAS_PRICE=100 NFT_STORAGE_KEY= @@ -32,4 +32,6 @@ EREBRUS_EU=abcd EREBRUS_JP=abcd SOTREUS_US=abcd SOTREUS_SG=abcd -EREBRUS_US=abcd \ No newline at end of file +EREBRUS_US=abcd +STRIPE_SECRET_KEY= +STRIPE_WEBHOOK_SECRET= diff --git a/api/v1/account/subscription/create.go b/api/v1/account/subscription/create.go new file mode 100644 index 0000000..78f8095 --- /dev/null +++ b/api/v1/account/subscription/create.go @@ -0,0 +1,72 @@ +package subscription + +import ( + "math" + "net/http" + + "github.com/NetSepio/gateway/api/middleware/auth/paseto" + "github.com/NetSepio/gateway/config/dbconfig" + "github.com/NetSepio/gateway/models" + "github.com/NetSepio/gateway/util/pkg/aptos" + "github.com/NetSepio/gateway/util/pkg/logwrapper" + "github.com/TheLazarusNetwork/go-helpers/httpo" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/stripe/stripe-go/v76" + "github.com/stripe/stripe-go/v76/paymentintent" +) + +func Buy111NFT(c *gin.Context) { + db := dbconfig.GetDb() + userId := c.GetString(paseto.CTX_USER_ID) + walletAddress := c.GetString(paseto.CTX_WALLET_ADDRES) + if walletAddress == "" { + logwrapper.Errorf("user has no wallet address") + httpo.NewErrorResponse(http.StatusBadRequest, "user doesn't have any wallet linked").SendD(c) + return + } + + coinPrice, err := aptos.GetCoinPrice() + if err != nil { + logwrapper.Errorf("failed to get coin price: %v", err) + httpo.NewErrorResponse(http.StatusInternalServerError, "internal server error").SendD(c) + return + } + params := &stripe.PaymentIntentParams{ + Amount: stripe.Int64(int64(math.Ceil(coinPrice * 11.1 * 100))), + Currency: stripe.String(string(stripe.CurrencyUSD)), + // In the latest version of the API, specifying the `automatic_payment_methods` parameter is optional because Stripe enables its functionality by default. + AutomaticPaymentMethods: &stripe.PaymentIntentAutomaticPaymentMethodsParams{ + Enabled: stripe.Bool(true), + }, + } + + pi, err := paymentintent.New(params) + if err != nil { + logwrapper.Errorf("failed to create session: %v", err) + httpo.NewErrorResponse(http.StatusInternalServerError, "internal server error").SendD(c) + return + } + + // type UsersStripePi struct { + // Id string `gorm:"primary_key" json:"id,omitempty"` + // UserId string `json:"userId,omitempty"` + // StripePiId string `json:"stripePiId,omitempty"` + // StripePiType string `json:"stripePiType,omitempty"` + // } + + // insert in above table + err = db.Create(&models.UserStripePi{ + Id: uuid.NewString(), + UserId: userId, + StripePiId: pi.ID, + StripePiType: models.Erebrus111NFT, + }).Error + if err != nil { + logwrapper.Errorf("failed to insert into users_stripe_pi: %v", err) + httpo.NewErrorResponse(http.StatusInternalServerError, "internal server error").SendD(c) + return + } + + httpo.NewSuccessResponseP(http.StatusOK, "payment intent created", BuyErebrusNFTResponse{ClientSecret: pi.ClientSecret}).SendD(c) +} diff --git a/api/v1/account/subscription/subscription.go b/api/v1/account/subscription/subscription.go new file mode 100644 index 0000000..08505d0 --- /dev/null +++ b/api/v1/account/subscription/subscription.go @@ -0,0 +1,16 @@ +package subscription + +import ( + "github.com/NetSepio/gateway/api/middleware/auth/paseto" + "github.com/gin-gonic/gin" +) + +// ApplyRoutes applies router to gin Router +func ApplyRoutes(r *gin.RouterGroup) { + g := r.Group("/subscription") + { + g.POST("payment-webhook", StripeWebhookHandler) + g.Use(paseto.PASETO(false)) + g.POST("erebrus", Buy111NFT) + } +} diff --git a/api/v1/account/subscription/type.go b/api/v1/account/subscription/type.go new file mode 100644 index 0000000..314ae55 --- /dev/null +++ b/api/v1/account/subscription/type.go @@ -0,0 +1,5 @@ +package subscription + +type BuyErebrusNFTResponse struct { + ClientSecret string `json:"clientSecret"` +} diff --git a/api/v1/account/subscription/webhook.go b/api/v1/account/subscription/webhook.go new file mode 100644 index 0000000..6627db7 --- /dev/null +++ b/api/v1/account/subscription/webhook.go @@ -0,0 +1,100 @@ +package subscription + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + + "github.com/NetSepio/gateway/config/dbconfig" + "github.com/NetSepio/gateway/config/envconfig" + "github.com/NetSepio/gateway/models" + "github.com/NetSepio/gateway/util/pkg/aptos" + "github.com/NetSepio/gateway/util/pkg/logwrapper" + "github.com/gin-gonic/gin" + "github.com/stripe/stripe-go/v76" + "github.com/stripe/stripe-go/v76/webhook" + "gorm.io/gorm" +) + +func StripeWebhookHandler(c *gin.Context) { + db := dbconfig.GetDb() + + const MaxBodyBytes = int64(65536) + c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, MaxBodyBytes) + payload, err := io.ReadAll(c.Request.Body) + if err != nil { + logwrapper.Errorf("Error reading request body: %v", err) + c.Status(http.StatusServiceUnavailable) + return + } + + event, err := webhook.ConstructEvent(payload, c.GetHeader("Stripe-Signature"), envconfig.EnvVars.STRIPE_WEBHOOK_SECRET) + if err != nil { + logwrapper.Errorf("Error verifying webhook signature: %v", err) + c.Status(http.StatusBadRequest) + return + } + switch event.Type { + case stripe.EventTypePaymentIntentSucceeded: + var paymentIntent stripe.PaymentIntent + err := json.Unmarshal(event.Data.Raw, &paymentIntent) + if err != nil { + fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err) + c.Status(http.StatusInternalServerError) + return + } + + // get user with stripe_pi_id + var userStripePi models.UserStripePi + if err := db.Where("stripe_pi_id = ?", paymentIntent.ID).First(&userStripePi).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + //warn and return success + logwrapper.Warnf("No user found with stripe_pi_id: %v", err) + c.JSON(http.StatusOK, gin.H{"status": "received"}) + return + } + logwrapper.Errorf("Error getting user with stripe_pi_id: %v", err) + c.Status(http.StatusInternalServerError) + return + } + + // get user with user_id + var user models.User + if err := db.Where("user_id = ?", userStripePi.UserId).First(&user).Error; err != nil { + logwrapper.Errorf("Error getting user with user_id: %v", err) + c.Status(http.StatusInternalServerError) + return + } + + if _, err = aptos.DelegateMintNft(*user.WalletAddress); err != nil { + logwrapper.Errorf("Error minting nft: %v", err) + c.Status(http.StatusInternalServerError) + return + } + fmt.Println("minting nft -- 111NFT") + + case stripe.EventTypePaymentIntentCanceled: + err := HandleCanceledOrFailedPaymentIntent(event.Data.Raw) + if err != nil { + logwrapper.Errorf("Error handling canceled payment intent: %v", err) + c.Status(http.StatusInternalServerError) + return + } + c.JSON(http.StatusOK, gin.H{"status": "received"}) + } + + c.JSON(http.StatusOK, gin.H{"status": "received"}) +} + +func HandleCanceledOrFailedPaymentIntent(eventDataRaw json.RawMessage) error { + var paymentIntent stripe.PaymentIntent + err := json.Unmarshal(eventDataRaw, &paymentIntent) + if err != nil { + return fmt.Errorf("error parsing webhook JSON: %w", err) + } + + return nil +} diff --git a/api/v1/v1.go b/api/v1/v1.go index d0394ba..2ddc203 100644 --- a/api/v1/v1.go +++ b/api/v1/v1.go @@ -2,6 +2,7 @@ package apiv1 import ( "github.com/NetSepio/gateway/api/v1/account" + "github.com/NetSepio/gateway/api/v1/account/subscription" authenticate "github.com/NetSepio/gateway/api/v1/authenticate" delegatereviewcreation "github.com/NetSepio/gateway/api/v1/delegateReviewCreation" "github.com/NetSepio/gateway/api/v1/deletereview" @@ -43,5 +44,6 @@ func ApplyRoutes(r *gin.RouterGroup) { report.ApplyRoutes(v1) account.ApplyRoutes(v1) siteinsights.ApplyRoutes(v1) + subscription.ApplyRoutes(v1) } } diff --git a/app/app.go b/app/app.go index 42c3cdb..3e5888c 100644 --- a/app/app.go +++ b/app/app.go @@ -6,6 +6,7 @@ import ( "github.com/NetSepio/gateway/api" "github.com/NetSepio/gateway/app/routines/reportroutine" "github.com/NetSepio/gateway/util/pkg/logwrapper" + "github.com/stripe/stripe-go/v76" "github.com/NetSepio/gateway/config/constants" "github.com/NetSepio/gateway/config/dbconfig" @@ -18,6 +19,7 @@ var GinApp *gin.Engine func Init() { envconfig.InitEnvVars() + stripe.Key = envconfig.EnvVars.STRIPE_SECRET_KEY constants.InitConstants() logwrapper.Init() diff --git a/config/envconfig/envconfig.go b/config/envconfig/envconfig.go index 246d272..87a1eb3 100644 --- a/config/envconfig/envconfig.go +++ b/config/envconfig/envconfig.go @@ -42,6 +42,8 @@ type config struct { EREBRUS_JP string `env:"EREBRUS_JP,notEmpty"` SOTREUS_US string `env:"SOTREUS_US,notEmpty"` SOTREUS_SG string `env:"SOTREUS_SG,notEmpty"` + STRIPE_WEBHOOK_SECRET string `env:"STRIPE_WEBHOOK_SECRET,notEmpty"` + STRIPE_SECRET_KEY string `env:"STRIPE_SECRET_KEY,notEmpty"` } var EnvVars config = config{} diff --git a/gateway b/gateway new file mode 100755 index 0000000..690f62b Binary files /dev/null and b/gateway differ diff --git a/go.mod b/go.mod index 7ec0906..608be96 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/lib/pq v1.10.9 github.com/sirupsen/logrus v1.8.1 github.com/stretchr/testify v1.8.4 + github.com/stripe/stripe-go/v76 v76.11.0 github.com/vk-rv/pvx v0.0.0-20210912195928-ac00bc32f6e7 golang.org/x/crypto v0.16.0 google.golang.org/api v0.154.0 diff --git a/go.sum b/go.sum index 1a49506..8da4f31 100644 --- a/go.sum +++ b/go.sum @@ -547,6 +547,8 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stripe/stripe-go/v76 v76.11.0 h1:5j+ZwRnybB/yzfjftcNy8FHkoqMMysMDW3OCYYFt2yA= +github.com/stripe/stripe-go/v76 v76.11.0/go.mod h1:rw1MxjlAKKcZ+3FOXgTHgwiOa2ya6CPq6ykpJ0Q6Po4= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= @@ -674,6 +676,7 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= diff --git a/migrations/000013_users_add_stripe_session_id.up.sql b/migrations/000013_users_add_stripe_session_id.up.sql new file mode 100644 index 0000000..8ba32f5 --- /dev/null +++ b/migrations/000013_users_add_stripe_session_id.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE user_stripe_pis ( + id UUID PRIMARY KEY, + user_id UUID REFERENCES users(user_id), + stripe_pi_id TEXT UNIQUE, + stripe_pi_type TEXT, + created_at timestamp with time zone DEFAULT current_timestamp +); \ No newline at end of file diff --git a/models/User.go b/models/User.go index c442c0e..76be61a 100644 --- a/models/User.go +++ b/models/User.go @@ -1,5 +1,7 @@ package models +import "time" + type User struct { UserId string `gorm:"primary_key" json:"userId,omitempty"` Name string `json:"name,omitempty"` @@ -12,3 +14,15 @@ type User struct { Feedbacks []UserFeedback `gorm:"foreignkey:UserId" json:"userFeedbacks"` EmailId *string `json:"emailId,omitempty"` } + +type TStripePiType string + +type UserStripePi struct { + Id string `gorm:"primary_key" json:"id,omitempty"` + UserId string `json:"userId,omitempty"` + StripePiId string `json:"stripePiId,omitempty"` + StripePiType TStripePiType `json:"stripePiType,omitempty"` + CreatedAt time.Time `json:"createdAt,omitempty"` +} + +var Erebrus111NFT TStripePiType = "Erebrus111NFT" diff --git a/util/pkg/aptos/price.go b/util/pkg/aptos/price.go new file mode 100644 index 0000000..d3e60d7 --- /dev/null +++ b/util/pkg/aptos/price.go @@ -0,0 +1,29 @@ +package aptos + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type CoinGeckoResponse struct { + Aptos struct { + USD float64 `json:"usd"` + } `json:"aptos"` +} + +func GetCoinPrice() (float64, error) { + url := "https://api.coingecko.com/api/v3/simple/price?ids=aptos&vs_currencies=usd" + resp, err := http.Get(url) + if err != nil { + return 0, fmt.Errorf("error fetching data: %w", err) + } + defer resp.Body.Close() + + var result CoinGeckoResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return 0, fmt.Errorf("error decoding JSON: %w", err) + } + + return result.Aptos.USD, nil +} diff --git a/util/pkg/aptos/smartcontract.go b/util/pkg/aptos/smartcontract.go index 2a34b4e..3706c75 100644 --- a/util/pkg/aptos/smartcontract.go +++ b/util/pkg/aptos/smartcontract.go @@ -75,6 +75,24 @@ func DeleteReview(metaDataUri string) (*TxResult, error) { return &txResult, err } +func DelegateMintNft(minter string) (*TxResult, error) { + command := fmt.Sprintf("move run --function-id %s::vpnv2::delegate_mint_NFT --max-gas %d --gas-unit-price %d --args", envconfig.EnvVars.APTOS_FUNCTION_ID, envconfig.EnvVars.GAS_UNITS, envconfig.EnvVars.GAS_PRICE) + args := append(strings.Split(command, " "), argA(minter)) + cmd := exec.Command("aptos", args...) + fmt.Println(strings.Join(args, " ")) + + o, err := cmd.Output() + if err != nil { + if err, ok := err.(*exec.ExitError); ok { + return nil, fmt.Errorf("stderr: %s out: %s err: %w", err.Stderr, o, err) + } + return nil, fmt.Errorf("out: %s err: %w", o, err) + } + + txResult, err := UnmarshalTxResult(o) + return &txResult, err +} + func UploadArchive(siteUrl string, siteIpfsHash string) (*TxResult, error) { command := fmt.Sprintf("move run --function-id %s::reviews::archive_link --max-gas %d --gas-unit-price %d --args", envconfig.EnvVars.APTOS_FUNCTION_ID, envconfig.EnvVars.GAS_UNITS, envconfig.EnvVars.GAS_PRICE) args := append(strings.Split(command, " "), argS(siteUrl), argS(siteIpfsHash))