-
Notifications
You must be signed in to change notification settings - Fork 0
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
Changes from all commits
c70304a
03d4b29
6993c37
6f569c5
000c4b1
d76ce91
255500e
f9af620
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
} |
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) | ||
} | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"` | ||
} |
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 | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why this is inserted on USERS table? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
); |
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 | ||
} |
There was a problem hiding this comment.
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