Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce stripe integration for 111 NFT #147

Merged
merged 8 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename?

stripe-webhook -> payment-webhook
111-nft -> erebrus

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Buy111NFTResponse -> BuyErebrusNFTResponse

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this is inserted on USERS table?

Copy link
Member Author

@thisisommore thisisommore Jan 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we can track for which users wallet to mint nft to after payment success

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But doesn't this restricts the user to only be able to buy only 1 subscription? When we launch another subscription does this get updated? Also what'll happen in the case of failed/abandoned payment requests?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to support multiple subscriptions, also abandoned/failed payments are current not handled since stripe should just stop that and have time limit but we can keep record of the payment intents in db so in case of any issues we can always access it in db

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
Loading