Oak

Check-in [f19b421d5c]
Login

Check-in [f19b421d5c]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:fix(nwc): Include client pubkey tag in NWC Response
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: f19b421d5cd900d78dfcc6561ff2ffad532ed2768bd8537c62dfd8b131ca88a9
User & Date: carlos 2023-09-21 20:06:11
Context
2023-09-23
06:30
ref: Bump dependencies check-in: 8910740ab9 user: carlos tags: trunk
2023-09-21
20:06
fix(nwc): Include client pubkey tag in NWC Response check-in: f19b421d5c user: carlos tags: trunk
2023-09-20
18:59
nit(ui): Use npubs in Nostr bot UI check-in: dec188c782 user: carlos tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to CHANGELOG.md.

8
9
10
11
12
13
14




15
16
17
18
19
20
21

Changed

* LNURL-pay: Removed deprecated description hash validation
* Nostr: When using more relays, latency is used to determine best relay
* Nostr: Improve connection reliability and choice of default relay during NIP-47 Wallet Connect setup





---

## [v0.3.7 (2023-06-20)](/timeline?p=v0.3.7&bt=v0.3.6)

Added

* Nostr: Support for NIP-47 Nostr Wallet Connect







>
>
>
>







8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

Changed

* LNURL-pay: Removed deprecated description hash validation
* Nostr: When using more relays, latency is used to determine best relay
* Nostr: Improve connection reliability and choice of default relay during NIP-47 Wallet Connect setup

Fixed

* Nostr: NWC responses include the client pubkey tag, allowing clients like Amethyst to show confirmations for successful NWC operations

---

## [v0.3.7 (2023-06-20)](/timeline?p=v0.3.7&bt=v0.3.6)

Added

* Nostr: Support for NIP-47 Nostr Wallet Connect

Changes to src/bot/nostr.rs.

37
38
39
40
41
42
43

44
45
46
47
48
49
50
            // For new entries: update get_env_figment()
        ]
    })
}

#[derive(Clone)]
pub struct NostrContext {

    pub(crate) client: Arc<Client>,
    pub(in crate::bot) nostr_bot_owner_nprofile: Profile,
}

impl NostrContext {
    /// Calculates a map with the current status of each relay used by the [Client]
    pub(crate) async fn get_relay_status(&self) -> HashMap<String, OakRelayStatus> {







>







37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
            // For new entries: update get_env_figment()
        ]
    })
}

#[derive(Clone)]
pub struct NostrContext {
    /// The nostr client
    pub(crate) client: Arc<Client>,
    pub(in crate::bot) nostr_bot_owner_nprofile: Profile,
}

impl NostrContext {
    /// Calculates a map with the current status of each relay used by the [Client]
    pub(crate) async fn get_relay_status(&self) -> HashMap<String, OakRelayStatus> {

Changes to src/bot/ons/nip47.rs.

1
2
3
4
5
6
7
8
9

10
11
12
13
14
15
16
use crate::bot::nostr::NostrContext;
use crate::bot::ons::Ons;
use crate::model::api::ApiError::GenericError;
use crate::model::api::ApiErrorLogged;
use crate::model::bot::nostr::OnsMetadata;
use crate::params::StartupParamsOptional;
use crate::Db;
use anyhow::{anyhow, Result};
use hex::ToHex;

use ln_sdk::client::lnd::{LndClient, LndClientOps, LndConfig};
use ln_sdk::HttpConfig;
use nostr::nips::nip47::*;
use nostr::prelude::{decrypt, encrypt, FromSkStr};
use nostr::{Event, EventBuilder, Filter, Keys, Kind, Tag, Timestamp};
use rocket_db_pools::Connection;
use serde::{Deserialize, Serialize};









>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use crate::bot::nostr::NostrContext;
use crate::bot::ons::Ons;
use crate::model::api::ApiError::GenericError;
use crate::model::api::ApiErrorLogged;
use crate::model::bot::nostr::OnsMetadata;
use crate::params::StartupParamsOptional;
use crate::Db;
use anyhow::{anyhow, Result};
use hex::ToHex;
use lightning_invoice::Bolt11Invoice;
use ln_sdk::client::lnd::{LndClient, LndClientOps, LndConfig};
use ln_sdk::HttpConfig;
use nostr::nips::nip47::*;
use nostr::prelude::{decrypt, encrypt, FromSkStr};
use nostr::{Event, EventBuilder, Filter, Keys, Kind, Tag, Timestamp};
use rocket_db_pools::Connection;
use serde::{Deserialize, Serialize};
69
70
71
72
73
74
75













76
77
78
79
80
81
82
    // TODO Allowance (once this runs out, disable)
    // TODO Allowance per day
    // TODO Onetime use
    // TODO Max per payment
    // TODO Max routing fee
    // TODO Optional description
}














pub(crate) struct Nip47 {}

#[async_trait]
impl Ons for Nip47 {
    fn id(&self) -> String {
        ID.to_string()







>
>
>
>
>
>
>
>
>
>
>
>
>







70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
    // TODO Allowance (once this runs out, disable)
    // TODO Allowance per day
    // TODO Onetime use
    // TODO Max per payment
    // TODO Max routing fee
    // TODO Optional description
}

/// Get [Keys] built from all available NWC secrets
fn available_nwc_secrets(spo: &StartupParamsOptional) -> Result<Vec<Keys>> {
    // Find pks associated with NWC secrets
    // They are the pks from which NWC requests will originate
    let res: Vec<Keys> = get_inner_config(spo)?
        .secrets
        .iter()
        .flat_map(|s| Keys::from_sk_str(&s.secret))
        .collect::<Vec<Keys>>();

    Ok(res)
}

pub(crate) struct Nip47 {}

#[async_trait]
impl Ons for Nip47 {
    fn id(&self) -> String {
        ID.to_string()
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146


147
148




149
150
151
152
153
154
155
156
157
158
159
160
161





162


163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193

194


195
196
197
198
199
200
201
202
203
204
205
206
207



208
209
210
211
212
213
214
215
216
            enabled: self.is_enabled(composite_params_opt),
        }
    }

