mirror of
https://github.com/Xahau/Validation-Ledger-Tx-Store-to-xPOP.git
synced 2025-11-04 20:35:49 +00:00
Beta, add webserver (socket, event, dirlisting), tx ordering, etc.
This commit is contained in:
32
README.md
32
README.md
@@ -12,7 +12,10 @@ Based on the work by @RichardAH: https://github.com/RichardAH/xpop-generator
|
||||
|
||||
This tool creates a folder structore in the `./store` directory, where it creates sub-directories like this:
|
||||
|
||||
> `store / {networkid} / {ledgerno(0, -6)} / {ledgerno(-6, -3)} / {ledgerno(-3)} /`
|
||||
> `store / {networkid} / {ledgerpath///} /`
|
||||
|
||||
The `ledgerpath` is the ledger index chunked from right to left in sections of three digits, making sure there
|
||||
are max. 1000 subfolders per level. This allows for easy dir listing & cleaning.
|
||||
|
||||
So e.g. for NetworkId `21338`, ledger index `82906790`, the path would be:
|
||||
|
||||
@@ -37,3 +40,30 @@ Every folder will contain the following files:
|
||||
`npm run dev` to launch (verbose)
|
||||
`npm run xpopgen` to launch, less verbose
|
||||
|
||||
## Webserver
|
||||
|
||||
This script also runs a webserver is the env. var is provided for the TCP port & URL Prefix where the app will run:
|
||||
|
||||
```bash
|
||||
EVENT_SOCKET_PORT="3000"
|
||||
URL_PREFIX="https://4849bf891e06.ngrok.app"
|
||||
```
|
||||
|
||||
#### WebSocket
|
||||
|
||||
You can listen for xPOP publish events (live, so you don't hve to poll).
|
||||
|
||||
By default you will get all xPOP events. If you want to filter on a specific address, provide
|
||||
the r-address in the URL path. If you also want to receive the xPOP Blob, also provide `/blob` in the URL path.
|
||||
|
||||
E.g. `/blob/rwietsevLFg8XSmG3bEZzFein1g8RBqWDZ` would listen for xPOPs for account `rwietsevLFg8XSmG3bEZzFein1g8RBqWDZ`
|
||||
and serve the (hex encoded) xPOP in the `xpop.blob` property.
|
||||
|
||||
#### HTTP File Browser
|
||||
|
||||
On the HTTP port a file listing is also provided & xPOPs can be downloaded at `/xpop/{tx hash}`.
|
||||
|
||||
Original source files to reconstruct the xPOP locally can be downloaded at `/{networkid}/`.
|
||||
|
||||
This file browser is for development and test purposes only, for production, put a static webserver
|
||||
in front of this application & reverse proxy only the WebSocket (HTTP Upgrade) server.
|
||||
|
||||
92
bin/webserver.mjs
Normal file
92
bin/webserver.mjs
Normal file
@@ -0,0 +1,92 @@
|
||||
import { WebSocket } from 'ws'
|
||||
import morgan from 'morgan'
|
||||
import express from 'express'
|
||||
import expressWs from 'express-ws'
|
||||
import serveIndex from 'serve-index'
|
||||
import 'dotenv/config'
|
||||
|
||||
let wss // WebSocket Server
|
||||
|
||||
if (!wss) {
|
||||
if (process.env?.EVENT_SOCKET_PORT && process.env?.URL_PREFIX) {
|
||||
const port = Number(process.env.EVENT_SOCKET_PORT)
|
||||
|
||||
try {
|
||||
const app = express()
|
||||
app.enable('trust proxy')
|
||||
app.use(morgan('combined', { }))
|
||||
|
||||
wss = expressWs(app)
|
||||
|
||||
// app.use(function middlware (req, res, next) {
|
||||
// return next()
|
||||
// })
|
||||
|
||||
app.use('/', express.static('./store/'))
|
||||
|
||||
app.use('/:networkId([0-9]{1,})', (req, res, next) => {
|
||||
return serveIndex('./store/' + req.params.networkId + '/', { icons: false })(req, res, next)
|
||||
})
|
||||
|
||||
// app.get('/', function route (req, res, next){
|
||||
// console.log('get route', req.testing)
|
||||
// res.end()
|
||||
// })
|
||||
|
||||
app.ws('*', function wsclient (ws, req) {
|
||||
Object.assign(ws, { req, })
|
||||
ws.on('message', function wsmsg (msg) {
|
||||
// Ignore
|
||||
// console.log(msg)
|
||||
})
|
||||
})
|
||||
|
||||
app.listen(port)
|
||||
console.log('Started Event Socket Service at TCP port', port)
|
||||
} catch (e) {
|
||||
console.log('Cannot start Webserver & Event Socket Service at port', port, e)
|
||||
}
|
||||
} else {
|
||||
console.log('Not starting Webserver & Event Socket Service, EVENT_SOCKET_PORT and/or URL_PREFIX unset')
|
||||
}
|
||||
}
|
||||
|
||||
const emit = _data => {
|
||||
if (wss) {
|
||||
wss.getWss().clients.forEach(function each (client) {
|
||||
const data = Object.assign({}, { ..._data })
|
||||
// Needed to prevent shared object manipulation with multiple clients
|
||||
data.xpop = Object.assign({}, _data.xpop)
|
||||
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
// console.log(client)
|
||||
// console.log(client?._xpopAccount)
|
||||
// console.log(client?._xpopBlob)
|
||||
|
||||
let account = ''
|
||||
const accountAddress = client.req.url.match(/r[a-zA-Z0-9]{18,}/)
|
||||
const blob = !!client.req.url.match(/\/blob/i)
|
||||
|
||||
if (accountAddress) {
|
||||
account = accountAddress[0]
|
||||
}
|
||||
|
||||
if (!blob && data?.xpop?.blob) {
|
||||
data.xpop.blob = undefined
|
||||
}
|
||||
|
||||
if (account === '' || data.account === account) {
|
||||
client.send(JSON.stringify(data), { binary: false })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return wss.getWss().clients.length
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export {
|
||||
emit,
|
||||
}
|
||||
15
index.mjs
15
index.mjs
@@ -2,13 +2,14 @@ import { XrplClient } from 'xrpl-client'
|
||||
import { createDirectory } from './lib/createDirectory.mjs'
|
||||
import { onValidation } from './lib/onValidation.mjs'
|
||||
import { onLedger } from './lib/onLedger.mjs'
|
||||
import { onTransaction } from './lib/onTransaction.mjs'
|
||||
import 'dotenv/config'
|
||||
import assert from 'assert'
|
||||
import './bin/webserver.mjs'
|
||||
|
||||
assert(process.env?.NODES, 'ENV var missing: NODES, containing: a comma separated list of websocket endpoints')
|
||||
|
||||
await createDirectory('store')
|
||||
await createDirectory('store/xpop')
|
||||
|
||||
process.env.NODES.split(',').map(h => h.trim())
|
||||
.map(h => new XrplClient(h)).map(async c => {
|
||||
@@ -22,8 +23,9 @@ process.env.NODES.split(',').map(h => h.trim())
|
||||
c.send({ command: "subscribe", streams: [
|
||||
"validations",
|
||||
"ledger",
|
||||
"transactions",
|
||||
"transactions_proposed"
|
||||
// No transactions, to make it easier for clients transactions are
|
||||
// processed in order (sorted on sequence) and emitted in order
|
||||
// to clients to prevent async tx sequence problems.
|
||||
] })
|
||||
|
||||
c.on("validation", validation => onValidation({
|
||||
@@ -38,11 +40,4 @@ process.env.NODES.split(',').map(h => h.trim())
|
||||
ledger,
|
||||
connection: c,
|
||||
}))
|
||||
|
||||
c.on("transaction", transaction => onTransaction({
|
||||
connectionUrl: c.getState()?.server?.uri,
|
||||
networkId: c.getState()?.server?.networkId,
|
||||
transaction,
|
||||
connection: c,
|
||||
}))
|
||||
})
|
||||
|
||||
91
lib/events/ledgerReady.mjs
Normal file
91
lib/events/ledgerReady.mjs
Normal file
@@ -0,0 +1,91 @@
|
||||
const ledgers = {}
|
||||
|
||||
const externalResolvablePromise = () => {
|
||||
let _resolve
|
||||
const meta = {
|
||||
resolved: false,
|
||||
}
|
||||
const promise = new Promise(resolve => {
|
||||
_resolve = (r) => {
|
||||
meta.resolved = true
|
||||
return resolve(r)
|
||||
}
|
||||
})
|
||||
|
||||
return { promise, resolve: _resolve, meta, }
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} ledger - Ledger Index
|
||||
* @param {(ledger_binary_transactions|ledger_info|vl|validation)} readyElement -
|
||||
*/
|
||||
|
||||
const ledgerReady = async (ledger, readyElement) => {
|
||||
// console.log('LedgerReady', ledger, readyElement)
|
||||
const ledgerIndexString = String(ledger)
|
||||
|
||||
if (!ledgers?.[ledgerIndexString]) {
|
||||
const ledger_binary_transactions = externalResolvablePromise()
|
||||
const ledger_info = externalResolvablePromise()
|
||||
const vl = externalResolvablePromise()
|
||||
|
||||
const ready = Promise.all([
|
||||
ledger_binary_transactions.promise,
|
||||
ledger_info.promise,
|
||||
vl.promise,
|
||||
])
|
||||
|
||||
Object.assign(ledgers, {
|
||||
[ledgerIndexString]: {
|
||||
ledger_binary_transactions,
|
||||
ledger_info,
|
||||
vl,
|
||||
validation: 0,
|
||||
ready,
|
||||
}
|
||||
})
|
||||
|
||||
// Set timeout to clean up
|
||||
setTimeout(() => {
|
||||
// console.log('Cleaning up', ledgerIndexString)
|
||||
if (ledgers?.[ledgerIndexString]) {
|
||||
ledgers?.[ledgerIndexString]?.ledger_binary_transactions?.resolve(false)
|
||||
ledgers?.[ledgerIndexString]?.ledger_info?.resolve(false)
|
||||
ledgers?.[ledgerIndexString]?.vl?.resolve(false)
|
||||
}
|
||||
|
||||
// Force GC
|
||||
setTimeout(() => {
|
||||
if (ledgers?.[ledgerIndexString]) delete ledgers?.[ledgerIndexString]
|
||||
}, 50)
|
||||
}, 20_000)
|
||||
}
|
||||
|
||||
if (
|
||||
readyElement === 'ledger_binary_transactions'
|
||||
|| readyElement === 'ledger_info'
|
||||
|| readyElement === 'vl'
|
||||
) {
|
||||
ledgers[ledgerIndexString][readyElement].resolve(new Date() / 1000)
|
||||
}
|
||||
if (readyElement === 'validation') {
|
||||
ledgers[ledgerIndexString][readyElement]++
|
||||
}
|
||||
}
|
||||
|
||||
const waitForLedgerReady = ledgerIndex => {
|
||||
return ledgers?.[String(ledgerIndex)]?.ready
|
||||
}
|
||||
|
||||
const isLedgerReady = ledgerIndex => {
|
||||
return ledgers?.[String(ledgerIndex)]?.ledger_binary_transactions.meta.resolved
|
||||
&& ledgers?.[String(ledgerIndex)]?.ledger_info.meta.resolved
|
||||
&& ledgers?.[String(ledgerIndex)]?.vl.meta.resolved
|
||||
}
|
||||
|
||||
export {
|
||||
ledgerReady,
|
||||
isLedgerReady,
|
||||
waitForLedgerReady,
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import { writeFile } from 'fs'
|
||||
import { ledgerIndexToFolders } from './ledgerIndexToFolders.mjs'
|
||||
import { computeBinaryTransactionHash } from './computeBinaryTransactionHash.mjs'
|
||||
import { dirExists } from './dirExists.mjs'
|
||||
import { ledgerReady, waitForLedgerReady } from './events/ledgerReady.mjs'
|
||||
import { onTransaction } from './onTransaction.mjs'
|
||||
import 'dotenv/config'
|
||||
|
||||
const obtainedHumanReadableLedgers = []
|
||||
@@ -18,7 +20,7 @@ const onLedger = async ({
|
||||
const storeDir = new URL('../' + relativeStoreDir, import.meta.url).pathname
|
||||
|
||||
if (await dirExists(storeDir)) {
|
||||
;[
|
||||
const ledgerData = [
|
||||
...(
|
||||
obtainedBinaryTxLedgers.indexOf(ledger.ledger_index) < 0
|
||||
? [
|
||||
@@ -37,6 +39,9 @@ const onLedger = async ({
|
||||
connection.send({
|
||||
command: 'ledger',
|
||||
ledger_index: ledger.ledger_index,
|
||||
transactions: true,
|
||||
expand: true,
|
||||
binary: false,
|
||||
})
|
||||
]
|
||||
: []),
|
||||
@@ -71,6 +76,8 @@ const onLedger = async ({
|
||||
writeFile(storeDir + '/ledger_binary_transactions.json', Buffer.from(JSON.stringify(results.ledger), 'utf8'), err => {
|
||||
if (err) {
|
||||
console.log('Error writing file @ ' + storeDir)
|
||||
} else {
|
||||
ledgerReady(results.ledger_index, 'ledger_binary_transactions')
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -87,16 +94,54 @@ const onLedger = async ({
|
||||
|
||||
console.log('Obtained ledger (JSON object)', relativeStoreDir, results.ledger_index, 'Hash', results.ledger.ledger_hash)
|
||||
|
||||
writeFile(storeDir + '/ledger_info.json', Buffer.from(JSON.stringify(results.ledger), 'utf8'), err => {
|
||||
writeFile(storeDir + '/ledger_info.json', Buffer.from(JSON.stringify({ ...results.ledger, transactions: undefined, }), 'utf8'), err => {
|
||||
if (err) {
|
||||
console.log('Error writing file @ ' + storeDir)
|
||||
} else {
|
||||
ledgerReady(ledger.ledger_index, 'ledger_info')
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
return results.ledger
|
||||
}))
|
||||
|
||||
/**
|
||||
* Deal with transactions & fire events
|
||||
*/
|
||||
waitForLedgerReady(ledger.ledger_index).then(async () => {
|
||||
if (ledgerData.length > 0) {
|
||||
const [binary, json] = await Promise.all(ledgerData)
|
||||
const sequetiallyMappedLedgerTxEvents = (json?.transactions || []).map(tx => {
|
||||
return {
|
||||
validated: true,
|
||||
ledger_index: ledger.ledger_index,
|
||||
transaction: tx,
|
||||
}
|
||||
})
|
||||
.sort((a, b) => a.transaction.Sequence - b.transaction.Sequence)
|
||||
.reduce((promiseChain, current) => {
|
||||
return promiseChain.then(() => {
|
||||
// console.log(' » Tx events: Processing', current.transaction.Sequence)
|
||||
return onTransaction({
|
||||
networkId,
|
||||
transaction: current,
|
||||
connection,
|
||||
})
|
||||
}).then(() => {
|
||||
// console.log(' » Tx events: Done ', current.transaction.Sequence)
|
||||
})
|
||||
}, Promise.resolve())
|
||||
|
||||
sequetiallyMappedLedgerTxEvents.then(() => {
|
||||
// console.log(' « « « « All transactions in ledger processed', ledger.ledger_index)
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { writeFile } from 'fs'
|
||||
import { dirExists } from './dirExists.mjs'
|
||||
import { ledgerIndexToFolders } from './ledgerIndexToFolders.mjs'
|
||||
import { generateV1 as xpop } from '../xpop/generateV1.mjs'
|
||||
import { xpopGenerate } from './xpopGenerate.mjs'
|
||||
import { waitForLedgerReady } from './events/ledgerReady.mjs'
|
||||
import { emit } from '../bin/webserver.mjs'
|
||||
import 'dotenv/config'
|
||||
|
||||
const xpopBinaryDir = new URL('../store/xpop', import.meta.url).pathname
|
||||
const lastSeenTransactions = []
|
||||
|
||||
const fields = (process.env?.FIELDSREQUIRED || '')
|
||||
@@ -21,52 +24,94 @@ const onTransaction = async ({
|
||||
networkId,
|
||||
transaction,
|
||||
}) => {
|
||||
if (transaction?.validated) {
|
||||
const { transaction: tx } = transaction
|
||||
if (transaction?.validated) {
|
||||
const { transaction: tx } = transaction
|
||||
|
||||
if (tx.hash && lastSeenTransactions.indexOf(tx.hash) < 0) {
|
||||
lastSeenTransactions.unshift(tx.hash)
|
||||
lastSeenTransactions.length = 3000
|
||||
|
||||
const validTx = hasRequiredFields(tx)
|
||||
if (!process.env?.NOELIGIBLEFULLTXLOG) {
|
||||
console.log('TX', tx.hash, validTx)
|
||||
}
|
||||
|
||||
if (validTx && transaction?.ledger_index) {
|
||||
const relativeStorDir = 'store/' + networkId + '/' + ledgerIndexToFolders(transaction.ledger_index)
|
||||
const storeDir = new URL('../' + relativeStorDir, import.meta.url).pathname
|
||||
|
||||
console.log('xPOP eligible', relativeStorDir, process.env?.NOELIGIBLEFULLTXLOG ? tx.hash : tx)
|
||||
|
||||
if (await dirExists(storeDir)) {
|
||||
const wroteTxFile = await new Promise(resolve => {
|
||||
writeFile(storeDir + '/tx_' + tx.hash + '.json', Buffer.from(JSON.stringify(transaction), 'utf8'), err => {
|
||||
if (err) {
|
||||
console.log('Error writing file @ ' + storeDir)
|
||||
resolve(false)
|
||||
}
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
|
||||
if (tx.hash && lastSeenTransactions.indexOf(tx.hash) < 0) {
|
||||
lastSeenTransactions.unshift(tx.hash)
|
||||
lastSeenTransactions.length = 3000
|
||||
|
||||
const validTx = hasRequiredFields(tx)
|
||||
if (!process.env?.NOELIGIBLEFULLTXLOG) {
|
||||
console.log('TX', tx.hash, validTx)
|
||||
}
|
||||
|
||||
if (validTx && transaction?.ledger_index) {
|
||||
const relativeStorDir = 'store/' + networkId + '/' + ledgerIndexToFolders(transaction.ledger_index)
|
||||
const storeDir = new URL('../' + relativeStorDir, import.meta.url).pathname
|
||||
|
||||
console.log('xPOP eligible', relativeStorDir, process.env?.NOELIGIBLEFULLTXLOG ? tx.hash : tx)
|
||||
|
||||
if (await dirExists(storeDir)) {
|
||||
writeFile(storeDir + '/tx_' + tx.hash + '.json', Buffer.from(JSON.stringify(transaction), 'utf8'), err => {
|
||||
if (err) {
|
||||
console.log('Error writing file @ ' + storeDir)
|
||||
} else {
|
||||
if (wroteTxFile) {
|
||||
await waitForLedgerReady(transaction.ledger_index)
|
||||
/**
|
||||
* TX all ready, written to filesystem, ...
|
||||
* This is where we start a slight delay to give the `onLedger`
|
||||
* routine some time to fetch & store and then we'll try to
|
||||
* generate an xPOP.
|
||||
*/
|
||||
setTimeout(async () => {
|
||||
await xpop({
|
||||
ledgerIndex: transaction.ledger_index,
|
||||
networkId,
|
||||
txHash: tx.hash,
|
||||
const xpopBinary = await xpopGenerate({
|
||||
ledgerIndex: transaction.ledger_index,
|
||||
networkId,
|
||||
txHash: tx.hash,
|
||||
})
|
||||
if (await dirExists(xpopBinaryDir)) {
|
||||
const xpopWritten = await new Promise(resolve => {
|
||||
writeFile(xpopBinaryDir + '/' + tx.hash, Buffer.from(xpopBinary, 'utf8'), err => {
|
||||
if (err) {
|
||||
console.log('Error writing binary XPOP', err)
|
||||
resolve(false)
|
||||
} else {
|
||||
console.log('Wrote binary xPOP: ' + xpopBinaryDir + '/' + tx.hash)
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
}, 500)
|
||||
// ^^ To check: is this enough? If e.g. retrieving the ledger info
|
||||
// would take longer this may not be enough. Best solution:
|
||||
// make this await the ledger fetching calls.
|
||||
// Dirty: extend to e.g. 2000.
|
||||
if (xpopWritten) {
|
||||
console.log(' ### EMIT XPOP READY FOR', tx?.Account, Number(tx.Sequence), tx.hash)
|
||||
|
||||
return await emit({
|
||||
account: tx?.Account,
|
||||
sequence: tx.Sequence,
|
||||
origin: {
|
||||
tx: tx.hash,
|
||||
networkId: networkId,
|
||||
ledgerIndex: transaction.ledger_index,
|
||||
burn: tx?.Fee,
|
||||
},
|
||||
destination: {
|
||||
networkId: tx?.OperationLimit,
|
||||
},
|
||||
...(
|
||||
process.env?.URL_PREFIX
|
||||
? {
|
||||
xpop: {
|
||||
binary: `${process.env.URL_PREFIX}/xpop/${tx.hash}`,
|
||||
source: `${process.env.URL_PREFIX}/${networkId}/${ledgerIndexToFolders(transaction.ledger_index)}/`,
|
||||
blob: xpopBinary,
|
||||
}
|
||||
}
|
||||
: {}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createDirectory } from './createDirectory.mjs'
|
||||
import 'dotenv/config'
|
||||
import { unlData } from './unlData.mjs'
|
||||
import { ledgerIndexToFolders } from './ledgerIndexToFolders.mjs'
|
||||
import { ledgerReady } from './events/ledgerReady.mjs'
|
||||
|
||||
const lastSeenValidations = []
|
||||
let lastCreatedLedgerDir
|
||||
@@ -34,6 +35,8 @@ const onValidation = async ({
|
||||
const relativeStorDir = 'store/' + networkId + '/' + ledgerIndexToFolders(validation.ledger_index)
|
||||
const storeDir = new URL('../' + relativeStorDir, import.meta.url).pathname
|
||||
|
||||
ledgerReady(validation.ledger_index, 'validation')
|
||||
|
||||
if (lastCreatedLedgerDir !== validation.ledger_index) {
|
||||
await createDirectory(relativeStorDir)
|
||||
lastCreatedLedgerDir = validation.ledger_index
|
||||
@@ -41,6 +44,8 @@ const onValidation = async ({
|
||||
writeFile(storeDir + '/vl.json', Buffer.from(JSON.stringify(unlData.data), 'utf8'), err => {
|
||||
if (err) {
|
||||
console.log('Error writing file @ ' + storeDir)
|
||||
} else {
|
||||
ledgerReady(validation.ledger_index, 'vl')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import assert from 'assert'
|
||||
import { xpop } from './xpopV1.mjs'
|
||||
import { xpop } from './xpop/v1.mjs'
|
||||
import { writeFile, readFile, readdir } from 'fs'
|
||||
import { ledgerIndexToFolders } from '../lib/ledgerIndexToFolders.mjs'
|
||||
import { dirExists } from '../lib/dirExists.mjs'
|
||||
@@ -20,7 +20,7 @@ const catjson = async file => {
|
||||
return JSON.parse(buffer.toString())
|
||||
}
|
||||
|
||||
const generateV1 = async ({
|
||||
const xpopGenerate = async ({
|
||||
ledgerIndex,
|
||||
networkId,
|
||||
txHash
|
||||
@@ -83,5 +83,5 @@ const generateV1 = async ({
|
||||
}
|
||||
|
||||
export {
|
||||
generateV1,
|
||||
xpopGenerate,
|
||||
}
|
||||
788
package-lock.json
generated
788
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -5,7 +5,7 @@
|
||||
"main": "index.mjs",
|
||||
"scripts": {
|
||||
"dev": "nodemon .",
|
||||
"xpopdev": "source .env && nodemon --max-old-space-size=50 .",
|
||||
"xpopdev": "source .env && nodemon --max-old-space-size=40 .",
|
||||
"serve": "serve ./store/"
|
||||
},
|
||||
"author": "Wietse Wind <w@xrpl-labs.com>",
|
||||
@@ -20,12 +20,20 @@
|
||||
"dotenv": "^16.3.1",
|
||||
"ed25519": "^0.0.5",
|
||||
"elliptic": "^6.5.4",
|
||||
"express-ws": "^5.0.2",
|
||||
"morgan": "^1.10.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"ripple-address-codec": "^4.3.0",
|
||||
"ripple-binary-codec": "^1.10.0",
|
||||
"serve-index": "^1.9.1",
|
||||
"ws": "^8.14.2",
|
||||
"xrpl-client": "^2.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"serve": "^14.2.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "^4.0.7",
|
||||
"utf-8-validate": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user