Skip to content

Commit

Permalink
Initial Stripe extension, that implements the standar stripe checkout…
Browse files Browse the repository at this point in the history
… process, assuming an embedded form, and a given stripe price id.

    The min and max number of tokes is defined (should be paremtrized) and just aflat rate is allowed.
    Adds the handlers:
    - `/createCheckoutSession/{referral}/{to}`
    - `/sessionStatus/{session_id}`
    - /webhook"
    and the following env vars:
    -STRIPEKEY
    -STRIPEPRICEID
    -STRIPEWEBHOOKSECRET
    -STRIPEMINQUANTITY
    -STRIPEMAXQUANTITY
    -STRIPEDEFAULTQUANTITY
  • Loading branch information
emmdim committed Jun 3, 2024
1 parent 08d5712 commit 32cedb4
Show file tree
Hide file tree
Showing 8 changed files with 288 additions and 2 deletions.
12 changes: 12 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ DB_TYPE=pebble
BASE_ROUTE=/v2
# authentication types to use (comma separated). Available: open, oauth
AUTH=open
# stripe secret key
STRIPE_KEY=
# stripe price id
STRIPE_PRICE_ID=
# min number of tokens
STRIPEMINQUANTITY=
# max number of tokens
STRIPEMAXQUANTITY=
# default number of tokens
STRIPEDEFAULTQUANTITY=
# stripe webhook secret
STRIPE_WEBHOOK_SECRET=

RESTART=unless-stopped

Expand Down
38 changes: 38 additions & 0 deletions faucet.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ type faucet struct {
authTypes map[string]uint64
waitPeriod time.Duration
storage *storage.Storage
stripe *stripeInfo
domain string
}

type stripeInfo struct {
key string
priceId string
minQuantity int
maxQuantity int
defaultAmount int
webhookSecret string
}

// prepareFaucetPackage prepares a faucet package, including the signature, for the given address.
Expand Down Expand Up @@ -46,3 +57,30 @@ func (f *faucet) prepareFaucetPackage(toAddr common.Address, authTypeName string
FaucetPackage: fpackageBytes,
}, nil
}