    fn filter(&self, _ctx: &NostrContext, spo: &StartupParamsOptional) -> Result<Filter> {
        info!("Preparing subscription to NIP-47 Nostr Wallet Connect calls by anyone with a valid connection string");

        let inner_config: Nip47InnerConfig = from_str(&spo.nip47.inner_config_json)?;

        let pks: Vec<String> = inner_config
            .secrets
            .iter()
            .flat_map(|s| Keys::from_sk_str(&s.secret))
            .map(|keys| keys.public_key())
            .map(|pk| pk.serialize().encode_hex::<String>())
            .collect::<Vec<String>>();

        Ok(Filter::new()
            .kind(Kind::WalletConnectRequest)
            .since(Timestamp::now())
            .authors(pks))
    }

    async fn handle(
        &self,
        ctx: &mut NostrContext,
        lnd_client: &LndClient,
        event: &Event,
        _spo: &StartupParamsOptional,
        _lnd_config: &LndConfig,
        _http_config: &HttpConfig,
    ) -> Result<Option<()>> {


        if let Kind::WalletConnectRequest = event.kind {
            let decrypted = decrypt(




                &ctx.client.keys().secret_key()?,
                &event.pubkey,
                &event.content,
            )?;

            let req = Request::from_json(&decrypted)?;
            match (req.method, req.params) {
                (
                    Method::PayInvoice,
                    RequestParams::PayInvoice(PayInvoiceRequestParams { invoice }),
                ) => {
                    let resp = match lnd_client.pay_invoice_simple(&invoice).await {
                        Ok(preimage) => {





                            info!("Payment was successful, preimage: {preimage}");


                            Response {
                                result_type: Method::PayInvoice,
                                error: None,
                                result: Some(ResponseResult::PayInvoice(
                                    PayInvoiceResponseResult { preimage },
                                )),
                            }
                        }
                        Err(e) => {
                            warn!("Payment failed: {e}");
                            Response {
                                result_type: Method::PayInvoice,
                                error: Some(NIP47Error {
                                    code: ErrorCode::Other,
                                    message: e.to_string(),
                                }),
                                result: None,
                            }
                        }
                    };

                    let resp_json_decrypted = resp.as_json();
                    let resp_json_encrypted = encrypt(
                        &ctx.client.keys().secret_key()?,
                        &event.pubkey,
                        resp_json_decrypted,
                    )?;

                    let resp_ev = EventBuilder::new(
                        Kind::WalletConnectResponse,
                        resp_json_encrypted,

                        &[Tag::Event(event.id, None, None)],


                    )
                    .to_event(&ctx.client.keys())?;

                    ctx.client.send_event(resp_ev).await?;
                    info!("Sent NWC response event");
                }
                _ => {
                    // TODO Handle new methods
                    // TODO Show errors for invalid method / request combos
                }
            }

            Ok(Some(()))



        } else {
            Ok(None)
        }
    }

