feat: 🚧 Built initial payment frontend

main
Keith Irwin 2024-04-22 23:08:35 -06:00
parent cc4f6ada2e
commit d80f59bd3b
Signed by: ki9
GPG Key ID: DF773B3F4A88DA86
10 changed files with 174 additions and 14 deletions

View File

@ -50,6 +50,7 @@ services:
--rpc-bind-port=18082
--password ${WALLET_PASSWORD}
--daemon-host=${MONERO_DAEMON_HOST}
--tx-notify "/usr/bin/curl -s http://pago_api/new_tx/%s"
networks:
monero:
@ -73,6 +74,8 @@ services:
container_name: pago_api
build: .
restart: unless-stopped
depends_on:
- wallet
command: start
networks:
monero:

48
main.js
View File

@ -1,6 +1,9 @@
const dotenv = require('dotenv')
const express = require('express')
const cors = require('cors')
const Wallet = require('monero-wallet-rpc-js')
const {createServer} = require('node:http')
const {Server} = require('socket.io')
;(async () => {
@ -61,7 +64,7 @@ const Wallet = require('monero-wallet-rpc-js')
}
return getAccount(i)
} else {
console.log(`Account #${i} exists with a balance of ${deAtomize(account.balance)} XMR.`)
console.log(`Using account #${i} (balance: ${deAtomize(account.balance)} XMR).`)
return account
}
}; getAccount(WALLET_ACCOUNT_INDEX)
@ -70,8 +73,20 @@ const Wallet = require('monero-wallet-rpc-js')
// Server
const app = express()
app.use(express.json())
app.listen(80)
app.use(cors({
// origin: 'http://example.com',
}))
const server = createServer(app)
const io = new Server(server)
server.listen(80)
// Test websockets
io.on('connection', (socket) => {
console.log('a user connected')
socket.on('disconnect', () => {
console.log('user disconnected')
})
})
// Healthchecks
app.get('/wallet/height', async (req, res) =>
@ -107,16 +122,16 @@ const Wallet = require('monero-wallet-rpc-js')
// Check payment
app.get('/payment/:addr', async (req, res) => {
let subaddr_index; try {
subaddr_index = (await wallet.getAddressIndex({
address: req.params.addr
})).index.minor
subaddr_index = (await wallet.getAddressIndex(req.params.addr)).index.minor
} catch (err) {
console.error(`Failed to get index of subaddress: ${err}`)
return res.status(500).sendText('Failed to determine subaddress index!')
return res.sendStatus(500)
}
let transfers; try {
transfers = Object.values(await wallet.getTransfers({
all_accounts: true,
in: true,
out: false,
pending: true,
failed: true,
pool: true,
@ -125,9 +140,28 @@ const Wallet = require('monero-wallet-rpc-js')
})).flat()
} catch (err) {
console.error(`Failed to get transactions for subaddress: ${err}`)
return res.status(500).sendText('Failed to get transactions!')
return res.sendStatus(500)
}
return res.json(transfers)
})
// Listen for new transactions
app.get('/new_tx/:txid', async (req, res) => {
// TODO: Retry, sleep, retry, give up
console.log(`New transaction: ${req.params.txid}`)
let result; try {
result = await wallet.getTransfersByTxid({
account_index: WALLET_ACCOUNT_INDEX,
txid: req.params.txid,
})
if (result.transfer===undefined) throw new Error('Multiple transfers?')
} catch (err) {
console.error(`Failed to query transaction: ${err}`)
return res.sendStatus(500)
}
console.log(`Transfer received at ${result.transfer.subaddr_index.minor} for ${deAtomize(result.transfer.amount)} XMR to ${result.transfer.address}`)
// TODO: Send websocket
return res.sendStatus(200)
})
})()

11
merch/index.html Normal file
View File

@ -0,0 +1,11 @@
<html>
<head>
<title>Merchant Dashboard</title>
</head>
<body>
<h1>Merchant Dashboard</h1>
<p></p>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="script.js"></script>
</body>
</html>

