Skip to content

Commit

Permalink
multi: add pay invoice form, update infos
Browse files Browse the repository at this point in the history
Add a payment invoice form in the service faucet.go
and update the static files to show this feature.

Rename package to avoid install a bin with name main.

Update confirmations to wait in channel creation.
  • Loading branch information
fguisso committed Aug 6, 2019
1 parent 509d66e commit 1fb8968
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 15 deletions.
175 changes: 162 additions & 13 deletions faucet.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"encoding/hex"
"fmt"
"html/template"
Expand Down Expand Up @@ -33,6 +34,10 @@ const (
// minChannelSize is the smallest channel that the faucet will extend
// to a peer.
minChannelSize int64 = 50000

// maxPaymentAtoms is the larget payment amount in atoms that the faucet
// will pay to an invoice
maxPaymentAtoms int64 = 1000
)

// chanCreationError is an enum which describes the exact nature of an error
Expand Down Expand Up @@ -89,9 +94,22 @@ const (
// InvoiceTimeNotElapsed indicates minimum time to create a new invoice has not elapsed
InvoiceTimeNotElapsed

// PayInvoiceTimeNotElapsed indicates minimum time to pay a new invoice has not elapsed
PayInvoiceTimeNotElapsed

// InvoiceAmountTooHigh indicates the user tried to generate an invoice
// that was too expensive.
InvoiceAmountTooHigh

// ErrorDecodingPayReq indicates the user tried to input wrong invoice to create Payment Request
ErrorDecodingPayReq

// PaymentStreamError indicates that you get an error in the payment stream proccess
PaymentStreamError

// ErrorPaymentAmount indicates the user tried to pay an invoice with
// a value greater then the limit
ErrorPaymentAmount
)

var (
Expand All @@ -100,16 +118,27 @@ var (
// invoice in seconds.
GenerateInvoiceTimeout = time.Duration(60) * time.Second

// PayInvoiceTimeout represents the minimum time to pay a new
// invoice in seconds.
PayInvoiceTimeout = time.Duration(60) * time.Second

// GenerateInvoiceAction represents an action to generate invoice on post forms
GenerateInvoiceAction = "generateinvoice"

// PayInvoiceAction represents an action to pay invoice on post forms
PayInvoiceAction = "payinvoice"

// OpenChannelAction represents an action to open channel on post forms
OpenChannelAction = "openchannel"
)

// lastGeneratedInvoiceTime stores the last time an invoice generation was
// attempted.
var lastGeneratedInvoiceTime time.Time
// lastGeneratedInvoiceTime stores the last time an invoice generation was
// attempted.
lastGeneratedInvoiceTime time.Time

// lastPayInvoiceTime stores the last time an invoice payment was
// attempted.
lastPayInvoiceTime time.Time
)

// String returns a human readable string describing the chanCreationError.
// This string is used in the templates in order to display the error to the
Expand Down Expand Up @@ -140,8 +169,17 @@ func (c chanCreationError) String() string {
return "Error generating Invoice"
case InvoiceTimeNotElapsed:
return "Please wait until you can generate a new invoice"
case PayInvoiceTimeNotElapsed:
return "Please wait until you can pay a new invoice"
case InvoiceAmountTooHigh:
return "Invoice amount too high"
case ErrorDecodingPayReq:
return "Error decoding payment request"
case PaymentStreamError:
return "Error on payment request, try again"
case ErrorPaymentAmount:
return fmt.Sprintf("The amount of invoice exceeds the limit of %d Atoms", maxPaymentAtoms)

default:
return fmt.Sprintf("%v", uint8(c))
}
Expand Down Expand Up @@ -400,11 +438,25 @@ type homePageContext struct {
// InvoicePaymentRequest the payment request generated by an invoice.
InvoicePaymentRequest string

// PayInvoiceRequest the pay request for an invoice.
PayInvoiceRequest string

// PayInvoiceAction action
PayInvoiceAction string

// OpenChannelAction indicates the form action to open a channel
OpenChannelAction string

// GenerateInvoiceAction indicates the form action to generate a new Invoice
GenerateInvoiceAction string

// Payment infos
PaymentDestination string
PaymentDescription string
PaymentAmount string
PaymentHash string
PaymentPreimage string
PaymentHops []*lnrpc.Hop
}

