Accepting Monero (XMR) payments using MoneroPay

UPDATE 2022-10-16: While the integration logic is still the same, there have been changes made to the API and the callback feature. Please refer to moneropay.eu for documentation.

MoneroPay is a simple backend service for merchants, developers and everyone who is looking to accept XMR. It sits on top of a wallet RPC server instance and provides a simple API. What makes MoneroPay stand out is its polling capabilities: when you’re expecting a payment, MoneroPay can notify you by callbacks whenever the payment is partially or fully completed.

Use cases for MoneroPay

MoneroPay is not simply a plugin for an existing e-commerce plugin. It’s a standalone backend daemon. For this reason, it can be used for anything but notably:

Example use case: Online store “Baskket”

In this blog post we will review how Baskket makes use of MoneroPay.

Cart

cart.html

Upon adding items to the cart, a JSON string is constructed and stored in localstorage.

[
  {
    "id": 4,
    "title": "Pixel art sticker 6x6",
    "price": 0.01,
    "atomic": 10000000000,
    "quantity": 2
  },
  {
    "id": 3,
    "title": "Pixel art sticker 8x8",
    "price": 0.015,
    "atomic": 15000000000,
    "quantity": 1
  },
  {
    "id": 1,
    "title": "IT guy's foot fungus",
    "price": 0.0001,
    "atomic": 100000000,
    "quantity": 1
  }
]

When an order is placed:

  1. Baskket will calculate the total expected amount and create an unique identifier (UID).
  2. Baskket will tell MoneroPay the expected amount and the callback URL constructed using the UID via a POST request and acquire a subaddress. This is so MoneroPay can tell Baskket when a transaction to the returned subaddress is received.
  3. Baskket will insert the following into the database:
    • UID (to match the callback with the order).
    • Subaddress (to remind the customer in case the received payment partially covers the expected amount).
    • Total (to compare with the already received amount).
    • The cart (to tell the seller about the order after the payment is completed).
    • Optional form data (delivery address, email).
  4. Baskket will render a page with the newly aqcuired subaddress, telling the customer where it expects the payment.

internal/order/order.go:

func Place(w http.ResponseWriter, r *http.Request) {
	cart := r.FormValue("cart")
	address := r.FormValue("address")
	email := r.FormValue("email")

	// Calculate total cost of items in cart.
	total, err := getCost(cart)
	if err != nil {
		log.Println(err)
		return
	}

	u := uuid.New()
	subaddr, err := getSubaddress(total, u)
	if err != nil {
		log.Println(err)
		return
	}

	err = database.ExecWithTimeout(r.Context(), 3 * time.Second,
	    "INSERT INTO orders (uuid, email, total, address, cart, subaddress)" +
	    "VALUES ($1, $2, $3, $4, $5, $6)", u, email, total, address, cart,
	    subaddr)
	if err != nil {
		log.Println(err)
		return
	}

	items := struct {
		Subaddress string
		Amount string
	}{
		Subaddress: subaddr,
		Amount: walletrpc.XMRToDecimal(total),
	}
	if err = page.Payment.Execute(w, items); err != nil {
		log.Fatal(err)
	}
}

func getSubaddress(total uint64, u string) (string, error) {
	resp, err := http.PostForm(config.MoneroPayHost,
	url.Values{"amount": {strconv.FormatUint(total, 10)},
	"callback_url": {config.CallbackAddr + "/callback/" + u},
		"description": {"baskket"}})
	if err != nil {
		return "", err
	}
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}
	var data models.ReceivePostResponse
	err = json.Unmarshal(body, &data)
	if err != nil {
		return "", err
	}
	return data.Address, nil
}

Callbacks

Baskket listens for callbacks on /callback/{uuid} . For security measures, the listening for callbacks can be done on another port which is bound to the local network only. But since the generated UIDs are random and long enough, we don’t worry about it.

When MoneroPay sees an incoming transfer to the subaddress, it will query for the callback URL associated with the subaddress and attempt to notify. Baskket’s listener will receive callback data from MoneroPay:

{
  "amount": 200000000,
  "fee": 9200000,
  "tx_hash": "0c9a7b40b15596fa9a06ba32463a19d781c075120bb59ab5e4ed2a97ab3b7f33",
  "address": "82j31dfbz1GPF7SWpusNjDAaucbit2NBZTMKyLYvqEfyUfWbRALx2bDaHDvvnbxngh56XRvqCYazsQ5xfGSAGWnYMciZVbe",
  "confirmations": 3297,
  "unlock_time": 0,
  "height": 2402648,
  "timestamp": "2021-07-11T19:19:05Z",
  "double_spend_seen": false
}

Baskket will query its database for the UID (uuid) in the URL it received the callback to. On a match, it will sum up the amount in the callback data with what has been previously received. If the received amount is higher or equal than the expected amount, the payment is completed and the seller will be notified. Otherwise, the customer will be notified about their partial payment with an email (if they provided an address).

internal/payment/payment.go:

func NotifyReceived(w http.ResponseWriter, r *http.Request) {
	uuid := mux.Vars(r)["uuid"]

	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		log.Fatal(err)
	}
	var data models.CallbackData
	err = json.Unmarshal(body, &data)
	if err != nil {
		log.Println(err)
		return
	}

	row, err := database.QueryRowWithTimeout(r.Context(), 3 * time.Second,
	"SELECT total, received, email, address, cart, subaddress FROM orders" +
	" WHERE uuid = $1", uuid)
        if err != nil {
		log.Println(err)
                return
        }
	var total, received uint64
	var email, address, cart, subaddr string
        err = row.Scan(&total, &received, &email, &address, &cart, &subaddr)
	if err != nil {
		log.Println(err)
                return
        }

	w.WriteHeader(http.StatusOK)

	xmr := received + data.Amount
	if (len(email) != 0) {
		if xmr >= total {
			// TODO: Templates for email.
			mail.Send(email, fmt.Sprintf(
				"Thank you! We are preparing your order." +
				" Received: %s/%s XMR.\nCart: %s\nAddress: %s",
				walletrpc.XMRToDecimal(xmr),
				walletrpc.XMRToDecimal(total), cart, address))
			mail.Send(config.MerchantMail, fmt.Sprintf(
				"Received order:\nPaid: %s XMR\nCart: %s" +
				"\nEmail: %s\nAddress: %s",
				walletrpc.XMRToDecimal(xmr), cart, email, address))
		} else {
			mail.Send(email, fmt.Sprintf(
				"Almost there! You need to transfer %s XMR more" +
				" to complete your order.\nReceived %s/%s XMR.\n" +
				"In case you lost it, your subaddress is: %s",
				walletrpc.XMRToDecimal(total - xmr),
				walletrpc.XMRToDecimal(xmr),
				walletrpc.XMRToDecimal(total), subaddr))
		}
	}

	err = database.ExecWithTimeout(r.Context(), 3 * time.Second,
	"UPDATE orders SET received = $1 WHERE uuid = $2", xmr, uuid)
	if err != nil {
		log.Println(err)
		return
	}
}

Conclusion

It’s this easy to get notified when you receive XMR using MoneroPay! If you have questions about MoneroPay, feel free to ask in #moneropay:kernal.eu. Consider donating to MoneroPay and Kernal here, we are not a part of the Monero Project’s Community Crowdfunding System (CCS).