    fn is_enabled(&self, composite_params_opt: &StartupParamsOptional) -> bool {
        composite_params_opt.nip47.enabled
    }
}







|
|
|
<
|
<















|



>
>

|
>
>
>
>
|
|
|
<

|
|
|
|
|
|
|
|
>
>
>
>
>
|
>
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|

|
|
|
>
|
>
>
|
|

|
|
|
|
|
|
|
|

|
>
>
>









129
130
131
132
133
134
135
136
137
138

139

140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169

170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
            enabled: self.is_enabled(composite_params_opt),
        }
    }

    fn filter(&self, _ctx: &NostrContext, spo: &StartupParamsOptional) -> Result<Filter> {
        info!("Preparing subscription to NIP-47 Nostr Wallet Connect calls by anyone with a valid connection string");

        // Find pks associated with NWC secrets
        // They are the pks from which NWC requests will originate
        let pks: Vec<String> = available_nwc_secrets(spo)?

            .into_iter()

            .map(|keys| keys.public_key())
            .map(|pk| pk.serialize().encode_hex::<String>())
            .collect::<Vec<String>>();

        Ok(Filter::new()
            .kind(Kind::WalletConnectRequest)
            .since(Timestamp::now())
            .authors(pks))
    }

    async fn handle(
        &self,
        ctx: &mut NostrContext,
        lnd_client: &LndClient,
        event: &Event,
        spo: &StartupParamsOptional,
        _lnd_config: &LndConfig,
        _http_config: &HttpConfig,
    ) -> Result<Option<()>> {
        let nwc_secrets = available_nwc_secrets(spo)?;

        if let Kind::WalletConnectRequest = event.kind {
            // Derived from secret part of NWC info
            // The pubkey part of the NWC info is this client's pubkey
            let matching_keys = nwc_secrets.iter().find(|&k| k.public_key() == event.pubkey);
            match matching_keys {
                Some(nwc_keys) => {
                    let sk = nwc_keys.secret_key()?;

                    let decrypted = decrypt(&sk, &ctx.client.keys().public_key(), &event.content)?;


                    let req = Request::from_json(&decrypted)?;
                    match (req.method, req.params) {
                        (
                            Method::PayInvoice,
                            RequestParams::PayInvoice(PayInvoiceRequestParams { invoice }),
                        ) => {
                            let resp = match lnd_client.pay_invoice_simple(&invoice).await {
                                Ok(preimage_base64) => {
                                    let preimage_decoded = base64::decode(preimage_base64)?;
                                    let preimage = hex::encode(preimage_decoded);

                                    let invoice_parsed = invoice.parse::<Bolt11Invoice>()?;

                                    info!("Payment was successful, amt={:?} msat, preimage: {preimage}",
                                            invoice_parsed.amount_milli_satoshis(),
                                        );
                                    Response {
                                        result_type: Method::PayInvoice,
                                        error: None,
                                        result: Some(ResponseResult::PayInvoice(
                                            PayInvoiceResponseResult { preimage },
                                        )),
                                    }
                                }
                                Err(e) => {
                                    warn!("Payment failed: {e}");
                                    Response {
                                        result_type: Method::PayInvoice,
                                        error: Some(NIP47Error {
                                            code: ErrorCode::Other,
                                            message: e.to_string(),
                                        }),
                                        result: None,
                                    }
                                }
                            };

                            let resp_json_decrypted = resp.as_json();
                            let resp_json_encrypted = encrypt(
                                &ctx.client.keys().secret_key()?,
                                &nwc_keys.public_key(),
                                resp_json_decrypted,
                            )?;

                            let resp_ev = EventBuilder::new(
                                Kind::WalletConnectResponse,
                                resp_json_encrypted,
                                &[
                                    Tag::Event(event.id, None, None),
                                    Tag::PubKey(nwc_keys.public_key(), None),
                                ],
                            )
                            .to_event(&ctx.client.keys())?;

                            ctx.client.send_event(resp_ev).await?;
                            info!("Sent NWC response event");
                        }
                        _ => {
                            // TODO Handle new methods
                            // TODO Show errors for invalid method / request combos
                        }
                    }

                    Ok(Some(()))
                }
                None => Ok(None),
            }
        } else {
            Ok(None)
        }
    }

    fn is_enabled(&self, composite_params_opt: &StartupParamsOptional) -> bool {
        composite_params_opt.nip47.enabled
    }
}