feat: 🚧 Built initial payment frontend
parent
cc4f6ada2e
commit
d80f59bd3b
|
@ -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
48
main.js
|
@ -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)
|
||||
})
|
||||
|
||||
})()
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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/
|
||||
|
|
Loading…
Reference in New Issue