diff --git a/faucet.go b/faucet.go index 74e64c4..d14d719 100644 --- a/faucet.go +++ b/faucet.go @@ -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. diff --git a/go.mod b/go.mod index 3559c36..36e9102 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index d6718b3..6e71849 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= diff --git a/handlers.go b/handlers.go index 4192df1..3fd0390 100644 --- a/handlers.go +++ b/handlers.go @@ -3,6 +3,8 @@ package main import ( "encoding/json" "fmt" + "io/ioutil" + "net/http" "github.com/ethereum/go-ethereum/common" "github.com/vocdoni/vocfaucet/aragondaohandler" @@ -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 @@ -24,6 +30,33 @@ func (f *faucet) registerHandlers(api *apirest.API) { log.Fatal(err) } + if err := api.RegisterMethod( + "/create-checkout-session", + "POST", + apirest.MethodAccessTypePublic, + f.createCheckoutSession, + ); err != nil { + log.Fatal(err) + } + + if err := api.RegisterMethod( + "/session-status", + "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}", @@ -67,6 +100,119 @@ 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 + params := &stripe.CheckoutSessionParams{ + UIMode: stripe.String("embedded"), + ReturnURL: stripe.String(f.domain + "/return.html?session_id={CHECKOUT_SESSION_ID}"), + LineItems: []*stripe.CheckoutSessionLineItemParams{ + &stripe.CheckoutSessionLineItemParams{ + // Provide the exact Price ID (for example, pr_1234) of the product you want to sell + Price: stripe.String(f.stripePriceId), + Quantity: stripe.Int64(1), + }, + }, + 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 + } + + data := &struct { + Status string `json:"status"` + CustomerEmail string `json:"customer_email"` + }{ + Status: string(s.Status), + CustomerEmail: string(s.CustomerDetails.Email), + } + return ctx.Send(new(HandlerResponse).Set(data).MustMarshall(), apirest.HTTPstatusOK) +} + +func (f *faucet) handleWebhook(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { + const MaxBodyBytes = int64(65536) + maxBytesBody := http.MaxBytesReader(ctx.Writer, ctx.Request.Body, MaxBodyBytes) + body, err := ioutil.ReadAll(maxBytesBody) + if err != nil { + errReason := fmt.Sprintf("Error reading request body: %v\n", err) + return ctx.Send(new(HandlerResponse).SetError(errReason).MustMarshall(), http.StatusServiceUnavailable) + } + // 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(body, ctx.Request.Header.Get("Stripe-Signature"), 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) + } + + // Handle the checkout.session.completed event + if event.Type == "checkout.session.completed" { + var session stripe.CheckoutSession + err := json.Unmarshal(event.Data.Raw, &session) + 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(session.ID, params) + lineItems := sessionWithLineItems.LineItems + // Fulfill the purchase... + // TODO recover number of tokens and address + // use https://stripe.com/docs/api/metadata + return processPaymentTransfer(ctx, lineItems) + } + + return ctx.Send([]byte("success"), http.StatusOK) +} + +func (f *faucet) processPaymentTransfer(ctx *httprouter.HTTPContext, amount uint64) error { + if amount == 0 { + return ctx.Send(new(HandlerResponse).SetError(ReasonErrUnsupportedAuthType).MustMarshall(), CodeErrUnsupportedAuthType) + } + addr, err := stringToAddress(ctx.URLParam("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.prepareFaucetPackage(addr, AuthTypeOpen) + if err != nil { + return err + } + if err := f.storage.addFundedUserID(addr.Bytes(), AuthTypeOpen); 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{ diff --git a/handlers_response.go b/handlers_response.go index 671a2a8..0421cb3 100644 --- a/handlers_response.go +++ b/handlers_response.go @@ -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 diff --git a/main.go b/main.go index b8b2231..484141a 100644 --- a/main.go +++ b/main.go @@ -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 @@ -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")) @@ -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, ",") @@ -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