mirror of
https://github.com/Permissionless-Software-Foundation/bch-dex.git
synced 2026-09-22 01:02:00 -07:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f5e694682 | ||
|
|
615638d705 | ||
|
|
9b46bbd7bb |
Vendored
+3
-1
@@ -47,7 +47,8 @@ export default {
|
||||
useFullStackCash: process.env.USE_FULLSTACKCASH ? true : false,
|
||||
consumerUrl: process.env.CONSUMER_URL
|
||||
? process.env.CONSUMER_URL
|
||||
: 'https://free-bch.fullstack.cash',
|
||||
// : 'https://free-bch.fullstack.cash',
|
||||
: 'https://dev-consumer.psfoundation.info',
|
||||
// : 'https://wa-usa-bch-consumer.fullstackcash.nl',
|
||||
|
||||
// P2WDB URL that will accept API calls from the p2wdb npm library.
|
||||
@@ -184,6 +185,7 @@ export default {
|
||||
// Nostr
|
||||
nostrRelay: process.env.NOSTR_RELAY ? process.env.NOSTR_RELAY : 'wss://nostr-relay.psfoundation.info',
|
||||
nostrTopic: process.env.NOSTR_TOPIC ? process.env.NOSTR_TOPIC : 'bch-dex-test-topic-02',
|
||||
nostrGlobalFeed: process.env.NOSTR_GLOBAL_FEED ? process.env.NOSTR_GLOBAL_FEED : 'slpdex-socialmedia',
|
||||
|
||||
// Account Configuration
|
||||
disableNewAccounts: process.env.DISABLE_NEW_ACCOUNTS ? true : false,
|
||||
|
||||
@@ -8,6 +8,7 @@ import Entries from './models/entry.js'
|
||||
import Order from './models/order.js'
|
||||
import Offer from './models/offer.js'
|
||||
import Usage from './models/usage.js'
|
||||
import SmAccount from './models/smAccount.js'
|
||||
|
||||
class LocalDB {
|
||||
constructor () {
|
||||
@@ -17,6 +18,7 @@ class LocalDB {
|
||||
this.Order = Order
|
||||
this.Offer = Offer
|
||||
this.Usage = Usage
|
||||
this.SmAccount = SmAccount
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
Social Media Account Model.
|
||||
|
||||
This model is used to track content creators using the DEX
|
||||
Nostr social media channel.
|
||||
*/
|
||||
|
||||
import mongoose from 'mongoose'
|
||||
|
||||
const SmAccount = new mongoose.Schema({
|
||||
npub: { type: String, required: true },
|
||||
bchAddr: { type: String, required: true, default: '' },
|
||||
pubkey: { type: String, required: true },
|
||||
pfp: { type: String, default: '' },
|
||||
createdAt: { type: Date, default: Date.now },
|
||||
updatedAt: { type: Date, default: Date.now },
|
||||
followers: { type: Number, default: 0 },
|
||||
following: { type: Number, default: 0 },
|
||||
posts: { type: Number, default: 0 },
|
||||
likes: { type: Number, default: 0 },
|
||||
comments: { type: Number, default: 0 },
|
||||
reposts: { type: Number, default: 0 }
|
||||
})
|
||||
|
||||
export default mongoose.model('smAccount', SmAccount)
|
||||
@@ -12,6 +12,7 @@ class NostrAdapter {
|
||||
constructor (localConfig = { nostrRelay: '', nostrTopic: '' }) {
|
||||
this.relayWs = localConfig.nostrRelay //
|
||||
this.topic = localConfig.nostrTopic
|
||||
this.globalFeed = localConfig.nostrGlobalFeed
|
||||
|
||||
if (!this.relayWs) {
|
||||
throw new Error(
|
||||
@@ -39,6 +40,8 @@ class NostrAdapter {
|
||||
this.post = this.post.bind(this)
|
||||
this.read = this.read.bind(this)
|
||||
this.eventId2note = this.eventId2note.bind(this)
|
||||
this.pubkey2npub = this.pubkey2npub.bind(this)
|
||||
this.readGlobalFeed = this.readGlobalFeed.bind(this)
|
||||
}
|
||||
|
||||
// Create nostr keys.
|
||||
@@ -131,6 +134,62 @@ class NostrAdapter {
|
||||
eventId2note (eventId) {
|
||||
return nip19.noteEncode(eventId)
|
||||
}
|
||||
|
||||
// Convert a pubkey into a `npubabc..` syntax that Astral expects.
|
||||
pubkey2npub (pubkey) {
|
||||
return nip19.npubEncode(pubkey)
|
||||
}
|
||||
|
||||
// Read the global feed.
|
||||
async readGlobalFeed (inObj = {}) {
|
||||
try {
|
||||
const { limit = 10 } = inObj
|
||||
|
||||
if (typeof limit !== 'number' || limit < 1) {
|
||||
throw new Error('Limit must be greater than 0')
|
||||
}
|
||||
|
||||
const relays = [this.relayWs]
|
||||
const pool = this.RelayPool(relays)
|
||||
|
||||
const nostrData = new Promise((resolve, reject) => {
|
||||
const messages = []
|
||||
|
||||
pool.on('open', (relay) => {
|
||||
// relay.subscribe('REQ', { ids: [eventId] })
|
||||
relay.subscribe('REQ', { limit, kinds: [1], '#t': [this.globalFeed] })
|
||||
})
|
||||
|
||||
pool.on('eose', (relay) => {
|
||||
relay.close()
|
||||
resolve(messages)
|
||||
})
|
||||
|
||||
pool.on('event', (relay, subId, ev) => {
|
||||
// console.log('ev: ', ev)
|
||||
|
||||
const { content, id, tags, pubkey } = ev
|
||||
|
||||
const msg = {
|
||||
content,
|
||||
id,
|
||||
tags,
|
||||
npub: this.pubkey2npub(pubkey),
|
||||
pubkey
|
||||
}
|
||||
|
||||
messages.push(msg)
|
||||
})
|
||||
})
|
||||
|
||||
const messages = await nostrData
|
||||
|
||||
return messages
|
||||
} catch (error) {
|
||||
console.log(`Error in nostr.js/readGlobalFeed() ${error.message} `)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default NostrAdapter
|
||||
|
||||
@@ -18,6 +18,7 @@ import OfferRouter from './offer/index.js'
|
||||
import OrderRouter from './order/index.js'
|
||||
import P2WDBRouter from './p2wdb/index.js'
|
||||
import UsageRESTController from './usage/index.js'
|
||||
import SmAccountRouter from './smAccount/index.js'
|
||||
|
||||
class RESTControllers {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -86,6 +87,9 @@ class RESTControllers {
|
||||
// Attach the REST API Controllers associated with the /usage route
|
||||
const usageRESTController = new UsageRESTController(dependencies)
|
||||
usageRESTController.attach(app)
|
||||
|
||||
const smAccountRouter = new SmAccountRouter(dependencies)
|
||||
smAccountRouter.attach(app)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
REST API Controller library for the /order route
|
||||
*/
|
||||
|
||||
// const { wlogger } = require('../../../adapters/wlogger')
|
||||
|
||||
let _this
|
||||
|
||||
class SmAccountRESTControllerLib {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating /order REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating /order REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.SmAccountModel = this.adapters.localdb.SmAccount
|
||||
// this.userUseCases = this.useCases.user
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
// curl -X GET http://localhost:5700/sm/list/all/0
|
||||
async listAccounts (ctx) {
|
||||
try {
|
||||
let page = ctx.params.page
|
||||
if (!page) page = 0
|
||||
|
||||
const offers = await _this.useCases.smAccount.listAccounts(page)
|
||||
|
||||
ctx.body = offers
|
||||
} catch (err) {
|
||||
console.log('Error in listAccounts REST API handler: ', err)
|
||||
_this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DRY error handler
|
||||
handleError (ctx, err) {
|
||||
console.log('err', err)
|
||||
|
||||
// If an HTTP status is specified by the buisiness logic, use that.
|
||||
if (err.status) {
|
||||
if (err.message) {
|
||||
ctx.throw(err.status, err.message)
|
||||
} else {
|
||||
ctx.throw(err.status)
|
||||
}
|
||||
} else {
|
||||
// By default use a 422 error if the HTTP status is not specified.
|
||||
ctx.throw(422, err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SmAccountRESTControllerLib
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
REST API library for /order route.
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
import Router from 'koa-router'
|
||||
|
||||
// Local libraries.
|
||||
import SmAccountRESTControllerLib from './controller.js'
|
||||
import Validators from '../middleware/validators.js'
|
||||
|
||||
let _this
|
||||
|
||||
class SmAccountRouter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating /sm REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating /sm REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
const dependencies = {
|
||||
adapters: this.adapters,
|
||||
useCases: this.useCases
|
||||
}
|
||||
|
||||
// Encapsulate dependencies.
|
||||
this.validators = new Validators()
|
||||
this.smAccountRESTController = new SmAccountRESTControllerLib(dependencies)
|
||||
|
||||
// Instantiate the router and set the base route.
|
||||
const baseUrl = '/sm'
|
||||
this.router = new Router({ prefix: baseUrl })
|
||||
|
||||
this.listAccounts = this.listAccounts.bind(this)
|
||||
_this = this
|
||||
}
|
||||
|
||||
attach (app) {
|
||||
if (!app) {
|
||||
throw new Error(
|
||||
'Must pass app object when attached REST API controllers.'
|
||||
)
|
||||
}
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.get('/list/all/:page', _this.smAccountRESTController.listAccounts)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(_this.router.routes())
|
||||
app.use(_this.router.allowedMethods())
|
||||
}
|
||||
|
||||
async listAccounts (ctx, next) {
|
||||
// await _this.validators.ensureUser(ctx, next)
|
||||
await _this.smAccountRESTController.listAccounts(ctx, next)
|
||||
}
|
||||
}
|
||||
|
||||
export default SmAccountRouter
|
||||
@@ -26,6 +26,7 @@ class TimerControllers {
|
||||
// Constants
|
||||
this.cleanUsageInterval = 60000 * 60 // 1 hour
|
||||
this.backupUsageInterval = 60000 * 10 // 10 minutes
|
||||
this.newSmAccountsInterval = 60000 * 60 // 60 minutes
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.config = config
|
||||
@@ -36,17 +37,16 @@ class TimerControllers {
|
||||
this.gcOffers = this.gcOffers.bind(this)
|
||||
this.checkDupOffers = this.checkDupOffers.bind(this)
|
||||
this.loadOffers = this.loadOffers.bind(this)
|
||||
// Bind 'this' object to all subfunctions.
|
||||
// this.exampleTimerFunc = this.exampleTimerFunc.bind(this)
|
||||
this.cleanUsage = this.cleanUsage.bind(this)
|
||||
this.backupUsage = this.backupUsage.bind(this)
|
||||
this.newSmAccounts = this.newSmAccounts.bind(this)
|
||||
|
||||
// State
|
||||
this.gcOrdersInt = null
|
||||
this.gcOffersInt = null
|
||||
this.checkDupOffersInt = null
|
||||
this.loadOffersInt = null
|
||||
this.cleanUsage = this.cleanUsage.bind(this)
|
||||
this.backupUsage = this.backupUsage.bind(this)
|
||||
this.newSmAccountsInt = null
|
||||
}
|
||||
|
||||
// Start all the time-based controllers.
|
||||
@@ -55,10 +55,9 @@ class TimerControllers {
|
||||
this.gcOffersInt = setInterval(this.gcOffers, 60000 * 5)
|
||||
// this.checkDupOffersInt = setInterval(this.checkDupOffers, 60000 * 4.5)
|
||||
this.loadOffersInt = setInterval(this.loadOffers, 60000 * 2)
|
||||
// Any new timer control functions can be added here. They will be started
|
||||
// when the server starts.
|
||||
this.cleanUsageHandle = setInterval(this.cleanUsage, this.cleanUsageInterval)
|
||||
this.backupUsageHandle = setInterval(this.backupUsage, this.backupUsageInterval)
|
||||
this.newSmAccountsInt = setInterval(this.newSmAccounts, this.newSmAccountsInterval)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -70,6 +69,7 @@ class TimerControllers {
|
||||
clearInterval(this.optimizeWalletHandle)
|
||||
clearInterval(this.cleanUsageHandle)
|
||||
clearInterval(this.backupUsageHandle)
|
||||
clearInterval(this.newSmAccountsInt)
|
||||
}
|
||||
|
||||
// Garbage Collect the Orders.
|
||||
@@ -165,6 +165,19 @@ class TimerControllers {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check for new Social Media Accounts.
|
||||
async newSmAccounts () {
|
||||
try {
|
||||
await this.useCases.smAccount.checkForNewSmAccounts()
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Error in time-controller.js/newSmAccounts(): ', err)
|
||||
|
||||
// Note: Do not throw an error. This is a top-level function.
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default TimerControllers
|
||||
|
||||
@@ -10,6 +10,7 @@ import EntryUseCases from './entry.js'
|
||||
import OfferUseCases from './offer/index.js'
|
||||
import OrderUseCases from './order.js'
|
||||
import { UsageUseCases } from './usage-use-cases.js'
|
||||
import SmAccountUseCases from './smAccount-use-cases.js'
|
||||
|
||||
class UseCases {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -27,6 +28,7 @@ class UseCases {
|
||||
localConfig.order = this.order
|
||||
this.offer = new OfferUseCases(localConfig)
|
||||
this.usage = new UsageUseCases(localConfig)
|
||||
this.smAccount = new SmAccountUseCases(localConfig)
|
||||
}
|
||||
|
||||
// Run any startup Use Cases at the start of the app.
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
Social Media Account Use Cases.
|
||||
*/
|
||||
|
||||
class smAccountUseCases {
|
||||
constructor (localConfig = {}) {
|
||||
// console.log('User localConfig: ', localConfig)
|
||||
|
||||
// Dependency injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of adapters must be passed in when instantiating Order Use Cases library.'
|
||||
)
|
||||
}
|
||||
|
||||
// Bind 'this' object to all methods
|
||||
this.checkForNewSmAccounts = this.checkForNewSmAccounts.bind(this)
|
||||
this.listAccounts = this.listAccounts.bind(this)
|
||||
}
|
||||
|
||||
// Check for new Social Media Accounts.
|
||||
async checkForNewSmAccounts () {
|
||||
try {
|
||||
console.log('Checking for new Social Media Accounts...')
|
||||
|
||||
const messages = await this.adapters.nostr.readGlobalFeed()
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const msg = messages[i]
|
||||
// const { content, id, tags, npub, pubkey } = msg
|
||||
const { tags, npub, pubkey } = msg
|
||||
let bchAddr = null
|
||||
// console.log('msg: ', JSON.stringify(msg, null, 2))
|
||||
|
||||
// Find the 'u' tag if it exists
|
||||
const uTag = tags.find(tag => tag[0] === 'u')
|
||||
|
||||
// If 'u' tag exists and contains a BCH address
|
||||
if (uTag && uTag[1].includes('bitcoincash:')) {
|
||||
bchAddr = uTag[1]
|
||||
// console.log('BCH address found:', bchAddr)
|
||||
}
|
||||
|
||||
const SmAccount = await this.adapters.localdb.SmAccount
|
||||
|
||||
if (bchAddr) {
|
||||
const smAccount = await SmAccount.findOne({ npub })
|
||||
if (!smAccount) {
|
||||
console.log(`Creating new Social Media Account with npub: ${npub} and bchAddr: ${bchAddr}`)
|
||||
const newSmAccount = new SmAccount({ npub, bchAddr, pubkey })
|
||||
await newSmAccount.save()
|
||||
} else {
|
||||
console.log(`Social Media Account with npub: ${npub} already exists`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Error in smAccount-use-cases.js/checkForNewSmAccounts(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// List all Social Media Accounts.
|
||||
async listAccounts (page = 0) {
|
||||
try {
|
||||
const SmAccount = await this.adapters.localdb.SmAccount
|
||||
const accounts = await SmAccount.find({}).skip(page * 10).limit(10)
|
||||
return accounts
|
||||
} catch (err) {
|
||||
console.error('Error in smAccount-use-cases.js/listAccounts(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default smAccountUseCases
|
||||
Reference in New Issue
Block a user