Create a Decentralized Database using Tangle and IPFS for beginners
In this tutorial, I am going to create a Database system for decentralized applications to do that I will use the MAM communication protocol and for storage, I will use IPFS.
If you are not familiar with MAM its simply create a Merkel Tree to save hashes. The hash of the root is capable of accessing all other hashes using side-key in our case as we use Restricted MAM. So IPFS will upload files then the returned hashed will be saved on MAM.
How the database will work
For sure our database will be in JSON format.
1- Upload JSON data to the IPFS and get the returned hash to access the files later.
2- Send the IPFS using MAM restricted.
To get Data:-
1-Fetch data using mama restricted and get the uploaded IPFS hash.
2- Fetch the hash of the IPFS to get the uploaded JSON data.
Let's Start Coding
The IPFS code to upload data. Check function on the documentation here.
/*Don't forget to write npm install --save ipfs-http-client*/const ipfsLibrary = require(‘ipfs-http-client’)
const ipfs = new ipfsLibrary({ host: ‘ipfs.infura.io’, port: 5001, protocol: ‘https’ })const add = async (data) =>{const hash = await ipfs.add(JSON.stringify(data))return hash[0].path}module.exports = {
execute : add
}
The IPFS code to fetch data cat.js Check function on the documentation here
const ipfsLibrary = require('ipfs-http-client')const ipfs = new ipfsLibrary({ host: 'ipfs.infura.io', port: 5001, protocol: 'https' })const cat = async (hash) =>{const content = await ipfs.cat(hash)console.log("read hash =",hash)return content.toString()}module.exports = {
execute : cat
}
The MAM code
For the MAM code, we will create 4 files:-
1-IotaGlobale.js: Add all global variables used on the 2 push and fetch functions
2-pushData.js: Upload to MAM
3-fetchRoot.js: Fetch data from MAM
4-mangeMam.js: Combine the 3 above files
`IotaGlobale.js`Important note! It cost me three days to solve the duplication of Merkel tree. The State of Merkel tree must remain global or each time you call the function all your previous data will be lost.
IotaGlobale.js:
/*On this file we will add all globale variables we will need to use*/
const iotaLibrary = require(‘@iota/core’)
const Converter = require(‘@iota/converter’)const Mam = require(‘@iota/mam’)const seed = ‘XGIVJKNUIDKDVAXGRK9SFXYFVOLEHSJOIZVROT9DUAMYUUXPPBZWYQWJNEYPVKOMKR9SNRYSZXUHDFKNB’// Define the depth that the node will use for tip selectionconst depth = 3;// Define the minimum weight magnitude for the Devnetconst minimumWeightMagnitude = 9;//MAMconst providerLink = ‘https://nodes.devnet.iota.org:443'const sideKey = ‘DONTSHARETHISPASSWORD’let mamState = Mam.init(providerLink)mamState = Mam.changeMode(mamState, ‘restricted’, sideKey)const iota =iotaLibrary.composeAPI({provider: providerLink})//Modified functionsconst lengthModifier = (str) =>{ return str.substring( str.lastIndexOf(“{“),str.lastIndexOf(“}”)+1) }module.exports = {iotaLibrary : iotaLibrary ,converter : Converter ,provider:providerLink,depth:depth,minimumWeightMagnitude:minimumWeightMagnitude,Mam : Mam ,mamState:mamState,seed : seed ,sideKey:sideKey,lengthModifier:lengthModifier,iota : iota}
Upload data to MAM ‘pushData.js’
const Mam = require('@iota/mam')
const { asciiToTrytes, trytesToAscii } = require('@iota/converter')
const iotaGlobal = require('./IotaGlobal')
const pushData = async(_secretKey,_provider,packet) =>{
const trytes = asciiToTrytes(JSON.stringify(packet))
const message = Mam.create(iotaGlobal.mamState, trytes)//Important to update the state
// Save new mamState
iotaGlobal.mamState = message.state
// Attach the payload
await Mam.attach(message.payload, message.address, 3, 9)
return message.root
}
module.exports ={
execute:pushData}
Fetch data from MAM ‘fetchRoot.js’
const iotaGlobal = require('./IotaGlobal')const { asciiToTrytes, trytesToAscii } = require('@iota/converter')const fetchRoot = async(_root)=>{const resp = await iotaGlobal.Mam.fetch(_root, 'restricted', iotaGlobal.sideKey)const tryteMessages = resp.messagesconst asciiMessages = []for (let index = 0; index < tryteMessages.length; index++) {asciiMessages.push(iotaGlobal.converter.trytesToAscii(tryteMessages[index]))}return asciiMessages}module.exports = {execute:fetchRoot}
manageMam.js
//import fetch and push data to MAM functions
const fetchRoot = require('./fetchRoot')
const sendData = require('./pushData')
const send = async(data) =>{const root = await sendData.execute(data)return root}const fetch = async(root) =>{const data = await fetchRoot.execute(root)return data}
You can check documentation examples for restricted MAM here.
Now, we are done with all the basic modules. Let’s create our ‘index.js’.
The first three lines are to import the two IPFS modules and the MAM manager.
const addToIPFS = require(‘./actions/Functions/IPFS/add’)
const manageMAM = require(‘./actions/Functions/MAM/manageMAM’)
const catFromIPFS = require(‘./actions/Functions/IPFS/cat’)
Then, to create a database we will make a create function
const create = async(DBJSON)=>{const ipfsHash = await addToIPFS.execute(JSON.parse(JSON.stringify(DBJSON)))const root = await manageMAM.send(ipfsHash)return root}
Then to read data
const read = async(root)=>{const fetchIPFShash = await manageMAM.fetch(root)const fetchedIPFS = catFromIPFS.execute(getLastHash(fetchIPFShash))return fetchedIPFS}
Update data
const update = async(root,key,value)=>{const DB = await read(root)const DBjson = JSON.parse(DB)DBjson[key] = valueconst newRoot = await create(DBjson)return newRoot}
Delete data
const deleteRaw = async(root,key)=>{const DB = await read(root)const DBjson = JSON.parse(DB)const newRoot = await create(DBjson)return newRoot}
The whole project is included in this repository https://github.com/yehia67/IPFS-Tangle-Database-System/tree/master