mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
Implements the NWC-321 (BIP-321 Lightning Payments) pay and receive methods, limited to BOLT-11 instructions: - pay parses the BIP-321 URI, selects the lightning (BOLT-11) instruction and rejects URIs without one (UNSUPPORTED_PAYMENT_INSTRUCTION), validates the invoice network against the node network (UNSUPPORTED_NETWORK), rejects conflicting or invalid amounts, unknown req- parameters and payer_note (undeliverable over BOLT-11) - receive returns a BIP-321 URI containing a single BOLT-11 invoice; a variable amount is rejected as zero-amount invoices are not supported - both methods reuse the existing pay_invoice / make_invoice scopes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
604 lines
20 KiB
Go
604 lines
20 KiB
Go
package phoenixd
|
|
|
|
import (
|
|
"context"
|
|
b64 "encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
decodepay "github.com/nbd-wtf/ln-decodepay"
|
|
|
|
"github.com/getAlby/hub/lnclient"
|
|
"github.com/getAlby/hub/logger"
|
|
"github.com/getAlby/hub/nip47/models"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// errNotFound indicates that a payment was not found at the queried endpoint.
|
|
var errNotFound = errors.New("phoenixd: payment not found")
|
|
|
|
type InvoiceResponse struct {
|
|
PaymentHash string `json:"paymentHash"`
|
|
Preimage string `json:"preimage"`
|
|
ExternalId string `json:"externalId"`
|
|
Description string `json:"description"`
|
|
Invoice string `json:"invoice"`
|
|
IsPaid bool `json:"isPaid"`
|
|
ReceivedSat int64 `json:"receivedSat"`
|
|
FeesSat int64 `json:"fees"`
|
|
CompletedAt int64 `json:"completedAt"`
|
|
CreatedAt int64 `json:"createdAt"`
|
|
}
|
|
|
|
type OutgoingPaymentResponse struct {
|
|
PaymentHash string `json:"paymentHash"`
|
|
Preimage string `json:"preimage"`
|
|
Invoice string `json:"invoice"`
|
|
IsPaid bool `json:"isPaid"`
|
|
Sent int64 `json:"sent"`
|
|
Fees int64 `json:"fees"`
|
|
CompletedAt int64 `json:"completedAt"`
|
|
CreatedAt int64 `json:"createdAt"`
|
|
}
|
|
|
|
type PayResponse struct {
|
|
PaymentHash string `json:"paymentHash"`
|
|
PaymentId string `json:"paymentId"`
|
|
PaymentPreimage string `json:"paymentPreimage"`
|
|
RoutingFeeSat int64 `json:"routingFeeSat"`
|
|
}
|
|
|
|
type MakeInvoiceResponse struct {
|
|
AmountSat int64 `json:"amountSat"`
|
|
PaymentHash string `json:"paymentHash"`
|
|
Serialized string `json:"serialized"`
|
|
}
|
|
|
|
type InfoResponse struct {
|
|
NodeId string `json:"nodeId"`
|
|
}
|
|
|
|
type BalanceResponse struct {
|
|
BalanceSat int64 `json:"balanceSat"`
|
|
FeeCreditSat int64 `json:"feeCreditSat"`
|
|
}
|
|
|
|
type PhoenixService struct {
|
|
Address string
|
|
Authorization string
|
|
pubkey string
|
|
nodeInfo *lnclient.NodeInfo
|
|
ctx context.Context
|
|
}
|
|
|
|
func NewPhoenixService(ctx context.Context, address string, authorization string) (result lnclient.LNClient, err error) {
|
|
authorizationBase64 := b64.StdEncoding.EncodeToString([]byte(":" + authorization))
|
|
// some environments (e.g. in a cloud environment like render.com) can only get the address and the port but not the protocol
|
|
// in those cases we default to http for local requests
|
|
if !strings.HasPrefix(address, "http") {
|
|
address = "http://" + address
|
|
}
|
|
phoenixService := &PhoenixService{ctx: ctx, Address: address, Authorization: authorizationBase64}
|
|
|
|
info, err := fetchNodeInfo(ctx, phoenixService)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
phoenixService.nodeInfo = info
|
|
phoenixService.pubkey = info.Pubkey
|
|
|
|
return phoenixService, nil
|
|
}
|
|
|
|
func (svc *PhoenixService) GetBalances(ctx context.Context, includeInactiveChannels bool) (*lnclient.BalancesResponse, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, svc.Address+"/getbalance", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Add("Authorization", "Basic "+svc.Authorization)
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
logger.Logger.WithFields(logrus.Fields{
|
|
"body": string(body),
|
|
"status_code": resp.StatusCode,
|
|
}).Error("phoenixd get balance endpoint returned non-success code")
|
|
return nil, fmt.Errorf("phoenixd get balance endpoint returned non-success code: %d %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var balanceRes BalanceResponse
|
|
if err := json.Unmarshal(body, &balanceRes); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
balance := balanceRes.BalanceSat * 1000
|
|
|
|
return &lnclient.BalancesResponse{
|
|
Onchain: lnclient.OnchainBalanceResponse{
|
|
PendingBalancesDetails: []lnclient.PendingBalanceDetails{},
|
|
PendingSweepBalancesDetails: []lnclient.PendingBalanceDetails{}},
|
|
Lightning: lnclient.LightningBalanceResponse{
|
|
TotalSpendableMsat: balance,
|
|
NextMaxSpendableMsat: balance,
|
|
NextMaxSpendableMPPMsat: balance,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (svc *PhoenixService) GetInfo(ctx context.Context) (info *lnclient.NodeInfo, err error) {
|
|
return svc.nodeInfo, nil
|
|
}
|
|
|
|
func fetchNodeInfo(ctx context.Context, svc *PhoenixService) (info *lnclient.NodeInfo, err error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, svc.Address+"/getinfo", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Add("Authorization", "Basic "+svc.Authorization)
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
logger.Logger.WithFields(logrus.Fields{
|
|
"body": string(body),
|
|
"status_code": resp.StatusCode,
|
|
}).Error("phoenixd get info endpoint returned non-success code")
|
|
return nil, fmt.Errorf("phoenixd get info endpoint returned non-success code: %d %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var infoRes InfoResponse
|
|
if err := json.Unmarshal(body, &infoRes); err != nil {
|
|
return nil, err
|
|
}
|
|
return &lnclient.NodeInfo{
|
|
Alias: "Phoenix",
|
|
Color: "",
|
|
Pubkey: infoRes.NodeId,
|
|
Network: "bitcoin",
|
|
BlockHeight: 0,
|
|
BlockHash: "",
|
|
}, nil
|
|
}
|
|
|
|
func (svc *PhoenixService) ListChannels(ctx context.Context) ([]lnclient.Channel, error) {
|
|
channels := []lnclient.Channel{}
|
|
return channels, nil
|
|
}
|
|
|
|
func (svc *PhoenixService) MakeInvoice(ctx context.Context, amountMsat int64, description string, descriptionHash string, expiry int64, throughNodePubkey *string) (transaction *lnclient.Transaction, err error) {
|
|
// TODO: support expiry
|
|
if expiry == 0 {
|
|
expiry = lnclient.DEFAULT_INVOICE_EXPIRY
|
|
}
|
|
form := url.Values{}
|
|
amountSat := strconv.FormatInt(amountMsat/1000, 10)
|
|
form.Add("amountSat", amountSat)
|
|
if description != "" {
|
|
form.Add("description", description)
|
|
} else if descriptionHash != "" {
|
|
form.Add("descriptionHash", descriptionHash)
|
|
} else {
|
|
form.Add("description", "invoice")
|
|
}
|
|
|
|
today := time.Now().UTC().Format("2006-02-01") // querying is too slow so we limit the invoices we query with the date - see list transactions
|
|
form.Add("externalId", today) // for some resone phoenixd requires an external id to query a list of invoices. thus we set this to nwc
|
|
logger.Logger.WithFields(logrus.Fields{
|
|
"externalId": today,
|
|
"amountSat": amountSat,
|
|
}).Infof("Requesting phoenix invoice")
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, svc.Address+"/createinvoice", strings.NewReader(form.Encode()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Add("Authorization", "Basic "+svc.Authorization)
|
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
logger.Logger.WithFields(logrus.Fields{
|
|
"body": string(body),
|
|
"status_code": resp.StatusCode,
|
|
}).Error("phoenixd create invoice endpoint returned non-success code")
|
|
return nil, fmt.Errorf("phoenixd create invoice endpoint returned non-success code: %d %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var invoiceRes MakeInvoiceResponse
|
|
if err := json.Unmarshal(body, &invoiceRes); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tx, err := svc.LookupInvoice(ctx, invoiceRes.PaymentHash)
|
|
if err != nil {
|
|
logger.Logger.WithError(err).Error("failed to lookup newly created invoice")
|
|
return nil, err
|
|
}
|
|
|
|
return tx, nil
|
|
}
|
|
|
|
func (svc *PhoenixService) MakeHoldInvoice(ctx context.Context, amountMsat int64, description string, descriptionHash string, expiry int64, paymentHash string, minCltvExpiryDelta *uint64) (transaction *lnclient.Transaction, err error) {
|
|
return nil, errors.New("not implemented")
|
|
}
|
|
|
|
func (svc *PhoenixService) SettleHoldInvoice(ctx context.Context, preimage string) (err error) {
|
|
return errors.New("not implemented")
|
|
}
|
|
|
|
func (svc *PhoenixService) CancelHoldInvoice(ctx context.Context, paymentHash string) (err error) {
|
|
return errors.New("not implemented")
|
|
}
|
|
|
|
// LookupInvoice looks up a transaction by payment hash. It first checks
|
|
// incoming payments, then falls back to outgoing payments if the incoming
|
|
// payment is not found (HTTP 404).
|
|
func (svc *PhoenixService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *lnclient.Transaction, err error) {
|
|
transaction, err = svc.lookupIncomingPayment(ctx, paymentHash)
|
|
if err == nil {
|
|
return transaction, nil
|
|
}
|
|
|
|
// Only fall back to outgoing lookup when incoming returns not-found.
|
|
if !errors.Is(err, errNotFound) {
|
|
return nil, err
|
|
}
|
|
|
|
return svc.lookupOutgoingPayment(ctx, paymentHash)
|
|
}
|
|
|
|
// lookupIncomingPayment fetches an incoming payment from Phoenixd by payment hash.
|
|
func (svc *PhoenixService) lookupIncomingPayment(ctx context.Context, paymentHash string) (*lnclient.Transaction, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, svc.Address+"/payments/incoming/"+paymentHash, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create phoenixd incoming payment request: %w", err)
|
|
}
|
|
req.Header.Add("Authorization", "Basic "+svc.Authorization)
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("call phoenixd incoming payment endpoint: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read phoenixd incoming payment response: %w", err)
|
|
}
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return nil, errNotFound
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("phoenixd incoming payments endpoint returned non-success code: %d %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var invoiceRes InvoiceResponse
|
|
if err := json.Unmarshal(body, &invoiceRes); err != nil {
|
|
return nil, fmt.Errorf("decode phoenixd incoming payment response: %w", err)
|
|
}
|
|
|
|
return phoenixInvoiceToTransaction(&invoiceRes)
|
|
}
|
|
|
|
// lookupOutgoingPayment fetches an outgoing payment from Phoenixd using the
|
|
// /payments/outgoingbyhash/{paymentHash} endpoint.
|
|
func (svc *PhoenixService) lookupOutgoingPayment(ctx context.Context, paymentHash string) (*lnclient.Transaction, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, svc.Address+"/payments/outgoingbyhash/"+paymentHash, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create phoenixd outgoing payment request: %w", err)
|
|
}
|
|
req.Header.Add("Authorization", "Basic "+svc.Authorization)
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("call phoenixd outgoing payment endpoint: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read phoenixd outgoing payment response: %w", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("phoenixd outgoing payment lookup returned non-success code: %d %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var paymentRes OutgoingPaymentResponse
|
|
if err := json.Unmarshal(body, &paymentRes); err != nil {
|
|
return nil, fmt.Errorf("decode phoenixd outgoing payment response: %w", err)
|
|
}
|
|
|
|
return outgoingPaymentToTransaction(&paymentRes)
|
|
}
|
|
|
|
func (svc *PhoenixService) SendPaymentSync(payReq string, amountMsat *uint64) (*lnclient.PayInvoiceResponse, error) {
|
|
// TODO: support 0-amount invoices
|
|
if amountMsat != nil {
|
|
return nil, errors.New("0-amount invoices not supported")
|
|
}
|
|
form := url.Values{}
|
|
form.Add("invoice", payReq)
|
|
req, err := http.NewRequestWithContext(svc.ctx, http.MethodPost, svc.Address+"/payinvoice", strings.NewReader(form.Encode()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Add("Authorization", "Basic "+svc.Authorization)
|
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
client := &http.Client{Timeout: 90 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("phoenixd /payinvoice returned non-success status: %d %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var payRes PayResponse
|
|
if err := json.Unmarshal(body, &payRes); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &lnclient.PayInvoiceResponse{
|
|
Preimage: payRes.PaymentPreimage,
|
|
FeeMsat: uint64(payRes.RoutingFeeSat) * 1000,
|
|
}, nil
|
|
}
|
|
|
|
func (svc *PhoenixService) SendKeysend(amountMsat uint64, destination string, custom_records []lnclient.TLVRecord, preimage string) (*lnclient.PayKeysendResponse, error) {
|
|
return nil, errors.New("not implemented")
|
|
}
|
|
|
|
func (svc *PhoenixService) RedeemOnchainFunds(ctx context.Context, toAddress string, amountSat uint64, feeRate *uint64, sendAll bool) (txId string, err error) {
|
|
return "", errors.New("not implemented")
|
|
}
|
|
|
|
func (svc *PhoenixService) ResetRouter(key string) error {
|
|
return errors.New("not implemented")
|
|
}
|
|
|
|
func (svc *PhoenixService) Shutdown() error {
|
|
// No specific shutdown actions needed for Phoenixd client via HTTP
|
|
return nil
|
|
}
|
|
|
|
func (svc *PhoenixService) GetNodeConnectionInfo(ctx context.Context) (nodeConnectionInfo *lnclient.NodeConnectionInfo, err error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, svc.Address+"/getinfo", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Add("Authorization", "Basic "+svc.Authorization)
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("phoenixd /getinfo returned non-success status: %d %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var infoRes InfoResponse
|
|
if err := json.Unmarshal(body, &infoRes); err != nil {
|
|
return nil, err
|
|
}
|
|
return &lnclient.NodeConnectionInfo{
|
|
Pubkey: infoRes.NodeId,
|
|
}, nil
|
|
}
|
|
|
|
func (svc *PhoenixService) ConnectPeer(ctx context.Context, connectPeerRequest *lnclient.ConnectPeerRequest) error {
|
|
return errors.New("not implemented")
|
|
}
|
|
func (svc *PhoenixService) OpenChannel(ctx context.Context, openChannelRequest *lnclient.OpenChannelRequest) (*lnclient.OpenChannelResponse, error) {
|
|
return nil, errors.New("not implemented")
|
|
}
|
|
|
|
func (svc *PhoenixService) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) error {
|
|
return errors.New("not implemented")
|
|
}
|
|
|
|
func (svc *PhoenixService) GetNewOnchainAddress(ctx context.Context) (string, error) {
|
|
return "", errors.New("not implemented")
|
|
}
|
|
|
|
func (svc *PhoenixService) GetOnchainBalance(ctx context.Context) (*lnclient.OnchainBalanceResponse, error) {
|
|
return nil, errors.New("not implemented")
|
|
}
|
|
|
|
func (svc *PhoenixService) SignMessage(ctx context.Context, message string) (string, error) {
|
|
return "", errors.New("not implemented")
|
|
}
|
|
|
|
func (svc *PhoenixService) ListPeers(ctx context.Context) ([]lnclient.PeerDetails, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (svc *PhoenixService) GetLogOutput(ctx context.Context, maxLen int) ([]byte, error) {
|
|
return []byte{}, nil
|
|
}
|
|
|
|
func (svc *PhoenixService) GetNodeStatus(ctx context.Context) (nodeStatus *lnclient.NodeStatus, err error) {
|
|
_, err = fetchNodeInfo(ctx, svc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &lnclient.NodeStatus{
|
|
IsReady: true,
|
|
}, nil
|
|
}
|
|
|
|
func (svc *PhoenixService) GetStorageDir() (string, error) {
|
|
return "", nil
|
|
}
|
|
|
|
func (svc *PhoenixService) GetNetworkGraph(ctx context.Context, nodeIds []string) (lnclient.NetworkGraphResponse, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (svc *PhoenixService) UpdateLastWalletSyncRequest() {}
|
|
|
|
func (svc *PhoenixService) DisconnectPeer(ctx context.Context, peerId string) error {
|
|
return errors.New("not implemented")
|
|
}
|
|
|
|
func (svc *PhoenixService) UpdateChannel(ctx context.Context, updateChannelRequest *lnclient.UpdateChannelRequest) error {
|
|
return errors.New("not implemented")
|
|
}
|
|
|
|
func (svc *PhoenixService) GetSupportedNIP47Methods() []string {
|
|
return []string{
|
|
models.PAY_INVOICE_METHOD,
|
|
models.GET_BALANCE_METHOD,
|
|
models.GET_BUDGET_METHOD,
|
|
models.GET_INFO_METHOD,
|
|
models.MAKE_INVOICE_METHOD,
|
|
models.LOOKUP_INVOICE_METHOD,
|
|
models.LIST_TRANSACTIONS_METHOD,
|
|
models.MULTI_PAY_INVOICE_METHOD,
|
|
models.PAY_METHOD,
|
|
models.RECEIVE_METHOD,
|
|
}
|
|
}
|
|
|
|
func (svc *PhoenixService) GetSupportedNIP47NotificationTypes() []string {
|
|
return []string{}
|
|
}
|
|
|
|
func (svc *PhoenixService) GetPubkey() string {
|
|
return svc.pubkey
|
|
}
|
|
|
|
func phoenixInvoiceToTransaction(invoiceRes *InvoiceResponse) (*lnclient.Transaction, error) {
|
|
var settledAt *int64
|
|
if invoiceRes.CompletedAt != 0 {
|
|
settledAtUnix := time.UnixMilli(invoiceRes.CompletedAt).Unix()
|
|
settledAt = &settledAtUnix
|
|
}
|
|
|
|
paymentRequest, err := decodepay.Decodepay(invoiceRes.Invoice)
|
|
if err != nil {
|
|
logger.Logger.WithFields(logrus.Fields{
|
|
"bolt11": invoiceRes.Invoice,
|
|
}).Errorf("Failed to decode bolt11 invoice: %v", err)
|
|
|
|
return nil, err
|
|
}
|
|
|
|
expiresAt := time.UnixMilli(int64(paymentRequest.CreatedAt) * 1000).Add(time.Duration(paymentRequest.Expiry) * time.Second).Unix()
|
|
|
|
return &lnclient.Transaction{
|
|
Type: "incoming",
|
|
Invoice: invoiceRes.Invoice,
|
|
Preimage: invoiceRes.Preimage,
|
|
PaymentHash: invoiceRes.PaymentHash,
|
|
AmountMsat: paymentRequest.MSatoshi,
|
|
FeesPaidMsat: invoiceRes.FeesSat * 1000,
|
|
CreatedAt: time.UnixMilli(invoiceRes.CreatedAt).Unix(),
|
|
Description: invoiceRes.Description,
|
|
SettledAt: settledAt,
|
|
ExpiresAt: &expiresAt,
|
|
DescriptionHash: paymentRequest.DescriptionHash,
|
|
}, nil
|
|
}
|
|
|
|
// outgoingPaymentToTransaction converts a Phoenixd OutgoingPaymentResponse
|
|
// to an lnclient.Transaction.
|
|
func outgoingPaymentToTransaction(payment *OutgoingPaymentResponse) (*lnclient.Transaction, error) {
|
|
var settledAt *int64
|
|
if payment.CompletedAt != 0 {
|
|
settledAtUnix := time.UnixMilli(payment.CompletedAt).Unix()
|
|
settledAt = &settledAtUnix
|
|
}
|
|
|
|
paymentRequest, err := decodepay.Decodepay(payment.Invoice)
|
|
if err != nil {
|
|
logger.Logger.WithFields(logrus.Fields{
|
|
"bolt11": payment.Invoice,
|
|
}).Errorf("Failed to decode bolt11 invoice: %v", err)
|
|
return nil, fmt.Errorf("decode phoenixd outgoing payment bolt11: %w", err)
|
|
}
|
|
|
|
expiresAt := time.UnixMilli(int64(paymentRequest.CreatedAt) * 1000).Add(time.Duration(paymentRequest.Expiry) * time.Second).Unix()
|
|
|
|
// unlike incoming payments, "fees" on outgoing payments is in millisats,
|
|
// and "sent" (in sats) includes the fees
|
|
amountMsat := paymentRequest.MSatoshi
|
|
if amountMsat == 0 {
|
|
amountMsat = payment.Sent*1000 - payment.Fees
|
|
}
|
|
|
|
return &lnclient.Transaction{
|
|
Type: "outgoing",
|
|
Invoice: payment.Invoice,
|
|
Preimage: payment.Preimage,
|
|
PaymentHash: payment.PaymentHash,
|
|
AmountMsat: amountMsat,
|
|
FeesPaidMsat: payment.Fees,
|
|
CreatedAt: time.UnixMilli(payment.CreatedAt).Unix(),
|
|
Description: paymentRequest.Description,
|
|
SettledAt: settledAt,
|
|
ExpiresAt: &expiresAt,
|
|
DescriptionHash: paymentRequest.DescriptionHash,
|
|
}, nil
|
|
}
|
|
|
|
func (svc *PhoenixService) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCommandDef {
|
|
return nil
|
|
}
|
|
|
|
func (svc *PhoenixService) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) {
|
|
return nil, lnclient.ErrUnknownCustomNodeCommand
|
|
}
|
|
|
|
func (svc *PhoenixService) MakeOffer(ctx context.Context, description string) (string, error) {
|
|
return "", errors.New("not supported")
|
|
}
|
|
|
|
func (svc *PhoenixService) ListOnchainTransactions(ctx context.Context) ([]lnclient.OnchainTransaction, error) {
|
|
return nil, errors.ErrUnsupported
|
|
}
|