0
merch/style.css Normal file
View File

9
package-lock.json generated
View File

@ -9,9 +9,10 @@
"version": "1.1.5",
"license": "MIT",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"monero-wallet-rpc-js": "^1.2.20",
"monero-wallet-rpc-js": "^1.2.22",
"socket.io": "^4.7.5"
}
},
@ -625,9 +626,9 @@
}
},
"node_modules/monero-wallet-rpc-js": {
"version": "1.2.20",
"resolved": "https://registry.npmjs.org/monero-wallet-rpc-js/-/monero-wallet-rpc-js-1.2.20.tgz",
"integrity": "sha512-Hvdxvugs911QL6lte9SaauQLUfTT+ZXrAJ1W9DX5acczEqhSncuneN9bSFQnXgGKeIg37FU9dkVfFK3G1Rkl6g==",
"version": "1.2.22",
"resolved": "https://registry.npmjs.org/monero-wallet-rpc-js/-/monero-wallet-rpc-js-1.2.22.tgz",
"integrity": "sha512-egQES7wI7yK11YhuLhM6+qgA0350Zco7itdhfIpcl6y9ksaVcGqUZeH8889eplN6KX2V7oF+DiIMZkCAQnf6ZA==",
"dependencies": {
"axios": "^1.6.8"
}

View File

@ -1,8 +1,9 @@
{
"name": "pago",
"version": "1.1.5",
"version": "1.2.0",
"description": "Lightweight monero payment gateway",
"main": "main.js",
"type": "commonjs",
"scripts": {
"start": "node main.js"
},
@ -16,9 +17,10 @@
"author": "Keith Irwin (www.ki9.us)",
"license": "MIT",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"monero-wallet-rpc-js": "^1.2.20",
"monero-wallet-rpc-js": "^1.2.22",
"socket.io": "^4.7.5"
}
}

106
pay/index.html Normal file
View File