// fetchHomeState is helper functions that populates the homePageContext with
Expand Down Expand Up @@ -462,12 +514,13 @@ func (l *lightningFaucet) fetchHomeState() (*homePageContext, error) {
NumCoins: dcrutil.Amount(walletBalance.ConfirmedBalance).ToCoin(),
GitCommitHash: strings.Replace(gitHash, "'", "", -1),
NodeAddr: nodeAddr,
NumConfs: 3,
NumConfs: 6,
FormFields: make(map[string]string),
ActiveChannels: activeChannels.Channels,
PendingChannels: pendingChannels.PendingOpenChannels,
OpenChannelAction: OpenChannelAction,
GenerateInvoiceAction: GenerateInvoiceAction,
PayInvoiceAction: PayInvoiceAction,
}, nil
}

Expand Down Expand Up @@ -501,15 +554,19 @@ func (l *lightningFaucet) faucetHome(w http.ResponseWriter, r *http.Request) {
// form to open a channel, so we'll pass that off to the openChannel
// handler.
case r.Method == http.MethodPost:
action := r.URL.Query()["action"]

// action == 0 is to open channel, action == 1 is to generate
if action[0] == OpenChannelAction {
l.openChannel(homeTemplate, homeInfo, w, r)
} else if action[0] == GenerateInvoiceAction {
l.generateInvoice(homeTemplate, homeInfo, w, r)
actions := r.URL.Query()["action"]
if len(actions) > 0 {
action := actions[0]
// action == 0 is to open channel, action == 1 is to generate, action == 2 is to pay
switch action {
case OpenChannelAction:
l.openChannel(homeTemplate, homeInfo, w, r)
case GenerateInvoiceAction:
l.generateInvoice(homeTemplate, homeInfo, w, r)
case PayInvoiceAction:
l.payInvoice(homeTemplate, homeInfo, w, r)
}
}

// If the method isn't either of those, then this is an error as we
// only support the two methods above.
default:
Expand Down Expand Up @@ -818,3 +875,95 @@ func (l *lightningFaucet) generateInvoice(homeTemplate *template.Template,
log.Errorf("unable to render home page: %v", err)
}
}

// payInvoice is a hybrid http.Handler that handles: the validation of the
// pay invoice form, rendering errors to the form, and finally streaming the
// payment if all the parameters check out.
func (l *lightningFaucet) payInvoice(homeTemplate *template.Template,
homeState *homePageContext, w http.ResponseWriter, r *http.Request) {

elapsed := time.Since(lastPayInvoiceTime)
if elapsed < PayInvoiceTimeout {
homeState.SubmissionError = PayInvoiceTimeNotElapsed
homeTemplate.Execute(w, homeState)
return
}
lastPayInvoiceTime = time.Now()

// Get the invoice and try to verify and decode.
ctxb := context.Background()
rawPayReq := r.FormValue("payinvoice")
payReq := strings.TrimSpace(rawPayReq)
payReqString := &lnrpc.PayReqString{PayReq: payReq}
decodedPayReq, err := l.lnd.DecodePayReq(ctxb, payReqString)
if err != nil {
log.Errorf("Error on decode pay_req: %v", err)
homeState.SubmissionError = ErrorDecodingPayReq
homeTemplate.Execute(w, homeState)
return
}

decodedAmount := decodedPayReq.GetNumAtoms()

// Verify invoice amount.
if decodedAmount > maxPaymentAtoms {
log.Errorf("Max payout, pay_amount: %v", decodedAmount)
homeState.SubmissionError = ErrorPaymentAmount
homeTemplate.Execute(w, homeState)
return
}

// Create the payment request.
req := &lnrpc.SendRequest{
PaymentRequest: payReq,
Amt: decodedAmount,
IgnoreMaxOutboundAmt: false,
}

// Create a payment stream to send your payment request.
paymentStream, err := l.lnd.SendPayment(ctxb)
if err != nil {
log.Errorf("Error on create Payment Stream: %v", err)
homeState.SubmissionError = PaymentStreamError
homeTemplate.Execute(w, homeState)
return
}

// Stream the payment request.
if err := paymentStream.Send(req); err != nil {
log.Errorf("Error on send pay_req: %v", err)
homeState.SubmissionError = PaymentStreamError
homeTemplate.Execute(w, homeState)
return
}

// Receive response from streamed payment request.
resp, err := paymentStream.Recv()
if err != nil {
log.Errorf("Error on receive pay_req response: %v", err)
homeState.SubmissionError = PaymentStreamError
homeTemplate.Execute(w, homeState)
return
}

paymentStream.CloseSend()

// Log response and send to homeState to create the html version.
log.Infof("Invoice has been paid destination=%v description=%v amount=%v pay_hash:%v preimage=%v",
decodedPayReq.Destination, decodedPayReq.Description,
dcrutil.Amount(resp.PaymentRoute.TotalAmt), hex.EncodeToString(resp.PaymentHash),
hex.EncodeToString(resp.PaymentPreimage))

amount := dcrutil.Amount(resp.PaymentRoute.TotalAmt)

homeState.PaymentDestination = decodedPayReq.Destination
homeState.PaymentDescription = decodedPayReq.Description
homeState.PaymentAmount = amount.String()
homeState.PaymentHash = hex.EncodeToString(resp.PaymentHash)
homeState.PaymentPreimage = hex.EncodeToString(resp.PaymentPreimage)
homeState.PaymentHops = resp.PaymentRoute.Hops

if err := homeTemplate.Execute(w, homeState); err != nil {
log.Errorf("unable to render home page: %v", err)
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module github.com/decred/lightning-faucet/main
module github.com/decred/lightning-faucet

go 1.12

Expand Down
52 changes: 51 additions & 1 deletion static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,6 @@ <h4>Target confirmation count reached</h4>

</div>
</div>

<div class="content mb-3 p-4">
<h2>Generate Invoice</h2>
<form id="generateInvoiceForm" method="post" action="/?action={{ .GenerateInvoiceAction }}">
Expand Down Expand Up @@ -184,6 +183,57 @@ <h4>Invoice successfully generated</h4>
</form>
</div>

<div class="content mb-3 p-4">
<h2>Pay Invoice</h2>
<form id="payInvoiceForm" method="post" action="/?action={{ .PayInvoiceAction }}">
<div class="form-group">

<input class="form-control {{if eq .SubmissionError 12 14 15 16 }}is-invalid{{end}}"
id="payinvoice" name="payinvoice" type="text" required="true" placeholder="Invoce code">

{{ if eq .SubmissionError 12 14 15 16 }}
<div class="invalid-feedback">{{printf "%v" .SubmissionError}}</div>
{{end}}
</div>

{{ if .PaymentDestination }}
<div class="form-group" >
<h4>Invoice successfully paid</h4>
<div class="content p-4" style="word-break: break-all">
<p>Destination: {{ .PaymentDestination }}</p>
<p>Description: {{ .PaymentDescription }}</p>
<p>Amount: {{ .PaymentAmount }}</p>
<p>PayHash: {{ .PaymentHash }}</p>
<p>Preimage: {{ .PaymentPreimage }}</p>
</div><br />
<h4>Hops in channels:</h4>
{{range $i, $hop := .PaymentHops}}
<div class="channel d-inline-block p-2">
<span style="font-weight:bold;">Hop: </span>{{ $i }}<br />
<span style="font-weight:bold;">Remote PubKey: </span>{{ $hop.PubKey }}<br />
<span style="font-weight:bold;">ChanId: </span>{{ $hop.ChanId }}<br />
<span style="font-weight:bold;">Fee: </span>{{ $hop.FeeMAtoms }}<br />
</div>
{{end}}
</div>
{{ end }}

<div class="form-group row justify-content-center">
<button class="btn btn-outline-primary btn-outline-primary--inverted d-lg-inline-block d-block mb-3 px-4" type="submit">Pay Invoice</button>
</div>

<script>
(function() {
$("input").change(function() {
$(this).removeClass("is-invalid");
});
})();
</script>
</form>
</div>



{{if eq .ChannelTxid ""}}
<div class="content mb-3 p-4">
{{ if gt (len $.PendingChannels) 0 }}
Expand Down

0 comments on commit 1fb8968

Please sign in to comment.