@smartinvoicexyz/utils
Version:
Unified source for utility functions used across the Smart Invoice protocol.
162 lines (161 loc) • 5.22 kB
JavaScript
import { logDebug } from '@smartinvoicexyz/shared';
import bs58 from 'bs58';
import _ from 'lodash';
import { Cache } from 'memory-cache';
const { PINATA_JWT } = process.env;
const IPFS_ENDPOINTS = [
`https://ipfs.io/ipfs/`,
`https://cloudflare-ipfs.com/ipfs/`,
`https://dweb.link/ipfs/`,
`https://w3s.link/ipfs/`,
`https://flk-ipfs.xyz/ipfs/`,
];
const cache = new Cache();
export const fetchFromIPFS = async (cid) => {
if (!cid) {
throw new Error('CID is required');
}
const cachedResponse = cache.get(cid);
if (cachedResponse) {
return cachedResponse;
}
const controllers = IPFS_ENDPOINTS.map(() => new AbortController());
try {
const response = await Promise.any(IPFS_ENDPOINTS.map(async (endpoint, index) => {
const controller = controllers[index];
const { signal } = controller;
const res = await fetch(`${endpoint}${cid}`, { signal });
if (res.ok) {
// Abort other requests once a successful one is found
controllers.forEach((ctrl, i) => {
if (i !== index)
ctrl.abort();
});
const data = await res.json();
cache.put(cid, data);
return data;
}
throw new Error(`Failed to fetch from ${endpoint}`);
}));
return response;
}
catch (error) {
console.error(`Failed to fetch from IPFS for CID: ${cid}: `, error);
throw new Error(`Failed to fetch from IPFS for CID: ${cid}`);
}
};
const pinJson = async (data, metadata, token) => {
const pinataData = JSON.stringify({
pinataOptions: {
cidVersion: 0,
},
pinataMetadata: {
...metadata,
},
pinataContent: {
...data,
},
});
try {
const res = await fetch('https://api.pinata.cloud/pinning/pinJSONToIPFS', {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
method: 'POST',
body: pinataData,
});
const json = await res.json();
return _.get(json, 'IpfsHash');
}
catch (e) {
logDebug({ pinataData, token });
console.error("Couldn't pin data to pinata: ", e);
return null;
}
};
export const handleDetailsPin = async ({ details, name, token, }) => pinJson(details, { name }, token);
export const fetchToken = async (count = 0) => {
const token = await fetch('/api/upload-start', {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify({ count }),
})
.then(res => res.text())
.catch(e => {
console.error("Couldn't fetch token", e);
return null;
});
return token;
};
/**
* Converts IPFS CID version 0 (Base58) to a 32 bytes hex string and adds initial 0x.
* @param cid - The 46 character long IPFS CID V0 string (starts with Qm).
* @returns string
*/
export function convertIpfsCidV0ToByte32(cid) {
return `0x${Buffer.from(bs58.decode(cid).slice(2)).toString('hex')}`;
}
/**
* Converts 32 byte hex string (initial 0x is removed) to Base58 IPFS content identifier version 0 address string (starts with Qm)
* @param str - The 32 byte long hex string to encode to IPFS CID V0 (without initial 0x).
* @returns string
*/
export function convertByte32ToIpfsCidV0(str) {
let newStr = str;
if (str.indexOf('0x') === 0) {
newStr = str.slice(2);
}
return bs58.encode(Buffer.from(`1220${newStr}`, 'hex'));
}
export const generateApiKey = async (keyRestrictions) => {
if (!PINATA_JWT) {
throw new Error('PINATA_JWT env variable is not set');
}
const options = {
method: 'POST',
headers: {
accept: 'application/json',
'content-type': 'application/json',
authorization: `Bearer ${PINATA_JWT}`,
},
body: JSON.stringify(keyRestrictions),
};
return fetch('https://api.pinata.cloud/users/generateApiKey', options)
.then(response => response.json())
.then(json => {
if (!_.includes(_.keys(json), 'JWT'))
throw new Error('No JWT found');
const { JWT } = json;
return JWT;
})
.catch(error => {
console.error(`Failed to generate API key: ${error}`);
return null;
});
};
export const keyRestrictions = () => {
const date = new Date();
return {
keyName: `Signed Upload JWT-${date.toISOString()}`,
maxUses: 2,
permissions: {
endpoints: {
data: {
pinList: false,
userPinnedDataTotal: false,
},
pinning: {
pinFileToIPFS: true, // image
pinJSONToIPFS: true, // json
pinJobs: false,
unpin: true, // image (both?)
userPinPolicy: false,
},
},
},
};
};