aboutsummaryrefslogtreecommitdiffstats
path: root/callback.go
diff options
context:
space:
mode:
authorSam Anthony <sam@samanthony.xyz>2026-06-30 16:03:45 -0230
committerSam Anthony <sam@samanthony.xyz>2026-06-30 16:03:45 -0230
commit7d0f3d56121feccfe84600a39c362ba8bfcaa042 (patch)
tree0e412db49a133af1020ae13d6a0f3930fa04ee06 /callback.go
downloadxmrpayclnt-7d0f3d56121feccfe84600a39c362ba8bfcaa042.zip
init
Diffstat (limited to 'callback.go')
-rw-r--r--callback.go164
1 files changed, 164 insertions, 0 deletions
diff --git a/callback.go b/callback.go
new file mode 100644
index 0000000..e721250
--- /dev/null
+++ b/callback.go
@@ -0,0 +1,164 @@
+package xmrpayclnt
+
+import (
+ "crypto/rand"
+ "encoding/base64"
+ "encoding/json"
+ "net/http"
+ "net/url"
+ "sync"
+
+ "gitlab.com/moneropay/moneropay/v2/pkg/model"
+)
+
+const callbackIdEntropy = 16 // 128 bits of entropy
+
+var callbackIdEnc = base64.URLEncoding
+
+type callbackId string
+
+func newCallbackId() callbackId {
+ raw := make([]byte, callbackIdEntropy)
+ rand.Read(raw)
+ return callbackId(callbackIdEnc.EncodeToString(raw))
+}
+
+// Callback receives information about a transaction.
+//
+// Whenever MoneroPay sends a POST request to the callback URL specified
+// in the POST /receive endpoint, the information is sent on C. C and
+// Complete are closed once the transfer's unlocked amount is >= the
+// requested amount, or if Close is called.
+type Callback struct {
+ id callbackId
+ url *url.URL
+ C <-chan model.CallbackResponse
+ Complete <-chan struct{}
+ in chan<- model.CallbackResponse // from http handler
+ kill chan<- struct{}
+}
+
+func newCallback(id callbackId, url *url.URL, done chan callbackId) Callback {
+ c := make(chan model.CallbackResponse)
+ complete := make(chan struct{})
+ in := make(chan model.CallbackResponse)
+ kill := make(chan struct{})
+
+ go func() {
+ defer func() {
+ done <- id // signal to handler
+ <-done // wait for handler to unlink us before closing input channel
+ close(in)
+
+ close(c)
+ close(complete)
+ }()
+
+ var info model.CallbackResponse
+ var fresh bool
+ for {
+ if fresh {
+ if info.Complete {
+ return
+ }
+ select {
+ case info = <-in:
+ case c <- info:
+ fresh = false
+ case <-kill:
+ return
+ }
+ } else {
+ select {
+ case info = <-in:
+ fresh = true
+ case <-kill:
+ return
+ }
+ }
+ }
+ }()
+
+ return Callback{id, url, c, complete, in, kill}
+}
+
+// Close causes the handler to stop listening for MoneroPay's callbacks
+// relating to this transaction and closes the Callback's channels.
+func (cb Callback) Close() { close(cb.kill) }
+
+func (cb Callback) URL() *url.URL { return cb.url }
+
+// CallbackHandler is a HTTP handler that listens for callback POST
+// requests from the MoneroPay server.
+type CallbackHandler struct {
+ base *url.URL
+ done chan callbackId
+
+ mu sync.Mutex // guards below
+ cbs map[callbackId]Callback
+}
+
+func NewCallbackHandler(base *url.URL) *CallbackHandler {
+ done := make(chan callbackId)
+ cbs := make(map[callbackId]Callback)
+ h := &CallbackHandler{base, done, sync.Mutex{}, cbs}
+ go func() {
+ for id := range done {
+ h.mu.Lock()
+ delete(cbs, id)
+ h.mu.Unlock()
+ done <- id // callback can continue cleaning up
+ }
+ }()
+ return h
+}
+
+// ServeHTTP implements http.Handler.
+func (h CallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ // Decode callback body
+ var info model.CallbackResponse
+ dec := json.NewDecoder(r.Body)
+ if err := dec.Decode(&info); err != nil {
+ lg.Printf("%s: %v\n", r.URL, err)
+ return
+ }
+
+ // Send to callback goroutine
+ h.mu.Lock()
+ defer h.mu.Unlock()
+ id := callbackId(r.URL.Query().Get("id"))
+ if cb, ok := h.cbs[id]; ok {
+ cb.in <- info
+ } else {
+ lg.Printf("%s: no such callback %q\n", r.URL, id)
+ }
+}
+
+// listen starts listening for callbacks for a new transaction.
+func (h CallbackHandler) listen() Callback {
+ id := newCallbackId()
+ url := callbackUrl(h.base, id)
+
+ h.mu.Lock()
+ defer h.mu.Unlock()
+ cb := newCallback(id, url, h.done)
+ h.cbs[id] = cb
+ return cb
+}
+
+func callbackUrl(base *url.URL, id callbackId) *url.URL {
+ url := copyUrl(base)
+ q := url.Query()
+ q.Set("id", string(id))
+ url.RawQuery = q.Encode()
+ return url
+}
+
+func copyUrl(u *url.URL) *url.URL {
+ cp := *u
+ if u.User != nil {
+ ucp := *u.User
+ cp.User = &ucp
+ }
+ return &cp
+}