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
  • Loading branch information
emmdim committed May 30, 2024
1 parent b1e022b commit cef1f26
Show file tree
Hide file tree
Showing 8 changed files with 286 additions and 9 deletions.
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ 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=
# stripe webhook secret
STRIPE_WEBHOOK_SECRET=

RESTART=unless-stopped

Expand Down
39 changes: 35 additions & 4 deletions faucet.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ import (
)

type faucet struct {
signer *ethereum.SignKeys
authTypes map[string]uint64
waitPeriod time.Duration
storage *storage
signer *ethereum.SignKeys
authTypes map[string]uint64
waitPeriod time.Duration
storage *storage
stripeKey string
stripePriceId string
stripeWebhookSecret string
domain string
}

// prepareFaucetPackage prepares a faucet package, including the signature, for the given address.
Expand Down Expand Up @@ -45,3 +49,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
193 changes: 193 additions & 0 deletions handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"encoding/json"
"fmt"
"net/http"

"github.com/ethereum/go-ethereum/common"
"github.com/vocdoni/vocfaucet/aragondaohandler"
Expand All @@ -11,6 +12,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 +29,33 @@ 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(
"/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 +99,167 @@ 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.stripeKey
to := ctx.URLParam("to")
referral := ctx.URLParam("referral")
params := &stripe.CheckoutSessionParams{
// A unique string to reference the Checkout Session.
// This can be a customer ID, a cart ID, or similar,
// and can be used to reconcile the Session with your internal systems.
ClientReferenceID: stripe.String(to),
UIMode: stripe.String("embedded"),
// TODO remove port from domain
ReturnURL: stripe.String("http://" + referral + ":5173/return/{CHECKOUT_SESSION_ID}"),
LineItems: []*stripe.CheckoutSessionLineItemParams{
{
// Provide the exact Price ID (for example, pr_1234) of the product you want to sell
Price: stripe.String(f.stripePriceId),
AdjustableQuantity: &stripe.CheckoutSessionLineItemAdjustableQuantityParams{
Enabled: stripe.Bool(true),
Minimum: stripe.Int64(100),
Maximum: stripe.Int64(10000),
},
Quantity: stripe.Int64(100),
},
// {
// // Provide the exact Price ID (for example, pr_1234) of the product you want to sell
// Price: stripe.String(f.stripePriceId),
// Quantity: stripe.Int64(10),
// },
},
Metadata: map[string]string{
"to": to,
"referral": referral,
},
// CustomFields: []*stripe.CheckoutSessionCustomFieldParams{
// &stripe.CheckoutSessionCustomFieldParams{
// Key: stripe.String("to"),
// Label: &stripe.CheckoutSessionCustomFieldLabelParams{
// Type: stripe.String("custom"),
// Custom: stripe.String("Recipient address"),
// },
// Type: stripe.String(string(stripe.CheckoutSessionCustomFieldTypeText)),
// Optional: stripe.Bool(false),
// Text: &stripe.CheckoutSessionCustomFieldTextParams{},
// },
// {},
// },
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 {
// s, _ := session.Get(r.URL.Query().Get("session_id"), nil)
stripe.Key = f.stripeKey
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)
}
// faucetPackage, err := json.Marshal(package)

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 {
// apiData.Data
// const MaxBodyBytes = int64(65536)
// body := http.MaxBytesReader(ctx.Writer, ctx.Request.Body, MaxBodyBytes)
// bodyBytes, err := io.ReadAll(body)
// if err != nil {
// errReason := fmt.Sprintf("Error reading request body: %v\n", err)
// return ctx.Send(new(HandlerResponse).SetError(errReason).MustMarshall(), http.StatusServiceUnavailable)
// }
sig := ctx.Request.Header.Get("Stripe-Signature")
// Pass the request body and Stripe-Signature header to ConstructEvent, along with the webhook signing key
// You can find your endpoint's secret in your webhook settings
event, err := webhook.ConstructEvent(apiData.Data, sig, f.stripeWebhookSecret)

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.stripeKey
// 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")
// params.AddExpand("price_data")

// 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
// Fulfill the purchase...
// TODO recover number of tokens and address
// use https://stripe.com/docs/api/metadata
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
}
// if funded, t := f.storage.checkIsFundedUserID(addr.Bytes(), AuthTypeOpen); funded {
// errReason := fmt.Sprintf("address %s already funded, wait until %s", addr.Hex(), t)
// return ctx.Send(new(HandlerResponse).SetError(errReason).MustMarshall(), CodeErrFlood)
// }
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
29 changes: 24 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ 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.String("stripeWebhookSecret", "", "stripe webhook secret key")
flag.Parse()

// Setting up viper
Expand Down Expand Up @@ -86,6 +89,15 @@ 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("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 @@ -121,8 +133,12 @@ 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")
stripeWebhookSecret := viper.GetString("stripeWebhookSecret")

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

// create the faucet instance
f := faucet{
signer: &signer,
authTypes: authTypes,
waitPeriod: waitPeriod,
storage: storage,
signer: &signer,
authTypes: authTypes,
waitPeriod: waitPeriod,
storage: storage,
stripeKey: stripeKey,
stripePriceId: stripePriceId,
stripeWebhookSecret: stripeWebhookSecret,
domain: fmt.Sprintf("http://%s:%d%s", listenHost, listenPort, baseRoute),
}

// init API
Expand Down
Loading

0 comments on commit cef1f26

Please sign in to comment.