mirror of
https://code.gri.mw/GUI/grim.git
synced 2026-07-14 10:48:55 +00:00
goblin: batch invoice URI with one approval, per-sale addresses (Build 149)
URI schema: pay/invoice URIs gain an optional `count=N` parameter (goblin:<npub|nprofile>?amount=X&count=N[&memo=...]) asking the wallet to issue N payment requests in one go. Default 1 when absent (single flow unchanged), capped at MAX_BATCH_COUNT = 20, fail-closed to 1 on zero/garbage, first occurrence wins — all payuri conventions, unit-tested. A count>=2 deep link whose recipient is a direct key (npub/nprofile) with an amount opens ONE batch-approval modal — "Approve N invoices", with the payer, per-invoice amount, total, and memo — instead of the send flow. On approve, the wallet mints N fresh per-sale proof addresses (Wallet::mint_proof_address; a mint failure stops the batch, already-issued requests stand) and fires the same NostrRequest task the single flow uses, once per invoice; the requests appear in activity as they dispatch. Anything else (count on a name that needs discovery, or no amount) degrades to the single flow with count ignored. The minted address is recorded on each request's metadata (TxNostrMeta::proof_address, serde-default None) so the per-sale machinery can read it; WalletTask::NostrRequest gains the optional address and the single flow passes None. New goblin.batch.* strings in all six locales.
This commit is contained in:
@@ -679,6 +679,10 @@ goblin:
|
||||
n59_blurb: "Verpackt Nachrichten, sodass Relays nicht sehen können, wer mit wem kommuniziert."
|
||||
n98_title: "HTTP-Auth"
|
||||
n98_blurb: "Signiert die Benutzernamen-Registrierungsanfrage an goblin.st."
|
||||
batch:
|
||||
title: "%{n} Rechnungen bestätigen"
|
||||
blurb: "Stelle %{n} Zahlungsanforderungen über je %{amount} an %{name} — insgesamt %{total}. Jede Anforderung erhält ihre eigene neue Zahlungsadresse."
|
||||
approve: "Bestätigen"
|
||||
identities:
|
||||
title: "Identitäten"
|
||||
switch_hint: "Identität wechseln"
|
||||
|
||||
@@ -679,6 +679,10 @@ goblin:
|
||||
n59_blurb: "Wraps messages so relays can't see who is talking to whom."
|
||||
n98_title: "HTTP auth"
|
||||
n98_blurb: "Signs the username registration request to goblin.st."
|
||||
batch:
|
||||
title: "Approve %{n} invoices"
|
||||
blurb: "Issue %{n} payment requests of %{amount} each to %{name} — %{total} in total. Each request gets its own fresh payment address."
|
||||
approve: "Approve"
|
||||
identities:
|
||||
title: "Identities"
|
||||
switch_hint: "Switch identity"
|
||||
|
||||
@@ -679,6 +679,10 @@ goblin:
|
||||
n59_blurb: "Enveloppe les messages pour que les relais ne voient pas qui parle à qui."
|
||||
n98_title: "Auth HTTP"
|
||||
n98_blurb: "Signe la demande d'enregistrement du nom d'utilisateur auprès de goblin.st."
|
||||
batch:
|
||||
title: "Approuver %{n} factures"
|
||||
blurb: "Émettre %{n} demandes de paiement de %{amount} chacune à %{name} — %{total} au total. Chaque demande reçoit sa propre adresse de paiement."
|
||||
approve: "Approuver"
|
||||
identities:
|
||||
title: "Identités"
|
||||
switch_hint: "Changer d'identité"
|
||||
|
||||
@@ -679,6 +679,10 @@ goblin:
|
||||
n59_blurb: "Оборачивает сообщения, чтобы реле не видели, кто с кем общается."
|
||||
n98_title: "HTTP-авторизация"
|
||||
n98_blurb: "Подписывает запрос регистрации имени на goblin.st."
|
||||
batch:
|
||||
title: "Подтвердить %{n} счетов"
|
||||
blurb: "Выставить %{n} запросов на оплату по %{amount} каждый для %{name} — всего %{total}. Каждый запрос получает свой новый платёжный адрес."
|
||||
approve: "Подтвердить"
|
||||
identities:
|
||||
title: "Личности"
|
||||
switch_hint: "Сменить личность"
|
||||
|
||||
@@ -679,6 +679,10 @@ goblin:
|
||||
n59_blurb: "Mesajları sarar, böylece relaylar kimin kiminle konuştuğunu göremez."
|
||||
n98_title: "HTTP kimlik doğrulama"
|
||||
n98_blurb: "goblin.st'ye gönderilen kullanıcı adı kayıt isteğini imzalar."
|
||||
batch:
|
||||
title: "%{n} faturayı onayla"
|
||||
blurb: "%{name} için her biri %{amount} olan %{n} ödeme isteği düzenle — toplam %{total}. Her istek kendi yeni ödeme adresini alır."
|
||||
approve: "Onayla"
|
||||
identities:
|
||||
title: "Kimlikler"
|
||||
switch_hint: "Kimlik değiştir"
|
||||
|
||||
@@ -679,6 +679,10 @@ goblin:
|
||||
n59_blurb: "包装消息,使中继无法看到通信双方是谁。"
|
||||
n98_title: "HTTP 认证"
|
||||
n98_blurb: "为向 goblin.st 注册用户名的请求签名。"
|
||||
batch:
|
||||
title: "批准 %{n} 张发票"
|
||||
blurb: "向 %{name} 发出 %{n} 个付款请求,每个 %{amount},共 %{total}。每个请求都有自己全新的收款地址。"
|
||||
approve: "批准"
|
||||
identities:
|
||||
title: "身份"
|
||||
switch_hint: "切换身份"
|
||||
|
||||
+166
-3
@@ -135,6 +135,8 @@ pub struct GoblinWalletView {
|
||||
/// When the first back of the double-back was pressed at Home, for the
|
||||
/// brief "press back again for the wallet switcher" hint. Display only.
|
||||
back_hint: Option<std::time::Instant>,
|
||||
/// A batch invoice request awaiting approval (count=N deep link).
|
||||
batch_invoice: Option<BatchInvoiceState>,
|
||||
}
|
||||
|
||||
/// Whether the per-identity cue is drawn on activity rows (owner-approved). The
|
||||
@@ -266,6 +268,7 @@ impl Default for GoblinWalletView {
|
||||
wipe_confirm: false,
|
||||
min_conf_edit: String::new(),
|
||||
back_hint: None,
|
||||
batch_invoice: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -341,6 +344,10 @@ const IDENTITY_DELETE_MODAL: &str = "goblin_identity_delete_modal";
|
||||
/// Cancel/Save), opened from the Settings wallet group.
|
||||
const MIN_CONF_MODAL: &str = "goblin_min_conf_modal";
|
||||
|
||||
/// Id of the batch-invoice approval modal (a `count=N` invoice-request URI):
|
||||
/// one approval for N payment requests, each on its own fresh proof address.
|
||||
const BATCH_INVOICE_MODAL: &str = "goblin_batch_invoice_modal";
|
||||
|
||||
/// A password-gated identity action the modal executes. Switching no longer uses
|
||||
/// the modal (it is instant and local); only these need the wallet password.
|
||||
#[derive(Clone)]
|
||||
@@ -389,6 +396,20 @@ struct IdentitySwitchState {
|
||||
result: std::sync::Arc<std::sync::Mutex<Option<Result<String, String>>>>,
|
||||
}
|
||||
|
||||
/// A batch invoice request (`count=N` URI) awaiting its one approval.
|
||||
struct BatchInvoiceState {
|
||||
/// Receiver public key, hex (URI recipient must be a direct npub/nprofile).
|
||||
hex: String,
|
||||
/// Relay hints from the nprofile, if any.
|
||||
relay_hints: Vec<String>,
|
||||
/// Amount PER invoice, raw decimal-GRIN string from the URI.
|
||||
amount: String,
|
||||
/// Optional memo threaded onto every request.
|
||||
memo: Option<String>,
|
||||
/// How many requests to issue (2..=MAX_BATCH_COUNT).
|
||||
count: u32,
|
||||
}
|
||||
|
||||
/// Inline "change name authority" (federation) editor state.
|
||||
#[derive(Default)]
|
||||
struct NameAuthorityState {
|
||||
@@ -602,10 +623,49 @@ impl GoblinWalletView {
|
||||
|
||||
// A pending payment deep link (`goblin:` / `nostr:` pay URI, routed here
|
||||
// from an OS launch/open) opens a prefilled send-review flow — the exact
|
||||
// destination a scanned checkout QR lands on.
|
||||
// destination a scanned checkout QR lands on. A BATCH invoice-request
|
||||
// (`count=N`, N >= 2, direct key + amount) opens the one batch-approval
|
||||
// modal instead; anything else (including count on a name that needs
|
||||
// discovery) degrades to the single flow, count ignored.
|
||||
if let Some(uri) = crate::take_pending_pay_uri() {
|
||||
let now = ui.input(|i| i.time);
|
||||
self.send = Some(SendFlow::from_deeplink(&uri, wallet, now));
|
||||
let pay = crate::nostr::payuri::parse(&uri);
|
||||
let batch = if pay.count >= 2 {
|
||||
match (
|
||||
send::decode_recipient_key(&pay.recipient),
|
||||
pay.amount.clone(),
|
||||
) {
|
||||
(Some((hex, relay_hints)), Some(amount)) => Some(BatchInvoiceState {
|
||||
hex,
|
||||
relay_hints,
|
||||
amount,
|
||||
memo: pay.memo.clone(),
|
||||
count: pay.count,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
match batch {
|
||||
Some(b) => {
|
||||
let n = b.count;
|
||||
self.batch_invoice = Some(b);
|
||||
Modal::new(BATCH_INVOICE_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("goblin.batch.title", n => n.to_string()))
|
||||
.show();
|
||||
}
|
||||
None => {
|
||||
let now = ui.input(|i| i.time);
|
||||
self.send = Some(SendFlow::from_deeplink(&uri, wallet, now));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Batch-invoice approval modal (locks the surface behind it).
|
||||
if Modal::opened() == Some(BATCH_INVOICE_MODAL) {
|
||||
Modal::ui(ui.ctx(), cb, |ui, _modal, _cb| {
|
||||
self.batch_invoice_modal_content(ui, wallet);
|
||||
});
|
||||
}
|
||||
|
||||
// Send flow takes the full surface when active.
|
||||
@@ -5718,6 +5778,109 @@ impl GoblinWalletView {
|
||||
});
|
||||
}
|
||||
|
||||
/// Content of the batch-invoice approval modal: ONE approval for N payment
|
||||
/// requests to the same payer, each request minted its own fresh per-sale
|
||||
/// proof address so no two sales share an address. Approve fires the same
|
||||
/// request task the single flow uses, N times; the requests then appear in
|
||||
/// activity as they dispatch, exactly like single requests.
|
||||
fn batch_invoice_modal_content(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
|
||||
let Some(b) = &self.batch_invoice else {
|
||||
Modal::close();
|
||||
return;
|
||||
};
|
||||
let name = wallet
|
||||
.nostr_service()
|
||||
.map(|s| data::contact_title(&s.store, &b.hex))
|
||||
.unwrap_or_else(|| data::short_npub(&b.hex));
|
||||
let each = grin_core::core::amount_from_hr_string(&b.amount).unwrap_or(0);
|
||||
let total = each.saturating_mul(b.count as u64);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.label(
|
||||
RichText::new(t!(
|
||||
"goblin.batch.blurb",
|
||||
n => b.count.to_string(),
|
||||
name => name,
|
||||
amount => format!("{}{}", w::amount_str(each), w::TSU),
|
||||
total => format!("{}{}", w::amount_str(total), w::TSU)
|
||||
))
|
||||
.size(16.0)
|
||||
.color(Colors::gray()),
|
||||
);
|
||||
if let Some(memo) = &b.memo {
|
||||
ui.add_space(6.0);
|
||||
ui.label(
|
||||
RichText::new(format!("\u{201C}{}\u{201D}", memo))
|
||||
.size(15.0)
|
||||
.color(Colors::gray()),
|
||||
);
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
});
|
||||
let mut cancel = false;
|
||||
let mut approve = false;
|
||||
ui.scope(|ui| {
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(
|
||||
ui,
|
||||
t!("modal.cancel"),
|
||||
Colors::white_or_black(false),
|
||||
|| {
|
||||
cancel = true;
|
||||
},
|
||||
);
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(
|
||||
ui,
|
||||
t!("goblin.batch.approve"),
|
||||
Colors::white_or_black(false),
|
||||
|| {
|
||||
approve = true;
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
});
|
||||
if cancel {
|
||||
self.batch_invoice = None;
|
||||
Modal::close();
|
||||
}
|
||||
if approve {
|
||||
if let Some(b) = self.batch_invoice.take() {
|
||||
if let Some(service) = wallet.nostr_service() {
|
||||
service.set_send_phase(crate::nostr::send_phase::WORKING);
|
||||
}
|
||||
let w = wallet.clone();
|
||||
std::thread::spawn(move || {
|
||||
for _ in 0..b.count {
|
||||
// Each request gets its OWN fresh proof address; a mint
|
||||
// failure stops the batch (already-issued requests stand).
|
||||
match w.mint_proof_address() {
|
||||
Ok((_index, addr)) => {
|
||||
w.task(crate::wallet::types::WalletTask::NostrRequest(
|
||||
each,
|
||||
b.hex.clone(),
|
||||
b.memo.clone(),
|
||||
b.relay_hints.clone(),
|
||||
Some(addr),
|
||||
));
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("batch invoice: mint failed: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Modal::close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Inline username-claim widget (availability check + registration).
|
||||
fn claim_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
|
||||
let t = theme::tokens();
|
||||
|
||||
@@ -1366,6 +1366,7 @@ impl SendFlow {
|
||||
recipient.npub.clone(),
|
||||
note,
|
||||
recipient.relay_hints.clone(),
|
||||
None,
|
||||
));
|
||||
} else {
|
||||
wallet.task(WalletTask::NostrSend(
|
||||
@@ -1643,7 +1644,7 @@ enum ScanPrefill {
|
||||
/// optional `nostr:` prefix) to a `(pubkey hex, relay hints)` pair. `None` for a
|
||||
/// name / `@handle` (which needs a NIP-05 network resolution) or garbage — those
|
||||
/// still go through the search box. Pure: no I/O.
|
||||
fn decode_recipient_key(input: &str) -> Option<(String, Vec<String>)> {
|
||||
pub(super) fn decode_recipient_key(input: &str) -> Option<(String, Vec<String>)> {
|
||||
use nostr_sdk::nips::nip19::Nip19Profile;
|
||||
use nostr_sdk::{FromBech32, PublicKey};
|
||||
let key = input.trim().strip_prefix("nostr:").unwrap_or(input.trim());
|
||||
|
||||
@@ -2241,6 +2241,7 @@ async fn handle_wrap(svc: &Arc<NostrService>, wallet: &Wallet, event: Event) {
|
||||
// opened it), NOT necessarily the active one. All identities
|
||||
// redeem into the one grin balance; this records provenance.
|
||||
recipient_pubkey: recipient_hex.clone(),
|
||||
proof_address: None,
|
||||
});
|
||||
// Commit dedup markers now the receive is durable, BEFORE
|
||||
// the reply + sync tail. A crash there must not let this
|
||||
@@ -2517,6 +2518,7 @@ mod tests {
|
||||
proof_delivered: false,
|
||||
receipt_sent: false,
|
||||
recipient_pubkey: String::new(),
|
||||
proof_address: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -174,6 +174,7 @@ mod tests {
|
||||
proof_delivered: false,
|
||||
receipt_sent: false,
|
||||
recipient_pubkey: String::new(),
|
||||
proof_address: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,6 +83,11 @@ pub struct PayUri {
|
||||
/// delivery to. Dropped if it is not a valid npub; absence simply means no
|
||||
/// encrypted delivery target (the plain receipt still publishes).
|
||||
pub notify: Option<String>,
|
||||
/// Batch size for an invoice-request URI: how many payment requests the
|
||||
/// wallet is asked to issue, each with its own fresh per-sale proof
|
||||
/// address. Default 1 (single flow, unchanged when absent); capped at
|
||||
/// [`MAX_BATCH_COUNT`]; fail-closed to 1 on anything unparseable or zero.
|
||||
pub count: u32,
|
||||
}
|
||||
|
||||
impl PayUri {
|
||||
@@ -95,10 +100,15 @@ impl PayUri {
|
||||
proof: None,
|
||||
order: None,
|
||||
notify: None,
|
||||
count: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cap on the `count` batch parameter: the most invoices one URI may ask the
|
||||
/// wallet to issue in a single approval.
|
||||
pub const MAX_BATCH_COUNT: u32 = 20;
|
||||
|
||||
/// Parse a scanned payload into a [`PayUri`]. Pure and total: never panics,
|
||||
/// never performs I/O, always returns a value. On any problem it falls back to
|
||||
/// recipient-only (fail-closed).
|
||||
@@ -132,6 +142,7 @@ pub fn parse(scanned: &str) -> PayUri {
|
||||
let mut proof = None;
|
||||
let mut order = None;
|
||||
let mut notify = None;
|
||||
let mut count = None;
|
||||
if let Some(query) = query {
|
||||
for pair in query.split('&') {
|
||||
let Some((key, val)) = pair.split_once('=') else {
|
||||
@@ -148,6 +159,7 @@ pub fn parse(scanned: &str) -> PayUri {
|
||||
"proof" if proof.is_none() => proof = validate_proof(val),
|
||||
"order" if order.is_none() => order = validate_order(val),
|
||||
"notify" if notify.is_none() => notify = validate_notify(val),
|
||||
"count" if count.is_none() => count = validate_count(val),
|
||||
// Unknown params are ignored for forward-compat.
|
||||
_ => {}
|
||||
}
|
||||
@@ -161,6 +173,17 @@ pub fn parse(scanned: &str) -> PayUri {
|
||||
proof,
|
||||
order,
|
||||
notify,
|
||||
count: count.unwrap_or(1),
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate a `count` value: a positive integer, clamped to [`MAX_BATCH_COUNT`].
|
||||
/// Fail-closed: unparseable or zero drops to `None` (treated as 1, the single
|
||||
/// flow), never blocking the URI.
|
||||
fn validate_count(raw: &str) -> Option<u32> {
|
||||
match raw.trim().parse::<u32>() {
|
||||
Ok(0) | Err(_) => None,
|
||||
Ok(n) => Some(n.min(MAX_BATCH_COUNT)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,6 +380,28 @@ fn hex_val(b: u8) -> Option<u8> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn count_parses_defaults_and_caps() {
|
||||
const NPUB: &str = "npub15gsytqvs5c78u83yv2agl4twjkk6qgem7gtwe2agu7s90tkelxys0xxely";
|
||||
// Absent -> 1 (single flow unchanged).
|
||||
assert_eq!(parse(&format!("goblin:{NPUB}?amount=1.5")).count, 1);
|
||||
// Present -> parsed.
|
||||
assert_eq!(parse(&format!("goblin:{NPUB}?amount=1.5&count=5")).count, 5);
|
||||
// Capped at MAX_BATCH_COUNT.
|
||||
assert_eq!(
|
||||
parse(&format!("goblin:{NPUB}?amount=1.5&count=999")).count,
|
||||
MAX_BATCH_COUNT
|
||||
);
|
||||
// Fail-closed: zero or garbage -> 1.
|
||||
assert_eq!(parse(&format!("goblin:{NPUB}?amount=1.5&count=0")).count, 1);
|
||||
assert_eq!(
|
||||
parse(&format!("goblin:{NPUB}?amount=1.5&count=abc")).count,
|
||||
1
|
||||
);
|
||||
// First occurrence wins.
|
||||
assert_eq!(parse(&format!("goblin:{NPUB}?count=3&count=9")).count, 3);
|
||||
}
|
||||
|
||||
const NPROFILE: &str =
|
||||
"nprofile1qqsw3v0m5v6h9q8n0hkxg6l4l5xk2z7z0n6f6q9m8x0q5v4l3k2j1h0gpz3mhxue69uhhyetvv9uju";
|
||||
|
||||
|
||||
@@ -126,6 +126,11 @@ pub struct TxNostrMeta {
|
||||
/// pre-feature rows, which are treated as identity #1 (the primary).
|
||||
#[serde(default)]
|
||||
pub recipient_pubkey: String,
|
||||
/// The fresh per-sale proof/slatepack address minted for this OUTGOING
|
||||
/// payment request (batch invoicing), so each sale has its own address.
|
||||
/// `None` for ordinary sends/requests.
|
||||
#[serde(default)]
|
||||
pub proof_address: Option<String>,
|
||||
}
|
||||
|
||||
/// A contact: another nostr user we can pay.
|
||||
|
||||
+3
-1
@@ -459,7 +459,9 @@ pub enum WalletTask {
|
||||
/// * receiver public key (hex)
|
||||
/// * optional note (subject line)
|
||||
/// * relay hints
|
||||
NostrRequest(u64, String, Option<String>, Vec<String>),
|
||||
/// * optional fresh per-sale proof address minted for this request (batch
|
||||
/// invoicing), recorded on the request's metadata
|
||||
NostrRequest(u64, String, Option<String>, Vec<String>, Option<String>),
|
||||
/// Republish our kind-0 profile (e.g. after toggling the incoming-requests
|
||||
/// preference) so the change propagates to relays immediately.
|
||||
NostrRepublishProfile,
|
||||
|
||||
@@ -3005,6 +3005,7 @@ async fn handle_task(w: &Wallet, t: WalletTask) {
|
||||
proof_delivered: false,
|
||||
receipt_sent: false,
|
||||
recipient_pubkey: service.public_key().to_hex(),
|
||||
proof_address: None,
|
||||
});
|
||||
let tx = w.retrieve_tx_by_id(None, Some(s.id));
|
||||
w.send_creating.store(false, Ordering::Relaxed);
|
||||
@@ -3105,7 +3106,7 @@ async fn handle_task(w: &Wallet, t: WalletTask) {
|
||||
}
|
||||
}
|
||||
}
|
||||
WalletTask::NostrRequest(a, receiver, note, relay_hints) => {
|
||||
WalletTask::NostrRequest(a, receiver, note, relay_hints, proof_address) => {
|
||||
let Some(service) = w.nostr_service() else {
|
||||
error!("nostr request: service not available");
|
||||
return;
|
||||
@@ -3146,6 +3147,9 @@ async fn handle_task(w: &Wallet, t: WalletTask) {
|
||||
proof_delivered: false,
|
||||
receipt_sent: false,
|
||||
recipient_pubkey: service.public_key().to_hex(),
|
||||
// The fresh per-sale address minted for this request (batch
|
||||
// invoicing); None for an ordinary request.
|
||||
proof_address: proof_address.clone(),
|
||||
});
|
||||
let tx = w.retrieve_tx_by_id(None, Some(s.id));
|
||||
w.invoice_creating.store(false, Ordering::Relaxed);
|
||||
@@ -3398,6 +3402,7 @@ async fn handle_task(w: &Wallet, t: WalletTask) {
|
||||
proof_delivered: false,
|
||||
receipt_sent: false,
|
||||
recipient_pubkey: service.public_key().to_hex(),
|
||||
proof_address: None,
|
||||
});
|
||||
match service
|
||||
.send_payment_dm(&request.npub, &text, None, &[])
|
||||
@@ -3449,6 +3454,7 @@ async fn handle_task(w: &Wallet, t: WalletTask) {
|
||||
proof_delivered: false,
|
||||
receipt_sent: false,
|
||||
recipient_pubkey: service.public_key().to_hex(),
|
||||
proof_address: None,
|
||||
});
|
||||
match service
|
||||
.send_payment_dm(&request.npub, &text, None, &[])
|
||||
|
||||
Reference in New Issue
Block a user