Skip to content

Commit

Permalink
Introduce stripe integration for 111 NFT (#147)
Browse files Browse the repository at this point in the history
* Introduce stripe integration for 111 NFT

* Remove unused env vars

* Integrate delegate mint

* Create payment intent with usd equivalent to 11.1 aptos

* `stripe-webhook` -> `payment-webhook`
`111-nft` -> `erebrus`
`Buy111NFTResponse` -> `BuyErebrusNFTResponse`

* Support for multiple subscriptions

* format: envconfig.go
  • Loading branch information
thisisommore authored Jan 22, 2024
1 parent baacbc9 commit 1b7b32a
Show file tree
Hide file tree
Showing 15 changed files with 275 additions and 2 deletions.
6 changes: 4 additions & 2 deletions .env-sample
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -32,4 +32,6 @@ EREBRUS_EU=abcd
EREBRUS_JP=abcd
SOTREUS_US=abcd
SOTREUS_SG=abcd
EREBRUS_US=abcd
EREBRUS_US=abcd
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
72 changes: 72 additions & 0 deletions api/v1/account/subscription/create.go
Original file line number Diff line number Diff line change
@@ -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)
}
16 changes: 16 additions & 0 deletions api/v1/account/subscription/subscription.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
5 changes: 5 additions & 0 deletions api/v1/account/subscription/type.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package subscription

type BuyErebrusNFTResponse struct {
ClientSecret string `json:"clientSecret"`
}
100 changes: 100 additions & 0 deletions api/v1/account/subscription/webhook.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 2 additions & 0 deletions api/v1/v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -43,5 +44,6 @@ func ApplyRoutes(r *gin.RouterGroup) {
report.ApplyRoutes(v1)
account.ApplyRoutes(v1)
siteinsights.ApplyRoutes(v1)
subscription.ApplyRoutes(v1)
}
}
2 changes: 2 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -18,6 +19,7 @@ var GinApp *gin.Engine

func Init() {
envconfig.InitEnvVars()
stripe.Key = envconfig.EnvVars.STRIPE_SECRET_KEY
constants.InitConstants()
logwrapper.Init()

Expand Down
2 changes: 2 additions & 0 deletions config/envconfig/envconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down
Binary file added gateway
Binary file not shown.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
7 changes: 7 additions & 0 deletions migrations/000013_users_add_stripe_session_id.up.sql
Original file line number Diff line number Diff line change
@@ -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
);
14 changes: 14 additions & 0 deletions models/User.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package models

import "time"

type User struct {
UserId string `gorm:"primary_key" json:"userId,omitempty"`
Name string `json:"name,omitempty"`
Expand All @@ -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"
29 changes: 29 additions & 0 deletions util/pkg/aptos/price.go
Original file line number Diff line number Diff line change
@@ -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
}
18 changes: 18 additions & 0 deletions util/pkg/aptos/smartcontract.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down

0 comments on commit 1b7b32a

Please sign in to comment.