// prepareFaucetPackage prepares a faucet package, including the signature, for the given address.
// Returns the faucet package as a marshaled json byte array, ready to be sent to the user.
func (f *faucet) prepareFaucetPackageAmmount(toAddr common.Address, amount uint64) (*vfaucet.FaucetResponse, error) {
// check if the auth type is supported
if amount <= 0 {
return nil, fmt.Errorf("invalid requested amount: %d", amount)
}

// generate faucet package
fpackage, err := vochain.GenerateFaucetPackage(f.signer, toAddr, amount)
if err != nil {
return nil, api.ErrCantGenerateFaucetPkg.WithErr(err)
}
fpackageBytes, err := json.Marshal(vfaucet.FaucetPackage{
FaucetPayload: fpackage.Payload,
Signature: fpackage.Signature,
})
if err != nil {
return nil, err
}
// send response
return &vfaucet.FaucetResponse{
Amount: fmt.Sprint(amount),
FaucetPackage: fpackageBytes,
}, nil
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/ethereum/go-ethereum v1.13.4
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.16.0
github.com/stripe/stripe-go/v78 v78.3.0
go.vocdoni.io/dvote v1.10.0
gopkg.in/yaml.v3 v3.0.1
)
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1465,6 +1465,8 @@ github.com/stretchr/testify v1.8.1/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/v78 v78.3.0 h1:FYlKhJKZdZ/1vATbuIN4T107DeL7w9oV13IcPOEwyPQ=
github.com/stripe/stripe-go/v78 v78.3.0/go.mod h1:GjncxVLUc1xoIOidFqVwq+y3pYiG7JLVWiVQxTsLrvQ=
github.com/stvp/go-udp-testing v0.0.0-20201019212854-469649b16807/go.mod h1:7jxmlfBCDBXRzr0eAQJ48XC1hBu1np4CS5+cHEYfwpc=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
Expand Down Expand Up @@ -1801,6 +1803,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
Expand Down
169 changes: 169 additions & 0 deletions handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package main
import (
"encoding/json"
"fmt"
"net/http"
"strconv"

"github.com/ethereum/go-ethereum/common"
"github.com/vocdoni/vocfaucet/aragondaohandler"
Expand All @@ -11,6 +13,10 @@ import (
"go.vocdoni.io/dvote/httprouter/apirest"
"go.vocdoni.io/dvote/log"
"go.vocdoni.io/dvote/types"

"github.com/stripe/stripe-go/v78"
"github.com/stripe/stripe-go/v78/checkout/session"
"github.com/stripe/stripe-go/v78/webhook"
)

// Register the handlers URLs
Expand All @@ -24,6 +30,42 @@ func (f *faucet) registerHandlers(api *apirest.API) {
log.Fatal(err)
}

if err := api.RegisterMethod(
"/createCheckoutSession/{referral}/{to}",
"POST",
apirest.MethodAccessTypePublic,
f.createCheckoutSession,
); err != nil {
log.Fatal(err)
}

if err := api.RegisterMethod(
"/createCheckoutSession/{referral}/{to}/{amount}",
"POST",
apirest.MethodAccessTypePublic,
f.createCheckoutSession,
); err != nil {
log.Fatal(err)
}

if err := api.RegisterMethod(
"/sessionStatus/{session_id}",
"GET",
apirest.MethodAccessTypePublic,
f.retrieveCheckoutSession,
); err != nil {
log.Fatal(err)
}

if err := api.RegisterMethod(
"/webhook",
"POST",
apirest.MethodAccessTypePublic,
f.handleWebhook,
); err != nil {
log.Fatal(err)
}

if f.authTypes[AuthTypeOpen] > 0 {
if err := api.RegisterMethod(
"/open/claim/{to}",
Expand Down Expand Up @@ -67,6 +109,133 @@ func (f *faucet) registerHandlers(api *apirest.API) {
}
}

// createCheckoutSession creates a new Stripe Checkout session
func (f *faucet) createCheckoutSession(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error {
stripe.Key = f.stripe.key
to := ctx.URLParam("to")
referral := ctx.URLParam("referral")
defaultAmount := f.stripe.defaultAmount
if amount := ctx.URLParam("amount"); amount != "" {
var err error
defaultAmount, err = strconv.Atoi(amount)
if err != nil {
return ctx.Send(new(HandlerResponse).SetError(err.Error()).MustMarshall(), CodeErrIncorrectParams)
}
}
params := &stripe.CheckoutSessionParams{
// A unique string to reference the Checkout Session.
ClientReferenceID: stripe.String(to),
UIMode: stripe.String("embedded"),
// TODO remove port from domain
ReturnURL: stripe.String("http://" + referral + ":5173/stripe/return/{CHECKOUT_SESSION_ID}"),
LineItems: []*stripe.CheckoutSessionLineItemParams{
{
Price: stripe.String(f.stripe.priceId),
AdjustableQuantity: &stripe.CheckoutSessionLineItemAdjustableQuantityParams{
Enabled: stripe.Bool(true),
Minimum: stripe.Int64(int64(f.stripe.minQuantity)),
Maximum: stripe.Int64(int64(f.stripe.maxQuantity)),
},
Quantity: stripe.Int64(int64(defaultAmount)),
},
},
Metadata: map[string]string{
"to": to,
"referral": referral,
},
Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
}
s, err := session.New(params)

if err != nil {
errReason := fmt.Sprintf("session.New: %v", err)
return ctx.Send(new(HandlerResponse).SetError(errReason).MustMarshall(), CodeErrProviderError)
//
}

data := &struct {
ClientSecret string `json:"clientSecret"`
}{
ClientSecret: s.ClientSecret,
}
return ctx.Send(new(HandlerResponse).Set(data).MustMarshall(), apirest.HTTPstatusOK)
}

func (f *faucet) retrieveCheckoutSession(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error {
stripe.Key = f.stripe.key
s, err := session.Get((ctx.URLParam("session_id")), nil)
if err != nil {
return err
}

faucetPackage, err := f.storage.GetSessionLastFaucetPackage(s.ID)
if err != nil {
log.Warnf("error getting last faucet package for session %s: %v", s.ID, err)
}

data := &struct {
Status string `json:"status"`
CustomerEmail string `json:"customer_email"`
FaucetPackage []byte `json:"faucet_package"`
Recipient string `json:"recipient"`
}{
Status: string(s.Status),
CustomerEmail: string(s.CustomerDetails.Email),
FaucetPackage: faucetPackage,
Recipient: s.Metadata["to"],
}
return ctx.Send(new(HandlerResponse).Set(data).MustMarshall(), apirest.HTTPstatusOK)
}

func (f *faucet) handleWebhook(apiData *apirest.APIdata, ctx *httprouter.HTTPContext) error {
sig := ctx.Request.Header.Get("Stripe-Signature")
// Pass the request body and Stripe-Signature header to ConstructEvent, along with the webhook signing key
event, err := webhook.ConstructEvent(apiData.Data, sig, f.stripe.webhookSecret)

if err != nil {
errReason := fmt.Sprintf("Error verifying webhook signature: %v\n", err)
return ctx.Send(new(HandlerResponse).SetError(errReason).MustMarshall(), http.StatusBadRequest)
}

stripe.Key = f.stripe.key
// Handle the checkout.session.completed event
if event.Type == "checkout.session.completed" {
var sess stripe.CheckoutSession
err := json.Unmarshal(event.Data.Raw, &sess)
if err != nil {
errReason := fmt.Sprintf("Error parsing webhook JSON: %v\n", err)
return ctx.Send(new(HandlerResponse).SetError(errReason).MustMarshall(), http.StatusBadRequest)
}

params := &stripe.CheckoutSessionParams{}
params.AddExpand("line_items")
// Retrieve the session. If you require line items in the response, you may include them by expanding line_items.
sessionWithLineItems, _ := session.Get(sess.ID, params)
lineItems := sessionWithLineItems.LineItems
return f.processPaymentTransfer(ctx, sess.ID, lineItems.Data[0].Quantity, sessionWithLineItems.Metadata["to"])
}

return ctx.Send([]byte("success"), http.StatusOK)
}

func (f *faucet) processPaymentTransfer(ctx *httprouter.HTTPContext, stipeSessionId string, amount int64, to string) error {
if amount == 0 {
return ctx.Send(new(HandlerResponse).SetError(ReasonErrUnsupportedAuthType).MustMarshall(), CodeErrUnsupportedAuthType)
}
addr, err := stringToAddress(to)
if err != nil {
return err
}
data, err := f.prepareFaucetPackageAmmount(addr, uint64(amount))
if err != nil {
return err
}
if err := f.storage.AddSessionLastFaucetPackage(stipeSessionId, data.FaucetPackage); err != nil {
return err
}
return ctx.Send(new(HandlerResponse).Set(data).MustMarshall(), apirest.HTTPstatusOK)
}

// Returns the list of supported auth types
func (f *faucet) authTypesHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error {
data := &AuthTypes{
Expand Down
2 changes: 2 additions & 0 deletions handlers_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const (
CodeErrIncorrectParams = 408
CodeErrInternalError = 409
ReasonErrAragonDaoAddress = "could not find the signer address in any Aragon DAO"
CodeErrProviderError = 410
ReasonErrProviderError = "error obtaining the oAuthToken"
)

// HandlerResponse is the response format for the Handlers
Expand Down
41 changes: 40 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ func main() {
flag.String("amounts", "100", "tokens to send per request (comma separated), the order must match the auth types")
flag.Duration("waitPeriod", 1*time.Hour, "wait period between requests for the same user")
flag.StringP("dbType", "t", db.TypePebble, fmt.Sprintf("key-value db type [%s,%s,%s]", db.TypePebble, db.TypeLevelDB, db.TypeMongo))
flag.String("stripeKey", "", "stripe secret key")
flag.String("stripePriceId", "", "stripe price id")
flag.Int("stripeMinQuantity", 100, "stripe min number of tokens")
flag.Int("stripeMaxQuantity", 100000, "stripe max number of tokens")
flag.Int("stripeDefaultQuantity", 100, "stripe default number of tokens")
flag.String("stripeWebhookSecret", "", "stripe webhook secret key")
flag.Parse()

// Setting up viper
Expand Down Expand Up @@ -87,6 +93,24 @@ func main() {
if err := viper.BindPFlag("dbType", flag.Lookup("dbType")); err != nil {
panic(err)
}
if err := viper.BindPFlag("stripeKey", flag.Lookup("stripeKey")); err != nil {
panic(err)
}
if err := viper.BindPFlag("stripePriceId", flag.Lookup("stripePriceId")); err != nil {
panic(err)
}
if err := viper.BindPFlag("stripeMinQuantity", flag.Lookup("stripeMinQuantity")); err != nil {
panic(err)
}
if err := viper.BindPFlag("stripeMaxQuantity", flag.Lookup("stripeMaxQuantity")); err != nil {
panic(err)
}
if err := viper.BindPFlag("stripeDefaultQuantity", flag.Lookup("stripeDefaultQuantity")); err != nil {
panic(err)
}
if err := viper.BindPFlag("stripeWebhookSecret", flag.Lookup("stripeWebhookSecret")); err != nil {
panic(err)
}

// check if config file exists
_, err := os.Stat(path.Join(dataDir, "faucet.yml"))
Expand Down Expand Up @@ -122,8 +146,15 @@ func main() {
privKey := viper.GetString("privKey")
auth := viper.GetString("auth")
amounts := viper.GetString("amounts")

waitPeriod := viper.GetDuration("waitPeriod")
dbType := viper.GetString("dbType")
stripeKey := viper.GetString("stripeKey")
stripePriceId := viper.GetString("stripePriceId")
stripeMinQuantity := viper.GetInt("stripeMinQuantity")
stripeMaxQuantity := viper.GetInt("stripeMaxQuantity")
stripeDefaultQuantity := viper.GetInt("stripeDefaultQuantity")
stripeWebhookSecret := viper.GetString("stripeWebhookSecret")

// parse auth types and amounts
authNames := strings.Split(auth, ",")
Expand Down Expand Up @@ -175,13 +206,21 @@ func main() {
if err != nil {
log.Fatal(err)
}

// create the faucet instance
f := faucet{
signer: &signer,
authTypes: authTypes,
waitPeriod: waitPeriod,
storage: storage,
stripe: &stripeInfo{
key: stripeKey,
priceId: stripePriceId,
minQuantity: stripeMinQuantity,
maxQuantity: stripeMaxQuantity,
defaultAmount: stripeDefaultQuantity,
webhookSecret: stripeWebhookSecret,
},
domain: fmt.Sprintf("http://%s:%d%s", listenHost, listenPort, baseRoute),
}

// init API
Expand Down
Loading

0 comments on commit 32cedb4

Please sign in to comment.