feat: NWC Funding source #2579 (#2631)

* feat: nwc funding source

* implement paid_invoices_stream, fix for unsettled invoices where settled_at is present but None

* cancel pending_payments_lookup_task on cleanup

* Rename subscription_timeout_task to timeout_task

* ensure preimage is not None

* Improve readability, return failed status on expiration in get_payment_status, ensure result_type is checked after error (some implementations might not set a result_type on error)

* fetch account info when possible

* workaround possible race condition on some nwc service providers, improve performance of fallback by using payment_hash from bolt11 invoice

* fundle

* make format

* fix formatting

* fix C901 `_on_message` is too complex (21 > 16)

* format

* fix lint

* format

* fix tests/wallets/test_nwc_wallets.py:80:11: C901 `run` is too complex (17 > 16)

* fix padding

* fix documentation for _verify_event method

* refactoring and fixes

* Split NWCWallet - NWCConnection

* refactor class methods into helpers

* update bundle

* format

* catch NWCError failure codes

* format and fix

* chore: bundle

* add example

* typos

---------

Co-authored-by: Riccardo Balbo <riccardo0blb@gmail.com>
Co-authored-by: benarc <ben@arc.wales>
Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
This commit is contained in:
dni ⚡
2024-08-07 09:56:53 +02:00
committed by GitHub
parent daa4b92331
commit 27b9e8254c
12 changed files with 2179 additions and 2 deletions

View File

