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 7, 2024
1 parent 08d5712 commit 9c99014
Show file tree
Hide file tree
Showing 9 changed files with 386 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
30 changes: 30 additions & 0 deletions faucet.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/ethereum/go-ethereum/common"
"github.com/vocdoni/vocfaucet/storage"
"github.com/vocdoni/vocfaucet/stripehandler"
"go.vocdoni.io/dvote/api"
vfaucet "go.vocdoni.io/dvote/api/faucet"
"go.vocdoni.io/dvote/crypto/ethereum"
Expand All @@ -18,6 +19,8 @@ type faucet struct {
authTypes map[string]uint64
waitPeriod time.Duration
storage *storage.Storage
stripe *stripehandler.StripeProvider
domain string
}

// prepareFaucetPackage prepares a faucet package, including the signature, for the given address.
Expand Down Expand Up @@ -46,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) prepareFaucetPackageWithAmount(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
121 changes: 121 additions & 0 deletions handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package main

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"

"github.com/ethereum/go-ethereum/common"
"github.com/vocdoni/vocfaucet/aragondaohandler"
Expand All @@ -24,6 +27,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 +106,88 @@ func (f *faucet) registerHandlers(api *apirest.API) {
}
}

// createCheckoutSession creates a new Stripe Checkout session
func (f *faucet) createCheckoutSession(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error {
to := ctx.URLParam("to")
referral := ctx.URLParam("referral")
defaultAmount := f.stripe.DefaultAmount
if amount := ctx.URLParam("amount"); amount != "" {
var err error
defaultAmount, err = strconv.ParseInt(amount, 10, 64)
if err != nil {
return ctx.Send(new(HandlerResponse).SetError(err.Error()).MustMarshall(), CodeErrIncorrectParams)
}
}
s, err := f.stripe.CreateCheckoutSession(defaultAmount, to, referral)
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 {
sessionId := ctx.URLParam("session_id")
status, err := f.stripe.RetrieveCheckoutSession(sessionId)
if err != nil {
return ctx.Send(new(HandlerResponse).SetError(err.Error()).MustMarshall(), CodeErrProviderError)
}
toFund, err := f.storage.GetPendingStripeSession(sessionId)
if err != nil {
return ctx.Send(new(HandlerResponse).SetError(err.Error()).MustMarshall(), CodeErrInternalError)
}
if toFund {
data, err := f.processPaymentTransfer(status.Quantity, status.Recipient)
if err != nil {
return ctx.Send(new(HandlerResponse).SetError(err.Error()).MustMarshall(), CodeErrInternalError)
}
if err := f.storage.RemovePendingStripeSession(sessionId); err != nil {
return ctx.Send(new(HandlerResponse).SetError(err.Error()).MustMarshall(), CodeErrInternalError)
}
status.FaucetPackage = data
return ctx.Send(new(HandlerResponse).Set(status).MustMarshall(), apirest.HTTPstatusOK)
}
return ctx.Send(new(HandlerResponse).Set(status).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
sessionId, err := f.stripe.HandleWebhook(apiData, sig)
if err != nil {
return ctx.Send(new(HandlerResponse).SetError(err.Error()).MustMarshall(), http.StatusBadRequest)
}
err = f.storage.AddPendingStripeSession(sessionId)
if err != nil {
return ctx.Send(new(HandlerResponse).SetError(err.Error()).MustMarshall(), http.StatusBadRequest)
}
return ctx.Send([]byte("success"), http.StatusOK)
}

func (f *faucet) processPaymentTransfer(amount int64, to string) ([]byte, error) {
if amount == 0 {
return nil, errors.New("invalid requested amount")
}
addr, err := stringToAddress(to)
if err != nil {
return nil, err
}
data, err := f.prepareFaucetPackageWithAmount(addr, uint64(amount))
if err != nil {
return nil, err
}

return data.FaucetPackage, nil
}

// 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
42 changes: 41 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
flag "github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/vocdoni/vocfaucet/storage"
"github.com/vocdoni/vocfaucet/stripehandler"
"go.vocdoni.io/dvote/crypto/ethereum"
"go.vocdoni.io/dvote/db"
"go.vocdoni.io/dvote/httprouter"
Expand All @@ -38,6 +39,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.Int64("stripeMinQuantity", 100, "stripe min number of tokens")
flag.Int64("stripeMaxQuantity", 100000, "stripe max number of tokens")
flag.Int64("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 +94,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 +147,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.GetInt64("stripeMinQuantity")
stripeMaxQuantity := viper.GetInt64("stripeMaxQuantity")
stripeDefaultQuantity := viper.GetInt64("stripeDefaultQuantity")
stripeWebhookSecret := viper.GetString("stripeWebhookSecret")

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

// create the faucet instance
f := faucet{
signer: &signer,
authTypes: authTypes,
waitPeriod: waitPeriod,
storage: storage,
stripe: &stripehandler.StripeProvider{
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
53 changes: 52 additions & 1 deletion storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,18 @@ func (st *Storage) AddFundedUserWithWaitTime(userID []byte, authType string) err
return tx.Commit()
}

// CheckFundedUserWithWaitTime checks if the given text is funded and returns true if it is, within
// addFundedUserID adds the given userID to the funded list, with the current time
// as the wait period end time.
func (st *Storage) AddSessionLastFaucetPackage(stipeSessionId string, faucetPackage []byte) error {
tx := st.kv.WriteTx()
defer tx.Discard()
if err := tx.Set([]byte(stipeSessionId), []byte(faucetPackage)); err != nil {
log.Error(err)
}
return tx.Commit()
}

// checkIsFundedUserID checks if the given text is funded and returns true if it is, within
// the wait period time window. Otherwise, it returns false.
func (st *Storage) CheckFundedUserWithWaitTime(userID []byte, authType string) (bool, time.Time) {
key := append(userID, []byte(authType)...)
Expand All @@ -84,3 +95,43 @@ func (st *Storage) CheckFundedUserWithWaitTime(userID []byte, authType string) (
wp := binary.LittleEndian.Uint64(wpBytes)
return wp >= uint64(time.Now().Unix()), time.Unix(int64(wp), 0)
}

// checkIsFundedUserID checks if the given text is funded and returns true if it is, within
// the wait period time window. Otherwise, it returns false.
func (st *Storage) GetSessionLastFaucetPackage(stipeSessionId string) ([]byte, error) {
wpBytes, err := st.kv.Get([]byte(stipeSessionId))
if err != nil {
return []byte{}, err
}

return wpBytes, nil
}

func (st *Storage) AddPendingStripeSession(sessionID string) error {
tx := st.kv.WriteTx()
defer tx.Discard()
if err := tx.Set([]byte(sessionID), []byte("true")); err != nil {
log.Error(err)
}
return tx.Commit()
}

func (st *Storage) GetPendingStripeSession(sessionID string) (bool, error) {
data, err := st.kv.Get([]byte(sessionID))
if err != nil {
return false, err
}
if string(data) == "true" {
return true, nil
}
return false, nil
}

func (st *Storage) RemovePendingStripeSession(sessionID string) error {
tx := st.kv.WriteTx()
defer tx.Discard()
if err := tx.Delete([]byte(sessionID)); err != nil {
return err
}
return tx.Commit()
}
Loading

0 comments on commit 9c99014

Please sign in to comment.