@ -0,0 +1,106 @@
<!DOCTYPE html>
<html>
<head>
<title>Payment</title>
</head>
<body>
<h1>Payment</h1>
<!-- <div data-bind="hidden:isSubmitted()">
<p>Please send <code data-bind="text:unsubmitted"></code> XMR to this address: </p>
<pre><code data-bind="text:xmr_address"></code></pre>
<p>You can also click, tap, or scan this qr code:</p>
<a data-bind="attr:{href:xmr_uri}"><div id="qr_wrapper"><div id="xmr_qr"></div></div></a>
</div> -->
<!-- <p>Your order is for <code data-bind="text:totalxmr_pretty"></code>.
<br><code data-bind="text:submitted_pretty"></code> have been submitted to the blockchain.
<br><code data-bind="text:unlocked_pretty"></code> have been unlocked.
</p> -->
<!--<div data-bind="hidden:isPaid">
<p>Transactions you send will appear on this page when they are accepted into a block. <b><i>THIS COULD TAKE TEN MINUTES OR MORE</i></b>. Transactions "unlock" after receiving 10 confirmations on the blockchain, each taking a few minutes each. Once the full <span data-bind="text:totalxmr_pretty"></span> are unlocked, we'll ship your order.
<p>You don't have to wait for the payment to unlock. After sumbitting it in your wallet app<span data-bind="visible:isSubmitted()"> (which you've done already)</span>, you can close this window. When the payment confirms, we'll send a link to this page to the email specified, if you ever want to come back and check on the order.</p>
</div><div data-bind="visible:isPaid">
<p>Your payment has been confirmed! We will ship your order as soon as possible and send an email with the tracking info.</p>
</div>-->
<!--<p data-bind="visible:isOverpaid">You <i>overpaid</I> us by <code data-bind="text:overpaidAmount_pretty"></code>! We would be happy to return the change to you at no extra cost. This process is done by hand so please email us{% if env.SALES_EMAIL %} at {{env.SALES_EMAIL}}{% endif %} to let us know that you overpaid. Be sure to include your order number, <code data-bind="text:orderId"></code>, as well as a monero address where you can receive the refund. </p>-->
<h2 data-bind="visible:transactions().length>0">Transactions</h2>
<table data-bind="visible:transactions().length>0">
<thead>
<th>Date</th>
<th>Time</th>
<th>Amt</th>
<th>Confs</th>
<th>Block</th>
<th>Stat</th>
</thead>
<tbody data-bind="foreach:transactions"><tr>
<td data-bind="text:date"></td>
<td data-bind="text:time"></td>
<td data-bind="text:amount"></td>
<td data-bind="text:confirmations"></td>
<td data-bind="text:height"></td>
<td style="cursor:default">
<span data-bind="visible:locked" title="LOCKED: Wait for 10 confirmations for this transaction to unlock">⏲️</span>
<span data-bind="visible:double_spend_seen" title="DOUBLE SPENT! Double-spend has been detected! This transaction is invalid."></span>
<span data-bind="hidden:(locked||double_spend_seen)" title="CONFIRMED! This transaction is valid and has been confirmed by the blockchain."></span>
</td>
</tr></tbody>
</table>
<!--<p data-bind="hidden:isPaid"><i data-bind="text:checkingStatus"></i></p>-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.5/socket.io.js" integrity="sha512-luMnTJZ7oEchNDZAtQhgjomP1eZefnl82ruTH/3Oj/Yu5qYtwL7+dVRccACS/Snp1lFXq188XFipHKYE75IaQQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script type="application/javascript">
// Read querystring
const qstr = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
})
const API = 'http://localhost:8080'
class Transaction { constructor(data) {
let self = this
self.amount = data.amount/1000000000000
self.confirmations = ko.observable(data.confirmations||0)
self.double_spend_seen = ko.observable(false)
self.fee = data.fee/1000000000000
self.height = data.height
self.datetime = new Date(data.timestamp)
self.date = self.datetime.toLocaleDateString()
self.time = self.datetime.toLocaleTimeString()
self.txid = data.txid
self.unlock_time = ko.observable(data.unlock_time||0)
self.locked = ko.observable(data.locked||false)
} }
class Payment { constructor(api) {
let self = this
self.transactions = ko.observableArray([])
;(async () => {
let res; try {
res = await fetch(`${API}/payment/${qstr.subaddr}`)
} catch (err) { console.error(err) }
let parsedRes; try {
parsedRes = await res.json()
} catch (err) { console.error(err) }
parsedRes.forEach( (tx) => {
self.transactions.push(new Transaction({
amount: tx.amount,
confirmations: tx.confirmations,
double_spent: tx.double_spend_seen,
fee: tx.fee,
height: tx.height,
timestamp: tx.timestamp,
txid: tx.id,
unlock_time: tx.unlock_time,
locked: tx.locked,
}) )
})
})()
} }
ko.applyBindings(new Payment(API))
</script>
</body>
</html>

0
pay/style.css Normal file
View File

View File

@ -23,6 +23,9 @@ WALLET_VIEWKEY='a23474fe2d3ef001b7f0d311d7bb10eb149bbf9c0e206f7f31cc91ab58781809
WALLET_RESTORE_HEIGHT=3133069
# Which wallet account to use for payments
WALLET_ACCOUNT_INDEX=1
# "Recipient" name that payers will see in their wallet history
# Change this to the name of your organization
PAYMENT_RECIPIENT='Sample company (www.example.com)'
# Wallet filename
WALLET_FILENAME='pago'
# Wallet RPC

View File

@ -37,7 +37,7 @@ FROM ubuntu:20.04
RUN set -ex && \
apt-get update && \
apt-get --no-install-recommends --yes install ca-certificates && \
apt-get --no-install-recommends --yes install ca-certificates curl && \
apt-get clean && \
rm -rf /var/lib/apt
COPY --from=builder /usr/local/src/monero/build/x86_64-linux-gnu/release/bin /usr/local/bin/