Successfully recieving result from file pin JSON RPC call

This commit is contained in:
Chris Troutner
2024-04-09 20:56:40 -07:00
parent 8956968027
commit 3c9bbb097e
6 changed files with 454 additions and 2 deletions
+5
View File
@@ -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
: ''
}
+2
View File
@@ -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)
+325
View File
@@ -0,0 +1,325 @@
/*
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
}
}
// Read an entry from the P2WDB, given an entry hash.
async getEntryByHash (hash) {
try {
// Input validation.
if (!hash || typeof hash !== 'string') {
throw new Error('getEntry() 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.selectedP2wdbProvider
if (!selectedProvider) {
throw new Error('No P2WDB Service provider available yet.')
}
const rpcData = {
endpoint: 'getByHash',
hash
}
// Generate a UUID for the call.
const rpcId = this.uid()
// Generate a JSON RPC command.
const cmd = this.jsonrpc.request(rpcId, 'p2wdb', 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)
return data
} catch (err) {
console.error('Error in getEntryByHash()')
throw err
}
}
async writeEntry (writeObj) {
try {
const { txid, signature, message, data } = writeObj
// Throw an error if this IPFS node has not yet made a connection to a
// wallet service provider.
const selectedProvider =
this.ipfs.ipfsCoordAdapter.state.selectedP2wdbProvider
if (!selectedProvider) {
throw new Error('No P2WDB Service provider available yet.')
}
const rpcData = {
endpoint: 'write',
txid,
signature,
message,
data
}
// Generate a UUID for the call.
const rpcId = this.uid()
// Generate a JSON RPC command.
const cmd = this.jsonrpc.request(rpcId, 'p2wdb', 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 result = await this.waitForRPCResponse(rpcId)
return result
} catch (err) {
console.error('Error in adapters/p2wdb/writeEntry()')
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
+96 -2
View File
@@ -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
@@ -308,6 +316,92 @@ 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
) {
_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) {
+6
View File
@@ -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 */
}
+20
View File
@@ -54,6 +54,8 @@ class IpfsUseCases {
// throw new Error('File has not been pinned. Not available.')
// }
await this.getCidMetadata({ cid })
const helia = this.adapters.ipfs.ipfs
// Convert the file to a Buffer.
@@ -81,6 +83,24 @@ class IpfsUseCases {
}
}
// 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)
} catch (err) {
console.error('Error in getCidMetadata(): ', err)
throw err
}
}
// async downloadCid2 (inObj = {}) {
// try {
// const { cid } = inObj