web-wallet/example/backend/main.go

277 lines
7.3 KiB
Go

package main
import (
"bytes"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"net/http"
"os/exec"
"path"
"strings"
"github.com/d4l3k/go-electrum/electrum"
)
// NOTE: This was initially hacked together for a demo. It doesn't do proper
// error checking etc. If this comment is still here it's probably not ready
// yet.
const PathBroadcastTransaction = "/transaction/broadcast"
const PathDecodeTransaction = "/transaction/decode"
const PathPSBT = "/transaction/psbt"
var LBRYCRD = path.Join(LBRYCRD_PATH, "lbrycrd-cli")
var node *electrum.Node
func getUnspentForAmount(
outputAddress string,
desiredAmount float64,
) (inputs []*electrum.Transaction, totalAmount float64, enough bool) {
unspent, _ := node.BlockchainAddressListUnspent(outputAddress)
for _, u := range unspent {
if desiredAmount > totalAmount {
totalAmount += float64(u.Value) / 100000000.0
inputs = append(inputs, u)
}
}
if desiredAmount <= totalAmount {
enough = true
}
return
}
func makePsbt(inputs []*electrum.Transaction, outputAddress string, amount float64) (string, error) {
var inputTxnParts []string
for _, input := range inputs {
inputTxnParts = append(
inputTxnParts,
fmt.Sprintf(`{"txid": "%s", "vout": %d}`, input.Hash, input.Pos),
)
}
var inputTxnsParam = "[" + strings.Join(inputTxnParts, ", ") + "]"
var outputAddressesParam = fmt.Sprintf(`[{"%s": %f}]`, outputAddress, amount)
var out bytes.Buffer
var errOut bytes.Buffer
fmt.Println(LBRYCRD, "createpsbt", string(inputTxnsParam), string(outputAddressesParam))
cmd := exec.Command(LBRYCRD, "createpsbt", string(inputTxnsParam), string(outputAddressesParam))
cmd.Stdout = &out
cmd.Stderr = &errOut
err := cmd.Run()
if err != nil {
fmt.Println(err.Error())
fmt.Println(out.String())
fmt.Println(errOut.String())
return "", err
} else {
psbtBytes, err := base64.StdEncoding.DecodeString(out.String())
if err != nil {
fmt.Println(err.Error())
}
return hex.EncodeToString(psbtBytes), nil
}
}
func decodeTransaction(txn string) (decoded string, err error) {
var out bytes.Buffer
var errOut bytes.Buffer
cmd := exec.Command(LBRYCRD, "decoderawtransaction", txn)
cmd.Stdout = &out
cmd.Stderr = &errOut
err = cmd.Run()
if err != nil {
err = fmt.Errorf(err.Error() + "\n" + out.String() + "\n" + errOut.String())
} else {
decoded = out.String()
}
return
}
func generateBlockRegtest() (cmdOut string, err error) {
var out bytes.Buffer
var errOut bytes.Buffer
cmd := exec.Command(LBRYCRD, "generate", "1")
cmd.Stdout = &out
cmd.Stderr = &errOut
err = cmd.Run()
if err != nil {
err = fmt.Errorf(err.Error() + "\n" + out.String() + "\n" + errOut.String())
} else {
cmdOut = out.String()
}
return
}
func main() {
// TODO Throw error if lbrycrdd is not running on mainnet
node = electrum.NewNode()
if err := node.ConnectTCP(ELECTRUM_SERVER); err != nil {
log.Fatal(err)
}
http.HandleFunc(PathBroadcastTransaction, broadcastTransaction)
http.HandleFunc(PathDecodeTransaction, getDecodedTransaction)
http.HandleFunc(PathPSBT, getPSBT)
fmt.Println("Serving at :8090")
http.ListenAndServe(":8090", nil)
}
type BroadcastTransactionRequest struct {
TransactionHex string `json:"transactionHex"`
}
type DecodeTransactionRequest struct {
TransactionHex string `json:"transactionHex"`
}
type PSBTRequest struct {
ToAddress string `json:"toAddress"`
FromAddress string `json:"fromAddress"`
DesiredAmount float64 `json:"desiredAmount"`
}
type PSBTResponse struct {
NonWitnessUtxoHexes []string `json:"nonWitnessUtxoHex"`
PSBTHex string `json:"psbtHex"`
Error string `json:"error"`
ActualAmount float64 `json:"actualAmount"`
}
func getPSBT(w http.ResponseWriter, req *http.Request) {
var psbtr PSBTRequest
var pr PSBTResponse
if err := json.NewDecoder(req.Body).Decode(&psbtr); err != nil {
http.Error(w, string("Malformed request body JSON"), http.StatusBadRequest)
return
}
w.Header().Set("Access-Control-Allow-Origin", CORS)
outputs, actualAmount, success := getUnspentForAmount(psbtr.FromAddress, psbtr.DesiredAmount)
if !success {
pr = PSBTResponse{Error: "Not enough funds"}
response, _ := json.Marshal(pr)
fmt.Fprintf(w, string(response))
return
}
// Doing the `desiredAmount` so that the remainder can go to fees. This
// might lead to super high fees but the server would stop us from
// broadcasting it. If we used `actualAmount`, we'd have zero fees. If we
// used `actualAmount` minus fees, it might be way more than `desiredAmount`
// and the server *wouldn't* stop us from broadcasting.
psbt, err := makePsbt(outputs, psbtr.ToAddress, psbtr.DesiredAmount)
if err != nil {
pr = PSBTResponse{Error: "getPsbt - err: " + err.Error()}
response, _ := json.Marshal(pr)
http.Error(w, string(response), http.StatusInternalServerError)
return
}
var fullTxns []string
for _, output := range outputs {
fullTxn, err := node.BlockchainTransactionGet(output.Hash)
if err != nil {
pr = PSBTResponse{Error: "getPsbt - err: " + err.Error()}
response, _ := json.Marshal(pr)
http.Error(w, string(response), http.StatusInternalServerError)
return
}
fullTxns = append(fullTxns, fullTxn)
}
pr = PSBTResponse{
NonWitnessUtxoHexes: fullTxns,
PSBTHex: psbt,
ActualAmount: actualAmount,
}
// TODO - is this check future-proof enough? (change in prices, etc)
if actualAmount-psbtr.DesiredAmount > 0.01 {
pr.Error = "Absurdly high fee"
}
response, _ := json.Marshal(pr)
fmt.Fprintf(w, string(response))
fmt.Println("get psbt")
}
type BroadcastTransactionResponse struct {
Txid string `json:"txid"`
}
func broadcastTransaction(w http.ResponseWriter, req *http.Request) {
var btr BroadcastTransactionRequest
if err := json.NewDecoder(req.Body).Decode(&btr); err != nil {
http.Error(w, string("Malformed request body JSON"), http.StatusBadRequest)
return
}
// TODO need some sort of actual feedback for web app for errors
w.Header().Set("Access-Control-Allow-Origin", CORS)
broadcastResult, err := node.BlockchainTransactionBroadcast([]byte(btr.TransactionHex))
if err != nil {
fmt.Println("broadcast failure")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
broadcastResultStr := fmt.Sprintf("+v", broadcastResult)
btresp := BroadcastTransactionResponse{Txid: broadcastResultStr}
response, _ := json.Marshal(btresp)
fmt.Fprintf(w, string(response))
fmt.Println("broadcast success:\n" + broadcastResultStr)
return
}
type DecodeTransactionResponse struct {
DecodedTransaction string `json:"decodedTransaction"`
}
func getDecodedTransaction(w http.ResponseWriter, req *http.Request) {
var dtr DecodeTransactionRequest
if err := json.NewDecoder(req.Body).Decode(&dtr); err != nil {
http.Error(w, string("Malformed request body JSON"), http.StatusBadRequest)
return
}
// TODO need some sort of actual feedback for web app for errors
w.Header().Set("Access-Control-Allow-Origin", CORS)
decodedTransaction, err := decodeTransaction(dtr.TransactionHex)
if err != nil {
fmt.Println("decode failure")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
dtresp := DecodeTransactionResponse{
DecodedTransaction: decodedTransaction,
}
response, _ := json.Marshal(dtresp)
fmt.Fprintf(w, string(response))
fmt.Printf(decodedTransaction)
fmt.Println("decode success")
return
}