mirror of
https://github.com/Permissionless-Software-Foundation/bch-dex.git
synced 2026-09-22 09:11:59 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2af50ecf02 | ||
|
|
f88a24c1b5 | ||
|
|
a4b4ff13d2 | ||
|
|
2bf4f9df28 | ||
|
|
19ba38e8ce | ||
|
|
373a52ebf1 | ||
|
|
fe82478dc4 | ||
|
|
bed281c8c3 | ||
|
|
01484b4e03 | ||
|
|
c86512254d | ||
|
|
7df9b9b8a4 | ||
|
|
5f91395766 | ||
|
|
dda6cb2aa4 | ||
|
|
578aade39b | ||
|
|
b09890a527 | ||
|
|
a32bd840b9 | ||
|
|
0bdc232938 | ||
|
|
f543cfce73 | ||
|
|
0f5e694682 | ||
|
|
615638d705 | ||
|
|
9b46bbd7bb |
Vendored
+8
-2
@@ -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,10 +185,15 @@ 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,
|
||||
|
||||
// Admin password
|
||||
adminPassword: process.env.ADMIN_PASSWORD
|
||||
adminPassword: process.env.ADMIN_PASSWORD,
|
||||
|
||||
// The Operator of BCH DEX can elect to receive a percentage of each sale.
|
||||
operatorAddress: process.env.OPERATOR_ADDRESS ? process.env.OPERATOR_ADDRESS : 'bitcoincash:qqsrke9lh257tqen99dkyy2emh4uty0vky9y0z0lsr',
|
||||
operatorPercentage: process.env.OPERATOR_PERCENTAGE ? parseFloat(process.env.OPERATOR_PERCENTAGE) : 2.0
|
||||
}
|
||||
|
||||
@@ -66,6 +66,9 @@ class Bch {
|
||||
// }
|
||||
// }
|
||||
|
||||
/** DEV NOTE: This function are deprecated.?
|
||||
* https://github.com/Permissionless-Software-Foundation/bch-message-lib/pull/41
|
||||
*/
|
||||
async getMerit (slpAddr) {
|
||||
try {
|
||||
if (!slpAddr || typeof slpAddr !== 'string') {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,8 +35,11 @@ const Offer = new mongoose.Schema({
|
||||
lokadId: { type: String },
|
||||
messageType: { type: Number },
|
||||
messageClass: { type: Number },
|
||||
nostrEventId: { type: String } // Nostr Event Id.
|
||||
nostrEventId: { type: String }, // Nostr Event Id.
|
||||
|
||||
// Operator fee and address
|
||||
operatorAddress: { type: String },
|
||||
operatorPercentage: { type: Number }
|
||||
})
|
||||
|
||||
export default mongoose.model('offer', Offer)
|
||||
|
||||
@@ -43,7 +43,11 @@ const Order = new mongoose.Schema({
|
||||
// Additional properties found in createOrder
|
||||
dataType: { type: String, required: true },
|
||||
|
||||
userId: { type: String, required: true }
|
||||
userId: { type: String, required: true },
|
||||
|
||||
// Operator fee and address
|
||||
operatorAddress: { type: String },
|
||||
operatorPercentage: { type: Number }
|
||||
})
|
||||
|
||||
export default mongoose.model('order', Order)
|
||||
|
||||
@@ -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 },
|
||||
followerCnt: { type: Number, default: 0 },
|
||||
followingCnt: { 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,10 @@ 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.npub2pubkey = this.npub2pubkey.bind(this)
|
||||
this.readGlobalFeed = this.readGlobalFeed.bind(this)
|
||||
this.getFollowers = this.getFollowers.bind(this)
|
||||
}
|
||||
|
||||
// Create nostr keys.
|
||||
@@ -131,6 +136,117 @@ class NostrAdapter {
|
||||
eventId2note (eventId) {
|
||||
return nip19.noteEncode(eventId)
|
||||
}
|
||||
|
||||
// Convert a pubkey into a `npubabc..` syntax that Astral expects.
|
||||
pubkey2npub (pubkey) {
|
||||
return nip19.npubEncode(pubkey)
|
||||
}
|
||||
|
||||
npub2pubkey (npub) {
|
||||
return nip19.decode(npub)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
async getFollowers (inObj = {}) {
|
||||
try {
|
||||
const { pubkey } = inObj
|
||||
|
||||
if (!pubkey || typeof pubkey !== 'string') {
|
||||
throw new Error('pubkey must be a string!')
|
||||
}
|
||||
|
||||
const relays = [this.relayWs]
|
||||
const pool = this.RelayPool(relays)
|
||||
|
||||
const nostrData = new Promise((resolve, reject) => {
|
||||
const followers = []
|
||||
|
||||
pool.on('open', (relay) => {
|
||||
// Query for kind 3 events where the target pubkey appears in tags
|
||||
// This finds all contact lists that include the target pubkey
|
||||
relay.subscribe('subid', {
|
||||
limit: 100,
|
||||
kinds: [3],
|
||||
'#p': [pubkey] // Filter by tag 'p' containing the target pubkey
|
||||
})
|
||||
})
|
||||
|
||||
pool.on('eose', relay => {
|
||||
relay.close()
|
||||
resolve(followers)
|
||||
})
|
||||
|
||||
pool.on('event', (relay, subId, ev) => {
|
||||
// Each event represents a contact list from someone who follows the target
|
||||
// The author of this event is a follower
|
||||
const follower = {
|
||||
pubkey: ev.pubkey,
|
||||
npub: this.pubkey2npub(ev.pubkey),
|
||||
contactList: ev.tags,
|
||||
createdAt: ev.created_at
|
||||
}
|
||||
|
||||
followers.push(follower)
|
||||
})
|
||||
})
|
||||
|
||||
const followers = await nostrData
|
||||
return followers
|
||||
} catch (error) {
|
||||
console.log(`Error in nostr.js/getFollowers() ${error.message} `)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default NostrAdapter
|
||||
|
||||
+28
-3
@@ -34,6 +34,7 @@ class WalletAdapter {
|
||||
this.config = config
|
||||
this.bitcoinJs = bitcoinJs
|
||||
this.BchWallet = BchWallet
|
||||
this.BchTokenSweep = BchTokenSweep
|
||||
this.bchWallet = {} // Will be replaced when initialized.
|
||||
// this.advancedConfig = localConfig.advancedConfig
|
||||
this.retryQueue = new RetryQueue({
|
||||
@@ -205,7 +206,9 @@ class WalletAdapter {
|
||||
// Generate a cryptographic signature, required to write to the P2WDB.
|
||||
async generateSignature (message) {
|
||||
try {
|
||||
// TODO: Add input validation for message.
|
||||
if (!message || typeof message !== 'string') {
|
||||
throw new Error('message is required!')
|
||||
}
|
||||
|
||||
const privKey = this.bchWallet.walletInfo.privateKey
|
||||
|
||||
@@ -370,6 +373,12 @@ class WalletAdapter {
|
||||
async moveTokens (inObj = {}) {
|
||||
try {
|
||||
const { tokenId, qty } = inObj
|
||||
if (!tokenId || typeof tokenId !== 'string') {
|
||||
throw new Error('tokenId must be a string!')
|
||||
}
|
||||
if (!qty || typeof qty !== 'number') {
|
||||
throw new Error('qty must be a number!')
|
||||
}
|
||||
|
||||
const keyPair = await this.getKeyPair()
|
||||
console.log('keyPair: ', keyPair)
|
||||
@@ -392,6 +401,10 @@ class WalletAdapter {
|
||||
x => x.tokenId === tokenId
|
||||
)
|
||||
|
||||
if (tokenUtxos.length === 0) {
|
||||
throw new Error('tokenId not found!')
|
||||
}
|
||||
|
||||
const txid = await this.bchWallet.sendTokens(receiver, 3)
|
||||
|
||||
const utxoInfo = {
|
||||
@@ -412,6 +425,10 @@ class WalletAdapter {
|
||||
// generate a segregated UTXO.
|
||||
async moveBch (amountSat) {
|
||||
try {
|
||||
if (!amountSat) {
|
||||
throw new Error('amountSat is required!')
|
||||
}
|
||||
|
||||
const keyPair = await this.getKeyPair()
|
||||
console.log('keyPair: ', keyPair)
|
||||
|
||||
@@ -444,6 +461,10 @@ class WalletAdapter {
|
||||
// representing the transaction.
|
||||
async deseralizeTx (txHex) {
|
||||
try {
|
||||
// Input validation
|
||||
if (!txHex || typeof txHex !== 'string') {
|
||||
throw new Error('hex must be a string!')
|
||||
}
|
||||
// Ensure the URL points at FullStack.cash, since the web 3 infra does not
|
||||
// yet support this call.
|
||||
const oldUrl = this.bchWallet.bchjs.RawTransactions.restURL
|
||||
@@ -555,8 +576,12 @@ class WalletAdapter {
|
||||
|
||||
// Given an Order database object, this function sends the tokens from the
|
||||
// HD index where they are stored, back to the root address of the wallet.
|
||||
async reclaimTokens (orderData) {
|
||||
async reclaimTokens (orderData = {}) {
|
||||
try {
|
||||
if (!orderData.hdIndex) {
|
||||
throw new Error('orderData.hdIndex is required!')
|
||||
}
|
||||
|
||||
// Get the key pair from the Order.
|
||||
const keyPair = await this.getKeyPair(orderData.hdIndex)
|
||||
console.log('keyPair: ', keyPair)
|
||||
@@ -576,7 +601,7 @@ class WalletAdapter {
|
||||
// const txid = await tempWallet.sendTokens(receiver)
|
||||
|
||||
// Sweep the tokens to the root address.
|
||||
const sweeper = new BchTokenSweep(
|
||||
const sweeper = new this.BchTokenSweep(
|
||||
keyPair.wif,
|
||||
this.bchWallet.walletInfo.privateKey,
|
||||
this.bchWallet,
|
||||
|
||||
+15
-13
@@ -6,13 +6,15 @@ import config from '../../config/index.js'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
let _this
|
||||
|
||||
class WebHook {
|
||||
constructor () {
|
||||
_this = this
|
||||
_this.config = config
|
||||
_this.axios = axios
|
||||
this.config = config
|
||||
this.axios = axios
|
||||
this.sleepTime = 2000
|
||||
this.createWebhook = this.createWebhook.bind(this)
|
||||
this.deleteWebhook = this.deleteWebhook.bind(this)
|
||||
this.waitUntilSuccess = this.waitUntilSuccess.bind(this)
|
||||
this.sleep = this.sleep.bind(this)
|
||||
}
|
||||
|
||||
// REST petition to create a webhook in p2wdb-service
|
||||
@@ -22,14 +24,14 @@ class WebHook {
|
||||
throw new Error('url must be a string')
|
||||
}
|
||||
|
||||
const endpoint = _this.config.webhookService
|
||||
const endpoint = this.config.webhookService
|
||||
|
||||
const obj = {
|
||||
appId: this.config.p2wdbAppId,
|
||||
url
|
||||
}
|
||||
|
||||
const result = await axios.post(endpoint, obj)
|
||||
const result = await this.axios.post(endpoint, obj)
|
||||
|
||||
return result.data
|
||||
} catch (err) {
|
||||
@@ -45,14 +47,14 @@ class WebHook {
|
||||
throw new Error('url must be a string')
|
||||
}
|
||||
|
||||
const endpoint = _this.config.webhookService
|
||||
const endpoint = this.config.webhookService
|
||||
|
||||
const obj = {
|
||||
appId: this.config.p2wdbAppId,
|
||||
url
|
||||
}
|
||||
|
||||
const result = await axios.delete(endpoint, { data: obj })
|
||||
const result = await this.axios.delete(endpoint, { data: obj })
|
||||
|
||||
return result.data
|
||||
} catch (err) {
|
||||
@@ -86,7 +88,7 @@ class WebHook {
|
||||
} catch (err) {
|
||||
const now = new Date()
|
||||
console.log(`${now.toLocaleString()}: Error trying to create webhook with P2WDB. Trying again...`)
|
||||
await sleep(2000)
|
||||
await this.sleep(this.sleepTime)
|
||||
}
|
||||
} while (!success)
|
||||
|
||||
@@ -96,10 +98,10 @@ class WebHook {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sleep (ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
async sleep (ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
}
|
||||
|
||||
export default WebHook
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
/*
|
||||
REST API Controller library for the /offer route
|
||||
|
||||
TODO:
|
||||
- Add api-doc documentation for all endpoints in this file.
|
||||
*/
|
||||
|
||||
import wlogger from '../../../adapters/wlogger.js'
|
||||
@@ -30,6 +33,7 @@ class OfferRESTControllerLib {
|
||||
this.listNftOffers = this.listNftOffers.bind(this)
|
||||
this.listFungibleOffers = this.listFungibleOffers.bind(this)
|
||||
this.takeOffer = this.takeOffer.bind(this)
|
||||
this.listOffersByAddress = this.listOffersByAddress.bind(this)
|
||||
this.handleError = this.handleError.bind(this)
|
||||
}
|
||||
|
||||
@@ -115,6 +119,21 @@ class OfferRESTControllerLib {
|
||||
}
|
||||
}
|
||||
|
||||
// List all offers being made by a given address.
|
||||
// curl -X GET http://localhost:5700/offer/list/addr/bitcoincash:qrpxtnrlhfz9wsuuse7z5k2mxmvw0r3pu5qepyhmq2
|
||||
async listOffersByAddress (ctx) {
|
||||
try {
|
||||
const addr = ctx.params.addr
|
||||
|
||||
const offers = await this.useCases.offer.listOffersByAddress(addr)
|
||||
|
||||
ctx.body = offers
|
||||
} catch (err) {
|
||||
console.log('Error in listOffersByAddress REST API handler: ', err)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DRY error handler
|
||||
handleError (ctx, err) {
|
||||
console.log('err', err.message)
|
||||
|
||||
@@ -8,8 +8,6 @@ import Router from 'koa-router'
|
||||
// Local libraries.
|
||||
import OfferRESTControllerLib from './controller.js'
|
||||
|
||||
let _this
|
||||
|
||||
class OfferRouter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
@@ -38,7 +36,8 @@ class OfferRouter {
|
||||
const baseUrl = '/offer'
|
||||
this.router = new Router({ prefix: baseUrl })
|
||||
|
||||
_this = this
|
||||
// Bind 'this' object to all subfunctions.
|
||||
this.attach = this.attach.bind(this)
|
||||
}
|
||||
|
||||
attach (app) {
|
||||
@@ -50,19 +49,20 @@ class OfferRouter {
|
||||
|
||||
// 12/3/24 CT:
|
||||
// Note: The createOffer() path was used by a P2WDB webhook to generate an
|
||||
// Offer from and Order. This has been deprecated and Offers are now created
|
||||
// Offer from an Order. This has been deprecated and Offers are now created
|
||||
// by a Timer Controller monitoring a Nostr topic.
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
// this.router.post('/', _this.offerRESTController.createOffer) // Deprecated.
|
||||
this.router.post('/take', _this.offerRESTController.takeOffer)
|
||||
this.router.get('/list/all/:page', _this.offerRESTController.listOffers)
|
||||
this.router.get('/list/nft/:page', _this.offerRESTController.listNftOffers)
|
||||
this.router.get('/list/fungible/:page', _this.offerRESTController.listFungibleOffers)
|
||||
this.router.post('/take', this.offerRESTController.takeOffer)
|
||||
this.router.get('/list/all/:page', this.offerRESTController.listOffers)
|
||||
this.router.get('/list/nft/:page', this.offerRESTController.listNftOffers)
|
||||
this.router.get('/list/fungible/:page', this.offerRESTController.listFungibleOffers)
|
||||
this.router.get('/list/addr/:addr', this.offerRESTController.listOffersByAddress)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(_this.router.routes())
|
||||
app.use(_this.router.allowedMethods())
|
||||
app.use(this.router.routes())
|
||||
app.use(this.router.allowedMethods())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /sm/list/all/{page} List all Social Media Accounts
|
||||
* @apiPermission public
|
||||
* @apiName ListAccounts
|
||||
* @apiGroup REST Social Media Accounts
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5700/sm/list/all/0
|
||||
*
|
||||
*/
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /sm/npub/{npub} Get Social Media Account by Npub
|
||||
* @apiPermission public
|
||||
* @apiName GetAccountByNpub
|
||||
* @apiGroup REST Social Media Accounts
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5700/sm/npub/npub1y3xq402pu9aqms3khetnt5gzm54t63pzcla56wwfmwshde0ws77qkff5gc
|
||||
*
|
||||
*/
|
||||
// curl -X GET http://localhost:5700/sm/npub/npub1y3xq402pu9aqms3khetnt5gzm54t63pzcla56wwfmwshde0ws77qkff5gc
|
||||
async getAccountByNpub (ctx) {
|
||||
try {
|
||||
const npub = ctx.params.npub
|
||||
if (!npub) {
|
||||
ctx.throw(400, 'Npub is required')
|
||||
}
|
||||
|
||||
const account = await _this.useCases.smAccount.getAccountByNpub(npub)
|
||||
|
||||
if (!account) {
|
||||
ctx.throw(404, 'Account not found')
|
||||
}
|
||||
|
||||
ctx.body = account
|
||||
} catch (err) {
|
||||
// console.log('Error in getAccountByNpub REST API handler: ', err)
|
||||
_this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /sm/bchAddr/{bchAddr} Get Social Media Account by BCH address
|
||||
* @apiPermission public
|
||||
* @apiName GetAccountByBchAddr
|
||||
* @apiGroup REST Social Media Accounts
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5700/sm/bchAddr/bitcoincash:qz6lf6gpmn3secx73g7ucfcgy8mrh3sz2y2ylk643x
|
||||
*
|
||||
*/
|
||||
// curl -X GET http://localhost:5700/sm/bchAddr/bitcoincash:qz6lf6gpmn3secx73g7ucfcgy8mrh3sz2y2ylk643x
|
||||
async getAccountByBchAddr (ctx) {
|
||||
try {
|
||||
const bchAddr = ctx.params.bchAddr
|
||||
if (!bchAddr) {
|
||||
ctx.throw(400, 'BCH address is required')
|
||||
}
|
||||
|
||||
const account = await _this.useCases.smAccount.getAccountByBchAddr(bchAddr)
|
||||
|
||||
if (!account) {
|
||||
ctx.throw(404, 'Account not found')
|
||||
}
|
||||
|
||||
ctx.body = account
|
||||
} catch (err) {
|
||||
// console.log('Error in getAccountByBchAddr 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,70 @@
|
||||
/*
|
||||
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)
|
||||
this.router.get('/npub/:npub', _this.smAccountRESTController.getAccountByNpub)
|
||||
this.router.get('/bchAddr/:bchAddr', _this.smAccountRESTController.getAccountByBchAddr)
|
||||
|
||||
// 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,8 @@ class TimerControllers {
|
||||
// Constants
|
||||
this.cleanUsageInterval = 60000 * 60 // 1 hour
|
||||
this.backupUsageInterval = 60000 * 10 // 10 minutes
|
||||
this.newSmAccountsInterval = 60000 * 60 // 60 minutes
|
||||
this.updateSmAccountsInterval = 60000 * 10 // 10 minutes
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.config = config
|
||||
@@ -36,17 +38,18 @@ 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)
|
||||
this.updateSmAccounts = this.updateSmAccounts.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
|
||||
this.updateSmAccountsInt = null
|
||||
}
|
||||
|
||||
// Start all the time-based controllers.
|
||||
@@ -55,11 +58,10 @@ 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)
|
||||
this.updateSmAccountsInt = setInterval(this.updateSmAccounts, this.updateSmAccountsInterval)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -70,6 +72,8 @@ class TimerControllers {
|
||||
clearInterval(this.optimizeWalletHandle)
|
||||
clearInterval(this.cleanUsageHandle)
|
||||
clearInterval(this.backupUsageHandle)
|
||||
clearInterval(this.newSmAccountsInt)
|
||||
clearInterval(this.updateSmAccountsInt)
|
||||
}
|
||||
|
||||
// Garbage Collect the Orders.
|
||||
@@ -165,6 +169,30 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
// Update the Social Media Accounts.
|
||||
async updateSmAccounts () {
|
||||
try {
|
||||
await this.useCases.smAccount.updateSmAccounts()
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Error in time-controller.js/updateSmAccounts(): ', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default TimerControllers
|
||||
|
||||
+12
-2
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Offer Entity
|
||||
An ffer is created when a new Signal is detected via the P2WDB webhook.
|
||||
An offer is created when a new Signal is detected via the P2WDB webhook.
|
||||
It's destroyed when the UTXO described in the Signal has been detected as spent.
|
||||
*/
|
||||
|
||||
@@ -33,7 +33,9 @@ class OfferEntity {
|
||||
makerAddr,
|
||||
ticker,
|
||||
tokenType,
|
||||
nostrEventId
|
||||
nostrEventId,
|
||||
operatorAddress,
|
||||
operatorPercentage
|
||||
} = offerData.data
|
||||
|
||||
// Input Validation
|
||||
@@ -80,6 +82,14 @@ class OfferEntity {
|
||||
throw new Error("Property 'nostrEventId' must be a string.")
|
||||
}
|
||||
|
||||
// Add the operator address and percentage to the offer.
|
||||
if (!operatorAddress || typeof operatorAddress !== 'string') {
|
||||
throw new Error("Property 'operatorAddress' must be a string.")
|
||||
}
|
||||
if (!operatorPercentage || typeof operatorPercentage !== 'number') {
|
||||
throw new Error("Property 'operatorPercentage' must be a number.")
|
||||
}
|
||||
|
||||
// Convert the timestamp to a number.
|
||||
let timestamp = new Date(offerData.timestamp)
|
||||
timestamp = timestamp.getTime()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -64,6 +64,7 @@ class OfferUseCases {
|
||||
this.removeStaleOffers = this.removeStaleOffers.bind(this)
|
||||
this.flagOffer = this.flagOffer.bind(this)
|
||||
this.loadOffers = this.loadOffers.bind(this)
|
||||
this.listOffersByAddress = this.listOffersByAddress.bind(this)
|
||||
|
||||
// State
|
||||
this.seenOffers = []
|
||||
@@ -110,7 +111,10 @@ class OfferUseCases {
|
||||
const utxoStatus = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.utxoIsValid, utxo)
|
||||
// console.log('utxoStatus: ', utxoStatus)
|
||||
// if (utxoStatus === null) return false
|
||||
if (!utxoStatus) return false
|
||||
if (!utxoStatus) {
|
||||
console.log(`UTXO txid: ${offerObj.data.utxoTxid}, vout: ${offerObj.data.utxoVout} has been spent. Skipping.`)
|
||||
return false
|
||||
}
|
||||
|
||||
// A new offer gets a status of 'posted'
|
||||
offerObj.data.offerStatus = 'posted'
|
||||
@@ -512,9 +516,8 @@ class OfferUseCases {
|
||||
// }
|
||||
}
|
||||
|
||||
// This function is called by the P2WDB webhook REST API handler. When a
|
||||
// Counter Offer is passed to bch-dex by the P2WDB, the data is then passed
|
||||
// to this function. It does due diligence on the Counter Offer, then signs
|
||||
// This function is called by loadOffers().
|
||||
// It does due diligence on the Counter Offer, then signs
|
||||
// and broadcasts the transaction to accept the Counter Offer.
|
||||
async acceptCounterOffer (offerData) {
|
||||
try {
|
||||
@@ -527,6 +530,8 @@ class OfferUseCases {
|
||||
return false
|
||||
}
|
||||
|
||||
console.log(`New counter offer detected: https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(offerData.data.nostrEventId)}`)
|
||||
|
||||
// See if this instance of bch-dex is managing the Order associated with
|
||||
// the incoming Counter Offer.
|
||||
|
||||
@@ -598,6 +603,16 @@ class OfferUseCases {
|
||||
throw new Error(`The Counter Offer has an output of ${satsOut}, which does not match the required ${satsToReceive} in the Offer.`)
|
||||
}
|
||||
|
||||
// Ensure the Counter Offer has an output for the Operator of bch-dex.
|
||||
if (!txObj.vout[3]) {
|
||||
console.log('The Counter Offer does not have an output for the Operator.')
|
||||
|
||||
// Add order to list of seen orders, so that we don't spent time trying to validate it again.
|
||||
this.seenOffers.push(eventId)
|
||||
|
||||
return 'N/A'
|
||||
}
|
||||
|
||||
// Ensure the 3rd output (vout=2) is going to the maker address specified
|
||||
// in the Offer.
|
||||
const addrInCounterOffer = txObj.vout[2].scriptPubKey.addresses[0]
|
||||
@@ -607,6 +622,31 @@ class OfferUseCases {
|
||||
throw new Error(`The Counter Offer has an output address of ${addrInCounterOffer}, which does not match the Maker address of ${makerAddr} in the Offer.`)
|
||||
}
|
||||
|
||||
// Ensure the 4th output (vout=3) is going to the operator address specified in the Offer.
|
||||
const operatorAddr = orderData.operatorAddress
|
||||
const hasCorrectOperatorAddr = operatorAddr === txObj.vout[3].scriptPubKey.addresses[0]
|
||||
if (!hasCorrectOperatorAddr) {
|
||||
throw new Error(`The Counter Offer has an output address of ${txObj.vout[3].scriptPubKey.addresses[0]}, which does not match the Operator address of ${operatorAddr} in the Offer.`)
|
||||
}
|
||||
|
||||
// Ensure the 4th output (vout=3) contains the required amount of BCH.
|
||||
const operatorSatsToReceive = Math.ceil(orderData.numTokens * parseInt(orderData.rateInBaseUnit))
|
||||
if (isNaN(operatorSatsToReceive)) {
|
||||
throw new Error('Could not calculate the amount of BCH offered in the Counter Offer')
|
||||
}
|
||||
const operatorSatsOut = this.adapters.wallet.bchWallet.bchjs.BitcoinCash.toSatoshi(txObj.vout[3].value)
|
||||
let estimatedOperatorFee = Math.floor(txObj.vout[3].value * orderData.operatorPercentage / 100)
|
||||
if (estimatedOperatorFee < 546) estimatedOperatorFee = 546
|
||||
console.log('operatorSatsOut: ', operatorSatsOut, 'estimatedOperatorFee: ', estimatedOperatorFee)
|
||||
if (operatorSatsOut < estimatedOperatorFee) {
|
||||
console.log(`Skipping: The Counter Offer has an output of ${operatorSatsOut}, which is less than the estimated operator fee of ${estimatedOperatorFee}.`)
|
||||
|
||||
// Add order to list of seen orders, so that we don't spent time trying to validate it again.
|
||||
this.seenOffers.push(eventId)
|
||||
|
||||
return 'N/A'
|
||||
}
|
||||
|
||||
// Get the User ID from the Order model.
|
||||
const userId = orderData.userId
|
||||
|
||||
@@ -717,7 +757,8 @@ class OfferUseCases {
|
||||
await thisOffer.remove()
|
||||
}
|
||||
|
||||
// If the Offer is older than 7 days, delete it.
|
||||
// TODO: Instead of deleting the offer, send the token back to the Makers wallet.
|
||||
// If the Offer is older than a threshold, delete it.
|
||||
const nowMs = now.getTime()
|
||||
const eightWeeks = 1000 * 60 * 60 * 24 * 7 * 8
|
||||
const eightWeeksAgo = nowMs - eightWeeks
|
||||
@@ -794,7 +835,7 @@ class OfferUseCases {
|
||||
if (offerObj.data.dataType === 'counter-offer') {
|
||||
// console.log('Counter offer detected: ', offerObj)
|
||||
// console.log('Counter offer detected: ', offerObj.data.nostrEventId)
|
||||
console.log(`Counter offer detected: https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(offerObj.data.nostrEventId)}`)
|
||||
// console.log(`Counter offer detected: https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(offerObj.data.nostrEventId)}`)
|
||||
await this.acceptCounterOffer(offerObj)
|
||||
}
|
||||
|
||||
@@ -808,6 +849,17 @@ class OfferUseCases {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// List all offers being made by a given address.
|
||||
async listOffersByAddress (addr) {
|
||||
try {
|
||||
const offers = await this.OfferModel.find({ makerAddr: addr })
|
||||
return offers
|
||||
} catch (err) {
|
||||
console.error('Error in listOffersByAddress(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default OfferUseCases
|
||||
|
||||
@@ -44,6 +44,7 @@ class OrderLib {
|
||||
|
||||
if (!entryObj.tokenId) throw new Error('entry does not contain required properties')
|
||||
|
||||
// Get the user data from the database, for the user creating the new Order.
|
||||
const user = await this.UserModel.findById(entryObj.userId)
|
||||
if (!user) throw new Error('user not found')
|
||||
|
||||
@@ -81,13 +82,7 @@ class OrderLib {
|
||||
// await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.optimize, {})
|
||||
await userWallet.optimize()
|
||||
|
||||
// Ensure sufficient tokens exist to create the order.
|
||||
// await this.ensureFunds(orderEntity)
|
||||
// await this.retryQueue.addToQueue(this.ensureFunds, orderEntity)
|
||||
|
||||
// Get Ticker for token ID.
|
||||
// const tokenData = await this.adapters.wallet.bchWallet.getTxData([entryObj.tokenId])
|
||||
// const tokenData = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTxData, [entryObj.tokenId])
|
||||
const tokenData = await userWallet.getTxData([entryObj.tokenId])
|
||||
// console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`)
|
||||
orderEntity.ticker = tokenData[0].tokenTicker
|
||||
@@ -107,15 +102,20 @@ class OrderLib {
|
||||
// await this.adapters.wallet.bchWallet.getUtxos()
|
||||
// await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.initialize, {})
|
||||
await userWallet.initialize()
|
||||
|
||||
// Update the order with the new UTXO information.
|
||||
orderEntity.utxoTxid = utxoInfo.txid
|
||||
orderEntity.utxoVout = utxoInfo.vout
|
||||
orderEntity.tokenType = utxoInfo.tokenType
|
||||
|
||||
// Add P2WDB specific flag for signaling that this is a new offer.
|
||||
// Add flag to signal that this is a new offer.
|
||||
orderEntity.dataType = 'offer'
|
||||
orderEntity.userId = user._id
|
||||
|
||||
// Add the operator address and percentage to the order.
|
||||
orderEntity.operatorAddress = this.config.operatorAddress
|
||||
orderEntity.operatorPercentage = this.config.operatorPercentage
|
||||
|
||||
// Post the new Order information to Nostr under the topic set in the
|
||||
// config file.
|
||||
const postObj = {
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
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)
|
||||
this.getAccountByNpub = this.getAccountByNpub.bind(this)
|
||||
this.getAccountByBchAddr = this.getAccountByBchAddr.bind(this)
|
||||
this.updateSmAccounts = this.updateSmAccounts.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
|
||||
}
|
||||
}
|
||||
|
||||
// Get a Social Media Account by Npub.
|
||||
async getAccountByNpub (npub) {
|
||||
try {
|
||||
const SmAccount = await this.adapters.localdb.SmAccount
|
||||
const account = await SmAccount.findOne({ npub })
|
||||
return account
|
||||
} catch (err) {
|
||||
console.error('Error in smAccount-use-cases.js/getAccountByNpub(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Get a Social Media Account by BCH address.
|
||||
async getAccountByBchAddr (bchAddr) {
|
||||
try {
|
||||
const SmAccount = await this.adapters.localdb.SmAccount
|
||||
const account = await SmAccount.findOne({ bchAddr })
|
||||
console.log('Account found: ', account)
|
||||
|
||||
return account
|
||||
} catch (err) {
|
||||
console.error('Error in smAccount-use-cases.js/getAccountByBchAddr(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Update metadata about social media accounts being tracked.
|
||||
async updateSmAccounts () {
|
||||
try {
|
||||
console.log('Updating Social Media Accounts...')
|
||||
const SmAccount = await this.adapters.localdb.SmAccount
|
||||
const accounts = await SmAccount.find({})
|
||||
for (let i = 0; i < accounts.length; i++) {
|
||||
const account = accounts[i]
|
||||
// console.log(`Account ${i}: `, account)
|
||||
|
||||
// Get the number of accounts following this account
|
||||
const pubkey = account.pubkey
|
||||
const followList = await this.adapters.nostr.getFollowers({ pubkey })
|
||||
// console.log(`Follow list ${i}: `, JSON.stringify(followList, null, 2))
|
||||
|
||||
// Update the follower count for this account.
|
||||
const followerCnt = followList.length
|
||||
await SmAccount.updateOne({ _id: account._id }, { $set: { followerCnt } })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in smAccount-use-cases.js/updateSmAccounts(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default smAccountUseCases
|
||||
@@ -24,19 +24,18 @@ describe('#adapters', () => {
|
||||
})
|
||||
|
||||
describe('#start', () => {
|
||||
// it('should start the async adapters', async () => {
|
||||
// // Mock dependencies
|
||||
// uut.config.getJwtAtStartup = true
|
||||
// uut.config.useIpfs = true
|
||||
// uut.config.env = 'not-a-test'
|
||||
// sandbox.stub(uut.fullStackJwt, 'getJWT').resolves()
|
||||
// sandbox.stub(uut.fullStackJwt, 'instanceBchjs').resolves()
|
||||
// sandbox.stub(uut.ipfs, 'start').resolves()
|
||||
it('should start the async adapters', async () => {
|
||||
// Mock dependencies
|
||||
uut.config.getJwtAtStartup = true
|
||||
uut.config.useIpfs = true
|
||||
uut.config.env = 'not-a-test'
|
||||
sandbox.stub(uut.wallet, 'instanceWallet').resolves()
|
||||
sandbox.stub(uut.ipfs, 'start').resolves()
|
||||
|
||||
// const result = await uut.start()
|
||||
const result = await uut.start()
|
||||
|
||||
// assert.equal(result, true)
|
||||
// })
|
||||
assert.equal(result, true)
|
||||
})
|
||||
|
||||
it('should not start ipfs on test enviroment', async () => {
|
||||
// Mock dependencies
|
||||
|
||||
@@ -135,18 +135,19 @@ describe('bch', () => {
|
||||
}
|
||||
})
|
||||
|
||||
// it('should return the merit ', async () => {
|
||||
// try {
|
||||
// // Mock live network calls.
|
||||
// sandbox.stub(uut.msgLib.merit, 'agMerit').resolves(100)
|
||||
//
|
||||
// const slpAddr =
|
||||
// 'simpleledger:qqgnksc6zr4nzxrye69fq625wu2myxey6uh9kzjy96'
|
||||
// const merit = await uut.getMerit(slpAddr)
|
||||
// assert.isNumber(merit)
|
||||
// } catch (err) {
|
||||
// assert.fail('Unexpected result')
|
||||
// }
|
||||
// })
|
||||
/* it('should return the merit ', async () => {
|
||||
try {
|
||||
// Mock live network calls.
|
||||
sandbox.stub(uut.msgLib.merit, 'agMerit').resolves(100)
|
||||
|
||||
const slpAddr =
|
||||
'simpleledger:qqgnksc6zr4nzxrye69fq625wu2myxey6uh9kzjy96'
|
||||
const merit = await uut.getMerit(slpAddr)
|
||||
assert.isNumber(merit)
|
||||
} catch (err) {
|
||||
console.log('err: ', err)
|
||||
assert.fail('Unexpected result')
|
||||
}
|
||||
}) */
|
||||
})
|
||||
})
|
||||
|
||||
@@ -128,4 +128,100 @@ describe('#nostr.js', () => {
|
||||
assert.equal(result.length, 1)
|
||||
})
|
||||
})
|
||||
describe('#eventId2note', () => {
|
||||
it('should handle encode error', async () => {
|
||||
try {
|
||||
await uut.eventId2note('invalid format')
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.include(err.message, 'Invalid byte sequence')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle valid eventId', async () => {
|
||||
const result = await uut.eventId2note('b1e4a3d5e40b6d0b3f1e2c234b6c9dbf4d6a80e9b3e8a2d765c7f9ed84c9a5ef')
|
||||
assert.isString(result)
|
||||
assert.include(result, 'note')
|
||||
})
|
||||
})
|
||||
describe('#pubkey2npub', () => {
|
||||
it('should handle encode error', async () => {
|
||||
try {
|
||||
await uut.pubkey2npub('invalid format')
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.include(err.message, 'Invalid byte sequence')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle valid pubkey', async () => {
|
||||
const result = await uut.pubkey2npub('f5a1b39d2cfae8e10e93b9b4d7ec8248eaf45fbe4a73409d8d4c67015e9729a3')
|
||||
assert.isString(result)
|
||||
assert.include(result, 'npub')
|
||||
})
|
||||
})
|
||||
describe('#npub2pubkey', () => {
|
||||
it('should handle decode error', async () => {
|
||||
try {
|
||||
await uut.npub2pubkey('npub17ksm88fvlt5wzr5nhx6d0myzfr40g')
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.include(err.message, 'Invalid checksum')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle valid pubkey', async () => {
|
||||
const result = await uut.npub2pubkey('npub17ksm88fvlt5wzr5nhx6d0myzfr40gha7ffe5p8vdf3nszh5h9x3sdwl7u2')
|
||||
assert.isObject(result)
|
||||
assert.property(result, 'type')
|
||||
assert.property(result, 'data')
|
||||
assert.equal(result.type, 'npub')
|
||||
assert.equal(result.data.length, 64)
|
||||
})
|
||||
})
|
||||
describe('#readGlobalFeed', () => {
|
||||
it('should throw an error if provided limit is wrong', async () => {
|
||||
try {
|
||||
await uut.readGlobalFeed({ limit: -5 })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.include(err.message, 'Limit must be greater than 0')
|
||||
}
|
||||
})
|
||||
|
||||
it('should read message', async () => {
|
||||
const result = await uut.readGlobalFeed()
|
||||
assert.isArray(result)
|
||||
})
|
||||
|
||||
it('should read message with custom limit', async () => {
|
||||
const result = await uut.readGlobalFeed({ limit: 1 })
|
||||
assert.isArray(result)
|
||||
})
|
||||
})
|
||||
describe('#getFollowers', () => {
|
||||
it('should throw an error if no pubkey is provided', async () => {
|
||||
try {
|
||||
await uut.getFollowers()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.include(err.message, 'pubkey must be a string!')
|
||||
}
|
||||
})
|
||||
|
||||
it('should get followers', async () => {
|
||||
const result = await uut.getFollowers({ pubkey: 'f5a1b39d2cfae8e10e93b9b4d7ec8248eaf45fbe4a73409d8d4c67015e9729a3' })
|
||||
assert.isArray(result)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,24 +9,56 @@ import sinon from 'sinon'
|
||||
|
||||
import WalletAdapter from '../../../src/adapters/wallet.js'
|
||||
import { MockBchWallet } from '../mocks/adapters/wallet.js'
|
||||
import { BchTokenSweepMock } from '../mocks/bch-token-sweep-mock.js'
|
||||
import offerMockData from '../mocks/use-cases/offer-mock-data.js'
|
||||
import fs from 'fs'
|
||||
import * as url from 'url'
|
||||
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
||||
|
||||
const testWalletFile = `${__dirname.toString()}/../../../test-wallet.json`
|
||||
|
||||
describe('#wallet', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
let testWalletData
|
||||
|
||||
before(() => {
|
||||
// try to remove the test wallet file
|
||||
try {
|
||||
fs.unlinkSync(testWalletFile)
|
||||
} catch (error) {
|
||||
console.log('error fsUnlinkSync: ', error)
|
||||
|
||||
// silently fail
|
||||
}
|
||||
})
|
||||
after(() => {
|
||||
// try to remove the test wallet file
|
||||
try {
|
||||
fs.unlinkSync(testWalletFile)
|
||||
} catch (error) {
|
||||
console.log('error fsUnlinkSync: ', error)
|
||||
// silently fail
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
uut = new WalletAdapter()
|
||||
uut.BchWallet = MockBchWallet
|
||||
uut.bchWallet = new MockBchWallet()
|
||||
uut.BchTokenSweep = BchTokenSweepMock
|
||||
uut.WALLET_FILE = testWalletFile
|
||||
uut.bitcoinJs = {
|
||||
Transaction: {
|
||||
fromHex: () => { return { } },
|
||||
SIGHASH_ALL: () => { return { } }
|
||||
fromHex: () => { return {} },
|
||||
SIGHASH_ALL: () => { return {} }
|
||||
},
|
||||
TransactionBuilder: {
|
||||
fromTransaction: () => { return { sign: () => { return { } }, build: () => { return { toHex: () => { return 'hex' } } } } }
|
||||
fromTransaction: () => { return { sign: () => { return {} }, build: () => { return { toHex: () => { return 'hex' } } } } }
|
||||
},
|
||||
ECPair: {
|
||||
fromWIF: () => { return { } }
|
||||
fromWIF: () => { return {} }
|
||||
}
|
||||
}
|
||||
sandbox = sinon.createSandbox()
|
||||
@@ -34,6 +66,278 @@ describe('#wallet', () => {
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#openWallet', () => {
|
||||
it('should create a new wallet when wallet file does not exist', async () => {
|
||||
// Ensure we open the test file, not the production wallet file.
|
||||
const result = await uut.openWallet()
|
||||
testWalletData = result
|
||||
// console.log('result: ', result)
|
||||
assert.property(result, 'mnemonic')
|
||||
assert.property(result, 'privateKey')
|
||||
assert.property(result, 'publicKey')
|
||||
assert.property(result, 'cashAddress')
|
||||
assert.property(result, 'address')
|
||||
assert.property(result, 'slpAddress')
|
||||
assert.property(result, 'legacyAddress')
|
||||
assert.property(result, 'hdPath')
|
||||
})
|
||||
|
||||
it('should open existing wallet file', async () => {
|
||||
// This test case uses the file created in the previous test case.
|
||||
// Ensure we open the test file, not the production wallet file.
|
||||
const result = await uut.openWallet()
|
||||
// console.log('result: ', result)
|
||||
assert.property(result, 'mnemonic')
|
||||
assert.property(result, 'privateKey')
|
||||
assert.property(result, 'publicKey')
|
||||
assert.property(result, 'cashAddress')
|
||||
assert.property(result, 'address')
|
||||
assert.property(result, 'slpAddress')
|
||||
assert.property(result, 'legacyAddress')
|
||||
assert.property(result, 'hdPath')
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
uut.WALLET_FILE = ''
|
||||
// Force an error
|
||||
uut.BchWallet = () => {
|
||||
}
|
||||
await uut.openWallet()
|
||||
// console.log('result: ', result)
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.include(err.message, 'this.BchWallet is not a constructor')
|
||||
}
|
||||
})
|
||||
})
|
||||
describe('#instanceWallet', () => {
|
||||
it('should create an instance of BchWallet with rest-api', async () => {
|
||||
uut.config.useFullStackCash = true
|
||||
const result = await uut.instanceWallet(testWalletData)
|
||||
assert.equal(result.advancedOptions.interface, 'rest-api')
|
||||
})
|
||||
|
||||
it('should create an instance of BchWallet with consumer-api', async () => {
|
||||
uut.config.useFullStackCash = false
|
||||
const result = await uut.instanceWallet(testWalletData)
|
||||
assert.equal(result.advancedOptions.interface, 'consumer-api')
|
||||
})
|
||||
it('should catch and throw an error if no mnemonic is provided', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
await uut.instanceWallet({})
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.include(err.message, 'Wallet data is not formatted correctly. Can not read mnemonic in wallet file!')
|
||||
}
|
||||
})
|
||||
})
|
||||
describe('#incrementNextAddress', () => {
|
||||
it('should increment the nextAddress property', async () => {
|
||||
const result = await uut.incrementNextAddress()
|
||||
assert.equal(result, 2)
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut, 'openWallet').rejects(new Error('test error'))
|
||||
await uut.incrementNextAddress()
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getKeyPair', () => {
|
||||
it('should return an object with a key pair', async () => {
|
||||
// Ensure we open the test file, not the production wallet file.
|
||||
// uut.WALLET_FILE = testWalletFile
|
||||
uut.config.walletFile = testWalletFile
|
||||
|
||||
const result = await uut.getKeyPair()
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'cashAddress')
|
||||
assert.property(result, 'wif')
|
||||
assert.property(result, 'hdIndex')
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox
|
||||
.stub(uut, 'incrementNextAddress')
|
||||
.rejects(new Error('test error'))
|
||||
await uut.getKeyPair()
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
describe('#generateSignature', () => {
|
||||
it('should sign message', async () => {
|
||||
const result = await uut.generateSignature('unit test message')
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.isString(result)
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
await uut.generateSignature()
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'message is required!')
|
||||
}
|
||||
})
|
||||
})
|
||||
describe('#generatePartialTx', () => {
|
||||
it('should generate tx for fungible token', async () => {
|
||||
const utxoInfo = {
|
||||
txid: '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
|
||||
vout: 0,
|
||||
sats: 1000,
|
||||
wif: 'L2fzmF5hCeTZRVnr7Cyu66ucJAAKhkhkfinGVuUAKDtvAfwEx8a3'
|
||||
}
|
||||
|
||||
sandbox.stub(uut.bchWallet, 'getTxData').resolves([offerMockData.fungibleTokenData01.genesisData])
|
||||
const result = await uut.generatePartialTx(offerMockData.fungibleOffer01, utxoInfo)
|
||||
|
||||
assert.isString(result)
|
||||
})
|
||||
it('should generate tx for nft', async () => {
|
||||
const utxoInfo = {
|
||||
txid: '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
|
||||
vout: 0,
|
||||
sats: 1000,
|
||||
wif: 'L2fzmF5hCeTZRVnr7Cyu66ucJAAKhkhkfinGVuUAKDtvAfwEx8a3'
|
||||
}
|
||||
|
||||
sandbox.stub(uut.bchWallet, 'getTxData').resolves([offerMockData.nftTokenData01.genesisData])
|
||||
const result = await uut.generatePartialTx(offerMockData.nftOffer01, utxoInfo)
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.isString(result)
|
||||
})
|
||||
|
||||
it('should handle unknown token type', async () => {
|
||||
try {
|
||||
const utxoInfo = {
|
||||
txid: '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
|
||||
vout: 0,
|
||||
sats: 1000,
|
||||
wif: 'L2fzmF5hCeTZRVnr7Cyu66ucJAAKhkhkfinGVuUAKDtvAfwEx8a3'
|
||||
}
|
||||
|
||||
const offerMock = Object.assign({}, offerMockData.nftOffer01)
|
||||
offerMock.tokenType = 100
|
||||
await uut.generatePartialTx(offerMock, utxoInfo)
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Unknown token type')
|
||||
}
|
||||
})
|
||||
it('should handle multiple outputs', async () => {
|
||||
const utxoInfo = {
|
||||
txid: '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
|
||||
vout: 0,
|
||||
sats: 1000,
|
||||
wif: 'L2fzmF5hCeTZRVnr7Cyu66ucJAAKhkhkfinGVuUAKDtvAfwEx8a3'
|
||||
}
|
||||
|
||||
sandbox.stub(uut.bchWallet, 'getTxData').resolves([offerMockData.nftTokenData01.genesisData])
|
||||
sandbox.stub(uut.bchWallet.bchjs.SLP.NFT1, 'generateNFTChildSendOpReturn').returns({ outputs: 5 })
|
||||
const res = await uut.generatePartialTx(offerMockData.nftOffer01, utxoInfo)
|
||||
assert.isUndefined(res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#moveTokens', () => {
|
||||
it('should move tokens', async () => {
|
||||
sandbox.stub(uut, 'getKeyPair').resolves({
|
||||
cashAddress: 'cashAddress',
|
||||
wif: 'wif',
|
||||
hdIndex: 11
|
||||
})
|
||||
const input = {
|
||||
tokenId: uut.bchWallet.utxos.utxoStore.slpUtxos.type1.tokens[0].tokenId,
|
||||
qty: 1
|
||||
}
|
||||
const res = await uut.moveTokens(input)
|
||||
assert.isObject(res)
|
||||
})
|
||||
|
||||
it('should throw an error if the tokenId is not found', async () => {
|
||||
try {
|
||||
sandbox.stub(uut, 'getKeyPair').resolves({
|
||||
cashAddress: 'cashAddress',
|
||||
wif: 'wif',
|
||||
hdIndex: 11
|
||||
})
|
||||
const input = {
|
||||
tokenId: '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
|
||||
qty: 1
|
||||
}
|
||||
await uut.moveTokens(input)
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'tokenId not found!')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if the tokenId is not provided', async () => {
|
||||
try {
|
||||
await uut.moveTokens({
|
||||
qty: 1
|
||||
})
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'tokenId must be a string!')
|
||||
}
|
||||
})
|
||||
it('should throw an error if the qty is not provided', async () => {
|
||||
try {
|
||||
await uut.moveTokens({
|
||||
tokenId: 'tokenId'
|
||||
})
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'qty must be a number!')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#moveBch', () => {
|
||||
it('should move bch', async () => {
|
||||
sandbox.stub(uut, 'getKeyPair').resolves({
|
||||
cashAddress: 'cashAddress',
|
||||
wif: 'wif',
|
||||
hdIndex: 11
|
||||
})
|
||||
|
||||
const res = await uut.moveBch(100)
|
||||
assert.isObject(res)
|
||||
})
|
||||
|
||||
it('should throw an error if the amountSat is not provided', async () => {
|
||||
try {
|
||||
sandbox.stub(uut, 'getKeyPair').resolves({
|
||||
cashAddress: 'cashAddress',
|
||||
wif: 'wif',
|
||||
hdIndex: 11
|
||||
})
|
||||
|
||||
await uut.moveBch()
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'amountSat is required!')
|
||||
}
|
||||
})
|
||||
})
|
||||
describe('#moveTokensFromCustomWallet', () => {
|
||||
it('should move tokens from a custom wallet', async () => {
|
||||
sandbox.stub(uut, 'getKeyPair').resolves({
|
||||
@@ -100,28 +404,27 @@ describe('#wallet', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
describe('#deseralizeTx', () => {
|
||||
it('should throw an error if the hex is not provided', async () => {
|
||||
try {
|
||||
await uut.deseralizeTx()
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'hex must be a string!')
|
||||
}
|
||||
})
|
||||
it('should decode hex', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.bchWallet.bchjs.RawTransactions, 'decodeRawTransaction').resolves({})
|
||||
const hex = '020000000286b2959cccdb80163b68bb1360abac8e0228ba7b70c779443168cc65c7c42d820100000000ffffffff879b066729eda979f1f062c83b7eccdb9e77f49b417e4723864b3861bf061c24000000006b483045022100b99c6161e18e4e34c1e438b6b03e808703695f217b30da42c3f787798a9f40cc02206d26b4f6799735779d4fac823bc60d276fbb3cea91c66dbfa136cf5b1bd0d8324121028dcd38a58c8295dbd105ef524014f77af4cddfa19b26c50926b4aaeebd95c7ceffffffff030000000000000000376a04534c500001010453454e4420a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b208000000000000006422020000000000001976a914bef6c5180648165704f22e71a1b011ba56336aee88acf81d0000000000001976a914c1f16a3876f8fbe3701a66d3cb3b9c7abb07a06c88ac00000000'
|
||||
const result = await uut.deseralizeTx(hex)
|
||||
assert.isObject(result)
|
||||
} catch (error) {
|
||||
assert.fail('Unexpected code path')
|
||||
}
|
||||
})
|
||||
})
|
||||
describe('#completeTx', () => {
|
||||
// TODO: This test needs to be refactored to use the new instanceWallet() method for loading the user wallet.
|
||||
|
||||
// it('should complete a transaction', async () => {
|
||||
// sandbox.stub(uut, 'getKeyPair').resolves({
|
||||
// cashAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
|
||||
// wif: 'L5D2UAam8tvo3uii5kpgaGyjvVMimdrXu8nWGQSQjuuAix6ji1YQ',
|
||||
// hdIndex: 11
|
||||
// })
|
||||
// sandbox.stub(uut, 'deseralizeTx').resolves({
|
||||
// txid: 'complete tx id result'
|
||||
// })
|
||||
// sandbox.stub(uut.retryQueue, 'addToQueue').resolves('complete tx id result')
|
||||
|
||||
// const hex = 'hex'
|
||||
// const hdIndex = 11
|
||||
// const mnemonic = 'course abstract aerobic deer try switch turtle diet fence affair butter top'
|
||||
|
||||
// const txid = await uut.completeTx(hex, hdIndex, mnemonic)
|
||||
// assert.equal(txid, 'complete tx id result')
|
||||
// })
|
||||
|
||||
it('should throw an error if the hex is not provided', async () => {
|
||||
try {
|
||||
const hdIndex = 11
|
||||
@@ -134,11 +437,60 @@ describe('#wallet', () => {
|
||||
it('should throw an error if the hdIndex is not provided', async () => {
|
||||
try {
|
||||
const hex = 'hex'
|
||||
await uut.completeTx(hex, null)
|
||||
await uut.completeTx(hex)
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'hdIndex must be a non-negative number!')
|
||||
}
|
||||
})
|
||||
it('should throw an error if the mnemonic is not provided', async () => {
|
||||
try {
|
||||
const hex = 'hex'
|
||||
const hdIndex = 11
|
||||
await uut.completeTx(hex, hdIndex)
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'mnemonic must be a string!')
|
||||
}
|
||||
})
|
||||
it('should complete a transaction', async () => {
|
||||
sandbox.stub(uut, 'getKeyPair').resolves({
|
||||
cashAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
|
||||
wif: 'L5D2UAam8tvo3uii5kpgaGyjvVMimdrXu8nWGQSQjuuAix6ji1YQ',
|
||||
hdIndex: 11
|
||||
})
|
||||
sandbox.stub(uut, 'deseralizeTx').resolves({
|
||||
txid: 'complete tx id result'
|
||||
})
|
||||
sandbox.stub(uut.retryQueue, 'addToQueue').resolves('complete tx id result')
|
||||
|
||||
const hex = 'hex'
|
||||
const hdIndex = 11
|
||||
const mnemonic = 'course abstract aerobic deer try switch turtle diet fence affair butter top'
|
||||
|
||||
const txid = await uut.completeTx(hex, hdIndex, mnemonic)
|
||||
assert.equal(txid, 'complete tx id result')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#reClaimTokens', () => {
|
||||
it('should re-claim tokens', async () => {
|
||||
const orderData = {
|
||||
hdIndex: 11,
|
||||
tokenId: 'tokenId',
|
||||
numTokens: 1
|
||||
}
|
||||
const res = await uut.reclaimTokens(orderData)
|
||||
assert.equal(res, 'fakeTxid')
|
||||
})
|
||||
it('should handle error if order hd index is not provided', async () => {
|
||||
try {
|
||||
const orderData = {}
|
||||
await uut.reclaimTokens(orderData)
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'orderData.hdIndex is required!')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -65,4 +65,51 @@ describe('#Webhook-Adapter', () => {
|
||||
assert.isString(result.id)
|
||||
})
|
||||
})
|
||||
describe('#deleteWebhook', () => {
|
||||
it('should throw error if input is not provided', async () => {
|
||||
try {
|
||||
await uut.deleteWebhook()
|
||||
assert.fail('unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'url must be a string')
|
||||
}
|
||||
})
|
||||
it('should delete webhook', async () => {
|
||||
sandbox.stub(uut.axios, 'delete').resolves({ data: { success: true } })
|
||||
const url = 'https://test.com/my-webhook'
|
||||
const result = await uut.deleteWebhook(url)
|
||||
assert.isObject(result)
|
||||
assert.property(result, 'success')
|
||||
})
|
||||
})
|
||||
describe('#waitUntilSuccess', () => {
|
||||
it('should handle error', async () => {
|
||||
try {
|
||||
sandbox.stub(uut, 'sleep').throws(new Error('test error'))
|
||||
sandbox.stub(uut, 'deleteWebhook')
|
||||
.onCall(0).throws(new Error('test error'))
|
||||
|
||||
sandbox.stub(uut, 'createWebhook')
|
||||
.onCall(0).throws(new Error('test error'))
|
||||
.onCall(1).resolves({ data: { success: true } })
|
||||
await uut.waitUntilSuccess()
|
||||
assert.fail('unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
it('should wait until success', async () => {
|
||||
uut.sleepTime = 100
|
||||
|
||||
sandbox.stub(uut, 'deleteWebhook')
|
||||
.onCall(0).throws(new Error('test error'))
|
||||
|
||||
sandbox.stub(uut, 'createWebhook')
|
||||
.onCall(0).throws(new Error('test error'))
|
||||
.onCall(1).resolves({ data: { success: true } })
|
||||
|
||||
const result = await uut.waitUntilSuccess()
|
||||
assert.isTrue(result)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -286,7 +286,9 @@ describe('#Offer-Entity', () => {
|
||||
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
|
||||
ticker: 'TROUT',
|
||||
tokenType: 1,
|
||||
nostrEventId: 'test'
|
||||
nostrEventId: 'test',
|
||||
operatorAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
|
||||
operatorPercentage: 10
|
||||
},
|
||||
timestamp: '2021-09-20T17:54:26.395Z',
|
||||
localTimeStamp: '9/20/2021, 10:54:26 AM',
|
||||
|
||||
@@ -12,8 +12,18 @@ const mockWallet = {
|
||||
nextAddress: 1
|
||||
};
|
||||
|
||||
class MockBchWallet {
|
||||
|
||||
class AdapterRoute {
|
||||
constructor() {
|
||||
this.sendTx = async () => {
|
||||
return 'fakeTxid';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MockBchWallet {
|
||||
constructor(mnemonic, advancedOptions) {
|
||||
this.advancedOptions = advancedOptions
|
||||
this.walletInfoPromise = true;
|
||||
this.walletInfo = mockWallet;
|
||||
this.initialize = async () => {}
|
||||
@@ -24,6 +34,9 @@ class MockBchWallet {
|
||||
this.sendTokens = async () => {
|
||||
return 'fakeTxid';
|
||||
};
|
||||
this.send = async () => {
|
||||
return 'fakeTxid';
|
||||
};
|
||||
this.utxoIsValid =async ()=>{}
|
||||
this.getUtxos = async () => { };
|
||||
this.getBalance = async () => { };
|
||||
@@ -33,7 +46,15 @@ class MockBchWallet {
|
||||
tokenTicker: 'TROUT'
|
||||
}];
|
||||
};
|
||||
this.getKeyPair = async () => {
|
||||
return {
|
||||
cashAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
|
||||
wif: 'L5D2UAam8tvo3uii5kpgaGyjvVMimdrXu8nWGQSQjuuAix6ji1YQ',
|
||||
hdIndex: 11
|
||||
}
|
||||
};
|
||||
this.optimize = async () => { };
|
||||
this.ar = new AdapterRoute()
|
||||
// Environment variable is used by wallet-balance.unit.js to force an error.
|
||||
if (process.env.NO_UTXO) {
|
||||
this.utxos = {};
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
class BchTokenSweepMock {
|
||||
constructor(wif, privateKey, bchWallet, fee, cashAddress) {
|
||||
this.wif = wif
|
||||
this.privateKey = privateKey
|
||||
this.bchWallet = bchWallet
|
||||
this.fee = fee
|
||||
this.cashAddress = cashAddress
|
||||
}
|
||||
populateObjectFromNetwork() {
|
||||
return {
|
||||
success: true,
|
||||
txid: 'txid'
|
||||
}
|
||||
}
|
||||
sweepTo() {
|
||||
return {
|
||||
success: true,
|
||||
txid: 'txid'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { BchTokenSweepMock }
|
||||
|
||||
export default {
|
||||
BchTokenSweepMock
|
||||
}
|
||||
@@ -115,6 +115,7 @@ const fungibleTokenData01 = {
|
||||
"documentUri": "troutsblog.com",
|
||||
"documentHash": "",
|
||||
"decimals": 2,
|
||||
"tokenDecimals": 2,
|
||||
"mintBatonIsActive": true,
|
||||
"tokensInCirculationBN": "100097954686",
|
||||
"tokensInCirculationStr": "100097954686",
|
||||
@@ -141,10 +142,37 @@ const offerMockData = {
|
||||
utxoVout: 0,
|
||||
makerAddr: 'address',
|
||||
tokenType: 1,
|
||||
nostrEventId: 'test'
|
||||
nostrEventId: 'test',
|
||||
operatorAddress: 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478',
|
||||
operatorPercentage: 10
|
||||
}
|
||||
}
|
||||
|
||||
const deserealizeTxMockNoOperatorOut = {
|
||||
//...
|
||||
vout: [
|
||||
{
|
||||
value: 0,
|
||||
scriptPubKey: {
|
||||
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
|
||||
}
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
scriptPubKey: {
|
||||
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
|
||||
}
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
scriptPubKey: {
|
||||
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
|
||||
}
|
||||
},
|
||||
//...
|
||||
]
|
||||
}
|
||||
|
||||
const deserealizeTxMock = {
|
||||
//...
|
||||
vout: [
|
||||
@@ -166,6 +194,12 @@ const deserealizeTxMock = {
|
||||
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
|
||||
}
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
scriptPubKey: {
|
||||
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
|
||||
}
|
||||
},
|
||||
//...
|
||||
]
|
||||
}
|
||||
@@ -178,5 +212,6 @@ export default {
|
||||
fungibleOffer01,
|
||||
fungibleTokenData01,
|
||||
offerMockData,
|
||||
deserealizeTxMockNoOperatorOut,
|
||||
deserealizeTxMock
|
||||
};
|
||||
|
||||
@@ -746,6 +746,24 @@ describe('#offer-use-case', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('should skip transactions that do not have an output for the operator', async () => {
|
||||
// Mock data
|
||||
const mock = Object.assign({}, mockData.offerMockData.data)
|
||||
mock.makerAddr = 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7477' // Unknow Adress
|
||||
mock.rateInBaseUnit = 0
|
||||
mock.numTokens = 0
|
||||
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock)
|
||||
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
|
||||
sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.BitcoinCash, 'toSatoshi').returns(0)
|
||||
|
||||
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMockNoOperatorOut)
|
||||
|
||||
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
|
||||
assert.equal(result, 'N/A')
|
||||
})
|
||||
|
||||
it('should handle error for wrong transaction output address', async () => {
|
||||
try {
|
||||
// Mock data
|
||||
|
||||
@@ -185,6 +185,7 @@ describe('#order-use-case', () => {
|
||||
assert.property(result, 'eventId')
|
||||
assert.property(result, 'noteId')
|
||||
})
|
||||
|
||||
it('should create an order with consumer-api', async () => {
|
||||
uut.config.useFullStackCash = false
|
||||
const entryObj = {
|
||||
@@ -214,6 +215,7 @@ describe('#order-use-case', () => {
|
||||
assert.property(result, 'eventId')
|
||||
assert.property(result, 'noteId')
|
||||
})
|
||||
|
||||
it('should throw error if user is not found', async () => {
|
||||
try {
|
||||
const entryObj = {
|
||||
|
||||
Reference in New Issue
Block a user