mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
feat: add NWC-321 pay and receive methods with BOLT-11 support
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>
This commit is contained in:
parent
363c22f6d3
commit
c2ae53dd07
15 changed files with 893 additions and 9 deletions
|
|
@ -69,6 +69,10 @@ const (
|
|||
ERROR_NOT_FOUND = "NOT_FOUND"
|
||||
ERROR_UNSUPPORTED_ENCRYPTION = "UNSUPPORTED_ENCRYPTION"
|
||||
ERROR_OTHER = "OTHER"
|
||||
|
||||
// NWC-321 (BIP-321 payments) errors
|
||||
ERROR_UNSUPPORTED_PAYMENT_INSTRUCTION = "UNSUPPORTED_PAYMENT_INSTRUCTION"
|
||||
ERROR_UNSUPPORTED_NETWORK = "UNSUPPORTED_NETWORK"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import (
|
|||
"github.com/getAlby/hub/events"
|
||||
"github.com/getAlby/hub/lnclient"
|
||||
"github.com/getAlby/hub/logger"
|
||||
"github.com/getAlby/hub/nip47/models"
|
||||
"github.com/getAlby/hub/nip47/notifications"
|
||||
)
|
||||
|
||||
|
|
@ -639,7 +640,18 @@ func (bs *BarkService) GetPubkey() string {
|
|||
}
|
||||
|
||||
func (bs *BarkService) GetSupportedNIP47Methods() []string {
|
||||
return []string{"pay_invoice", "get_balance", "get_budget", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice"}
|
||||
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 (bs *BarkService) GetSupportedNIP47NotificationTypes() []string {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/getAlby/hub/constants"
|
||||
"github.com/getAlby/hub/lnclient"
|
||||
"github.com/getAlby/hub/logger"
|
||||
"github.com/getAlby/hub/nip47/models"
|
||||
decodepay "github.com/nbd-wtf/ln-decodepay"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
|
@ -408,7 +409,18 @@ func (cs *CashuService) checkOutgoingPayment(meltQuote *storage.MeltQuote) {
|
|||
}
|
||||
|
||||
func (cs *CashuService) GetSupportedNIP47Methods() []string {
|
||||
return []string{"pay_invoice", "get_balance", "get_budget", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice"}
|
||||
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 (cs *CashuService) GetSupportedNIP47NotificationTypes() []string {
|
||||
|
|
|
|||
|
|
@ -1458,6 +1458,8 @@ func (c *CLNService) GetSupportedNIP47Methods() []string {
|
|||
models.MULTI_PAY_INVOICE_METHOD,
|
||||
models.MULTI_PAY_KEYSEND_METHOD,
|
||||
models.SIGN_MESSAGE_METHOD,
|
||||
models.PAY_METHOD,
|
||||
models.RECEIVE_METHOD,
|
||||
}
|
||||
|
||||
if c.holdEnabled {
|
||||
|
|
|
|||
|
|
@ -2159,6 +2159,8 @@ func (ls *LDKService) GetSupportedNIP47Methods() []string {
|
|||
models.MAKE_HOLD_INVOICE_METHOD,
|
||||
models.SETTLE_HOLD_INVOICE_METHOD,
|
||||
models.CANCEL_HOLD_INVOICE_METHOD,
|
||||
models.PAY_METHOD,
|
||||
models.RECEIVE_METHOD,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1510,6 +1510,8 @@ func (svc *LNDService) GetSupportedNIP47Methods() []string {
|
|||
models.MAKE_HOLD_INVOICE_METHOD,
|
||||
models.SETTLE_HOLD_INVOICE_METHOD,
|
||||
models.CANCEL_HOLD_INVOICE_METHOD,
|
||||
models.PAY_METHOD,
|
||||
models.RECEIVE_METHOD,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import (
|
|||
|
||||
"github.com/getAlby/hub/lnclient"
|
||||
"github.com/getAlby/hub/logger"
|
||||
"github.com/getAlby/hub/nip47/models"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
|
@ -490,7 +491,18 @@ func (svc *PhoenixService) UpdateChannel(ctx context.Context, updateChannelReque
|
|||
}
|
||||
|
||||
func (svc *PhoenixService) GetSupportedNIP47Methods() []string {
|
||||
return []string{"pay_invoice", "get_balance", "get_budget", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice"}
|
||||
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 {
|
||||
|
|
|
|||
326
nip47/controllers/pay_controller.go
Normal file
326
nip47/controllers/pay_controller.go
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/getAlby/go-nostr"
|
||||
"github.com/getAlby/hub/constants"
|
||||
"github.com/getAlby/hub/db"
|
||||
"github.com/getAlby/hub/logger"
|
||||
"github.com/getAlby/hub/nip47/models"
|
||||
decodepay "github.com/nbd-wtf/ln-decodepay"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const instructionTypeBolt11 = "bolt11"
|
||||
|
||||
type payParams struct {
|
||||
Payment string `json:"payment"`
|
||||
Amount *uint64 `json:"amount"`
|
||||
PayerNote string `json:"payer_note"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type payResult struct {
|
||||
TransactionId string `json:"transaction_id"`
|
||||
State string `json:"state"`
|
||||
InstructionType string `json:"instruction_type"`
|
||||
Amount uint64 `json:"amount"`
|
||||
FeesPaid uint64 `json:"fees_paid"`
|
||||
PaymentHash string `json:"payment_hash,omitempty"`
|
||||
Preimage string `json:"preimage,omitempty"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
SettledAt *int64 `json:"settled_at,omitempty"`
|
||||
}
|
||||
|
||||
// parsed payment instructions from a BIP-321 URI
|
||||
type bip321Payment struct {
|
||||
bolt11 string
|
||||
// from the BIP-321 "amount" parameter (BTC), converted to msat
|
||||
amountMsat *uint64
|
||||
}
|
||||
|
||||
// HandlePayEvent handles the NWC-321 pay method. Currently only BOLT-11
|
||||
// instructions (the "lightning" URI parameter) are supported.
|
||||
func (controller *nip47Controller) HandlePayEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, app *db.App, publishResponse publishFunc) {
|
||||
payParams := &payParams{}
|
||||
resp := decodeRequest(nip47Request, payParams)
|
||||
if resp != nil {
|
||||
publishResponse(resp, nostr.Tags{})
|
||||
return
|
||||
}
|
||||
|
||||
publishError := func(nip47Error *models.Error) {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"request_event_id": requestEventId,
|
||||
"app_id": app.ID,
|
||||
"payment": payParams.Payment,
|
||||
"code": nip47Error.Code,
|
||||
}).Error(nip47Error.Message)
|
||||
|
||||
publishResponse(&models.Response{
|
||||
ResultType: nip47Request.Method,
|
||||
Error: nip47Error,
|
||||
}, nostr.Tags{})
|
||||
}
|
||||
|
||||
// BOLT-11 does not support payer-provided messages, and per NWC-321 the
|
||||
// note must either be delivered or the request rejected before payment
|
||||
if payParams.PayerNote != "" {
|
||||
publishError(&models.Error{
|
||||
Code: constants.ERROR_BAD_REQUEST,
|
||||
Message: "payer_note is not supported: only BOLT-11 payments are supported",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
bip321, nip47Error := parseBip321Uri(payParams.Payment)
|
||||
if nip47Error != nil {
|
||||
publishError(nip47Error)
|
||||
return
|
||||
}
|
||||
|
||||
bolt11 := strings.ToLower(bip321.bolt11)
|
||||
paymentRequest, err := decodepay.Decodepay(bolt11)
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"request_event_id": requestEventId,
|
||||
"app_id": app.ID,
|
||||
"bolt11": bolt11,
|
||||
}).WithError(err).Error("Failed to decode bolt11 invoice")
|
||||
|
||||
publishError(&models.Error{
|
||||
Code: constants.ERROR_BAD_REQUEST,
|
||||
Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// the invoice must be for the network this node runs on
|
||||
nodeInfo, err := controller.lnClient.GetInfo(ctx)
|
||||
if err != nil {
|
||||
publishError(&models.Error{
|
||||
Code: constants.ERROR_INTERNAL,
|
||||
Message: fmt.Sprintf("Failed to get node info: %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
expectedPrefix := networkToInvoicePrefix(nodeInfo.Network)
|
||||
if expectedPrefix != "" && !strings.EqualFold(paymentRequest.Currency, expectedPrefix) {
|
||||
publishError(&models.Error{
|
||||
Code: constants.ERROR_UNSUPPORTED_NETWORK,
|
||||
Message: fmt.Sprintf("the payment instruction is for a different network than the wallet network (%s)", nodeInfo.Network),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
amountMsat, nip47Error := resolvePayAmount(uint64(paymentRequest.MSatoshi), payParams.Amount, bip321.amountMsat)
|
||||
if nip47Error != nil {
|
||||
publishError(nip47Error)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"request_event_id": requestEventId,
|
||||
"app_id": app.ID,
|
||||
"bolt11": bolt11,
|
||||
}).Info("Sending payment")
|
||||
|
||||
transaction, err := controller.transactionsService.SendPaymentSync(bolt11, amountMsat, payParams.Metadata, controller.lnClient, &app.ID, &requestEventId)
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"request_event_id": requestEventId,
|
||||
"app_id": app.ID,
|
||||
"bolt11": bolt11,
|
||||
}).WithError(err).Error("Failed to send payment")
|
||||
|
||||
publishResponse(&models.Response{
|
||||
ResultType: nip47Request.Method,
|
||||
Error: mapNip47Error(err),
|
||||
}, nostr.Tags{})
|
||||
return
|
||||
}
|
||||
|
||||
var settledAt *int64
|
||||
if transaction.SettledAt != nil {
|
||||
settledAtUnix := transaction.SettledAt.Unix()
|
||||
settledAt = &settledAtUnix
|
||||
}
|
||||
|
||||
publishResponse(&models.Response{
|
||||
ResultType: nip47Request.Method,
|
||||
Result: payResult{
|
||||
TransactionId: transaction.PaymentHash,
|
||||
State: strings.ToLower(transaction.State),
|
||||
InstructionType: instructionTypeBolt11,
|
||||
Amount: transaction.AmountMsat,
|
||||
FeesPaid: transaction.FeeMsat,
|
||||
PaymentHash: transaction.PaymentHash,
|
||||
Preimage: *transaction.Preimage,
|
||||
CreatedAt: transaction.CreatedAt.Unix(),
|
||||
SettledAt: settledAt,
|
||||
},
|
||||
}, nostr.Tags{})
|
||||
}
|
||||
|
||||
// parseBip321Uri parses a BIP-321 URI and returns the BOLT-11 invoice from its
|
||||
// "lightning" parameter and the optional "amount" parameter, or a NIP-47 error
|
||||
// if the URI is invalid or contains no supported payment instruction.
|
||||
func parseBip321Uri(payment string) (*bip321Payment, *models.Error) {
|
||||
parsed, err := url.Parse(payment)
|
||||
if err != nil || !strings.EqualFold(parsed.Scheme, "bitcoin") {
|
||||
return nil, &models.Error{
|
||||
Code: constants.ERROR_BAD_REQUEST,
|
||||
Message: "payment must be a valid BIP-321 URI",
|
||||
}
|
||||
}
|
||||
|
||||
query, err := url.ParseQuery(parsed.RawQuery)
|
||||
if err != nil {
|
||||
return nil, &models.Error{
|
||||
Code: constants.ERROR_BAD_REQUEST,
|
||||
Message: "payment must be a valid BIP-321 URI",
|
||||
}
|
||||
}
|
||||
|
||||
result := &bip321Payment{}
|
||||
// BIP-321 URIs may be fully uppercased (e.g. for QR codes)
|
||||
for key, values := range query {
|
||||
// per BIP-321, a URI with an unknown required ("req-" prefixed)
|
||||
// parameter must be considered invalid. This includes "req-pop",
|
||||
// as we cannot open proof-of-payment callbacks. The optional "pop"
|
||||
// and other optional parameters we do not understand are ignored,
|
||||
// as BIP-321 permits.
|
||||
if strings.HasPrefix(strings.ToLower(key), "req-") {
|
||||
return nil, &models.Error{
|
||||
Code: constants.ERROR_BAD_REQUEST,
|
||||
Message: fmt.Sprintf("unsupported required parameter in BIP-321 URI: %s", key),
|
||||
}
|
||||
}
|
||||
if strings.EqualFold(key, "lightning") && len(values) > 0 && values[0] != "" {
|
||||
result.bolt11 = values[0]
|
||||
}
|
||||
if strings.EqualFold(key, "amount") && len(values) > 0 && values[0] != "" {
|
||||
amountMsat, err := parseBtcAmountToMsat(values[0])
|
||||
if err != nil {
|
||||
return nil, &models.Error{
|
||||
Code: constants.ERROR_BAD_REQUEST,
|
||||
Message: fmt.Sprintf("invalid amount parameter in BIP-321 URI: %s", values[0]),
|
||||
}
|
||||
}
|
||||
result.amountMsat = &amountMsat
|
||||
}
|
||||
}
|
||||
|
||||
if result.bolt11 == "" {
|
||||
return nil, &models.Error{
|
||||
Code: constants.ERROR_UNSUPPORTED_PAYMENT_INSTRUCTION,
|
||||
Message: "no supported payment instruction found: only BOLT-11 (lightning) is supported",
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// resolvePayAmount validates the invoice amount against the amounts provided
|
||||
// in the request params and the BIP-321 URI, per NWC-321: "The wallet service
|
||||
// MUST reject conflicting or invalid amounts before payment." It returns the
|
||||
// amount to pay for a zero-amount invoice, or nil if the invoice has one.
|
||||
func resolvePayAmount(invoiceMsat uint64, paramAmountMsat *uint64, uriAmountMsat *uint64) (*uint64, *models.Error) {
|
||||
if paramAmountMsat != nil && *paramAmountMsat == 0 {
|
||||
return nil, &models.Error{
|
||||
Code: constants.ERROR_BAD_REQUEST,
|
||||
Message: "amount must be greater than 0",
|
||||
}
|
||||
}
|
||||
if uriAmountMsat != nil && *uriAmountMsat == 0 {
|
||||
return nil, &models.Error{
|
||||
Code: constants.ERROR_BAD_REQUEST,
|
||||
Message: "amount parameter in BIP-321 URI must be greater than 0",
|
||||
}
|
||||
}
|
||||
|
||||
if invoiceMsat > 0 {
|
||||
if paramAmountMsat != nil && *paramAmountMsat != invoiceMsat {
|
||||
return nil, &models.Error{
|
||||
Code: constants.ERROR_BAD_REQUEST,
|
||||
Message: "amount conflicts with the amount of the payment instruction",
|
||||
}
|
||||
}
|
||||
if uriAmountMsat != nil && *uriAmountMsat != invoiceMsat {
|
||||
return nil, &models.Error{
|
||||
Code: constants.ERROR_BAD_REQUEST,
|
||||
Message: "amount parameter in BIP-321 URI conflicts with the amount of the payment instruction",
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if paramAmountMsat != nil && uriAmountMsat != nil && *paramAmountMsat != *uriAmountMsat {
|
||||
return nil, &models.Error{
|
||||
Code: constants.ERROR_BAD_REQUEST,
|
||||
Message: "amount conflicts with the amount parameter in the BIP-321 URI",
|
||||
}
|
||||
}
|
||||
amountMsat := paramAmountMsat
|
||||
if amountMsat == nil {
|
||||
amountMsat = uriAmountMsat
|
||||
}
|
||||
if amountMsat == nil {
|
||||
return nil, &models.Error{
|
||||
Code: constants.ERROR_BAD_REQUEST,
|
||||
Message: "amount is required when the payment instruction has no amount",
|
||||
}
|
||||
}
|
||||
return amountMsat, nil
|
||||
}
|
||||
|
||||
// parseBtcAmountToMsat converts a BIP-321 decimal BTC amount (e.g. "0.00000123")
|
||||
// to millisatoshis.
|
||||
func parseBtcAmountToMsat(value string) (uint64, error) {
|
||||
intPart, fracPart, _ := strings.Cut(value, ".")
|
||||
if intPart == "" && fracPart == "" {
|
||||
return 0, fmt.Errorf("empty amount")
|
||||
}
|
||||
if intPart == "" {
|
||||
intPart = "0"
|
||||
}
|
||||
// millisatoshi precision is 11 decimal places of a bitcoin
|
||||
if len(fracPart) > 11 {
|
||||
return 0, fmt.Errorf("too many decimal places")
|
||||
}
|
||||
fracPart = fracPart + strings.Repeat("0", 11-len(fracPart))
|
||||
|
||||
whole, err := strconv.ParseUint(intPart, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if whole > 21_000_000 {
|
||||
return 0, fmt.Errorf("amount too large")
|
||||
}
|
||||
frac, err := strconv.ParseUint(fracPart, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return whole*100_000_000_000 + frac, nil
|
||||
}
|
||||
|
||||
// networkToInvoicePrefix maps an LNClient network name to the BOLT-11 invoice
|
||||
// human-readable-part network prefix. Returns "" for unknown network names.
|
||||
func networkToInvoicePrefix(network string) string {
|
||||
switch strings.ToLower(network) {
|
||||
case "bitcoin", "mainnet":
|
||||
return "bc"
|
||||
case "testnet", "testnet3", "testnet4":
|
||||
return "tb"
|
||||
case "signet", "mutinynet":
|
||||
return "tbs"
|
||||
case "regtest":
|
||||
return "bcrt"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
324
nip47/controllers/pay_controller_test.go
Normal file
324
nip47/controllers/pay_controller_test.go
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/getAlby/go-nostr"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/getAlby/hub/constants"
|
||||
"github.com/getAlby/hub/db"
|
||||
"github.com/getAlby/hub/nip47/models"
|
||||
"github.com/getAlby/hub/tests"
|
||||
)
|
||||
|
||||
const nip47PayJson = `
|
||||
{
|
||||
"method": "pay",
|
||||
"params": {
|
||||
"payment": "bitcoin:?lightning=` + tests.MockInvoice + `"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const nip47PayZeroAmountNoAmountJson = `
|
||||
{
|
||||
"method": "pay",
|
||||
"params": {
|
||||
"payment": "bitcoin:?lightning=` + tests.MockZeroAmountInvoice + `"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const nip47PayBolt12Json = `
|
||||
{
|
||||
"method": "pay",
|
||||
"params": {
|
||||
"payment": "bitcoin:?lno=lno1zrxq8pjw7qjlm68mtp7e3yvxee4y5xrgjhhyf2fxhlphpckrvevh50u0qf"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const nip47PayInvalidUriJson = `
|
||||
{
|
||||
"method": "pay",
|
||||
"params": {
|
||||
"payment": "lightning:lnbc123"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const nip47PayPayerNoteJson = `
|
||||
{
|
||||
"method": "pay",
|
||||
"params": {
|
||||
"payment": "bitcoin:?lightning=` + tests.MockInvoice + `",
|
||||
"payer_note": "hello"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const nip47PayRequiredParamJson = `
|
||||
{
|
||||
"method": "pay",
|
||||
"params": {
|
||||
"payment": "bitcoin:?lightning=` + tests.MockInvoice + `&req-something=1"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func setupPayTest(t *testing.T) (*tests.TestService, *db.App, *db.RequestEvent) {
|
||||
svc, err := tests.CreateTestService(t)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(svc.Remove)
|
||||
|
||||
app, _, err := tests.CreateApp(svc)
|
||||
require.NoError(t, err)
|
||||
|
||||
appPermission := &db.AppPermission{
|
||||
AppId: app.ID,
|
||||
App: *app,
|
||||
Scope: constants.PAY_INVOICE_SCOPE,
|
||||
}
|
||||
err = svc.DB.Create(appPermission).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
dbRequestEvent := &db.RequestEvent{}
|
||||
err = svc.DB.Create(&dbRequestEvent).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
return svc, app, dbRequestEvent
|
||||
}
|
||||
|
||||
func TestHandlePayEvent(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
svc, app, dbRequestEvent := setupPayTest(t)
|
||||
|
||||
nip47Request := &models.Request{}
|
||||
err := json.Unmarshal([]byte(nip47PayJson), nip47Request)
|
||||
require.NoError(t, err)
|
||||
|
||||
var publishedResponse *models.Response
|
||||
|
||||
publishResponse := func(response *models.Response, tags nostr.Tags) {
|
||||
publishedResponse = response
|
||||
}
|
||||
|
||||
NewTestNip47Controller(svc).
|
||||
HandlePayEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
|
||||
|
||||
require.Nil(t, publishedResponse.Error)
|
||||
result := publishedResponse.Result.(payResult)
|
||||
assert.Equal(t, "settled", result.State)
|
||||
assert.Equal(t, "bolt11", result.InstructionType)
|
||||
assert.Equal(t, "123preimage", result.Preimage)
|
||||
assert.Equal(t, tests.MockPaymentHash, result.PaymentHash)
|
||||
assert.Equal(t, tests.MockPaymentHash, result.TransactionId)
|
||||
assert.Equal(t, uint64(123000), result.Amount)
|
||||
assert.NotNil(t, result.SettledAt)
|
||||
}
|
||||
|
||||
func TestHandlePayEvent_ZeroAmountInvoiceWithoutAmount(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
svc, app, dbRequestEvent := setupPayTest(t)
|
||||
|
||||
nip47Request := &models.Request{}
|
||||
err := json.Unmarshal([]byte(nip47PayZeroAmountNoAmountJson), nip47Request)
|
||||
require.NoError(t, err)
|
||||
|
||||
var publishedResponse *models.Response
|
||||
|
||||
publishResponse := func(response *models.Response, tags nostr.Tags) {
|
||||
publishedResponse = response
|
||||
}
|
||||
|
||||
NewTestNip47Controller(svc).
|
||||
HandlePayEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
|
||||
|
||||
assert.Nil(t, publishedResponse.Result)
|
||||
assert.Equal(t, constants.ERROR_BAD_REQUEST, publishedResponse.Error.Code)
|
||||
}
|
||||
|
||||
func TestHandlePayEvent_Bolt12Unsupported(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
svc, app, dbRequestEvent := setupPayTest(t)
|
||||
|
||||
nip47Request := &models.Request{}
|
||||
err := json.Unmarshal([]byte(nip47PayBolt12Json), nip47Request)
|
||||
require.NoError(t, err)
|
||||
|
||||
var publishedResponse *models.Response
|
||||
|
||||
publishResponse := func(response *models.Response, tags nostr.Tags) {
|
||||
publishedResponse = response
|
||||
}
|
||||
|
||||
NewTestNip47Controller(svc).
|
||||
HandlePayEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
|
||||
|
||||
assert.Nil(t, publishedResponse.Result)
|
||||
assert.Equal(t, constants.ERROR_UNSUPPORTED_PAYMENT_INSTRUCTION, publishedResponse.Error.Code)
|
||||
}
|
||||
|
||||
func runPayTest(t *testing.T, requestJson string) *models.Response {
|
||||
t.Helper()
|
||||
ctx := context.TODO()
|
||||
svc, app, dbRequestEvent := setupPayTest(t)
|
||||
|
||||
nip47Request := &models.Request{}
|
||||
err := json.Unmarshal([]byte(requestJson), nip47Request)
|
||||
require.NoError(t, err)
|
||||
|
||||
var publishedResponse *models.Response
|
||||
|
||||
publishResponse := func(response *models.Response, tags nostr.Tags) {
|
||||
publishedResponse = response
|
||||
}
|
||||
|
||||
NewTestNip47Controller(svc).
|
||||
HandlePayEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
|
||||
|
||||
require.NotNil(t, publishedResponse)
|
||||
return publishedResponse
|
||||
}
|
||||
|
||||
func TestHandlePayEvent_WrongNetwork(t *testing.T) {
|
||||
// regtest invoice (lnbcrt...) on a signet mock node
|
||||
response := runPayTest(t, `
|
||||
{
|
||||
"method": "pay",
|
||||
"params": {
|
||||
"payment": "bitcoin:?lightning=lnbcrt5u1pjuywzppp5h69dt59cypca2wxu69sw8ga0g39a3yx7dqug5nthrw3rcqgfdu4qdqqcqzzsxqyz5vqsp5gzlpzszyj2k30qmpme7jsfzr24wqlvt9xdmr7ay34lfelz050krs9qyyssq038x07nh8yuv8hdpjh5y8kqp7zcd62ql9na9xh7pla44htjyy02sz23q7qm2tza6ct4ypljk54w9k9qsrsu95usk8ce726ytep6vhhsq9mhf9a"
|
||||
}
|
||||
}
|
||||
`)
|
||||
assert.Nil(t, response.Result)
|
||||
assert.Equal(t, constants.ERROR_UNSUPPORTED_NETWORK, response.Error.Code)
|
||||
}
|
||||
|
||||
func TestHandlePayEvent_ConflictingParamAmount(t *testing.T) {
|
||||
// MockInvoice has an amount of 123000 msat
|
||||
response := runPayTest(t, `
|
||||
{
|
||||
"method": "pay",
|
||||
"params": {
|
||||
"payment": "bitcoin:?lightning=`+tests.MockInvoice+`",
|
||||
"amount": 999
|
||||
}
|
||||
}
|
||||
`)
|
||||
assert.Nil(t, response.Result)
|
||||
assert.Equal(t, constants.ERROR_BAD_REQUEST, response.Error.Code)
|
||||
}
|
||||
|
||||
func TestHandlePayEvent_ConflictingUriAmount(t *testing.T) {
|
||||
// MockInvoice has an amount of 123000 msat, URI says 124000 msat
|
||||
response := runPayTest(t, `
|
||||
{
|
||||
"method": "pay",
|
||||
"params": {
|
||||
"payment": "bitcoin:?lightning=`+tests.MockInvoice+`&amount=0.00000124"
|
||||
}
|
||||
}
|
||||
`)
|
||||
assert.Nil(t, response.Result)
|
||||
assert.Equal(t, constants.ERROR_BAD_REQUEST, response.Error.Code)
|
||||
}
|
||||
|
||||
func TestHandlePayEvent_MatchingUriAmount(t *testing.T) {
|
||||
// MockInvoice has an amount of 123000 msat = 0.00000123 BTC
|
||||
response := runPayTest(t, `
|
||||
{
|
||||
"method": "pay",
|
||||
"params": {
|
||||
"payment": "bitcoin:?lightning=`+tests.MockInvoice+`&amount=0.00000123"
|
||||
}
|
||||
}
|
||||
`)
|
||||
require.Nil(t, response.Error)
|
||||
result := response.Result.(payResult)
|
||||
assert.Equal(t, "settled", result.State)
|
||||
assert.Equal(t, uint64(123000), result.Amount)
|
||||
}
|
||||
|
||||
func TestHandlePayEvent_ZeroAmountInvoiceWithUriAmount(t *testing.T) {
|
||||
// zero-amount invoice funded by the BIP-321 amount parameter (1234 msat)
|
||||
response := runPayTest(t, `
|
||||
{
|
||||
"method": "pay",
|
||||
"params": {
|
||||
"payment": "bitcoin:?lightning=`+tests.MockZeroAmountInvoice+`&amount=0.00000001234"
|
||||
}
|
||||
}
|
||||
`)
|
||||
require.Nil(t, response.Error)
|
||||
result := response.Result.(payResult)
|
||||
assert.Equal(t, "settled", result.State)
|
||||
assert.Equal(t, uint64(1234), result.Amount)
|
||||
}
|
||||
|
||||
func TestHandlePayEvent_PayerNoteUnsupported(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
svc, app, dbRequestEvent := setupPayTest(t)
|
||||
|
||||
nip47Request := &models.Request{}
|
||||
err := json.Unmarshal([]byte(nip47PayPayerNoteJson), nip47Request)
|
||||
require.NoError(t, err)
|
||||
|
||||
var publishedResponse *models.Response
|
||||
|
||||
publishResponse := func(response *models.Response, tags nostr.Tags) {
|
||||
publishedResponse = response
|
||||
}
|
||||
|
||||
NewTestNip47Controller(svc).
|
||||
HandlePayEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
|
||||
|
||||
assert.Nil(t, publishedResponse.Result)
|
||||
assert.Equal(t, constants.ERROR_BAD_REQUEST, publishedResponse.Error.Code)
|
||||
}
|
||||
|
||||
func TestHandlePayEvent_UnknownRequiredParam(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
svc, app, dbRequestEvent := setupPayTest(t)
|
||||
|
||||
nip47Request := &models.Request{}
|
||||
err := json.Unmarshal([]byte(nip47PayRequiredParamJson), nip47Request)
|
||||
require.NoError(t, err)
|
||||
|
||||
var publishedResponse *models.Response
|
||||
|
||||
publishResponse := func(response *models.Response, tags nostr.Tags) {
|
||||
publishedResponse = response
|
||||
}
|
||||
|
||||
NewTestNip47Controller(svc).
|
||||
HandlePayEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
|
||||
|
||||
assert.Nil(t, publishedResponse.Result)
|
||||
assert.Equal(t, constants.ERROR_BAD_REQUEST, publishedResponse.Error.Code)
|
||||
}
|
||||
|
||||
func TestHandlePayEvent_InvalidUri(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
svc, app, dbRequestEvent := setupPayTest(t)
|
||||
|
||||
nip47Request := &models.Request{}
|
||||
err := json.Unmarshal([]byte(nip47PayInvalidUriJson), nip47Request)
|
||||
require.NoError(t, err)
|
||||
|
||||
var publishedResponse *models.Response
|
||||
|
||||
publishResponse := func(response *models.Response, tags nostr.Tags) {
|
||||
publishedResponse = response
|
||||
}
|
||||
|
||||
NewTestNip47Controller(svc).
|
||||
HandlePayEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
|
||||
|
||||
assert.Nil(t, publishedResponse.Result)
|
||||
assert.Equal(t, constants.ERROR_BAD_REQUEST, publishedResponse.Error.Code)
|
||||
}
|
||||
76
nip47/controllers/receive_controller.go
Normal file
76
nip47/controllers/receive_controller.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/getAlby/go-nostr"
|
||||
"github.com/getAlby/hub/constants"
|
||||
"github.com/getAlby/hub/logger"
|
||||
"github.com/getAlby/hub/nip47/models"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type receiveParams struct {
|
||||
Amount *uint64 `json:"amount"`
|
||||
Description string `json:"description"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type receiveResult struct {
|
||||
Bip321 string `json:"bip321"`
|
||||
TransactionId string `json:"transaction_id,omitempty"`
|
||||
}
|
||||
|
||||
// HandleReceiveEvent handles the NWC-321 receive method. Currently only
|
||||
// BOLT-11 instructions (the "lightning" URI parameter) are returned.
|
||||
func (controller *nip47Controller) HandleReceiveEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, appId uint, publishResponse publishFunc) {
|
||||
receiveParams := &receiveParams{}
|
||||
resp := decodeRequest(nip47Request, receiveParams)
|
||||
if resp != nil {
|
||||
publishResponse(resp, nostr.Tags{})
|
||||
return
|
||||
}
|
||||
|
||||
if receiveParams.Amount == nil {
|
||||
// variable-amount (zero-amount) invoices are not supported
|
||||
publishResponse(&models.Response{
|
||||
ResultType: nip47Request.Method,
|
||||
Error: &models.Error{
|
||||
Code: constants.ERROR_BAD_REQUEST,
|
||||
Message: "amount is required",
|
||||
},
|
||||
}, nostr.Tags{})
|
||||
return
|
||||
}
|
||||
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"request_event_id": requestEventId,
|
||||
"app_id": appId,
|
||||
"amount": *receiveParams.Amount,
|
||||
"description": receiveParams.Description,
|
||||
}).Debug("Handling receive request")
|
||||
|
||||
transaction, err := controller.transactionsService.MakeInvoice(ctx, *receiveParams.Amount, receiveParams.Description, "", 0, receiveParams.Metadata, controller.lnClient, &appId, &requestEventId, nil)
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"request_event_id": requestEventId,
|
||||
"app_id": appId,
|
||||
"amount": *receiveParams.Amount,
|
||||
"description": receiveParams.Description,
|
||||
}).Infof("Failed to make invoice: %v", err)
|
||||
|
||||
publishResponse(&models.Response{
|
||||
ResultType: nip47Request.Method,
|
||||
Error: mapNip47Error(err),
|
||||
}, nostr.Tags{})
|
||||
return
|
||||
}
|
||||
|
||||
publishResponse(&models.Response{
|
||||
ResultType: nip47Request.Method,
|
||||
Result: receiveResult{
|
||||
Bip321: "bitcoin:?lightning=" + transaction.PaymentRequest,
|
||||
TransactionId: transaction.PaymentHash,
|
||||
},
|
||||
}, nostr.Tags{})
|
||||
}
|
||||
102
nip47/controllers/receive_controller_test.go
Normal file
102
nip47/controllers/receive_controller_test.go
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/getAlby/go-nostr"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/getAlby/hub/constants"
|
||||
"github.com/getAlby/hub/db"
|
||||
"github.com/getAlby/hub/nip47/models"
|
||||
"github.com/getAlby/hub/tests"
|
||||
)
|
||||
|
||||
const nip47ReceiveJson = `
|
||||
{
|
||||
"method": "receive",
|
||||
"params": {
|
||||
"amount": 123000,
|
||||
"description": "test receive"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const nip47ReceiveNoAmountJson = `
|
||||
{
|
||||
"method": "receive",
|
||||
"params": {
|
||||
"description": "test receive"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func setupReceiveTest(t *testing.T) (*tests.TestService, *db.App, *db.RequestEvent) {
|
||||
svc, err := tests.CreateTestService(t)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(svc.Remove)
|
||||
|
||||
app, _, err := tests.CreateApp(svc)
|
||||
require.NoError(t, err)
|
||||
|
||||
appPermission := &db.AppPermission{
|
||||
AppId: app.ID,
|
||||
App: *app,
|
||||
Scope: constants.MAKE_INVOICE_SCOPE,
|
||||
}
|
||||
err = svc.DB.Create(appPermission).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
dbRequestEvent := &db.RequestEvent{}
|
||||
err = svc.DB.Create(&dbRequestEvent).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
return svc, app, dbRequestEvent
|
||||
}
|
||||
|
||||
func TestHandleReceiveEvent(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
svc, app, dbRequestEvent := setupReceiveTest(t)
|
||||
|
||||
nip47Request := &models.Request{}
|
||||
err := json.Unmarshal([]byte(nip47ReceiveJson), nip47Request)
|
||||
require.NoError(t, err)
|
||||
|
||||
var publishedResponse *models.Response
|
||||
|
||||
publishResponse := func(response *models.Response, tags nostr.Tags) {
|
||||
publishedResponse = response
|
||||
}
|
||||
|
||||
NewTestNip47Controller(svc).
|
||||
HandleReceiveEvent(ctx, nip47Request, dbRequestEvent.ID, app.ID, publishResponse)
|
||||
|
||||
require.Nil(t, publishedResponse.Error)
|
||||
result := publishedResponse.Result.(receiveResult)
|
||||
assert.Equal(t, "bitcoin:?lightning="+tests.MockInvoice, result.Bip321)
|
||||
assert.Equal(t, tests.MockPaymentHash, result.TransactionId)
|
||||
}
|
||||
|
||||
func TestHandleReceiveEvent_NoAmount(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
svc, app, dbRequestEvent := setupReceiveTest(t)
|
||||
|
||||
nip47Request := &models.Request{}
|
||||
err := json.Unmarshal([]byte(nip47ReceiveNoAmountJson), nip47Request)
|
||||
require.NoError(t, err)
|
||||
|
||||
var publishedResponse *models.Response
|
||||
|
||||
publishResponse := func(response *models.Response, tags nostr.Tags) {
|
||||
publishedResponse = response
|
||||
}
|
||||
|
||||
NewTestNip47Controller(svc).
|
||||
HandleReceiveEvent(ctx, nip47Request, dbRequestEvent.ID, app.ID, publishResponse)
|
||||
|
||||
assert.Nil(t, publishedResponse.Result)
|
||||
assert.Equal(t, constants.ERROR_BAD_REQUEST, publishedResponse.Error.Code)
|
||||
}
|
||||
|
|
@ -438,6 +438,12 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, pool nostrmodels.Simpl
|
|||
case models.SETTLE_HOLD_INVOICE_METHOD:
|
||||
controller.
|
||||
HandleSettleHoldInvoiceEvent(ctx, nip47Request, requestEvent.ID, app.ID, publishResponse)
|
||||
case models.PAY_METHOD:
|
||||
controller.
|
||||
HandlePayEvent(ctx, nip47Request, requestEvent.ID, &app, publishResponse)
|
||||
case models.RECEIVE_METHOD:
|
||||
controller.
|
||||
HandleReceiveEvent(ctx, nip47Request, requestEvent.ID, app.ID, publishResponse)
|
||||
default:
|
||||
publishResponse(&models.Response{
|
||||
ResultType: nip47Request.Method,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ const (
|
|||
MAKE_HOLD_INVOICE_METHOD = "make_hold_invoice"
|
||||
CANCEL_HOLD_INVOICE_METHOD = "cancel_hold_invoice"
|
||||
SETTLE_HOLD_INVOICE_METHOD = "settle_hold_invoice"
|
||||
PAY_METHOD = "pay"
|
||||
RECEIVE_METHOD = "receive"
|
||||
)
|
||||
|
||||
type Transaction struct {
|
||||
|
|
|
|||
|
|
@ -117,13 +117,13 @@ func scopesToRequestMethods(scopes []string) []string {
|
|||
func scopeToRequestMethods(scope string) []string {
|
||||
switch scope {
|
||||
case constants.PAY_INVOICE_SCOPE:
|
||||
return []string{models.PAY_INVOICE_METHOD, models.PAY_KEYSEND_METHOD, models.MULTI_PAY_INVOICE_METHOD, models.MULTI_PAY_KEYSEND_METHOD}
|
||||
return []string{models.PAY_INVOICE_METHOD, models.PAY_KEYSEND_METHOD, models.MULTI_PAY_INVOICE_METHOD, models.MULTI_PAY_KEYSEND_METHOD, models.PAY_METHOD}
|
||||
case constants.GET_BALANCE_SCOPE:
|
||||
return []string{models.GET_BALANCE_METHOD}
|
||||
case constants.GET_INFO_SCOPE:
|
||||
return []string{models.GET_INFO_METHOD}
|
||||
case constants.MAKE_INVOICE_SCOPE:
|
||||
return []string{models.MAKE_INVOICE_METHOD, models.MAKE_HOLD_INVOICE_METHOD, models.SETTLE_HOLD_INVOICE_METHOD, models.CANCEL_HOLD_INVOICE_METHOD}
|
||||
return []string{models.MAKE_INVOICE_METHOD, models.MAKE_HOLD_INVOICE_METHOD, models.SETTLE_HOLD_INVOICE_METHOD, models.CANCEL_HOLD_INVOICE_METHOD, models.RECEIVE_METHOD}
|
||||
case constants.LOOKUP_INVOICE_SCOPE:
|
||||
return []string{models.LOOKUP_INVOICE_METHOD}
|
||||
case constants.LIST_TRANSACTIONS_SCOPE:
|
||||
|
|
@ -153,7 +153,7 @@ func RequestMethodsToScopes(requestMethods []string) ([]string, error) {
|
|||
|
||||
func RequestMethodToScope(requestMethod string) (string, error) {
|
||||
switch requestMethod {
|
||||
case models.PAY_INVOICE_METHOD, models.PAY_KEYSEND_METHOD, models.MULTI_PAY_INVOICE_METHOD, models.MULTI_PAY_KEYSEND_METHOD:
|
||||
case models.PAY_INVOICE_METHOD, models.PAY_KEYSEND_METHOD, models.MULTI_PAY_INVOICE_METHOD, models.MULTI_PAY_KEYSEND_METHOD, models.PAY_METHOD:
|
||||
return constants.PAY_INVOICE_SCOPE, nil
|
||||
case models.GET_BALANCE_METHOD:
|
||||
return constants.GET_BALANCE_SCOPE, nil
|
||||
|
|
@ -169,7 +169,7 @@ func RequestMethodToScope(requestMethod string) (string, error) {
|
|||
return constants.LIST_TRANSACTIONS_SCOPE, nil
|
||||
case models.SIGN_MESSAGE_METHOD:
|
||||
return constants.SIGN_MESSAGE_SCOPE, nil
|
||||
case models.MAKE_HOLD_INVOICE_METHOD, models.SETTLE_HOLD_INVOICE_METHOD, models.CANCEL_HOLD_INVOICE_METHOD:
|
||||
case models.MAKE_HOLD_INVOICE_METHOD, models.SETTLE_HOLD_INVOICE_METHOD, models.CANCEL_HOLD_INVOICE_METHOD, models.RECEIVE_METHOD:
|
||||
return constants.MAKE_INVOICE_SCOPE, nil
|
||||
case models.CREATE_CONNECTION_METHOD:
|
||||
return constants.SUPERUSER_SCOPE, nil
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ var MockNodeInfo = lnclient.NodeInfo{
|
|||
Alias: "bob",
|
||||
Color: "#3399FF",
|
||||
Pubkey: "123pubkey",
|
||||
Network: "testnet",
|
||||
Network: "signet", // MockInvoice etc. are signet ("lntbs") invoices
|
||||
BlockHeight: 12,
|
||||
BlockHash: "123blockhash",
|
||||
}
|
||||
|
|
@ -238,7 +238,9 @@ func (mln *MockLn) UpdateChannel(ctx context.Context, updateChannelRequest *lncl
|
|||
}
|
||||
|
||||
func (mln *MockLn) GetSupportedNIP47Methods() []string {
|
||||
return []string{"pay_invoice", "pay_keysend", "get_balance", "get_budget", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice", "multi_pay_keysend", "sign_message"}
|
||||
// NOTE: string literals because the tests package cannot import
|
||||
// nip47/models without creating an import cycle in test binaries
|
||||
return []string{"pay_invoice", "pay_keysend", "get_balance", "get_budget", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice", "multi_pay_keysend", "sign_message", "pay", "receive"}
|
||||
}
|
||||
func (mln *MockLn) GetSupportedNIP47NotificationTypes() []string {
|
||||
if mln.SupportedNotificationTypes != nil {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue