feat: 🚧 Built initial payment frontend
parent
cc4f6ada2e
commit
d80f59bd3b
|
@ -50,6 +50,7 @@ services:
|
||||||
--rpc-bind-port=18082
|
--rpc-bind-port=18082
|
||||||
--password ${WALLET_PASSWORD}
|
--password ${WALLET_PASSWORD}
|
||||||
--daemon-host=${MONERO_DAEMON_HOST}
|
--daemon-host=${MONERO_DAEMON_HOST}
|
||||||
|
--tx-notify "/usr/bin/curl -s http://pago_api/new_tx/%s"
|
||||||
networks:
|
networks:
|
||||||
monero:
|
monero:
|
||||||
|
|
||||||
|
@ -73,6 +74,8 @@ services:
|
||||||
container_name: pago_api
|
container_name: pago_api
|
||||||
build: .
|
build: .
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- wallet
|
||||||
command: start
|
command: start
|
||||||
networks:
|
networks:
|
||||||
monero:
|
monero:
|
||||||
|
|
48
main.js
48
main.js
|
@ -1,6 +1,9 @@
|
||||||
const dotenv = require('dotenv')
|
const dotenv = require('dotenv')
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
|
const cors = require('cors')
|
||||||
const Wallet = require('monero-wallet-rpc-js')
|
const Wallet = require('monero-wallet-rpc-js')
|
||||||
|
const {createServer} = require('node:http')
|
||||||
|
const {Server} = require('socket.io')
|
||||||
|
|
||||||
;(async () => {
|
;(async () => {
|
||||||
|
|
||||||
|
@ -61,7 +64,7 @@ const Wallet = require('monero-wallet-rpc-js')
|
||||||
}
|
}
|
||||||
return getAccount(i)
|
return getAccount(i)
|
||||||
} else {
|
} 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
|
return account
|
||||||
}
|
}
|
||||||
}; getAccount(WALLET_ACCOUNT_INDEX)
|
}; getAccount(WALLET_ACCOUNT_INDEX)
|
||||||
|
@ -70,8 +73,20 @@ const Wallet = require('monero-wallet-rpc-js')
|
||||||
// Server
|
// Server
|
||||||
const app = express()
|
const app = express()
|
||||||
app.use(express.json())
|
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
|
// Healthchecks
|
||||||
app.get('/wallet/height', async (req, res) =>
|
app.get('/wallet/height', async (req, res) =>
|
||||||
|
@ -107,16 +122,16 @@ const Wallet = require('monero-wallet-rpc-js')
|
||||||
// Check payment
|
// Check payment
|
||||||
app.get('/payment/:addr', async (req, res) => {
|
app.get('/payment/:addr', async (req, res) => {
|
||||||
let subaddr_index; try {
|
let subaddr_index; try {
|
||||||
subaddr_index = (await wallet.getAddressIndex({
|
subaddr_index = (await wallet.getAddressIndex(req.params.addr)).index.minor
|
||||||
address: req.params.addr
|
|
||||||
})).index.minor
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Failed to get index of subaddress: ${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 {
|
let transfers; try {
|
||||||
transfers = Object.values(await wallet.getTransfers({
|
transfers = Object.values(await wallet.getTransfers({
|
||||||
|
all_accounts: true,
|
||||||
in: true,
|
in: true,
|
||||||
|
out: false,
|
||||||
pending: true,
|
pending: true,
|
||||||
failed: true,
|
failed: true,
|
||||||
pool: true,
|
pool: true,
|
||||||
|
@ -125,9 +140,28 @@ const Wallet = require('monero-wallet-rpc-js')
|
||||||
})).flat()
|
})).flat()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Failed to get transactions for subaddress: ${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)
|
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",
|
"version": "1.1.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"monero-wallet-rpc-js": "^1.2.20",
|
"monero-wallet-rpc-js": "^1.2.22",
|
||||||
"socket.io": "^4.7.5"
|
"socket.io": "^4.7.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -625,9 +626,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/monero-wallet-rpc-js": {
|
"node_modules/monero-wallet-rpc-js": {
|
||||||
"version": "1.2.20",
|
"version": "1.2.22",
|
||||||
"resolved": "https://registry.npmjs.org/monero-wallet-rpc-js/-/monero-wallet-rpc-js-1.2.20.tgz",
|
"resolved": "https://registry.npmjs.org/monero-wallet-rpc-js/-/monero-wallet-rpc-js-1.2.22.tgz",
|
||||||
"integrity": "sha512-Hvdxvugs911QL6lte9SaauQLUfTT+ZXrAJ1W9DX5acczEqhSncuneN9bSFQnXgGKeIg37FU9dkVfFK3G1Rkl6g==",
|
"integrity": "sha512-egQES7wI7yK11YhuLhM6+qgA0350Zco7itdhfIpcl6y9ksaVcGqUZeH8889eplN6KX2V7oF+DiIMZkCAQnf6ZA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.8"
|
"axios": "^1.6.8"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
{
|
{
|
||||||
"name": "pago",
|
"name": "pago",
|
||||||
"version": "1.1.5",
|
"version": "1.2.0",
|
||||||
"description": "Lightweight monero payment gateway",
|
"description": "Lightweight monero payment gateway",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node main.js"
|
"start": "node main.js"
|
||||||
},
|
},
|
||||||
|
@ -16,9 +17,10 @@
|
||||||
"author": "Keith Irwin (www.ki9.us)",
|
"author": "Keith Irwin (www.ki9.us)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"monero-wallet-rpc-js": "^1.2.20",
|
"monero-wallet-rpc-js": "^1.2.22",
|
||||||
"socket.io": "^4.7.5"
|
"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
|
WALLET_RESTORE_HEIGHT=3133069
|
||||||
# Which wallet account to use for payments
|
# Which wallet account to use for payments
|
||||||
WALLET_ACCOUNT_INDEX=1
|
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
|
||||||
WALLET_FILENAME='pago'
|
WALLET_FILENAME='pago'
|
||||||
# Wallet RPC
|
# Wallet RPC
|
||||||
|
|
|
@ -37,7 +37,7 @@ FROM ubuntu:20.04
|
||||||
|
|
||||||
RUN set -ex && \
|
RUN set -ex && \
|
||||||
apt-get update && \
|
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 && \
|
apt-get clean && \
|
||||||
rm -rf /var/lib/apt
|
rm -rf /var/lib/apt
|
||||||
COPY --from=builder /usr/local/src/monero/build/x86_64-linux-gnu/release/bin /usr/local/bin/
|
COPY --from=builder /usr/local/src/monero/build/x86_64-linux-gnu/release/bin /usr/local/bin/
|
||||||
|
|
Loading…
Reference in New Issue