mirror of
https://github.com/Permissionless-Software-Foundation/ipfs-bch-wallet-consumer.git
synced 2026-09-22 01:02:04 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43025f0265 | ||
|
|
7ea17fb57c | ||
|
|
efb29104d7 | ||
|
|
971474d493 | ||
|
|
71a6de03b3 | ||
|
|
98dd904fa5 | ||
|
|
b5c7e2efb3 | ||
|
|
3c9bbb097e | ||
|
|
8956968027 | ||
|
|
14c724bf9c |
Vendored
+6
-1
@@ -27,7 +27,7 @@ const ipfsCoordName = process.env.COORD_NAME
|
||||
|
||||
export default {
|
||||
// Configure TCP port.
|
||||
port: process.env.PORT || 5005,
|
||||
port: process.env.PORT || 5015,
|
||||
|
||||
// Password for HTML UI that displays logs.
|
||||
logPass: 'test',
|
||||
@@ -159,6 +159,11 @@ export default {
|
||||
// Preferred P2WDB provider
|
||||
preferredP2wdbProvider: process.env.PREFERRED_P2WDB_PROVIDER
|
||||
? process.env.PREFERRED_P2WDB_PROVIDER
|
||||
: '',
|
||||
|
||||
// Preferred P2WDB provider
|
||||
preferredIpfsFileProvider: process.env.PREFERRED_IPFS_FILE_PROVIDER
|
||||
? process.env.PREFERRED_IPFS_FILE_PROVIDER
|
||||
: ''
|
||||
|
||||
}
|
||||
|
||||
Generated
+3
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ipfs-bch-wallet-consumer",
|
||||
"version": "3.0.0",
|
||||
"version": "3.1.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ipfs-bch-wallet-consumer",
|
||||
"version": "3.0.0",
|
||||
"version": "3.1.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@chainsafe/libp2p-gossipsub": "11.0.1",
|
||||
@@ -43,6 +43,7 @@
|
||||
"koa2-ratelimit": "0.9.1",
|
||||
"libp2p": "1.2.1",
|
||||
"line-reader": "0.4.0",
|
||||
"mime-types": "2.1.35",
|
||||
"minimal-slp-wallet": "5.11.2",
|
||||
"mongoose": "5.13.14",
|
||||
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"koa2-ratelimit": "0.9.1",
|
||||
"libp2p": "1.2.1",
|
||||
"line-reader": "0.4.0",
|
||||
"mime-types": "2.1.35",
|
||||
"minimal-slp-wallet": "5.11.2",
|
||||
"mongoose": "5.13.14",
|
||||
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
|
||||
|
||||
@@ -15,6 +15,7 @@ import Passport from './passport.js'
|
||||
import Nodemailer from './nodemailer.js'
|
||||
import BCH from './bch/index.js'
|
||||
import P2WDB from './p2wdb/index.js'
|
||||
import IpfsFiles from './ipfs-files/index.js'
|
||||
|
||||
// const { wlogger } = require('./wlogger')
|
||||
import JSONFiles from './json-files.js'
|
||||
@@ -45,6 +46,7 @@ class Adapters {
|
||||
this.bch = new BCH(localConfig)
|
||||
this.p2wdb = new P2WDB(localConfig)
|
||||
this.wallet = new Wallet(localConfig)
|
||||
this.ipfsFiles = new IpfsFiles(localConfig)
|
||||
|
||||
// Get a valid JWT API key and instance bch-js.
|
||||
this.fullStackJwt = new FullStackJWT(config)
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
This library contains code for interfacing to the ipfs-file-pin-service using
|
||||
JSON RPC over IPFS. Most of these functions are called by the /files REST API
|
||||
endpoints and the IPFS Use Cases library.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
import { v4 as uid } from 'uuid'
|
||||
import jsonrpc from 'jsonrpc-lite'
|
||||
|
||||
// Local libraries
|
||||
import wlogger from '../wlogger.js'
|
||||
|
||||
// let _this
|
||||
|
||||
class IpfsFilesAdapter {
|
||||
constructor (localConfig = {}) {
|
||||
// console.log('BCH localConfig: ', localConfig)
|
||||
this.ipfs = localConfig.ipfs
|
||||
if (!this.ipfs) {
|
||||
throw new Error(
|
||||
'An instance of IPFS must be passed when instantiating the IPFS Files Adapter library.'
|
||||
)
|
||||
}
|
||||
// this.eventEmitter = localConfig.eventEmitter
|
||||
// if (!this.eventEmitter) {
|
||||
// throw new Error(
|
||||
// 'An instance of an EventEmitter must be passed when instantiating the adapters.'
|
||||
// )
|
||||
// }
|
||||
|
||||
// Connect the RPC handler when the event fires with new data.
|
||||
// this.eventEmitter.on('rpcData', this.rpcHandler)
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.uid = uid
|
||||
this.jsonrpc = jsonrpc
|
||||
|
||||
// A queue for holding RPC data that has arrived.
|
||||
this.rpcDataQueue = []
|
||||
|
||||
// _this = this // Global handle on instance of this Class.
|
||||
|
||||
// Bind 'this' object to all subfunctions for this Class
|
||||
this.rpcHandler = this.rpcHandler.bind(this)
|
||||
this.getStatus = this.getStatus.bind(this)
|
||||
this.selectProvider = this.selectProvider.bind(this)
|
||||
}
|
||||
|
||||
// This handler is triggered when RPC data comes in over IPFS.
|
||||
// Handle RPC input, and add the response to the RPC queue.
|
||||
// Once in the queue, it will get processed by waitForRPCResponse()
|
||||
rpcHandler (data) {
|
||||
try {
|
||||
// Convert string input into an object.
|
||||
// const jsonData = JSON.parse(data)
|
||||
|
||||
// console.log(`JSON RPC response for ID ${data.payload.id} received.`)
|
||||
|
||||
this.rpcDataQueue.push(data)
|
||||
} catch (err) {
|
||||
console.error('Error in files/rpcHandler(): ', err)
|
||||
// Do not throw error. This is a top-level function.
|
||||
}
|
||||
}
|
||||
|
||||
// Get the status of BCH wallet services this node can talk to. Returns an
|
||||
// array of BCH wallet service providers.
|
||||
async getStatus () {
|
||||
try {
|
||||
const peerData = this.ipfs.ipfsCoordAdapter.ipfsCoord.thisNode.peerData
|
||||
// console.log(`peerData: ${JSON.stringify(peerData, null, 2)}`)
|
||||
|
||||
// const status = {
|
||||
// state: this.ipfs.ipfsCoordAdapter.state
|
||||
// }
|
||||
|
||||
// Add names to the IPFS IDs for each provider.
|
||||
const initialServiceProviders =
|
||||
this.ipfs.ipfsCoordAdapter.state.ipfsFileProviders
|
||||
const serviceProviders = []
|
||||
for (let i = 0; i < initialServiceProviders.length; i++) {
|
||||
const thisProvider = initialServiceProviders[i]
|
||||
|
||||
const providerData = peerData.filter((x) => x.from === thisProvider)
|
||||
|
||||
if (providerData.length) {
|
||||
const provObj = {
|
||||
ipfsId: thisProvider,
|
||||
name: providerData[0].data.jsonLd.name
|
||||
}
|
||||
|
||||
serviceProviders.push(provObj)
|
||||
}
|
||||
}
|
||||
|
||||
// console.log(
|
||||
// `serviceProviders: ${JSON.stringify(serviceProviders, null, 2)}`
|
||||
// )
|
||||
|
||||
const outObj = {
|
||||
serviceProviders,
|
||||
selectedProvider: this.ipfs.ipfsCoordAdapter.state.selectedIpfsFileProvider
|
||||
}
|
||||
// console.log('outObj: ', outObj)
|
||||
|
||||
return outObj
|
||||
} catch (err) {
|
||||
// console.log('createUser() error: ', err)
|
||||
wlogger.error('Error in adapters/files/getStatus()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Choose the ipfs-file-pin-service to use.
|
||||
async selectProvider (providerId) {
|
||||
try {
|
||||
this.ipfs.ipfsCoordAdapter.config.selectedIpfsFileProvider = providerId
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
// console.log('createUser() error: ', err)
|
||||
wlogger.error('Error in adapters/files/getStatus()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Given a CID, query the ipfs-file-pin-service to get the file metadata, like
|
||||
// the file name.
|
||||
async getFileMetadata (inObj = {}) {
|
||||
try {
|
||||
const { cid } = inObj
|
||||
|
||||
// Input validation.
|
||||
if (!cid || typeof cid !== 'string') {
|
||||
throw new Error('getFileMetadata() cid input hash must be a string.')
|
||||
}
|
||||
|
||||
// Throw an error if this IPFS node has not yet made a connection to a
|
||||
// wallet service provider.
|
||||
const selectedProvider =
|
||||
this.ipfs.ipfsCoordAdapter.state.selectedIpfsFileProvider
|
||||
if (!selectedProvider) {
|
||||
throw new Error('No IPFS File Pin Service provider available yet.')
|
||||
}
|
||||
|
||||
const rpcData = {
|
||||
endpoint: 'getFileMetadata',
|
||||
cid
|
||||
}
|
||||
|
||||
// Generate a UUID for the call.
|
||||
const rpcId = this.uid()
|
||||
|
||||
// Generate a JSON RPC command.
|
||||
const cmd = this.jsonrpc.request(rpcId, 'file-pin', rpcData)
|
||||
const cmdStr = JSON.stringify(cmd)
|
||||
// console.log('cmdStr: ', cmdStr)
|
||||
|
||||
// Send the RPC command to selected wallet service.
|
||||
const thisNode = this.ipfs.ipfsCoordAdapter.ipfsCoord.thisNode
|
||||
await this.ipfs.ipfsCoordAdapter.ipfsCoord.useCases.peer.sendPrivateMessage(
|
||||
selectedProvider,
|
||||
cmdStr,
|
||||
thisNode
|
||||
)
|
||||
|
||||
// Wait for data to come back from the wallet service.
|
||||
const data = await this.waitForRPCResponse(rpcId)
|
||||
// console.log('getFileMetadata() data: ', data)
|
||||
|
||||
return data
|
||||
} catch (err) {
|
||||
wlogger.error('Error in adapters/files/getFileMetadata()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a promise that resolves to data when the RPC response is recieved.
|
||||
async waitForRPCResponse (rpcId) {
|
||||
try {
|
||||
// Initialize variables for tracking the return data.
|
||||
let dataFound = false
|
||||
let cnt = 0
|
||||
|
||||
// Default return value, if the remote computer does not respond in time.
|
||||
let data = {
|
||||
success: false,
|
||||
message: 'request timed out',
|
||||
data: ''
|
||||
}
|
||||
|
||||
// Loop that waits for a response from the service provider.
|
||||
do {
|
||||
// console.log(`this.rpcDataQueue.length: ${this.rpcDataQueue.length}`)
|
||||
for (let i = 0; i < this.rpcDataQueue.length; i++) {
|
||||
const rawData = this.rpcDataQueue[i]
|
||||
// console.log(`rawData: ${JSON.stringify(rawData, null, 2)}`)
|
||||
|
||||
if (rawData.payload.id === rpcId) {
|
||||
dataFound = true
|
||||
// console.log('data was found in the queue')
|
||||
|
||||
data = rawData.payload.result.value
|
||||
|
||||
// Remove the data from the queue
|
||||
this.rpcDataQueue.splice(i, 1)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Wait between loops.
|
||||
// await this.sleep(1000)
|
||||
await this.ipfs.ipfsCoordAdapter.wallet.bchjs.Util.sleep(2000)
|
||||
|
||||
cnt++
|
||||
|
||||
// Exit if data was returned, or the window for a response expires.
|
||||
} while (!dataFound && cnt < 10)
|
||||
// console.log(`dataFound: ${dataFound}, cnt: ${cnt}`)
|
||||
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('Error in waitForRPCResponse()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// module.exports = P2wdbAdapter
|
||||
export default IpfsFilesAdapter
|
||||
@@ -23,6 +23,8 @@ const MIN_BCH_WALLET_VERSION = '1.11.11'
|
||||
const WALLET_PROTOCOL = 'bch-wallet'
|
||||
const MIN_P2WDB_VERSION = '1.4.0'
|
||||
const P2WDB_PROTOCOL = 'p2wdb'
|
||||
const MIN_FILE_PIN_VERSION = '1.0.0'
|
||||
const FILE_PIN_PROTOCOL = 'ipfs-file-pin-service'
|
||||
|
||||
let _this
|
||||
|
||||
@@ -56,9 +58,13 @@ class IpfsCoordAdapter {
|
||||
|
||||
// Periodically poll services for available wallet service providers.
|
||||
this.pollBchServiceInterval = setInterval(this.pollForBchServices, 10000)
|
||||
this.pollIpfsFileServiceInterval = setInterval(
|
||||
this.pollForIpfsFileServices,
|
||||
11000
|
||||
)
|
||||
this.pollP2wdbServiceInterval = setInterval(
|
||||
this.pollForP2wdbServices,
|
||||
11000
|
||||
12000
|
||||
)
|
||||
|
||||
// State object. TODO: Make this more robust.
|
||||
@@ -66,7 +72,9 @@ class IpfsCoordAdapter {
|
||||
serviceProviders: [],
|
||||
selectedServiceProvider: '',
|
||||
p2wdbProviders: [],
|
||||
selectedP2wdbProvider: ''
|
||||
selectedP2wdbProvider: '',
|
||||
ipfsFileProviders: [],
|
||||
selectedIpfsFileProvider: ''
|
||||
}
|
||||
|
||||
_this = this
|
||||
@@ -278,6 +286,10 @@ class IpfsCoordAdapter {
|
||||
_this.config.preferredProvider &&
|
||||
thisPeer === _this.config.preferredProvider
|
||||
) {
|
||||
if (_this.state.selectedServiceProvider !== thisPeer) {
|
||||
console.log(`---->BCH wallet service switched to preferred peer: ${thisPeer}`)
|
||||
}
|
||||
|
||||
_this.state.selectedServiceProvider = thisPeer
|
||||
}
|
||||
|
||||
@@ -308,6 +320,96 @@ class IpfsCoordAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
// Poll the ipfs-coord coordination channel for available ipfs-file-pin-service
|
||||
// providers. This method is called periodically by a timer-interval.
|
||||
pollForIpfsFileServices () {
|
||||
try {
|
||||
// console.log('pollForIpfsFileServices() polling for BCH service')
|
||||
|
||||
// An array of IPFS IDs of other nodes in the coordination pubsub channel.
|
||||
const peers = _this.ipfsCoord.thisNode.peerList
|
||||
// console.log(`peers: ${JSON.stringify(peers, null, 2)}`)
|
||||
|
||||
// Array of objects. Each object is the IPFS ID of the peer and contains
|
||||
// data about that peer.
|
||||
const peerData = _this.ipfsCoord.thisNode.peerData
|
||||
// console.log(`peerData: ${JSON.stringify(peerData, null, 2)}`)
|
||||
|
||||
for (let i = 0; i < peers.length; i++) {
|
||||
const thisPeer = peers[i]
|
||||
const thisData = peerData.filter((x) => x.from === thisPeer)
|
||||
const thisPeerData = thisData[0]
|
||||
|
||||
// Create a 'fingerprint' that defines the wallet service.
|
||||
const protocol = thisPeerData.data.jsonLd.protocol
|
||||
const version = thisPeerData.data.jsonLd.version
|
||||
// console.log(
|
||||
// `debug: peer ${thisPeer} uses protocol: ${protocol} v${version}`,
|
||||
// )
|
||||
|
||||
let versionMatches = false
|
||||
if (version) {
|
||||
// versionMatches = _this.semver.gt(version, MIN_FILE_PIN_VERSION)
|
||||
versionMatches = _this.semver.satisfies(version, `>=${MIN_FILE_PIN_VERSION}`)
|
||||
}
|
||||
|
||||
// Ignore any peers that don't match the fingerprint for a BCH wallet
|
||||
// service.
|
||||
if (protocol && protocol.includes(FILE_PIN_PROTOCOL) && versionMatches) {
|
||||
// console.log('Matching peer: ', thisPeerData)
|
||||
|
||||
// Temporary business logic.
|
||||
// Use the first available wallet service detected.
|
||||
if (_this.state.ipfsFileProviders.length === 0) {
|
||||
_this.state.selectedIpfsFileProvider = thisPeer
|
||||
|
||||
// Persist the config setting, so it can be used by other commands.
|
||||
// _this.conf.set('selectedService', thisPeer)
|
||||
console.log(`---->IPFS File service selected: ${thisPeer}`)
|
||||
}
|
||||
|
||||
// console.log('preferredProvider: ', _this.config.preferredProvider)
|
||||
|
||||
// If a preferred provider is set in the config file, then connect
|
||||
// to the preferred provider when it's discovered.
|
||||
if (
|
||||
_this.config.preferredIpfsFileProvider &&
|
||||
thisPeer === _this.config.preferredIpfsFileProvider
|
||||
) {
|
||||
if (_this.state.selectedIpfsFileProvider !== thisPeer) {
|
||||
console.log(`---->IPFS File service switched to preferred peer: ${thisPeer}`)
|
||||
}
|
||||
|
||||
_this.state.selectedIpfsFileProvider = thisPeer
|
||||
}
|
||||
|
||||
// console.log('selectedServiceProvider: ', _this.state.selectedServiceProvider)
|
||||
|
||||
// Check if the peer has already been added to the list of providers.
|
||||
const alreadyExists = _this.state.ipfsFileProviders.filter(
|
||||
(x) => x === thisPeer
|
||||
)
|
||||
|
||||
// Add the peer to the list of serviceProviders if it doesn't already
|
||||
// exist in the list.
|
||||
if (!alreadyExists.length) {
|
||||
_this.state.ipfsFileProviders.push(thisPeer)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// catch and handle known failure mode.
|
||||
if (
|
||||
err.message.includes("Cannot read property 'peerList' of undefined")
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
console.error('Error in pollForIpfsFileServices(): ', err)
|
||||
// Do not throw error. This is a top-level function.
|
||||
}
|
||||
}
|
||||
|
||||
// This method handles input coming in from other IPFS peers.
|
||||
// It passes the data on to the REST API library by emitting an event.
|
||||
// peerInputHandler (data) {
|
||||
|
||||
@@ -97,6 +97,7 @@ class JSONRPC {
|
||||
wlogger.info(
|
||||
`JSON RPC received from ${from}, ID: ${parsedData.payload.id}, type: ${parsedData.type}`
|
||||
)
|
||||
// console.log('parsedData: ', JSON.stringify(parsedData, null, 2))
|
||||
}
|
||||
// console.log(`parsedData: ${JSON.stringify(parsedData, null, 2)}`)
|
||||
|
||||
@@ -113,6 +114,11 @@ class JSONRPC {
|
||||
console.log('routing to BCH adapter')
|
||||
retObj = await _this.adapters.bch.rpcHandler(parsedData)
|
||||
}
|
||||
|
||||
if (parsedData.payload.result.method === 'file-pin') {
|
||||
console.log('routing to IPFS Files adapter')
|
||||
retObj = await _this.adapters.ipfsFiles.rpcHandler(parsedData)
|
||||
}
|
||||
} catch (err) {
|
||||
/* exit quietly */
|
||||
}
|
||||
|
||||
@@ -723,6 +723,33 @@ class BchRESTControllerLib {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /bch/service Get the IPFS ID for the Wallet service
|
||||
* @apiPermission public
|
||||
* @apiName getService
|
||||
* @apiGroup REST BCH
|
||||
* @apiDescription Get the IPFS ID for the ipfs-bch-wallet-service node that
|
||||
* this app is using to retrieve blockchain data from.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5015/bch/service
|
||||
*
|
||||
*/
|
||||
async getService (ctx) {
|
||||
try {
|
||||
const selectedServiceProvider = this.adapters.ipfs.ipfsCoordAdapter.state.selectedServiceProvider
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
selectedServiceProvider
|
||||
}
|
||||
} catch (err) {
|
||||
// wlogger.error('Error in ipfs/controller.js/viewFile(): ', err)
|
||||
console.log('Error in ipfs/controller.js/getService(): ', err)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DRY error handler
|
||||
handleError (ctx, err) {
|
||||
// If an HTTP status is specified by the buisiness logic, use that.
|
||||
|
||||
@@ -63,6 +63,7 @@ class BchRouter {
|
||||
this.router.post('/utxoIsValid', this.utxoIsValid)
|
||||
this.router.post('/getTokenData', this.getTokenData)
|
||||
this.router.post('/getTokenData2', this.getTokenData2)
|
||||
this.router.get('/service', this.getService)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(this.router.routes())
|
||||
@@ -116,6 +117,10 @@ class BchRouter {
|
||||
async getTokenData2 (ctx, next) {
|
||||
await _this.bchRESTController.getTokenData2(ctx, next)
|
||||
}
|
||||
|
||||
async getService (ctx, next) {
|
||||
await _this.bchRESTController.getService(ctx, next)
|
||||
}
|
||||
}
|
||||
|
||||
// module.exports = BchRouter
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import mime from 'mime-types'
|
||||
|
||||
// Local libraries
|
||||
import wlogger from '../../../adapters/wlogger.js'
|
||||
@@ -34,13 +35,16 @@ class IpfsRESTControllerLib {
|
||||
this.handleError = this.handleError.bind(this)
|
||||
this.connect = this.connect.bind(this)
|
||||
this.getThisNode = this.getThisNode.bind(this)
|
||||
this.viewFile = this.viewFile.bind(this)
|
||||
this.getService = this.getService.bind(this)
|
||||
this.getFileInfo = this.getFileInfo.bind(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /ipfs Get status on IPFS infrastructure
|
||||
* @apiPermission public
|
||||
* @apiName GetIpfsStatus
|
||||
* @apiGroup REST BCH
|
||||
* @apiGroup REST IPFS
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs
|
||||
@@ -107,7 +111,7 @@ class IpfsRESTControllerLib {
|
||||
* @api {get} /ipfs/node Get a copy of the thisNode object from helia-coord
|
||||
* @apiPermission public
|
||||
* @apiName GetThisNode
|
||||
* @apiGroup REST BCH
|
||||
* @apiGroup REST IPFS
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs/node
|
||||
@@ -125,6 +129,101 @@ class IpfsRESTControllerLib {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /ipfs/view/:cid Retrieve and display a file via its IPFS CID
|
||||
* @apiPermission public
|
||||
* @apiName GetCidView
|
||||
* @apiGroup REST IPFS
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs/view/bafkreieaqtdhfywyddomswogynzymukosqqgqo7lkt5lch2zwfnc55m6om
|
||||
*
|
||||
*/
|
||||
async viewFile (ctx) {
|
||||
try {
|
||||
const { cid } = ctx.params
|
||||
|
||||
// const file = await this.adapters.ipfs.ipfs.blockstore.get(cid)
|
||||
// return file
|
||||
|
||||
// const cid = ctx.params.cid
|
||||
|
||||
const { filename, readStream } = await this.useCases.ipfs.downloadCid({ cid })
|
||||
|
||||
// ctx.body = ctx.req.pipe(readStream)
|
||||
|
||||
// Lookup the mime type from the filename.
|
||||
const contentType = mime.lookup(filename)
|
||||
|
||||
ctx.set('Content-Type', contentType)
|
||||
ctx.set(
|
||||
'Content-Disposition',
|
||||
// 'inline; filename="' + filename + '"'
|
||||
`inline; filename="${filename}"`
|
||||
)
|
||||
ctx.body = readStream
|
||||
} catch (err) {
|
||||
// wlogger.error('Error in ipfs/controller.js/viewFile(): ', err)
|
||||
console.log('Error in ipfs/controller.js/viewFile(): ', err)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /ipfs/service Get the IPFS ID for the File Pin service
|
||||
* @apiPermission public
|
||||
* @apiName GetService
|
||||
* @apiGroup REST IPFS
|
||||
* @apiDescription Get the IPFS ID for the ipfs-file-pin-service node that
|
||||
* this app is using to retrieve file data from.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5015/ipfs/service
|
||||
*
|
||||
*/
|
||||
async getService (ctx) {
|
||||
try {
|
||||
const selectedIpfsFileProvider = this.adapters.ipfs.ipfsCoordAdapter.state.selectedIpfsFileProvider
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
selectedIpfsFileProvider
|
||||
}
|
||||
} catch (err) {
|
||||
// wlogger.error('Error in ipfs/controller.js/viewFile(): ', err)
|
||||
console.log('Error in ipfs/controller.js/getService(): ', err)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /ipfs/file-info/:cid Get file pin info about a CID
|
||||
* @apiPermission public
|
||||
* @apiName GetFileInfo
|
||||
* @apiGroup REST IPFS
|
||||
* @apiDescription Get file metadata and pin status information give a CID.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5015/ipfs/file-info/bafkreieaqtdhfywyddomswogynzymukosqqgqo7lkt5lch2zwfnc55m6om
|
||||
*
|
||||
*/
|
||||
async getFileInfo (ctx) {
|
||||
try {
|
||||
const { cid } = ctx.params
|
||||
|
||||
const ipfsFiles = this.adapters.ipfsFiles
|
||||
|
||||
const metadata = await ipfsFiles.getFileMetadata({ cid })
|
||||
console.log('getCidMetadata() metadata: ', metadata)
|
||||
|
||||
ctx.body = metadata
|
||||
} catch (err) {
|
||||
// wlogger.error('Error in ipfs/controller.js/viewFile(): ', err)
|
||||
console.log('Error in ipfs/controller.js/getService(): ', err)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DRY error handler
|
||||
handleError (ctx, err) {
|
||||
// If an HTTP status is specified by the buisiness logic, use that.
|
||||
|
||||
@@ -56,6 +56,9 @@ class IpfsRouter {
|
||||
this.router.post('/relays', this.ipfsRESTController.getRelays)
|
||||
this.router.post('/connect', this.ipfsRESTController.connect)
|
||||
this.router.get('/node', this.ipfsRESTController.getThisNode)
|
||||
this.router.get('/view/:cid', this.ipfsRESTController.viewFile)
|
||||
this.router.get('/service', this.ipfsRESTController.getService)
|
||||
this.router.get('/file-info/:cid', this.ipfsRESTController.getFileInfo)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(this.router.routes())
|
||||
|
||||
@@ -48,7 +48,7 @@ class TimerControllers {
|
||||
// Replace this example function with your own timer handler.
|
||||
exampleTimerFunc (negativeTest) {
|
||||
try {
|
||||
console.log('Example timer controller executed.')
|
||||
// console.log('Example timer controller executed.')
|
||||
|
||||
if (negativeTest) throw new Error('test error')
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
https://troutsblog.com/blog/clean-architecture
|
||||
*/
|
||||
|
||||
// Local libraries
|
||||
import UserUseCases from './user.js'
|
||||
import IpfsUseCases from './ipfs-use-cases.js'
|
||||
|
||||
class UseCases {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -23,6 +25,7 @@ class UseCases {
|
||||
|
||||
// console.log('use-cases/index.js localConfig: ', localConfig)
|
||||
this.user = new UserUseCases(localConfig)
|
||||
this.ipfs = new IpfsUseCases(localConfig)
|
||||
}
|
||||
|
||||
// Run any startup Use Cases at the start of the app.
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
Use cases for working with IPFS.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
// import Wallet from 'minimal-slp-wallet'
|
||||
// import { CID } from 'multiformats'
|
||||
// import RetryQueue from '@chris.troutner/retry-queue'
|
||||
// import { exporter } from 'ipfs-unixfs-exporter'
|
||||
import { Duplex } from 'stream'
|
||||
|
||||
// Local libraries
|
||||
// import PinEntity from '../entities/pin.js'
|
||||
// import config from '../../config/index.js'
|
||||
|
||||
// const PSF_TOKEN_ID = '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0'
|
||||
|
||||
class IpfsUseCases {
|
||||
constructor (localConfig = {}) {
|
||||
// console.log('User localConfig: ', localConfig)
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of adapters must be passed in when instantiating IPFS Use Cases library.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
// this.exporter = exporter
|
||||
|
||||
// Bind 'this' object to all class subfunctions.
|
||||
this.downloadCid = this.downloadCid.bind(this)
|
||||
// this.downloadCid2 = this.downloadCid2.bind(this)
|
||||
}
|
||||
|
||||
// Download a pinned file, given its CID.
|
||||
// Returns a readable stream.
|
||||
async downloadCid (inObj = {}) {
|
||||
try {
|
||||
const { cid } = inObj
|
||||
|
||||
if (!cid) throw new Error('CID is undefined')
|
||||
|
||||
// const Pins = this.adapters.localdb.Pins
|
||||
// let existingModel = await Pins.find({ cid })
|
||||
// existingModel = existingModel[0]
|
||||
// console.log('existingModel: ', existingModel)
|
||||
|
||||
// if (!existingModel) {
|
||||
// throw new Error(`Database model for CID ${cid} does not exist.`)
|
||||
// }
|
||||
//
|
||||
// if (!existingModel.dataPinned) {
|
||||
// throw new Error('File has not been pinned. Not available.')
|
||||
// }
|
||||
|
||||
const filename = await this.getCidMetadata({ cid })
|
||||
|
||||
const helia = this.adapters.ipfs.ipfs
|
||||
|
||||
// Convert the file to a Buffer.
|
||||
const fileChunks = []
|
||||
for await (const chunk of helia.fs.cat(cid)) {
|
||||
fileChunks.push(chunk)
|
||||
}
|
||||
const fileBuf = Buffer.concat(fileChunks)
|
||||
|
||||
// Convert the Buffer into a readable stream
|
||||
const bufferToStream = (myBuffer) => {
|
||||
const tmp = new Duplex()
|
||||
tmp.push(myBuffer)
|
||||
tmp.push(null)
|
||||
return tmp
|
||||
}
|
||||
const readStream = bufferToStream(fileBuf)
|
||||
|
||||
// const filename = 'test.jpg'
|
||||
|
||||
return { filename, readStream }
|
||||
} catch (err) {
|
||||
console.error('Error in use-cases/ipfs.js/dowloadCid()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Given a CID, this function will retrieve file metadata from the
|
||||
// ipfs-file-pin-service, using the IPFS JSON-RPC. This metadata includes
|
||||
// the filename, which can be used to infer mime-type, so that it can be
|
||||
// delivered to a web browser.
|
||||
async getCidMetadata (inObj = {}) {
|
||||
try {
|
||||
const { cid } = inObj
|
||||
|
||||
const ipfsFiles = this.adapters.ipfsFiles
|
||||
|
||||
const metadata = await ipfsFiles.getFileMetadata({ cid })
|
||||
console.log('getCidMetadata() metadata: ', metadata)
|
||||
|
||||
const filename = metadata.fileMetadata.filename
|
||||
|
||||
return filename
|
||||
} catch (err) {
|
||||
console.error('Error in getCidMetadata(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// async downloadCid2 (inObj = {}) {
|
||||
// try {
|
||||
// const { cid } = inObj
|
||||
//
|
||||
// // console.log(`downloadFile() retrieving this CID: ${cid}, with fileName: ${fileName}, and path: ${path}`)
|
||||
//
|
||||
// const blockstore = this.adapters.ipfs.ipfs.blockstore
|
||||
// const entry = await this.exporter(cid, blockstore)
|
||||
//
|
||||
// console.info(entry.cid) // Qmqux
|
||||
// console.log('entry: ', entry)
|
||||
// // console.info(entry.unixfs.fileSize()) // 4
|
||||
//
|
||||
// // const filePath = `${path}/${fileName}`
|
||||
// // console.log(`filePath: ${filePath}`)
|
||||
// // const writableStream = this.fs.createWriteStream(filePath)
|
||||
// //
|
||||
// // writableStream.on('error', this.writeStreamError)
|
||||
// //
|
||||
// // writableStream.on('finish', this.writeStreamFinished)
|
||||
// //
|
||||
//
|
||||
// const fileChunks = []
|
||||
// for await (const buf of entry.content()) {
|
||||
// fileChunks.push(buf)
|
||||
// }
|
||||
// const fileBuf = Buffer.concat(fileChunks)
|
||||
//
|
||||
// //
|
||||
// // writableStream.end()
|
||||
//
|
||||
// // Convert the Buffer into a readable stream
|
||||
// const bufferToStream = (myBuffer) => {
|
||||
// const tmp = new Duplex()
|
||||
// tmp.push(myBuffer)
|
||||
// tmp.push(null)
|
||||
// return tmp
|
||||
// }
|
||||
// const readStream = bufferToStream(fileBuf)
|
||||
//
|
||||
// const filename = 'test.jpg'
|
||||
//
|
||||
// return { filename, readStream }
|
||||
//
|
||||
// // return { cid }
|
||||
// } catch (err) {
|
||||
// console.error('Error in ipfs-use-cases.js/downloadCid()')
|
||||
// throw err
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
export default IpfsUseCases
|
||||
Reference in New Issue
Block a user