2019-10-01 12:22:30 -03:00
|
|
|
package libwallet
|
|
|
|
|
|
|
|
import (
|
2021-01-29 18:51:08 -03:00
|
|
|
"fmt"
|
2019-10-01 12:22:30 -03:00
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
|
2020-11-09 10:05:29 -03:00
|
|
|
"github.com/muun/libwallet/addresses"
|
2021-01-29 18:51:08 -03:00
|
|
|
"github.com/muun/libwallet/errors"
|
2020-11-09 10:05:29 -03:00
|
|
|
|
2019-10-01 12:22:30 -03:00
|
|
|
"github.com/btcsuite/btcd/txscript"
|
|
|
|
"github.com/btcsuite/btcutil"
|
2020-11-09 10:05:29 -03:00
|
|
|
"google.golang.org/protobuf/proto"
|
2019-10-01 12:22:30 -03:00
|
|
|
)
|
|
|
|
|
2019-12-16 17:59:11 -03:00
|
|
|
// These constants are here for clients usage.
|
|
|
|
const (
|
2020-11-09 10:05:29 -03:00
|
|
|
AddressVersionSwapsV1 = addresses.SubmarineSwapV1
|
|
|
|
AddressVersionSwapsV2 = addresses.SubmarineSwapV2
|
2019-12-16 17:59:11 -03:00
|
|
|
)
|
|
|
|
|
2019-10-01 12:22:30 -03:00
|
|
|
// MuunPaymentURI is muun's uri struct
|
|
|
|
type MuunPaymentURI struct {
|
|
|
|
Address string
|
|
|
|
Label string
|
|
|
|
Message string
|
|
|
|
Amount string
|
|
|
|
URI string
|
|
|
|
BIP70Url string
|
|
|
|
CreationTime string
|
|
|
|
ExpiresTime string
|
|
|
|
Invoice *Invoice
|
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
bitcoinScheme = "bitcoin:"
|
|
|
|
muunScheme = "muun:"
|
|
|
|
)
|
|
|
|
|
2019-12-17 17:32:12 -03:00
|
|
|
// GetPaymentURI builds a MuunPaymentURI from text (Bitcoin Uri, Muun Uri or address) and a network
|
|
|
|
func GetPaymentURI(rawInput string, network *Network) (*MuunPaymentURI, error) {
|
2019-10-01 12:22:30 -03:00
|
|
|
|
2020-11-09 10:05:29 -03:00
|
|
|
bitcoinUri, components := buildUriFromString(rawInput, bitcoinScheme)
|
|
|
|
if components == nil {
|
2021-01-29 18:51:08 -03:00
|
|
|
return nil, errors.Errorf(ErrInvalidURI, "failed to parse uri %v", rawInput)
|
2019-10-01 12:22:30 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
if components.Scheme != "bitcoin" {
|
2021-01-29 18:51:08 -03:00
|
|
|
return nil, errors.New(ErrInvalidURI, "Invalid scheme")
|
2019-10-01 12:22:30 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
base58Address := components.Opaque
|
|
|
|
|
|
|
|
// When URIs are bitcoin:// the address comes in host
|
|
|
|
// this happens in iOS that mostly ignores bitcoin: format
|
|
|
|
if len(base58Address) == 0 {
|
|
|
|
base58Address = components.Host
|
|
|
|
}
|
|
|
|
|
|
|
|
queryValues, err := url.ParseQuery(components.RawQuery)
|
|
|
|
if err != nil {
|
2021-01-29 18:51:08 -03:00
|
|
|
return nil, errors.Errorf(ErrInvalidURI, "Couldn't parse query: %v", err)
|
2019-10-01 12:22:30 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
var label, message, amount string
|
|
|
|
|
|
|
|
if len(queryValues["label"]) != 0 {
|
|
|
|
label = queryValues["label"][0]
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(queryValues["message"]) != 0 {
|
|
|
|
message = queryValues["message"][0]
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(queryValues["amount"]) != 0 {
|
|
|
|
amount = queryValues["amount"][0]
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(queryValues["lightning"]) != 0 {
|
|
|
|
invoice, err := ParseInvoice(queryValues["lightning"][0], network)
|
|
|
|
|
|
|
|
if err == nil {
|
2019-12-16 17:59:11 -03:00
|
|
|
return &MuunPaymentURI{Invoice: invoice}, nil
|
2019-10-01 12:22:30 -03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//BIP70 check
|
|
|
|
if len(queryValues["r"]) != 0 {
|
|
|
|
if len(base58Address) > 0 {
|
|
|
|
return &MuunPaymentURI{
|
|
|
|
Address: base58Address,
|
|
|
|
Label: label,
|
|
|
|
Message: message,
|
|
|
|
Amount: amount,
|
2019-12-17 17:32:12 -03:00
|
|
|
URI: bitcoinUri,
|
2019-10-01 12:22:30 -03:00
|
|
|
BIP70Url: queryValues["r"][0],
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
return &MuunPaymentURI{
|
|
|
|
Label: label,
|
|
|
|
Message: message,
|
|
|
|
Amount: amount,
|
2019-12-17 17:32:12 -03:00
|
|
|
URI: bitcoinUri,
|
2019-10-01 12:22:30 -03:00
|
|
|
BIP70Url: queryValues["r"][0],
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Bech32 check
|
|
|
|
validatedBase58Address, err := btcutil.DecodeAddress(base58Address, network.network)
|
|
|
|
if err != nil {
|
2021-01-29 18:51:08 -03:00
|
|
|
return nil, fmt.Errorf("invalid address: %w", err)
|
2019-10-01 12:22:30 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
if !validatedBase58Address.IsForNet(network.network) {
|
2021-01-29 18:51:08 -03:00
|
|
|
return nil, errors.New(ErrInvalidURI, "Network mismatch")
|
2019-10-01 12:22:30 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
return &MuunPaymentURI{
|
|
|
|
Address: validatedBase58Address.String(),
|
|
|
|
Label: label,
|
|
|
|
Message: message,
|
|
|
|
Amount: amount,
|
2019-12-17 17:32:12 -03:00
|
|
|
URI: bitcoinUri,
|
2019-10-01 12:22:30 -03:00
|
|
|
}, nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// DoPaymentRequestCall builds a MuunPaymentUri from a url and a network. Handling BIP70 to 72
|
|
|
|
func DoPaymentRequestCall(url string, network *Network) (*MuunPaymentURI, error) {
|
|
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
|
|
if err != nil {
|
2021-01-29 18:51:08 -03:00
|
|
|
return nil, fmt.Errorf("failed to create request to: %s", url)
|
2019-10-01 12:22:30 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Set("Accept", "application/bitcoin-paymentrequest")
|
|
|
|
|
|
|
|
client := &http.Client{}
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
if err != nil {
|
2021-01-29 18:51:08 -03:00
|
|
|
return nil, errors.Errorf(ErrNetwork, "failed to make request to: %s", url)
|
2019-10-01 12:22:30 -03:00
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
2021-01-29 18:51:08 -03:00
|
|
|
return nil, errors.Errorf(ErrNetwork, "Failed to read body response: %w", err)
|
2019-10-01 12:22:30 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
payReq := &PaymentRequest{}
|
|
|
|
err = proto.Unmarshal(body, payReq)
|
|
|
|
if err != nil {
|
2021-01-29 18:51:08 -03:00
|
|
|
return nil, fmt.Errorf("failed to unmarshal payment request: %w", err)
|
2019-10-01 12:22:30 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
payDetails := &PaymentDetails{}
|
|
|
|
|
|
|
|
err = proto.Unmarshal(payReq.SerializedPaymentDetails, payDetails)
|
|
|
|
if err != nil {
|
2021-01-29 18:51:08 -03:00
|
|
|
return nil, fmt.Errorf("failed to unmarshall payment details: %w", err)
|
2019-10-01 12:22:30 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(payDetails.Outputs) == 0 {
|
2021-01-29 18:51:08 -03:00
|
|
|
return nil, fmt.Errorf("no outputs provided")
|
2019-10-01 12:22:30 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
address, err := getAddressFromScript(payDetails.Outputs[0].Script, network)
|
|
|
|
if err != nil {
|
2021-01-29 18:51:08 -03:00
|
|
|
return nil, fmt.Errorf("failed to get address: %w", err)
|
2019-10-01 12:22:30 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
return &MuunPaymentURI{
|
|
|
|
Address: address,
|
2020-11-09 10:05:29 -03:00
|
|
|
Message: payDetails.Memo,
|
|
|
|
Amount: strconv.FormatUint(payDetails.Outputs[0].Amount, 10),
|
2019-10-01 12:22:30 -03:00
|
|
|
BIP70Url: url,
|
2020-11-09 10:05:29 -03:00
|
|
|
CreationTime: strconv.FormatUint(payDetails.Time, 10),
|
|
|
|
ExpiresTime: strconv.FormatUint(payDetails.Expires, 10),
|
2019-10-01 12:22:30 -03:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func getAddressFromScript(script []byte, network *Network) (string, error) {
|
|
|
|
pkScript, err := txscript.ParsePkScript(script)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
address, err := pkScript.Address(network.network)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return address.String(), nil
|
|
|
|
}
|
|
|
|
|
2020-11-09 10:05:29 -03:00
|
|
|
func buildUriFromString(rawInput string, targetScheme string) (string, *url.URL) {
|
2019-12-17 17:32:12 -03:00
|
|
|
newUri := rawInput
|
2019-10-01 12:22:30 -03:00
|
|
|
|
2020-11-09 10:05:29 -03:00
|
|
|
newUri = strings.Replace(newUri, muunScheme, targetScheme, 1)
|
2019-10-01 12:22:30 -03:00
|
|
|
|
2020-11-09 10:05:29 -03:00
|
|
|
if !strings.HasPrefix(strings.ToLower(newUri), targetScheme) {
|
|
|
|
newUri = targetScheme + rawInput
|
|
|
|
}
|
|
|
|
|
|
|
|
components, err := url.Parse(newUri)
|
|
|
|
if err != nil {
|
|
|
|
return "", nil
|
2019-10-01 12:22:30 -03:00
|
|
|
}
|
|
|
|
|
2020-11-09 10:05:29 -03:00
|
|
|
return newUri, components
|
2019-10-01 12:22:30 -03:00
|
|
|
}
|