UNPKG

@filecoin-renewable-energy-purchases/js-api

Version:

JS API for Filecoin Renewable Energy purchases

875 lines (757 loc) 29.3 kB
import { create } from 'ipfs-core' import { CID } from 'multiformats/cid' import { Octokit } from '@octokit/core' import axios from 'axios' import { Blob, Buffer } from 'buffer' import Papa from 'papaparse' import { delegatedPeerRouting } from '@libp2p/delegated-peer-routing' import { delegatedContentRouting} from '@libp2p/delegated-content-routing' import { kadDHT } from '@libp2p/kad-dht' import { gossipsub } from '@chainsafe/libp2p-gossipsub' import { create as createIpfsHttpClient } from 'kubo-rpc-client' import { multiaddr } from '@multiformats/multiaddr' import { webSockets } from '@libp2p/websockets' // Define "source of thruth" github repo and conventions const REPO = 'filecoin-renewables-purchases' const REPO_OWNER = 'protocol' const TRANSACTION_FOLDER = '_transaction_' const STEP_2_FILE_NAME_SUFFIX = '_step2_orderSupply.csv' const STEP_3_FILE_NAME_SUFFIX = '_step3_match.csv' const STEP_5_FILE_NAME_SUFFIX = '_step5_redemption_information.csv' const STEP_6_FILE_NAME_SUFFIX = '_step6_generationRecords.csv' const STEP_7_FILE_NAME_SUFFIX = '_step7_certificate_to_contract.csv' const ws = new webSockets() const ipfsClient = createIpfsHttpClient({ protocol: 'https', port: 5002, host: 'green.filecoin.space' }) // Init Octokit const octokit = new Octokit({ auth: `${process.env.github_personal_access_token}` }) export class RenewableEnergyPurchases { peers = [ '/dns4/green.filecoin.space/tcp/5004/wss/p2p/12D3KooWJmYbQp2sgKX22vZgSRVURkpMQ5YCSc8vf3toHesJc5Y9', '/dns4/web1.co2.storage/tcp/5004/wss/p2p/12D3KooWCPzmui9TSQQG8HTNcZeFiHz6AGS19aaCwxJdjykVqq7f', '/dns4/web2.co2.storage/tcp/5004/wss/p2p/12D3KooWFBCcWEDW9GYr9Aw8D2QL7hZakPAw1DGfeZCwfsrjd43b', '/dns4/proxy.co2.storage/tcp/5004/wss/p2p/12D3KooWGWHSrAxr6sznTpdcGuqz6zfQ2Y43PZQzhg22uJmGP9n1', ] // ipfsRepoName = './ipfs_repo_' + Math.random() ipfsRepoName = './.ipfs' ipfsNodeOpts = { config: { Addresses: { Delegates: [/* '/dns4/green.filecoin.space/tcp/5002/https', '/dns4/web1.co2.storage/tcp/5002/https', '/dns4/web2.co2.storage/tcp/5002/https', '/dns4/proxy.co2.storage/tcp/5002/https',*/ '/dns4/node0.delegate.ipfs.io/tcp/443/https', '/dns4/node1.delegate.ipfs.io/tcp/443/https', '/dns4/node2.delegate.ipfs.io/tcp/443/https', '/dns4/node3.delegate.ipfs.io/tcp/443/https' ] }, Bootstrap: [ '/dns4/green.filecoin.space/tcp/5004/wss/p2p/12D3KooWJmYbQp2sgKX22vZgSRVURkpMQ5YCSc8vf3toHesJc5Y9', '/dns4/web1.co2.storage/tcp/5004/wss/p2p/12D3KooWCPzmui9TSQQG8HTNcZeFiHz6AGS19aaCwxJdjykVqq7f', '/dns4/web2.co2.storage/tcp/5004/wss/p2p/12D3KooWFBCcWEDW9GYr9Aw8D2QL7hZakPAw1DGfeZCwfsrjd43b', '/dns4/proxy.co2.storage/tcp/5004/wss/p2p/12D3KooWGWHSrAxr6sznTpdcGuqz6zfQ2Y43PZQzhg22uJmGP9n1', ] }, libp2p: { transports: [ws], connectionManager: { autoDial: false }, config: { dht: { enabled: true, clientMode: true } }, contentRouting: [ delegatedContentRouting(ipfsClient) ], peerRouting: [ delegatedPeerRouting(ipfsClient) ], services: { dht: kadDHT(), pubsub: gossipsub() } } } ipfs = null ipfsStarting = false ipfsStarted = false contractsAndAllocationsKey = '/ipns/k51qzi5uqu5dlwhffqq4a8ksdtr14d3vckvhldpuxd68r84g3eqsjqgqdvxazc' certificatesAndAttestationsKey = '/ipns/k51qzi5uqu5dkllf259064y4qyr6ra1zk7u8qgigsoahwo04m0efnf88827ume' repo = REPO repoOwner = REPO_OWNER transactionFolder = TRANSACTION_FOLDER contractsFileNameSuffix = STEP_2_FILE_NAME_SUFFIX allocationsFileNameSuffix = STEP_3_FILE_NAME_SUFFIX redemptionsFileNameSuffix = STEP_5_FILE_NAME_SUFFIX attestationsFileNameSuffix = STEP_6_FILE_NAME_SUFFIX certificatesFileNameSuffix = STEP_7_FILE_NAME_SUFFIX // Constructor constructor(options) { if(!options) return if(options.ipfsRepoName != undefined) this.ipfsRepoName = options.ipfsRepoName if(options.ipfsNodeOpts != undefined) this.ipfsNodeOpts = Object.assign(this.ipfsNodeOpts, options.ipfsNodeOpts) if(options.contractsAndAllocationsKey != undefined) this.contractsAndAllocationsKey = options.contractsAndAllocationsKey if(options.certificatesAndAttestationsKey != undefined) this.certificatesAndAttestationsKey = options.certificatesAndAttestationsKey if(options.repoOwner != undefined) this.repoOwner = options.repoOwner if(options.repo != undefined) this.repo = options.repo if(options.transactionFolder != undefined) this.transactionFolder = options.transactionFolder if(options.contractsFileNameSuffix != undefined) this.contractsFileNameSuffix = options.contractsFileNameSuffix if(options.allocationsFileNameSuffix != undefined) this.allocationsFileNameSuffix = options.allocationsFileNameSuffix if(options.redemptionsFileNameSuffix != undefined) this.redemptionsFileNameSuffix = options.redemptionsFileNameSuffix if(options.attestationsFileNameSuffix != undefined) this.attestationsFileNameSuffix = options.attestationsFileNameSuffix if(options.certificatesFileNameSuffix != undefined) this.certificatesFileNameSuffix = options.certificatesFileNameSuffix } // Simple sleep function sleep = ms => new Promise(r => setTimeout(r, ms)) // Create IPFS node and connect swarm peers async startIpfs() { const that = this this.ipfsStarting = true this.ipfsStarted = false let ipfsOpts = {} ipfsOpts = Object.assign({ repo: this.ipfsRepoName }, this.ipfsNodeOpts) this.ipfs = await create(ipfsOpts) const config = await this.ipfs.config.getAll() const hostPeerId = config.Identity.PeerID for (const peer of this.peers) { if(peer.indexOf(hostPeerId) == -1) try { const ma = multiaddr(peer) await this.ipfs.bootstrap.add(ma) await this.ipfs.swarm.connect(ma) } catch (error) { console.log(peer, error) } } this.ipfsStarted = true this.ipfsStarting = false return this.ipfs } // Stop IPFS node async stopIpfs() { if(this.ipfs != null) { await this.ipfs.stop() this.ipfs = null this.ipfsStarted = false this.ipfsStarting = false } } // Run to ensure IPFS node will start eventually async ensureIpfsIsRunning() { if(!this.ipfsStarted && !this.ipfsStarting) { this.ipfs = await this.startIpfs() } else if(!this.ipfsStarted) { while(!this.ipfsStarted) { await this.sleep(1000) } } return this.ipfs } // Resolve IPNS name/key async resolveIpnsName(id) { let cid for await (const value of await this.ipfs.dht.get(id)) { if(!value.error && value.name.toLowerCase() == 'value') { const resp = new TextDecoder().decode(value.value) const cidStartIndex = resp.indexOf('/ipfs/') + 6 const cidEndIndex = cidStartIndex + 59 cid = resp.substring(cidStartIndex, cidEndIndex) break } } return cid } // Retrieve DAG object from IPNS async getDagFromIPNS(id) { const hash = (await this.resolveIpnsName(id)).replace('/ipfs/', '') // Create CID const cid = CID.parse(hash) return await this.getDag(cid) } // Retrieve DAG object from the content address async getDag(cid) { let dag // Grab DAG dag = await this.ipfs.dag.get(cid, {}) return { cid: cid, dag: dag.value } } // Get accolations and contracts objects from IPNS/IPFS async getContractsAndAllocations() { if(!this.ipfs) await this.ensureIpfsIsRunning() return (await this.getDag((await this.getDagFromIPNS(this.contractsAndAllocationsKey)).dag.transactions_cid)).dag } // Get certificates and attestations objects from IPNS/IPFS async getCertificatesAndAttestations() { if(!this.ipfs) await this.ensureIpfsIsRunning() return (await this.getDag((await this.getDagFromIPNS(this.certificatesAndAttestationsKey)).dag.deliveries_cid)).dag } // Get contents of base repo directory async getGithubRepoContents() { const repoItems = await octokit.request('GET /repos/{owner}/{repo}/contents', { owner: this.repoOwner, repo: this.repo }) if(repoItems.status != 200) return new Promise((resolve, reject) => { reject(repoItems) }) return new Promise((resolve, reject) => { resolve(repoItems) }) } // Get all transactions folders from Github repo async getTransactionsFolders() { let repoItems try { repoItems = await this.getGithubRepoContents() } catch (error) { return new Promise((resolve, reject) => { reject(error) }) } // Search through the base repo directory for folders containing TRANSACTION_FOLDER in its name return new Promise((resolve, reject) => { resolve(repoItems.data.filter((item) => { return item.name.indexOf(this.transactionFolder) > -1 && item.type == 'dir' })) }) } // Get content from URI async getUriContent(getUri, headers, responseType) { return axios(getUri, { method: 'get', headers: headers, responseType: responseType }) } unicodeDecodeB64(str) { return decodeURIComponent(atob(str)); } // Get raw content from Github repo async getRawFromGithub(path, fileName, type, contentType) { const uri = `https://api.github.com/repos/${this.repoOwner}/${this.repo}/contents/${path}/${fileName}` const headers = { 'Authorization': `Bearer ${process.env.github_personal_access_token}`, 'X-GitHub-Api-Version': '2022-11-28' } let responseType switch (type) { case 'arraybuffer': responseType = 'arraybuffer' break default: responseType = null break } const resp = await this.getUriContent(uri, headers, responseType) if(resp.status != 200) return new Promise((resolve, reject) => { reject(resp) }) switch (type) { case 'csv': const csv = this.unicodeDecodeB64(resp.data.content) let rows = [] return new Promise((resolve, reject) => { Papa.parse(csv, { worker: true, header: true, dynamicTyping: true, comments: "#", step: (row) => { rows.push(row.data) }, complete: () => { resolve(rows) } }) }) case 'arraybuffer': return new Promise((resolve, reject) => { let content = [] content.push(resp.data.content) // const blob = new Blob(content, {type: contentType}) const blob = new Buffer.from(content) resolve(blob) }) default: return new Promise((resolve, reject) => { resolve(resp.data.content) }) } } async getContractsAndAllocationsFromGithub() { let contracts = {} let allocations = {} let transactions = {} let transactionFolders try { transactionFolders = await this.getTransactionsFolders() } catch (error) { return new Promise((resolve, reject) => { reject(error) }) } for (const transactionFolder of transactionFolders) { // Get contents of transactions directory const transactionFolderItems = await octokit.request('GET /repos/{owner}/{repo}/contents/{path}', { owner: this.repoOwner, repo: this.repo, path: transactionFolder.path }) if(transactionFolderItems.status != 200) return new Promise((resolve, reject) => { reject(transactionFolderItems) }) // Search for contracts CSV file const contractsCsvFileName = transactionFolder.name + this.contractsFileNameSuffix // Check if contracts CSV file is present in the folder const contractsCsvFile = transactionFolderItems.data.filter((item) => { return item.name == contractsCsvFileName && item.type == 'file' }) if(contractsCsvFile.length != 1) // not valid TRANSACTION_FOLDER continue // Get CSV content (contracts for this specific order) contracts[transactionFolder.name] = await this.getRawFromGithub(transactionFolder.path, contractsCsvFileName, 'csv') // Search through allocations CSV file for match const allocationsCsvFileName = transactionFolder.name + this.allocationsFileNameSuffix // Check if CSV file is present in the folder const allocationsCsvFile = transactionFolderItems.data.filter((item) => { return item.name == allocationsCsvFileName && item.type == 'file' }) if(allocationsCsvFile.length != 1) // No allocation file found continue // Get CSV content (allocations for this specific order) allocations[transactionFolder.name] = await this.getRawFromGithub(transactionFolder.path, allocationsCsvFileName, 'csv') // Delete mutable columns and at same create DAG structures for allocations for (let allocation of allocations[transactionFolder.name]) { // Check if there is valid CSV line if(!allocation.contract_id) continue // Delete mutable columns delete allocation.step4_ZL_contract_complete delete allocation.step5_redemption_data_complete delete allocation.step6_attestation_info_complete delete allocation.step7_certificates_matched_to_supply delete allocation.step8_IPLDrecord_complete delete allocation.step9_transaction_complete delete allocation.step10_volta_complete delete allocation.step11_finalRecord_complete // Make sure MWh are Numbers if(typeof allocation.volume_MWh == "string") { allocation.volume_MWh = allocation.volume_MWh.replace(',', '') allocation.volume_MWh = allocation.volume_MWh.trim() allocation.volume_MWh = Number(allocation.volume_MWh) } } // Delete mutable columns and at same create DAG structures for contracts for (let contract of contracts[transactionFolder.name]) { // Delete mutable columns delete contract.step2_order_complete delete contract.step3_match_complete delete contract.step4_ZL_contract_complete delete contract.step5_redemption_data_complete delete contract.step6_attestation_info_complete delete contract.step7_certificates_matched_to_supply delete contract.step8_IPLDrecord_complete delete contract.step9_transaction_complete delete contract.step10_volta_complete delete contract.step11_finalRecord_complete // Make sure MWh are Numbers if(typeof contract.volume_MWh == "string") { contract.volume_MWh = contract.volume_MWh.replace(',', '') contract.volume_MWh = contract.volume_MWh.trim() contract.volume_MWh = Number(contract.volume_MWh) } // Add links to demands contract.allocations = allocations[transactionFolder.name].filter((allocation)=>{ return allocation.contract_id == contract.contract_id }) } // Create transaction object const transaction = { name: transactionFolder.name, contracts: contracts[transactionFolder.name], allocations: allocations[transactionFolder.name] } // Add allocations to transaction object for this transaction transactions[transactionFolder.name] = transaction } return new Promise((resolve, reject) => { resolve(transactions) }) } async getAttestationsAndCertificatesFromGithub() { let redemptions = {} let attestations = {} let certificates = {} let deliveries = {} let transactionFolders try { transactionFolders = await this.getTransactionsFolders() } catch (error) { return new Promise((resolve, reject) => { reject(error) }) } for (const transactionFolder of transactionFolders) { // Get contents of transactions directory const transactionFolderItems = await octokit.request('GET /repos/{owner}/{repo}/contents/{path}', { owner: this.repoOwner, repo: this.repo, path: transactionFolder.path }) if(transactionFolderItems.status != 200) return new Promise((resolve, reject) => { reject(transactionFolderItems) }) // Search for redemption CSV file const redemptionsCsvFileName = transactionFolder.name + this.redemptionsFileNameSuffix // Check if CSV file is present in the folder const redemptionsCsvFile = transactionFolderItems.data.filter((item) => { return item.name == redemptionsCsvFileName && item.type == 'file' }) if(redemptionsCsvFile.length != 1) continue // Get CSV content (redemptions for this specific order) redemptions[transactionFolder.name] = await this.getRawFromGithub(transactionFolder.path, redemptionsCsvFileName, 'csv') // Theoretically we should search folder path with each attestation // but since all redemptions point to the same folder let take it from line 1 if(redemptions[transactionFolder.name].length == 0) { console.error(`Empty redemptions file ${redemptionsCsvFileName}`) continue } const attestationFolder = redemptions[transactionFolder.name][0].attestation_folder if(!attestationFolder || !attestationFolder.length) { console.error(`Invalid attestation folder specified in ${redemptionsCsvFileName}`) continue } // Look for attestation folder and its contents let attestationFolderItems try { attestationFolderItems = await octokit.request('GET /repos/{owner}/{repo}/contents/{path}', { owner: this.repoOwner, repo: this.repo, path: attestationFolder }) } catch (error) { console.error(error) continue } if(attestationFolderItems.status != 200) { console.error(attestationFolderItems) continue } // Search for attestations CSV file const attestationsCsvFileName = attestationFolder + this.attestationsFileNameSuffix // Check if CSV file is present in the folder const attestationsCsvFile = attestationFolderItems.data.filter((item) => { return item.name == attestationsCsvFileName && item.type == 'file' }) if(attestationsCsvFile.length != 1) { console.error(`No attestation file ${attestationsCsvFileName} exists in ${attestationFolder}`) continue } // Get CSV content (acctually attestations and attestations) attestations[attestationFolder] = await this.getRawFromGithub(attestationFolder, attestationsCsvFileName, 'csv') // Search for match / supply CSV file const certificatesCsvFileName = attestationFolder + this.certificatesFileNameSuffix // Check if CSV file is present in the folder const certificatesCsvFile = attestationFolderItems.data.filter((item) => { return item.name == certificatesCsvFileName && item.type == 'file' }) if(certificatesCsvFile.length != 1) { console.error(`No certificates file ${certificatesCsvFileName} exists in ${attestationFolder}`) continue } // Get CSV content (certificates) certificates[attestationFolder] = await this.getRawFromGithub(attestationFolder, certificatesCsvFileName, 'csv') // Itterate over attestations and associate certificates and allocations for (let attestation of attestations[attestationFolder]) { const allocations = certificates[attestationFolder].filter((certificate)=>{ return certificate.certificate == attestation.certificate }) attestation.allocations = allocations } // Create delivery object const delivery = { name: transactionFolder.name, attestations: attestations[attestationFolder], "certificate-allocations": certificates[attestationFolder] } // Add allocations to transaction object for this transaction deliveries[transactionFolder.name] = delivery } return new Promise((resolve, reject) => { resolve(deliveries) }) } async getAllAllocationsFromGithub() { let contracts = {} let allocations = {} let allAllocations = [] let transactionFolders try { transactionFolders = await this.getTransactionsFolders() } catch (error) { return new Promise((resolve, reject) => { reject(error) }) } for (const transactionFolder of transactionFolders) { // Get contents of transactions directory const transactionFolderItems = await octokit.request('GET /repos/{owner}/{repo}/contents/{path}', { owner: this.repoOwner, repo: this.repo, path: transactionFolder.path }) if(transactionFolderItems.status != 200) return new Promise((resolve, reject) => { reject(transactionFolderItems) }) // Search for contracts CSV file const contractsCsvFileName = transactionFolder.name + this.contractsFileNameSuffix // Check if contracts CSV file is present in the folder const contractsCsvFile = transactionFolderItems.data.filter((item) => { return item.name == contractsCsvFileName && item.type == 'file' }) if(contractsCsvFile.length != 1) // not valid TRANSACTION_FOLDER continue // Get CSV content (contracts for this specific order) contracts[transactionFolder.name] = await this.getRawFromGithub(transactionFolder.path, contractsCsvFileName, 'csv') // Search through allocations CSV file for match const allocationsCsvFileName = transactionFolder.name + this.allocationsFileNameSuffix // Check if CSV file is present in the folder const allocationsCsvFile = transactionFolderItems.data.filter((item) => { return item.name == allocationsCsvFileName && item.type == 'file' }) if(allocationsCsvFile.length != 1) // No allocation file found continue // Get CSV content (allocations for this specific order) allocations[transactionFolder.name] = await this.getRawFromGithub(transactionFolder.path, allocationsCsvFileName, 'csv') // Delete mutable columns and at same create DAG structures for allocations for (let allocation of allocations[transactionFolder.name]) { // Check if there is valid CSV line if(!allocation.contract_id) continue // Delete mutable columns delete allocation.step4_ZL_contract_complete delete allocation.step5_redemption_data_complete delete allocation.step6_attestation_info_complete delete allocation.step7_certificates_matched_to_supply delete allocation.step8_IPLDrecord_complete delete allocation.step9_transaction_complete delete allocation.step10_volta_complete delete allocation.step11_finalRecord_complete // Make sure MWh are Numbers if(typeof allocation.volume_MWh == "string") { allocation.volume_MWh = allocation.volume_MWh.replace(',', '') allocation.volume_MWh = allocation.volume_MWh.trim() allocation.volume_MWh = Number(allocation.volume_MWh) } // Add contract fields to the record const contractFilter = contracts[transactionFolder.name].filter((c)=>{ return c.contract_id == allocation.contract_id }) if(contractFilter.length != 1) return new Promise((resolve, reject) => { reject(`An allocation must refer to exactly one contract. ${allocation.allocation_id} referes to ${contractFilter.length} contracts.`) }) let contract = JSON.parse(JSON.stringify(contractFilter[0])) // Delete mutable columns delete contract.step2_order_complete delete contract.step3_match_complete delete contract.step4_ZL_contract_complete delete contract.step5_redemption_data_complete delete contract.step6_attestation_info_complete delete contract.step7_certificates_matched_to_supply delete contract.step8_IPLDrecord_complete delete contract.step9_transaction_complete delete contract.step10_volta_complete delete contract.step11_finalRecord_complete // Make sure MWh are Numbers if(typeof contract.volume_MWh == "string") { contract.volume_MWh = contract.volume_MWh.replace(',', '') contract.volume_MWh = contract.volume_MWh.trim() contract.volume_MWh = Number(contract.volume_MWh) } // Rename contract volume_MWh and allocation volume_MWh delete Object.assign(contract, {contract_volume_MWh: contract.volume_MWh }).volume_MWh delete Object.assign(allocation, {allocation_volume_MWh: allocation.volume_MWh }).volume_MWh // Itterate over contract object and add its propoerties to allocation object allocation = Object.assign(allocation, contract) // Add allocation to allAllocations allAllocations.push(allocation) } } return new Promise((resolve, reject) => { resolve(allAllocations) }) } async getAllCertificateAllocationsFromGithub() { let redemptions = {} let attestations = {} let certificates = {} let allCertificates = [] let transactionFolders try { transactionFolders = await this.getTransactionsFolders() } catch (error) { return new Promise((resolve, reject) => { reject(error) }) } for (const transactionFolder of transactionFolders) { // Get contents of transactions directory const transactionFolderItems = await octokit.request('GET /repos/{owner}/{repo}/contents/{path}', { owner: this.repoOwner, repo: this.repo, path: transactionFolder.path }) if(transactionFolderItems.status != 200) return new Promise((resolve, reject) => { reject(transactionFolderItems) }) // Search for redemption CSV file const redemptionsCsvFileName = transactionFolder.name + this.redemptionsFileNameSuffix // Check if CSV file is present in the folder const redemptionsCsvFile = transactionFolderItems.data.filter((item) => { return item.name == redemptionsCsvFileName && item.type == 'file' }) if(redemptionsCsvFile.length != 1) continue // Get CSV content (redemptions for this specific order) redemptions[transactionFolder.name] = await this.getRawFromGithub(transactionFolder.path, redemptionsCsvFileName, 'csv') // Theoretically we should search folder path with each attestation // but since all redemptions point to the same folder let take it from line 1 if(redemptions[transactionFolder.name].length == 0) { console.error(`Empty redemptions file ${redemptionsCsvFileName}`) continue } const attestationFolder = redemptions[transactionFolder.name][0].attestation_folder if(!attestationFolder || !attestationFolder.length) { console.error(`Invalid attestation folder specified in ${redemptionsCsvFileName}`) continue } // Look for attestation folder and its contents let attestationFolderItems try { attestationFolderItems = await octokit.request('GET /repos/{owner}/{repo}/contents/{path}', { owner: this.repoOwner, repo: this.repo, path: attestationFolder }) } catch (error) { console.error(error) continue } if(attestationFolderItems.status != 200) { console.error(attestationFolderItems) continue } // Search for attestations CSV file const attestationsCsvFileName = attestationFolder + this.attestationsFileNameSuffix // Check if CSV file is present in the folder const attestationsCsvFile = attestationFolderItems.data.filter((item) => { return item.name == attestationsCsvFileName && item.type == 'file' }) if(attestationsCsvFile.length != 1) { console.error(`No attestation file ${attestationsCsvFileName} exists in ${attestationFolder}`) continue } // Get CSV content (acctually attestations and attestations) attestations[attestationFolder] = await this.getRawFromGithub(attestationFolder, attestationsCsvFileName, 'csv') // Search for match / supply CSV file const certificatesCsvFileName = attestationFolder + this.certificatesFileNameSuffix // Check if CSV file is present in the folder const certificatesCsvFile = attestationFolderItems.data.filter((item) => { return item.name == certificatesCsvFileName && item.type == 'file' }) if(certificatesCsvFile.length != 1) { console.error(`No certificates file ${certificatesCsvFileName} exists in ${attestationFolder}`) continue } // Get CSV content (certificates) certificates[attestationFolder] = await this.getRawFromGithub(attestationFolder, certificatesCsvFileName, 'csv') // Find appertain attestation record and join it for (let certificate of certificates[attestationFolder]) { const attestationsFilter = attestations[attestationFolder].filter((a)=>{ return a.certificate == certificate.certificate }) if(attestationsFilter.length != 1) return new Promise((resolve, reject) => { reject(`An certificate must be found in attestation. ${certificate.certificate} referes to ${attestationsFilter.length} attestations.`) }) let attestation = JSON.parse(JSON.stringify(attestationsFilter[0])) // Add also attestation folder for the output attestation.attestation_folder = attestationFolder // Find batchID from redemptions const redemptionsFilter = redemptions[transactionFolder.name].filter((r)=>{ return r.attestation_id == attestation.attestation_id }) if(redemptionsFilter.length != 1) return new Promise((resolve, reject) => { reject(`A attestation must be found in redemoptions. ${attestation.attestation_id} referes to ${redemptionsFilter.length} redemptions.`) }) const batchID = redemptionsFilter[0].batchID // Add batch Id to attestation record attestation.batchID = batchID // Rename attestation volume_Wh and allocation/certificate volume_MWh delete Object.assign(attestation, {attestation_volume_Wh: attestation.volume_Wh }).volume_Wh delete Object.assign(certificate, {allocation_volume_MWh: certificate.volume_MWh }).volume_MWh // Itterate over attestation object and add its propoerties to certificate object certificate = Object.assign(certificate, attestation) // Add allocation to allAllocations allCertificates.push(certificate) } } return new Promise((resolve, reject) => { resolve(allCertificates) }) } }