Compare commits

..
10 Commits
13 changed files with 230 additions and 18 deletions
+1
View File
@@ -11,6 +11,7 @@ const User = new mongoose.Schema({
name: { type: String },
username: { type: String },
password: { type: String, required: true },
mnemonic: { type: String, required: true },
email: {
type: String,
required: true,
+7
View File
@@ -20,6 +20,13 @@ class Wallet {
// Bind 'this' object to all subfunctions
this.instanceWalletWithoutInitialization = this.instanceWalletWithoutInitialization.bind(this)
this._instanceWallet = this._instanceWallet.bind(this)
this.openWallet = this.openWallet.bind(this)
this.instanceWallet = this.instanceWallet.bind(this)
this.incrementNextAddress = this.incrementNextAddress.bind(this)
this.getKeyPair = this.getKeyPair.bind(this)
this.optimize = this.optimize.bind(this)
this.getBalance = this.getBalance.bind(this)
}
// This is used for initializing the wallet, without waiting to update the wallet
+55
View File
@@ -1,5 +1,7 @@
/*
Adapter library for working with a wallet.
This library is specific to bch-dex. The other wallet adapter library is
inherited from ipfs-service-provider.
*/
// Public npm libraries
@@ -41,7 +43,18 @@ class WalletAdapter {
})
// Bind the 'this' object
this.openWallet = this.openWallet.bind(this)
this.instanceWallet = this.instanceWallet.bind(this)
this.incrementNextAddress = this.incrementNextAddress.bind(this)
this.getKeyPair = this.getKeyPair.bind(this)
this.generateSignature = this.generateSignature.bind(this)
this.generatePartialTx = this.generatePartialTx.bind(this)
this.moveTokens = this.moveTokens.bind(this)
this.moveBch = this.moveBch.bind(this)
this.deseralizeTx = this.deseralizeTx.bind(this)
this.completeTx = this.completeTx.bind(this)
this.reclaimTokens = this.reclaimTokens.bind(this)
this.moveTokensFromCustomWallet = this.moveTokensFromCustomWallet.bind(this)
}
// Open the wallet file, or create one if the file doesn't exist.
@@ -563,6 +576,48 @@ class WalletAdapter {
throw err
}
}
async moveTokensFromCustomWallet (inObj = {}) {
try {
const { tokenId, qty, wallet } = inObj
const keyPair = await this.getKeyPair()
console.log('keyPair: ', keyPair)
const receiver = {
address: keyPair.cashAddress,
tokenId,
qty
}
console.log('receiver: ', receiver)
// Update the UTXO store of the wallet.
await wallet.initialize()
// Get the token type of the token being moved.
// Combine Fungible and NFT token UTXOs.
let tokenUtxos = wallet.utxos.utxoStore.slpUtxos.type1.tokens.concat(
wallet.utxos.utxoStore.slpUtxos.nft.tokens)
// Get token UTXOs that match the token in the order.
tokenUtxos = tokenUtxos.filter(
x => x.tokenId === tokenId
)
const txid = await wallet.sendTokens(receiver, 3)
console.log('txid: ', txid)
const utxoInfo = {
txid,
vout: 1,
hdIndex: wallet.walletInfo.hdIndex,
tokenType: tokenUtxos[0].tokenType
}
return utxoInfo
} catch (err) {
console.error('Error in wallet.js/moveTokens()')
throw err
}
}
}
export default WalletAdapter
@@ -35,6 +35,8 @@ class OrderRESTControllerLib {
// console.log('body: ', ctx.request.body)
const orderObj = ctx.request.body.order
const user = ctx.state.user
orderObj.userId = user._id
console.log('orderObj: ', orderObj)
const { eventId, noteId } = await _this.useCases.order.createOrder(orderObj)
+9 -1
View File
@@ -7,6 +7,7 @@ import Router from 'koa-router'
// Local libraries.
import OrderRESTControllerLib from './controller.js'
import Validators from '../middleware/validators.js'
let _this
@@ -32,12 +33,14 @@ class OrderRouter {
}
// Encapsulate dependencies.
this.validators = new Validators()
this.orderRESTController = new OrderRESTControllerLib(dependencies)
// Instantiate the router and set the base route.
const baseUrl = '/order'
this.router = new Router({ prefix: baseUrl })
this.createOrder = this.createOrder.bind(this)
_this = this
}
@@ -49,7 +52,7 @@ class OrderRouter {
}
// Define the routes and attach the controller.
this.router.post('/', _this.orderRESTController.createOrder)
this.router.post('/', this.createOrder)
this.router.get('/list/all/:page', _this.orderRESTController.listOrders)
this.router.post('/delete', _this.orderRESTController.deleteOrder)
@@ -57,6 +60,11 @@ class OrderRouter {
app.use(_this.router.routes())
app.use(_this.router.allowedMethods())
}
async createOrder (ctx, next) {
await _this.validators.ensureUser(ctx, next)
await _this.orderRESTController.createOrder(ctx, next)
}
}
export default OrderRouter
+39 -12
View File
@@ -25,6 +25,7 @@ class OrderLib {
// Encapsulate dependencies
this.orderEntity = new OrderEntity()
this.OrderModel = this.adapters.localdb.Order
this.UserModel = this.adapters.localdb.Users
this.bch = this.adapters.bch
this.config = config
this.retryQueue = new RetryQueue({ retryPeriod: 1000, attempts: 3 })
@@ -43,8 +44,31 @@ class OrderLib {
if (!entryObj.tokenId) throw new Error('entry does not contain required properties')
const user = await this.UserModel.findById(entryObj.userId)
if (!user) throw new Error('user not found')
console.log(`Using FullStack.cash: ${this.config.useFullStackCash}`)
const advancedConfig = {}
if (this.config.useFullStackCash) {
advancedConfig.interface = 'rest-api'
advancedConfig.restURL = this.config.apiServer
advancedConfig.apiToken = this.config.apiToken
} else {
advancedConfig.interface = 'consumer-api'
advancedConfig.restURL = this.config.consumerUrl
}
// Instantiate minimal-slp-wallet with the user's mnemonic.
const BchWallet = this.adapters.wallet.BchWallet
const userWallet = new BchWallet(user.mnemonic, advancedConfig)
// Wait for wallet to initialize.
await userWallet.walletInfoPromise
await userWallet.initialize()
// Specify the address to send payment.
entryObj.makerAddr = this.adapters.wallet.bchWallet.walletInfo.cashAddress
entryObj.makerAddr = userWallet.walletInfo.cashAddress
console.log('entryObj.makerAddr: ', entryObj.makerAddr)
// console.log('entryObj.makerAddr: ', entryObj.makerAddr)
// Input Validation
@@ -53,7 +77,8 @@ class OrderLib {
// Optimize the wallet to speed up working with it.
console.log('Optimizing wallet before creating new order.')
await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.optimize, {})
// await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.optimize, {})
await userWallet.optimize()
// Ensure sufficient tokens exist to create the order.
// await this.ensureFunds(orderEntity)
@@ -61,24 +86,26 @@ class OrderLib {
// 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 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
// Move the tokens to holding address.
const moveObj = {
tokenId: orderEntity.tokenId,
qty: orderEntity.numTokens
qty: orderEntity.numTokens,
wallet: userWallet
}
// const utxoInfo = await this.adapters.wallet.moveTokens(moveObj)
const utxoInfo = await this.retryQueue.addToQueue(this.adapters.wallet.moveTokens, moveObj)
const utxoInfo = await this.retryQueue.addToQueue(this.adapters.wallet.moveTokensFromCustomWallet, moveObj)
// console.log('utxoInfo: ', utxoInfo)
// Update the UTXO store for the wallet.
await this.adapters.wallet.bchWallet.bchjs.Util.sleep(3000)
await userWallet.bchjs.Util.sleep(3000)
// await this.adapters.wallet.bchWallet.getUtxos()
await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.initialize, {})
// 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
@@ -159,7 +186,7 @@ class OrderLib {
)
}
//
//
} else {
// Buy Order
throw new Error('Buy orders are not supported yet.')
@@ -286,11 +313,11 @@ class OrderLib {
try {
const data = await this.OrderModel.find({})
// Sort entries so newest entries show first.
// Sort entries so newest entries show first.
.sort('-timestamp')
// Skip to the start of the selected page.
// Skip to the start of the selected page.
.skip(page * DEFAULT_ENTRIES_PER_PAGE)
// Only return 20 results.
// Only return 20 results.
.limit(DEFAULT_ENTRIES_PER_PAGE)
return data
+10 -1
View File
@@ -16,7 +16,7 @@ class UserLib {
'Instance of adapters must be passed in when instantiating User Use Cases library.'
)
}
this.BchWallet = this.adapters.wallet.BchWallet
// Encapsulate dependencies
this.UserEntity = new UserEntity()
this.UserModel = this.adapters.localdb.Users
@@ -30,6 +30,11 @@ class UserLib {
const userEntity = this.UserEntity.validate(userObj)
const user = new this.UserModel(userEntity)
const wallet = new this.BchWallet()
const walletInfo = await wallet.walletInfoPromise
const mnemonic = walletInfo.mnemonic
user.mnemonic = mnemonic
// Enforce default value of 'user'
user.type = 'user'
// console.log('user: ', user)
@@ -102,6 +107,7 @@ class UserLib {
// Input Validation
// Optional inputs, but they must be strings if included.
if (newData.email && typeof newData.email !== 'string') {
throw new Error("Property 'email' must be a string!")
}
@@ -111,6 +117,9 @@ class UserLib {
if (newData.password && typeof newData.password !== 'string') {
throw new Error("Property 'password' must be a string!")
}
if (newData.mnemonic) {
throw new Error("Property 'mnemonic' cannot be updated!")
}
// Save a copy of the original user type.
const userType = existingUser.type
+3 -1
View File
@@ -173,6 +173,7 @@ if (!config.noMongo) {
)
assert.property(result.data, 'token', 'Token property exists.')
assert.equal(result.data.user.type, 'user')
assert.property(result.data.user, 'mnemonic')
})
})
@@ -265,7 +266,7 @@ if (!config.noMongo) {
const users = result.data.users
// console.log(`users: ${util.inspect(users)}`)
assert.hasAnyKeys(users[0], ['type', '_id', 'email'])
assert.hasAnyKeys(users[0], ['type', '_id', 'email', 'mnemonic'])
assert.isNumber(users.length)
})
@@ -375,6 +376,7 @@ if (!config.noMongo) {
assert.property(user, 'type')
assert.property(user, 'email')
assert.property(user, 'mnemonic')
assert.property(user, '_id')
assert.equal(user._id, _id)
+2 -1
View File
@@ -31,7 +31,8 @@ describe('#User-Adapter', () => {
testuser = new User({
email: 'test983@test.com',
name: 'test983',
password: 'password'
password: 'password',
mnemonic: '12 words mnemonic'
})
})
@@ -65,6 +65,9 @@ describe('#Order-REST-Router', () => {
ctx.request.body = {
order: {}
}
ctx.state.user = {
id: 'testUserId'
}
// Mock dependencies
sandbox.stub(uut.useCases.order, 'createOrder').resolves({ eventId: 'testEventId', noteId: 'testNoteId' })
@@ -80,6 +83,9 @@ describe('#Order-REST-Router', () => {
ctx.request.body = {
order: {}
}
ctx.state.user = {
id: 'testUserId'
}
// Force an error
sandbox
+2
View File
@@ -150,7 +150,9 @@ const wallet = {
return { cashAddress: 'fakeAddr', wif: 'fakeWif', hdIndex: 1 }
},
bchWallet: new MockBchWallet(),
BchWallet: MockBchWallet,
moveTokens: async () => {},
moveTokensFromCustomWallet: async () => {},
moveBch: async () => {},
reclaimTokens: async ()=>{},
generatePartialTx: async ()=>{},
+51 -1
View File
@@ -173,8 +173,9 @@ describe('#order-use-case', () => {
// Mock dependencies and force expected code path
sandbox.stub(uut.orderEntity, 'inputValidate').returns(entryObj)
sandbox.stub(uut, 'ensureFunds').resolves()
sandbox.stub(uut.UserModel, 'findById').resolves({ mnemonic: 'testMnemonic' })
sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.Util, 'sleep').resolves()
sandbox.stub(uut.adapters.wallet, 'moveTokens').resolves({ txid: 'fakeTxid', vout: 0, hdIndex: 1 })
sandbox.stub(uut.adapters.wallet, 'moveTokensFromCustomWallet').resolves({ txid: 'fakeTxid', vout: 0, hdIndex: 1 })
sandbox.stub(uut.adapters.wallet.bchWallet, 'initialize').resolves()
sandbox.stub(uut.adapters.nostr, 'post').resolves('fakeEvenetId')
@@ -184,6 +185,55 @@ 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 = {
lokadId: 'SWP',
messageType: 1,
messageClass: 1,
tokenId: 'token-id',
buyOrSell: 'sell',
rateInBaseUnit: 1000,
minUnitsToExchange: 1250,
numTokens: 1,
ticker: 'TEST'
}
// Mock dependencies and force expected code path
sandbox.stub(uut.orderEntity, 'inputValidate').returns(entryObj)
sandbox.stub(uut, 'ensureFunds').resolves()
sandbox.stub(uut.UserModel, 'findById').resolves({ mnemonic: 'testMnemonic' })
sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.Util, 'sleep').resolves()
sandbox.stub(uut.adapters.wallet, 'moveTokensFromCustomWallet').resolves({ txid: 'fakeTxid', vout: 0, hdIndex: 1 })
sandbox.stub(uut.adapters.wallet.bchWallet, 'initialize').resolves()
sandbox.stub(uut.adapters.nostr, 'post').resolves('fakeEvenetId')
const result = await uut.createOrder(entryObj)
console.log('result: ', result)
assert.property(result, 'eventId')
assert.property(result, 'noteId')
})
it('should throw error if user is not found', async () => {
try {
const entryObj = {
lokadId: 'SWP',
messageType: 1,
messageClass: 1,
tokenId: 'token-id',
buyOrSell: 'sell',
rateInBaseUnit: 1000,
minUnitsToExchange: 1250,
numTokens: 1,
ticker: 'TEST'
}
await uut.createOrder(entryObj)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'user not found')
}
})
it('should catch and throw an error', async () => {
try {
+43 -1
View File
@@ -117,6 +117,29 @@ describe('#users-use-case', () => {
assert.include(err.message, 'test error')
}
})
it('should handle wallet errors', async () => {
try {
// Force an error with the database.
class MockErrorBchWallet {
constructor () {
this.walletInfoPromise = Promise.reject(new Error('test error'))
}
}
uut.BchWallet = MockErrorBchWallet
const usrObj = {
email: 'test@test.com',
password: 'password',
name: 'test'
}
await uut.createUser(usrObj)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should create a new user in the DB', async () => {
// Note: The user created in this test is used by the getUser, update,
@@ -129,7 +152,6 @@ describe('#users-use-case', () => {
}
const { userData, token } = await uut.createUser(usrObj)
testUser = userData
// Commented out because there is some sophisticated mocking required that
@@ -327,6 +349,26 @@ describe('#users-use-case', () => {
)
}
})
it('should throw an error if mnemonic is provided', async () => {
try {
const newData = {
email: 'test@test.com',
password: 'password',
name: 'test',
mnemonic: 'test'
}
await uut.updateUser(testUser, newData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(
err.message,
"Property 'mnemonic' cannot be updated!"
)
}
})
it('should update the user model', async () => {
const newData = {