diff options
| author | Sam Anthony <sam@samanthony.xyz> | 2026-06-30 16:03:45 -0230 |
|---|---|---|
| committer | Sam Anthony <sam@samanthony.xyz> | 2026-06-30 16:03:45 -0230 |
| commit | 7d0f3d56121feccfe84600a39c362ba8bfcaa042 (patch) | |
| tree | 0e412db49a133af1020ae13d6a0f3930fa04ee06 /callback.go | |
| download | xmrpayclnt-7d0f3d56121feccfe84600a39c362ba8bfcaa042.zip | |
init
Diffstat (limited to 'callback.go')
| -rw-r--r-- | callback.go | 164 |
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 +} |