@@ -0,0 +1,571 @@
{
"funding_sources": {
"nwc": {
"wallet_class": "NWCWallet",
"settings": {
"nwc_pairing_url": "nostr+walletconnect://be927be01ce2b3ab0fc33ffec6c6ab590381d7fc883a392d163d3966fc5840b3?relay=ws://127.0.0.1:8555&secret=d1b1d3b0f4a1fcba4c15094d34ff0569cae3c8c7af939b3473ccd564cce3bfa3"
},
"mock_settings": {
"service_public_key": "be927be01ce2b3ab0fc33ffec6c6ab590381d7fc883a392d163d3966fc5840b3",
"service_private_key": "ad6a224c9c2f2a7ac7181092348d99671a94f28974e331e0f0afe3bcdab72bed",
"user_public_key": "bef38893a567e717110478cc5729f4bf881727f5a7d7859edad2b2b21ee81f7e",
"user_private_key": "d1b1d3b0f4a1fcba4c15094d34ff0569cae3c8c7af939b3473ccd564cce3bfa3",
"port": 8555,
"supported_methods": [
"get_balance",
"make_invoice",
"pay_invoice",
"lookup_invoice"
]
}
}
},
"functions": {
"status": {
"mocks": {
"nwc": {
"get_balance": {}
}
},
"tests": [
{
"description": "success",
"call_params": {},
"expect": {
"error_message": null,
"balance_msat": 55000
},
"mocks": {
"nwc": {
"get_balance": [
{
"request_type": "json",
"request_body": {
"method": "get_balance",
"params": {}
},
"response_type": "json",
"response": {
"result_type": "get_balance",
"result": {
"balance": 55000
}
}
}
]
}
}
},
{
"description": "error",
"call_params": {},
"expect": {
"error_message": "TEST ERROR test-error",
"balance_msat": 0
},
"mocks": {
"nwc": {
"get_balance": [
{
"request_type": "json",
"request_body": {
"method": "get_balance",
"params": {}
},
"response_type": "json",
"response": {
"result_type": "get_balance",
"error": {
"code": "TEST ERROR",
"message": "test-error"
}
}
}
]
}
}
}
]
},
"create_invoice": {
"mocks": {
"nwc": {
"make_invoice": {}
},
"nwc-bad": {
"make_invoice": {}
}
},
"tests": [
{
"description": "success",
"call_params": {
"amount": 555,
"memo": "Test Invoice",
"label": "test-label"
},
"expect": {
"error_message": null,
"success": true,
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
"payment_request": "lnbc5550n1pnq9jg3sp52rvwstvjcypjsaenzdh0h30jazvzsf8aaye0julprtth9kysxtuspp5e5s3z7felv4t9zrcc6wpn7ehvjl5yzewanzl5crljdl3jgeffyhqdq2f38xy6t5wvxqzjccqpjrzjq0yzeq76ney45hmjlnlpvu0nakzy2g35hqh0dujq8ujdpr2e42pf2rrs6vqpgcsqqqqqqqqqqqqqqeqqyg9qxpqysgqwftcx89k5pp28435pgxfl2vx3ksemzxccppw2j9yjn0ngr6ed7wj8ztc0d5kmt2mvzdlcgrludhz7jncd5l5l9w820hc4clpwhtqj3gq62g66n"
},
"mocks": {
"nwc": {
"make_invoice": [
{
"request_type": "json",
"request_body": {
"method": "make_invoice",
"params": {
"amount": 555000,
"description": "Test Invoice"
}
},
"response_type": "json",
"response": {
"result_type": "make_invoice",
"result": {
"type": "incoming",
"invoice": "lnbc5550n1pnq9jg3sp52rvwstvjcypjsaenzdh0h30jazvzsf8aaye0julprtth9kysxtuspp5e5s3z7felv4t9zrcc6wpn7ehvjl5yzewanzl5crljdl3jgeffyhqdq2f38xy6t5wvxqzjccqpjrzjq0yzeq76ney45hmjlnlpvu0nakzy2g35hqh0dujq8ujdpr2e42pf2rrs6vqpgcsqqqqqqqqqqqqqqeqqyg9qxpqysgqwftcx89k5pp28435pgxfl2vx3ksemzxccppw2j9yjn0ngr6ed7wj8ztc0d5kmt2mvzdlcgrludhz7jncd5l5l9w820hc4clpwhtqj3gq62g66n",
"description": "Test Invoice",
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
"amount": 555000,
"fees_paid": 0,
"created_at": 0,
"expires_at": 0
}
}
}
]
}
}
},
{
"description": "error",
"call_params": {
"amount": 555,
"memo": "Test Invoice",
"label": "test-label"
},
"expect": {
"error_message": "TEST ERROR test-error",
"success": false,
"checking_id": null,
"payment_request": null
},
"mocks": {
"nwc": {
"make_invoice": [
{
"request_type": "json",
"request_body": {
"method": "make_invoice",
"params": {
"amount": 555000,
"description": "Test Invoice"
}
},
"response_type": "json",
"response": {
"result_type": "make_invoice",
"error": {
"code": "TEST ERROR",
"message": "test-error"
}
}
}
]
}
}
}
]
},
"pay_invoice": {
"mocks": {
"nwc": {
"pay_invoice": {},
"lookup_invoice": {}
}
},
"tests": [
{
"description": "success",
"call_params": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"fee_limit_msat": 25000
},
"expect": {
"error_message": null,
"success": true,
"pending": false,
"failed": false,
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
"fee_msat": 50,
"preimage": "0000000000000000000000000000000000000000000000000000000000000000"
},
"mocks": {
"nwc": {
"pay_invoice": [
{
"request_type": "json",
"request_body": {
"method": "pay_invoice",
"params": {
"invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu"
}
},
"response_type": "json",
"response": {
"result_type": "pay_invoice",
"result": {
"preimage": "0000000000000000000000000000000000000000000000000000000000000000"
}
}
}
],
"lookup_invoice": [
{
"request_type": "json",
"request_body": {
"method": "lookup_invoice",
"params": {
"invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu"
}
},
"response_type": "json",
"response": {
"result_type": "lookup_invoice",
"result": {
"type": "outgoing",
"invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"description": "Test invoice",
"preimage": "0000000000000000000000000000000000000000000000000000000000000000",
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
"amount": 1000,
"fees_paid": 50,
"created_at": 0,
"settled_at": 1
}
}
}
]
}
}
},
{
"description": "failed",
"call_params": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"fee_limit_msat": 25000
},
"expect": {
"success": false,
"pending": false,
"failed": true,
"checking_id": null,
"fee_msat": null,
"preimage": null
},
"mocks": {
"nwc": {
"pay_invoice": [
{
"request_type": "json",
"request_body": {
"method": "pay_invoice",
"params": {
"invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu"
}
},
"response_type": "json",
"response": {
"result_type": "pay_invoice",
"error": {
"code": "TEST ERROR",
"message": "test-error"
}
}
}
]
}
}
},
{
"description": "error",
"call_params": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"fee_limit_msat": 25000
},
"expect": {
"success": false,
"pending": true,
"failed": false,
"checking_id": null,
"fee_msat": null,
"preimage": null,
"error_message": "TEST ERROR test-error"
},
"mocks": {
"nwc": {
"pay_invoice": [
{
"request_type": "json",
"request_body": {
"method": "pay_invoice",
"params": {
"invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu"
}
},
"response_type": "json",
"response": {
"result_type": "pay_invoice",
"error": {
"code": "TEST ERROR",
"message": "test-error"
}
}
}
]
}
}
}
]
},
"get_invoice_status": {
"mocks": {
"nwc": {
"lookup_invoice": {}
},
"nwc-bad": {
"lookup_invoice": {}
}
},
"tests": [
{
"description": "paid",
"call_params": {
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"expect": {
"success": true,
"failed": false,
"pending": false
},
"mocks": {
"nwc": {
"lookup_invoice": [
{
"request_type": "json",
"request_body": {
"method": "lookup_invoice",
"params": {
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
}
},
"response_type": "json",
"response": {
"result_type": "lookup_invoice",
"result": {
"type": "incoming",
"invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"description": "Test Invoice",
"preimage": "0000000000000000000000000000000000000000000000000000000000000000",
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
"amount": 123,
"fees_paid": 123,
"created_at": 0,
"expires_at": 0,
"settled_at": 1,
"metadata": {}
}
}
}
]
}
}
},
{
"description": "failed",
"description1": "pending should be false in the 'expect', this is a bug",
"call_params": {
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"expect": {
"success": false,
"failed": true,
"pending": true
},
"mocks": {
"nwc": [
{
"description": "nwc.py doesn't handle the 'failed' status for `get_invoice_status`"
}
]
}
},
{
"description": "pending",
"call_params": {
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"expect": {
"success": false,
"failed": false,
"pending": true
},
"mocks": {
"nwc": {
"lookup_invoice": [
{
"request_type": "json",
"request_body": {
"method": "lookup_invoice",
"params": {
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
}
},
"response_type": "json",
"response": {
"result_type": "lookup_invoice",
"result": {
"type": "incoming",
"invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"description": "Test Invoice",
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
"amount": 123,
"fees_paid": 0,
"created_at": 0,
"expires_at": 0,
"metadata": {}
}
}
}
]
}
}
}
]
},
"get_payment_status": {
"mocks": {
"nwc": {
"lookup_invoice": {}
},
"nwc-bad": {
"lookup_invoice": {}
}
},
"tests": [
{
"description": "paid",
"call_params": {
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"expect": {
"fee_msat": 1000,
"preimage": "0000000000000000000000000000000000000000000000000000000000000000",
"success": true,
"failed": false,
"pending": false
},
"mocks": {
"nwc": {
"lookup_invoice": [
{
"request_type": "json",
"request_body": {
"method": "lookup_invoice",
"params": {
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
}
},
"response_type": "json",
"response": {
"result_type": "lookup_invoice",
"result": {
"type": "incoming",
"invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"description": "Test Invoice",
"preimage": "0000000000000000000000000000000000000000000000000000000000000000",
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
"amount": 123,
"fees_paid": 1000,
"created_at": 0,
"expires_at": 0,
"settled_at": 1,
"metadata": {}
}
}
}
]
}
}
},
{
"description": "failed",
"description1": "pending should be false in the 'expect', this is a bug",
"call_params": {
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"expect": {
"preimage": null,
"success": false,
"failed": true,
"pending": true
},
"mocks": {
"nwc": [
{
"description": "nwc.py doesn't handle the 'failed' status for `get_payment_status`"
}
]
}
},
{
"description": "pending",
"call_params": {
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"expect": {
"preimage": null,
"success": false,
"failed": false,
"pending": true
},
"mocks": {
"nwc": {
"lookup_invoice": [
{
"request_type": "json",
"request_body": {
"method": "lookup_invoice",
"params": {
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
}
},
"response_type": "json",
"response": {
"result_type": "lookup_invoice",
"result": {
"type": "incoming",
"invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"description": "Test Invoice",
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
"amount": 123,
"fees_paid": 0,
"created_at": 0,
"expires_at": 0,
"metadata": {}
}
}
}
]
}
}
}
]
}
}
}

View File

@@ -0,0 +1,418 @@
{
"funding_sources": {
"nwc-bad": {
"description": "NWC service that supports only pay_invoice",
"wallet_class": "NWCWallet",
"settings": {
"nwc_pairing_url": "nostr+walletconnect://be927be01ce2b3ab0fc33ffec6c6ab590381d7fc883a392d163d3966fc5840b3?relay=ws://127.0.0.1:8555&secret=d1b1d3b0f4a1fcba4c15094d34ff0569cae3c8c7af939b3473ccd564cce3bfa3"
},
"mock_settings": {
"service_public_key": "be927be01ce2b3ab0fc33ffec6c6ab590381d7fc883a392d163d3966fc5840b3",
"service_private_key": "ad6a224c9c2f2a7ac7181092348d99671a94f28974e331e0f0afe3bcdab72bed",
"user_public_key": "bef38893a567e717110478cc5729f4bf881727f5a7d7859edad2b2b21ee81f7e",
"user_private_key": "d1b1d3b0f4a1fcba4c15094d34ff0569cae3c8c7af939b3473ccd564cce3bfa3",
"port": 8555,
"supported_methods": ["pay_invoice"]
}
}
},
"functions": {
"status": {
"mocks": {
"nwc-bad": {
"get_balance": {}
}
},
"tests": [
{
"description": "success",
"call_params": {},
"expect": {
"error_message": null,
"balance_msat": 0
},
"mocks": {
"nwc-bad": {}
}
}
]
},
"create_invoice": {
"mocks": {
"nwc-bad": {
"make_invoice": {}
}
},
"tests": [
{
"description": "success",
"call_params": {
"amount": 555,
"memo": "Test Invoice",
"label": "test-label"
},
"expect": {
"error_message": "make_invoice is not supported by this NWC service.",
"success": false
},
"mocks": {
"nwc-bad": {}
}
}
]
},
"pay_invoice": {
"mocks": {
"nwc-bad": {
"pay_invoice": {},
"lookup_invoice": {}
}
},
"tests": [
{
"description": "success",
"call_params": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"fee_limit_msat": 25000
},
"expect": {
"error_message": null,
"success": true,
"pending": false,
"failed": false,
"checking_id": "66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925",
"fee_msat": 50,
"preimage": "0000000000000000000000000000000000000000000000000000000000000000"
},
"mocks": {
"nwc-bad": {
"pay_invoice": [
{
"request_type": "json",
"request_body": {
"method": "pay_invoice",
"params": {
"invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu"
}
},
"response_type": "json",
"response": {
"result_type": "pay_invoice",
"result": {
"preimage": "0000000000000000000000000000000000000000000000000000000000000000"
}
}
}
]
}
}
},
{
"description": "failed",
"call_params": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"fee_limit_msat": 25000
},
"expect": {
"success": false,
"pending": false,
"failed": true,
"checking_id": null,
"fee_msat": null,
"preimage": null
},
"mocks": {
"nwc-bad": {
"pay_invoice": [
{
"request_type": "json",
"request_body": {
"method": "pay_invoice",
"params": {
"invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu"
}
},
"response_type": "json",
"response": {
"result_type": "pay_invoice",
"error": {
"code": "TEST ERROR",
"message": "test-error"
}
}
}
]
}
}
},
{
"description": "error",
"call_params": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"fee_limit_msat": 25000
},
"expect": {
"success": false,
"pending": true,
"failed": false,
"checking_id": null,
"fee_msat": null,
"preimage": null,
"error_message": "TEST ERROR test-error"
},
"mocks": {
"nwc-bad": {
"pay_invoice": [
{
"request_type": "json",
"request_body": {
"method": "pay_invoice",
"params": {
"invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu"
}
},
"response_type": "json",
"response": {
"result_type": "pay_invoice",
"error": {
"code": "TEST ERROR",
"message": "test-error"
}
}
}
]
}
}
}
]
},
"get_invoice_status": {
"mocks": {
"nwc-bad": {
"lookup_invoice": {}
}
},
"tests": [
{
"description": "paid",
"call_params": {
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"expect": {
"success": false,
"failed": false,
"pending": true
},
"mocks": {
"nwc-bad": {
"lookup_invoice": [
{
"request_type": "json",
"request_body": {
"method": "lookup_invoice",
"params": {
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
}
},
"response_type": "json",
"response": {
"result_type": "lookup_invoice",
"result": {
"type": "incoming",
"invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"description": "Test Invoice",
"preimage": "0000000000000000000000000000000000000000000000000000000000000000",
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
"amount": 123,
"fees_paid": 123,
"created_at": 0,
"expires_at": 0,
"settled_at": 1,
"metadata": {}
}
}
}
]
}
}
},
{
"description": "failed",
"description1": "pending should be false in the 'expect', this is a bug",
"call_params": {
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"expect": {
"success": false,
"failed": false,
"pending": true
},
"mocks": {
"nwc-bad": [
{
"description": "nwc.py doesn't handle the 'failed' status for `get_invoice_status`"
}
]
}
},
{
"description": "pending",
"call_params": {
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"expect": {
"success": false,
"failed": false,
"pending": true
},
"mocks": {
"nwc-bad": {
"lookup_invoice": [
{
"request_type": "json",
"request_body": {
"method": "lookup_invoice",
"params": {
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
}
},
"response_type": "json",
"response": {
"result_type": "lookup_invoice",
"result": {
"type": "incoming",
"invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"description": "Test Invoice",
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
"amount": 123,
"fees_paid": 0,
"created_at": 0,
"expires_at": 0,
"metadata": {}
}
}
}
]
}
}
}
]
},
"get_payment_status": {
"mocks": {
"nwc-bad": {
"lookup_invoice": {}
}
},
"tests": [
{
"description": "paid",
"call_params": {
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"expect": {
"fee_msat": null,
"preimage": null,
"success": false,
"failed": false,
"pending": true
},
"mocks": {
"nwc-bad": {
"lookup_invoice": [
{
"request_type": "json",
"request_body": {
"method": "lookup_invoice",
"params": {
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
}
},
"response_type": "json",
"response": {
"result_type": "lookup_invoice",
"result": {
"type": "incoming",
"invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"description": "Test Invoice",
"preimage": "0000000000000000000000000000000000000000000000000000000000000000",
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
"amount": 123,
"fees_paid": 1000,
"created_at": 0,
"expires_at": 0,
"settled_at": 1,
"metadata": {}
}
}
}
]
}
}
},
{
"description": "failed",
"description1": "pending should be false in the 'expect', this is a bug",
"call_params": {
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"expect": {
"preimage": null,
"success": false,
"failed": true,
"pending": true
},
"mocks": {
"nwc-bad": [
{
"description": "nwc.py doesn't handle the 'failed' status for `get_payment_status`"
}
]
}
},
{
"description": "pending",
"call_params": {
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"expect": {
"preimage": null,
"success": false,
"failed": false,
"pending": true
},
"mocks": {
"nwc-bad": {
"lookup_invoice": [
{
"request_type": "json",
"request_body": {
"method": "lookup_invoice",
"params": {
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
}
},
"response_type": "json",
"response": {
"result_type": "lookup_invoice",
"result": {
"type": "incoming",
"invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"description": "Test Invoice",
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
"amount": 123,
"fees_paid": 0,
"created_at": 0,
"expires_at": 0,
"metadata": {}
}
}
}
]
}
}
}
]
}
}
}

View File

@@ -8,6 +8,7 @@ class FundingSourceConfig(BaseModel):
skip: Optional[bool]
wallet_class: str
settings: dict
mock_settings: Optional[dict]
class FunctionMock(BaseModel):

View File

@@ -0,0 +1,194 @@
import base64
import hashlib
import json
import time
from typing import Dict, cast
import pytest
import secp256k1
from Cryptodome import Random
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import pad, unpad
from websockets.server import serve as ws_serve
from lnbits.wallets.nwc import NWCWallet
from tests.wallets.helpers import (
WalletTest,
build_test_id,
check_assertions,
load_funding_source,
wallet_fixtures_from_json,
)
def encrypt_content(priv_key, dest_pub_key, content):
p = secp256k1.PublicKey(bytes.fromhex("02" + dest_pub_key), True)
shared = p.tweak_mul(bytes.fromhex(priv_key)).serialize()[1:]
iv = Random.new().read(AES.block_size)
aes = AES.new(shared, AES.MODE_CBC, iv)
content_bytes = content.encode("utf-8")
content_bytes = pad(content_bytes, AES.block_size)
encrypted_b64 = base64.b64encode(aes.encrypt(content_bytes)).decode("ascii")
iv_b64 = base64.b64encode(iv).decode("ascii")
encrypted_content = encrypted_b64 + "?iv=" + iv_b64
return encrypted_content
def decrypt_content(priv_key, source_pub_key, content):
p = secp256k1.PublicKey(bytes.fromhex("02" + source_pub_key), True)
shared = p.tweak_mul(bytes.fromhex(priv_key)).serialize()[1:]
(encrypted_content_b64, iv_b64) = content.split("?iv=")
encrypted_content = base64.b64decode(encrypted_content_b64.encode("ascii"))
iv = base64.b64decode(iv_b64.encode("ascii"))
aes = AES.new(shared, AES.MODE_CBC, iv)
decrypted_bytes = aes.decrypt(encrypted_content)
decrypted_bytes = unpad(decrypted_bytes, AES.block_size)
return decrypted_bytes.decode("utf-8")
def json_dumps(data):
if isinstance(data, Dict):
data = {k: v for k, v in data.items() if v is not None}
return json.dumps(data, separators=(",", ":"), ensure_ascii=False)
def sign_event(pub_key, priv_key, event):
signature_data = json_dumps(
[
0,
pub_key,
event["created_at"],
event["kind"],
event["tags"],
event["content"],
]
)
event_id = hashlib.sha256(signature_data.encode()).hexdigest()
event["id"] = event_id
event["pubkey"] = pub_key
s = secp256k1.PrivateKey(bytes.fromhex(priv_key))
signature = (s.schnorr_sign(bytes.fromhex(event_id), None, raw=True)).hex()
event["sig"] = signature
return event
async def handle(wallet, mock_settings, data, websocket, path):
async for message in websocket:
if not wallet:
continue
msg = json.loads(message)
if msg[0] == "REQ":
sub_id = msg[1]
sub_filter = msg[2]
kinds = sub_filter["kinds"]
if 13194 in kinds: # Send info event
event = {
"kind": 13194,
"content": " ".join(mock_settings["supported_methods"]),
"created_at": int(time.time()),
"tags": [],
}
sign_event(
mock_settings["service_public_key"],
mock_settings["service_private_key"],
event,
)
await websocket.send(json.dumps(["EVENT", sub_id, event]))
elif msg[0] == "EVENT":
event = msg[1]
decrypted_content = decrypt_content(
mock_settings["service_private_key"],
mock_settings["user_public_key"],
event["content"],
)
content = json.loads(decrypted_content)
mock = None
for m in data.mocks:
rb = m.request_body
if rb and rb["method"] == content["method"]:
p1 = rb["params"]
p2 = content["params"]
p1 = json_dumps({k: v for k, v in p1.items() if v is not None})
p2 = json_dumps({k: v for k, v in p2.items() if v is not None})
if p1 == p2:
mock = m
break
if mock:
sub_id = None
nwcwallet = cast(NWCWallet, wallet)
for subscription in nwcwallet.conn.subscriptions.values():
if subscription["event_id"] == event["id"]:
sub_id = subscription["sub_id"]
break
if sub_id:
response = mock.response
encrypted_content = encrypt_content(
mock_settings["service_private_key"],
mock_settings["user_public_key"],
json_dumps(response),
)
response_event = {
"kind": 23195,
"content": encrypted_content,
"created_at": int(time.time()),
"tags": [
["e", event["id"]],
["p", mock_settings["user_public_key"]],
],
}
sign_event(
mock_settings["service_public_key"],
mock_settings["service_private_key"],
response_event,
)
await websocket.send(json.dumps(["EVENT", sub_id, response_event]))
else:
raise Exception(
"No mock found for "
+ content["method"]
+ " "
+ json_dumps(content["params"])
)
async def run(data: WalletTest):
if data.skip:
pytest.skip()
wallet = None
mock_settings = data.funding_source.mock_settings
if mock_settings is None:
return
async def handler(websocket, path):
return await handle(wallet, mock_settings, data, websocket, path)
if mock_settings is not None:
async with ws_serve(handler, "localhost", mock_settings["port"]) as server:
await server.start_serving()
wallet = load_funding_source(data.funding_source)
await check_assertions(wallet, data)
nwcwallet = cast(NWCWallet, wallet)
await nwcwallet.cleanup()
@pytest.mark.asyncio
@pytest.mark.parametrize(
"test_data",
wallet_fixtures_from_json("tests/wallets/fixtures/json/fixtures_nwc.json"),
ids=build_test_id,
)
async def test_nwc_wallet(test_data: WalletTest):
await run(test_data)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"test_data",
wallet_fixtures_from_json("tests/wallets/fixtures/json/fixtures_nwc_bad.json"),
ids=build_test_id,
)
async def test_nwc_wallet_bad(test_data: WalletTest):
await run(test_data)