diff --git a/spec/more_speech/bech32_spec.clj b/spec/more_speech/bech32_spec.clj index 9456d7d..badb96f 100644 --- a/spec/more_speech/bech32_spec.clj +++ b/spec/more_speech/bech32_spec.clj @@ -1,7 +1,7 @@ (ns more-speech.bech32-spec - (:require [speclj.core :refer :all] - [more-speech.bech32 :refer :all] - [more-speech.nostr.util :as util])) + (:require [more-speech.bech32 :refer :all] + [more-speech.nostr.util :as util] + [speclj.core :refer :all])) (describe "bech32" (context "charset translations" @@ -87,4 +87,33 @@ (should= pubkey (address->number "npub19mun7qwdyjf7qs3456u8kyxncjn5u2n7klpu4utgy68k4aenzj6synjnft"))) ) ) + + (context "decoding into strings" + (it "decodes into a string" + (let [lnurl (encode-str "lnurl" "this is the string")] + (should= "this is the string" (address->str lnurl)))) + + (it "decodes a long string" + (let [long-string "https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df" + lnurl (encode-str "lnurl" long-string)] + (should= long-string (address->str lnurl)))) + + (it "encodes and decodes a one byte string" + (let [known-string "t" + known-lnurl "lnurl1ws4pqzkn"] + (should= known-lnurl (encode-str "lnurl" known-string)) + (should= known-string (address->str known-lnurl)))) + + (it "encodes and decodes a known short string" + (let [known-string "this is the string" + known-lnurl "lnurl1w35xjueqd9ejqargv5s8xarjd9hxw9sar9p"] + (should= known-lnurl (encode-str "lnurl" known-string)) + (should= known-string (address->str known-lnurl)))) + + (it "encodes and decodes a known long string" + (let [known-string "https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df" + known-lnurl "lnurl1dp68gurn8ghj7um9wfmxjcm99e3k7mf0v9cxj0m385ekvcenxc6r2c35xvukxefcv5mkvv34x5ekzd3ev56nyd3hxqurzepexejxxepnxscrvwfnv9nxzcn9xq6xyefhvgcxxcmyxymnserxfq5fns"] + (should= known-string (address->str known-lnurl)) + (should= known-lnurl (encode-str "lnurl" known-string)))) + ) ) \ No newline at end of file diff --git a/spec/more_speech/nostr/event_composers_spec.clj b/spec/more_speech/nostr/event_composers_spec.clj index 6a53dfc..5489069 100644 --- a/spec/more_speech/nostr/event_composers_spec.clj +++ b/spec/more_speech/nostr/event_composers_spec.clj @@ -7,7 +7,7 @@ [more-speech.nostr.elliptic-signature :refer :all] [more-speech.nostr.event-composers :refer :all] [more-speech.nostr.event-composers :refer :all] - [more-speech.nostr.event-handlers :refer :all] + [more-speech.nostr.event-dispatcher :refer :all] [more-speech.nostr.events :refer :all] [more-speech.nostr.util :as util] [more-speech.nostr.util :refer :all] diff --git a/spec/more_speech/nostr/event_handlers_spec.clj b/spec/more_speech/nostr/event_handlers_spec.clj index e4a9523..7a37756 100644 --- a/spec/more_speech/nostr/event_handlers_spec.clj +++ b/spec/more_speech/nostr/event_handlers_spec.clj @@ -1,6 +1,6 @@ (ns more-speech.nostr.event-handlers-spec (:require [speclj.core :refer :all] - [more-speech.nostr.event-handlers :as handlers] + [more-speech.nostr.event-dispatcher :as handlers] [more-speech.db.gateway :as gateway] [more-speech.db.in-memory :as in-memory] [more-speech.config :as config] diff --git a/spec/more_speech/nostr/events_spec.clj b/spec/more_speech/nostr/events_spec.clj index cfd3e26..15ae613 100644 --- a/spec/more_speech/nostr/events_spec.clj +++ b/spec/more_speech/nostr/events_spec.clj @@ -3,7 +3,7 @@ [more-speech.db.gateway :as gateway] [more-speech.db.in-memory :as in-memory] [more-speech.nostr.events :refer :all] - [more-speech.nostr.event-handlers :refer :all] + [more-speech.nostr.event-dispatcher :refer :all] [more-speech.nostr.elliptic-signature :refer :all] [more-speech.nostr.util :refer :all] [more-speech.mem :refer :all] diff --git a/spec/more_speech/nostr/zaps_spec.clj b/spec/more_speech/nostr/zaps_spec.clj index 4b74b54..ab4d7d0 100644 --- a/spec/more_speech/nostr/zaps_spec.clj +++ b/spec/more_speech/nostr/zaps_spec.clj @@ -1,5 +1,6 @@ (ns more-speech.nostr.zaps-spec (:require + [more-speech.bech32 :as bech32] [more-speech.config :as config] [more-speech.db.gateway :as gateway] [more-speech.db.in-memory :as in-memory] @@ -102,6 +103,7 @@ amount 100 comment "comment" lnurl "lnurl" + b32-lnurl (bech32/encode-str "lnurl" lnurl) my-privkey 0xb0b my-pubkey (util/bytes->num (es/get-pub-key (util/num->bytes 32 my-privkey))) _ (set-mem :pubkey my-pubkey) @@ -120,7 +122,7 @@ (should= (util/get-now) created_at) (should (contains? tags ["relays" "relay-r1" "relay-r2"])) (should (contains? tags ["amount" "100"])) - (should (contains? tags ["lnurl" "lnurl1qqqxcmn4wfkqzejtan"])) + (should (contains? tags ["lnurl" b32-lnurl])) (should (contains? tags ["p" (util/hexify recipient-id)])) (should (contains? tags ["e" (util/hexify event-id)]))))) diff --git a/src/more_speech/bech32.clj b/src/more_speech/bech32.clj index 18064ec..a34e142 100644 --- a/src/more_speech/bech32.clj +++ b/src/more_speech/bech32.clj @@ -46,16 +46,22 @@ :else cksum)) -(defn parse-address [address] - (let [address (validate-address-length (.toLowerCase address)) - hrp-end (find-separator-char address) - hrp (validate-hrp (subs address 0 hrp-end)) - data-and-cksum (subs address (inc hrp-end)) - cksum (apply str (take-last 6 data-and-cksum)) - cksum (validate-cksum cksum) - data (apply str (drop-last 6 data-and-cksum)) - data (validate-data data)] - [hrp data cksum])) +(defn parse-address + ([address] + (parse-address address #{})) + + ([address options] + (let [address (.toLowerCase address) + _ (when-not (contains? options :no-length-restriction) + (validate-address-length address)) + hrp-end (find-separator-char address) + hrp (validate-hrp (subs address 0 hrp-end)) + data-and-cksum (subs address (inc hrp-end)) + cksum (apply str (take-last 6 data-and-cksum)) + cksum (validate-cksum cksum) + data (apply str (drop-last 6 data-and-cksum)) + data (validate-data data)] + [hrp data cksum]))) (def generator [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]) @@ -120,8 +126,64 @@ (recur (quot id 32) (conj data (int (rem id 32))) (dec chars)))) cksum-data (create-checksum hrp data) ] - (str hrp "1" (apply str (map to-char (concat data cksum-data)))) - )) + (str hrp "1" (apply str (map to-char (concat data cksum-data)))))) + +; The bech32 coding converts streams of bytes into streams of 5-bit integers. +; The numbers 5 and 8 are mutually prime so there will usually be a remainder. +; That remainder is the number of zero bits on the end of the encoded stream. +; Thus, a single byte abcdefgh gets encoded as two five bit integer +; as abcde fgh00. + +(defn- align-num [n the-string] + (let [n-bits (* 8 (count the-string)) + r (rem n-bits 5)] + (if (zero? r) + n + (let [shift-n (- 5 r) + shift-factor (reduce * (repeat shift-n 2))] + (* n shift-factor))))) + +(defn- str->num [the-string] + (loop [s the-string + n 0N] + (if (empty? s) + (align-num n the-string) + (recur (rest s) (+ (* 256 n) (int (first s))))))) + +(defn- num->32bit-data [n] + (loop [n n + data (list)] + (if (zero? n) + data + (recur (quot n 32) (conj data (int (rem n 32))))))) + +(defn encode-str + "create a bech32 representation of a string" + [hrp s] + (let [big-number (str->num s) + data (num->32bit-data big-number) + cksum-data (create-checksum hrp data)] + (str hrp "1" (apply str (map to-char (concat data cksum-data)))))) + +(defn address->str [address] + (let [[hrp data cksum] (parse-address address #{:no-length-restriction}) + valid? (verify-checksum? [hrp data cksum])] + (if valid? + (let [values (map to-n data) + accumulator (reduce (fn [n value] + (+ value (* n 32))) + 0N values) + n-bits (* 5 (count data)) + shift-n (rem n-bits 8) + aligned-accumulator (quot accumulator (reduce * (repeat shift-n 2)))] + (loop [n aligned-accumulator + chars (list)] + (if (zero? n) + (apply str chars) + (recur (quot n 256) (conj chars (char (rem n 256))))))) + (throw (Exception. "bech32/address->str: invalid checksum"))) + ) + ) (defn address->number [address] (let [[hrp data cksum] (parse-address address) diff --git a/src/more_speech/core.clj b/src/more_speech/core.clj index cd31ef0..9876fb6 100644 --- a/src/more_speech/core.clj +++ b/src/more_speech/core.clj @@ -25,7 +25,7 @@ (log-pr 1 'main arg 'start) (when (= "test" arg) (config/test-run!)) - (when (re-matches #"hours:\d+" arg) + (when (and (some? arg) (re-matches #"hours:\d+" arg)) (let [hours (Integer/parseInt (subs arg 6))] (set-mem :request-hours-ago hours)) ) diff --git a/src/more_speech/data_storage.clj b/src/more_speech/data_storage.clj index 1c62251..12952dc 100644 --- a/src/more_speech/data_storage.clj +++ b/src/more_speech/data_storage.clj @@ -6,7 +6,7 @@ [more-speech.db.xtdb :as xtdb] [more-speech.logger.default :refer [log-pr]] [more-speech.mem :refer :all] - [more-speech.nostr.event-handlers :as handlers] + [more-speech.nostr.event-dispatcher :as handlers] [more-speech.nostr.relays :as relays] [more-speech.nostr.util :as util] [more-speech.ui.formatter-util :as fu] diff --git a/src/more_speech/migrator.clj b/src/more_speech/migrator.clj index a0fd0b3..e80cba3 100644 --- a/src/more_speech/migrator.clj +++ b/src/more_speech/migrator.clj @@ -7,7 +7,7 @@ [more-speech.nostr [util :as util] [elliptic-signature :as ecc] - [event-handlers :as handlers]] + [event-dispatcher :as handlers]] [more-speech.data-storage :as data-storage] [more-speech.user-configuration :as user-configuration] [more-speech.db.gateway :as gateway] diff --git a/src/more_speech/nostr/event_handlers.clj b/src/more_speech/nostr/event_dispatcher.clj similarity index 97% rename from src/more_speech/nostr/event_handlers.clj rename to src/more_speech/nostr/event_dispatcher.clj index 7cf1d0e..6aa3693 100644 --- a/src/more_speech/nostr/event_handlers.clj +++ b/src/more_speech/nostr/event_dispatcher.clj @@ -1,4 +1,4 @@ -(ns more-speech.nostr.event-handlers +(ns more-speech.nostr.event-dispatcher (:require [clojure.data.json :as json] [more-speech.config :as config :refer [get-db]] [more-speech.db.gateway :as gateway] @@ -8,9 +8,8 @@ [more-speech.nostr.elliptic-signature :as ecc] [more-speech.nostr.events :as events] [more-speech.nostr.relays :as relays] - [more-speech.nostr.util :refer :all] - [more-speech.nostr.util :refer [unhexify]] - [more-speech.nostr.util :as util]) + [more-speech.nostr.zaps :as zaps] + [more-speech.nostr.util :refer :all :as util]) (:import (ecdhJava SECP256K1))) (defprotocol event-handler @@ -126,6 +125,7 @@ 3 (contact-list/process-contact-list db event) 4 (process-text-event db event url) 7 (process-reaction db event) + 9735 (zaps/process-zap-receipt event) nil)))) (defn decrypt-his-dm [event] diff --git a/src/more_speech/nostr/protocol.clj b/src/more_speech/nostr/protocol.clj index 34913b4..85efa4c 100644 --- a/src/more_speech/nostr/protocol.clj +++ b/src/more_speech/nostr/protocol.clj @@ -4,7 +4,7 @@ [more-speech.mem :refer :all] [more-speech.mem :refer :all] [more-speech.nostr.contact-list :as contact-list] - [more-speech.nostr.event-handlers :as handlers] + [more-speech.nostr.event-dispatcher :as handlers] [more-speech.nostr.events :as events] [more-speech.nostr.util :as util] [more-speech.relay :as relay] diff --git a/src/more_speech/nostr/util.clj b/src/more_speech/nostr/util.clj index e167696..3bcb929 100644 --- a/src/more_speech/nostr/util.clj +++ b/src/more_speech/nostr/util.clj @@ -1,5 +1,7 @@ (ns more-speech.nostr.util - (:import (java.security MessageDigest SecureRandom))) + (:import (java.awt Toolkit) + (java.awt.datatransfer StringSelection) + (java.security MessageDigest SecureRandom))) (defn num->bytes "Returns the byte-array representation of n. @@ -87,3 +89,10 @@ (defn get-now [] (quot (get-now-ms) 1000)) + +(defn get-clipboard [] + (.getSystemClipboard (Toolkit/getDefaultToolkit))) + +(defn copy-to-clipboard [text] + (let [selection (StringSelection. text)] + (.setContents (get-clipboard) selection selection))) diff --git a/src/more_speech/nostr/zaps.clj b/src/more_speech/nostr/zaps.clj index 25fe3cc..3500035 100644 --- a/src/more_speech/nostr/zaps.clj +++ b/src/more_speech/nostr/zaps.clj @@ -7,10 +7,12 @@ [more-speech.config :refer [get-db]] [more-speech.db.gateway :as gateway] [more-speech.logger.default :refer [log-pr]] + [more-speech.mem :refer :all] [more-speech.nostr.event-composers :as composers] [more-speech.nostr.events :as events] [more-speech.nostr.relays :as relays] - [more-speech.nostr.util :as util]) + [more-speech.nostr.util :as util] + [more-speech.ui.formatter-util :as formatter-util]) (:use (seesaw [core])) (:import (java.net URLEncoder))) @@ -52,7 +54,8 @@ (get-zap-address-from-profile event)))) (defn parse-lud16 [lud16] - (let [match (re-matches config/lud16-pattern lud16)] + (let [lud16 (.toLowerCase lud16) + match (re-matches config/lud16-pattern lud16)] (if (some? match) [(nth match 1) (nth match 2)] (throw (Exception. (str "bad lud16 format " lud16))))) @@ -80,7 +83,7 @@ :content comment :tags [(concat ["relays"] (relays/relays-for-reading)) ["amount" (str amount)] - ["lnurl" (bech32/encode "lnurl" (util/bytes->num (.getBytes lnurl)))] + ["lnurl" (bech32/encode-str "lnurl" lnurl)] ["p" (util/hexify recipient)] ["e" (util/hexify (:id event))]]} [_ request] (composers/body->event body)] @@ -114,10 +117,23 @@ _ (when (and (some? json-status) (= "ERROR" json-status)) (throw (Exception. (str "Invoice request error: " (get invoice-json "reason"))))) invoice (get invoice-json "pr")] - (prn 'zap-author invoice) - (prn 'zap-author 'metadata metadata)) + (update-mem :pending-zaps assoc invoice {:id (:id event) + :amount amount}) + (util/copy-to-clipboard invoice) + (alert (str "Invoice is copied to clipboard.\n" + "Paste it into your wallet and Zap!\n\n" + (formatter-util/abbreviate invoice 20) + (subs invoice (- (count invoice) 5))))) ) (catch Exception e (log-pr 1 'zap-author (.getMessage e)) - (alert (str "Cannot zap. " (.getMessage e))))) - ) + (alert (str "Cannot zap. " (.getMessage e)))))) + +(defn process-zap-receipt [event] + (prn 'zap-receipt event) + (let [[[receipt-invoice]] (events/get-tag event :bolt11) + transaction (get-mem [:pending-zaps receipt-invoice])] + (when (some? transaction) + (let [{:keys [id amount]} transaction] + (prn 'got-zap-receipt (util/hexify id) (/ amount 1000) 'sats) + (update-mem :pending-zaps dissoc receipt-invoice))))) diff --git a/src/more_speech/ui/swing/main_window.clj b/src/more_speech/ui/swing/main_window.clj index b3053f1..c41bcdb 100644 --- a/src/more_speech/ui/swing/main_window.clj +++ b/src/more_speech/ui/swing/main_window.clj @@ -4,7 +4,7 @@ [more-speech.db.gateway :as gateway] [more-speech.logger.default :refer [log-pr]] [more-speech.mem :refer :all] - [more-speech.nostr.event-handlers :as handlers] + [more-speech.nostr.event-dispatcher :as handlers] [more-speech.nostr.util :as util] [more-speech.ui.formatter-util :as formatter-util] [more-speech.ui.swing.article-panel :as article-panel] diff --git a/src/more_speech/ui/swing/util.clj b/src/more_speech/ui/swing/util.clj index 01e69dc..d2b27b3 100644 --- a/src/more_speech/ui/swing/util.clj +++ b/src/more_speech/ui/swing/util.clj @@ -3,7 +3,8 @@ [more-speech.config :as config] [more-speech.db.gateway :as gateway] [more-speech.mem :refer :all] - [more-speech.nostr.event-handlers :as event-handlers] + [more-speech.nostr.event-dispatcher :as event-handlers] + [more-speech.nostr.util :as util] [more-speech.ui.swing.article-tree-util :as at-util]) (:use (seesaw [core])) (:import (java.awt.datatransfer StringSelection))) @@ -96,12 +97,8 @@ (let [send-chan (get-mem :send-chan)] (future (async/>!! send-chan [:relaunch])))) -(defn- get-clipboard [] - (.getSystemClipboard (java.awt.Toolkit/getDefaultToolkit))) - (defn copy-to-clipboard [text _e] - (let [selection (StringSelection. text)] - (.setContents (get-clipboard) selection selection))) + (util/copy-to-clipboard text)) (defn load-event [id] (let [event (gateway/get-event (config/get-db) id)