Created
July 11, 2024 11:54
-
-
Save zoernert/39548c0a3a86ba08dad6ccf5625663c7 to your computer and use it in GitHub Desktop.
Sample Usage of Cori-Wallet (Tracking of Scope2 footprint data derived from smart meter readings)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const coriwallet = new CoriWallet("https://app.gruenstromindex.de/assets/js/deployment.json"); | |
await coriwallet.waitInit(); | |
console.log("Your Wallet:",coriwallet.wallet.address); | |
// add a Meter to account | |
await coriwallet.addTracker({ | |
zip: '69256', | |
ownerId: coriwallet.wallet.address, | |
name: 'TestMeter', | |
reading: 1234, // Initial Meter Reading in Wh | |
iat: Math.round(new Date().getTime() / 1000), | |
type: "consumption" | |
}); | |
// Get all meters of account | |
const trackers = await coriwallet.getTrackers(); | |
console.log('First Meter List with Data',trackers); | |
// Update Reading of first meter | |
const updateDID = JSON.parse(JSON.stringify(trackers[0])).did; | |
await coriwallet.updateTracker(updateDID,2000); | |
// Retrieve last by refreshing meters list | |
const trackers2 = await coriwallet.getTrackers(); | |
console.log('Second Meter List with Data',trackers2); | |
// Securization (mint tokens) | |
const secureDID = JSON.parse(JSON.stringify(trackers[0])).did; | |
coriwallet.securitization(secureDID,function(progress) { | |
console.log(progress+"%"); | |
}); | |
// By now the users wallet should have received tokens for the emission and consumption. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class CoriWallet { | |
/** | |
* Constructor for CoriWallet class. | |
* Will first ensure that a deployment configuration is available to initialize wallet. | |
* Uses window.localStorage if present to cache deployment configuration. | |
* | |
* @param {deploymentURL} deploymentURL - (optional) Deployment configuration defaults to https://app.gruenstromindex.de/assets/js/deployment.json | |
* @param {privateKey} privateKey - (optional) The private key for wallet management. | |
*/ | |
constructor(deploymentURL,privateKey,cbAlert) { | |
this.isInsecure = true; | |
this.cbAlert = cbAlert; | |
if((typeof deploymentURL == 'undefined') || (deploymentURL == null)) deploymentURL = "https://app.gruenstromindex.de/assets/js/deployment.json"; | |
this.deployment = null; | |
if(typeof window !== 'undefined') { | |
this.deployment = window.localStorage.getItem("deployment"); | |
if((typeof this.deployment !== 'undefined') && (this.deployment !== null)) { | |
this.deployment = JSON.parse(this.deployment); | |
} | |
} | |
if(this.deployment !== null) { | |
this._initWallet(privateKey); | |
} else { | |
const parent = this; | |
fetch(deploymentURL) | |
.then(response => response.json()) | |
.then(data => { | |
parent.deployment = data; | |
if(typeof window !== 'undefined') window.localStorage.setItem("deployment",JSON.stringify(parent.deployment)); | |
parent._initWallet(privateKey); | |
}) | |
.catch(error => { | |
console.error('Unable to fetch deployment configuration:', error); | |
}); | |
} | |
} | |
/** | |
* Will use privateKey if pressent to initialize an Ethers JS based wallet object. | |
* If not present it will try to use Metamask (web3 Provider) to get wallet object. | |
* Fallback is light browser wallet with storage of private key in local storage. | |
*/ | |
_initWallet = async (privateKey) => { | |
this.isNewWallet = false; | |
if((typeof window == 'undefined') && ((typeof privateKey == "undefined") || (privateKey == null))) { | |
throw new Error("Unable to manage private key. Either private key needs to be specified of metamask/window object present."); | |
} | |
this.privateKey = privateKey; | |
// Check if privateKey got injected during instanziation | |
if((typeof privateKey !== 'undefined')&&(privateKey!==null)) { | |
this.provider = new ethers.providers.JsonRpcProvider(this.deployment.RPC); | |
this.wallet = new ethers.Wallet(privateKey, this.provider); | |
} else { | |
/** | |
* The function first checks if the window object is defined (i.e., it's running in a browser environment). | |
* If it is, it retrieves the value of the "deviceKey" item from the browser's localStorage. | |
* If the value is undefined or null, it generates a new random Ethereum wallet using ethers.Wallet.createRandom() | |
* function and sets the parent.isNewWallet property to true. | |
* It then stores the private key of the generated wallet in the localStorage under the "deviceKey" key. | |
* Finally, it creates a new ethers.Wallet object using the retrieved or generated private key and the | |
* parent.provider object. | |
* If the window object is defined, it also logs a warning message to the console indicating that an | |
* insecure browser wallet is being used and suggests using Metamask instead. | |
*/ | |
const initBrowserwallet = async function(parent) { | |
parent.provider = new ethers.providers.JsonRpcProvider(parent.deployment.RPC); | |
let privateKey = null; | |
if(typeof window !== 'undefined') { | |
privateKey = window.localStorage.getItem("deviceKey"); | |
} | |
if((typeof privateKey == 'undefined') || (privateKey == null)) { | |
const wallet = ethers.Wallet.createRandom(); | |
parent.isNewWallet = true; | |
privateKey = wallet.privateKey; | |
if(typeof window !== 'undefined') window.localStorage.setItem("deviceKey",privateKey); | |
} | |
parent.wallet = new ethers.Wallet(privateKey, parent.provider); | |
if(typeof window !== 'undefined') console.warn("Using insecure browser wallet. Consider using Metamask."); | |
} | |
/** | |
* This function initializes Metamask by requesting user accounts, creating a Web3Provider with Metamask, | |
* getting the signer, setting the provider and wallet on the parent object, and handling account changes | |
* by reloading the page. | |
* If initialization fails, it logs an error and falls back to initializing a browser wallet using | |
*/ | |
const initMetamask = async function(parent) { | |
try { | |
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }); | |
const provider = new ethers.providers.Web3Provider(window.ethereum); | |
const signer = provider.getSigner(); | |
parent.provider = provider; | |
parent.wallet = signer; | |
parent.wallet.address = await signer.getAddress(); | |
window.ethereum.on('accountsChanged', function (accounts) { | |
if(typeof window !== 'undefined') window.localStorage.clear(); | |
if(typeof location !== 'undefined') location.reload(); | |
}); | |
parent.isInsecure = false; | |
} catch(e) { | |
console.log("initMetamask failed",e); | |
initBrowserwallet(parent); | |
} | |
} | |
/** | |
* Checks if Metamask (Web3Provider) is useable | |
*/ | |
if((typeof window !== 'undefined') && (window.ethereum)) { | |
initMetamask(this); | |
} else { | |
initBrowserwallet(this); | |
} | |
} | |
} | |
/** | |
* Alert Handler as Callback for UIs | |
* @param {*} error | |
*/ | |
alert = function(error) { | |
if(typeof this.cbAlert !== 'undefined') { | |
this.cbAlert(error); | |
} | |
} | |
/** | |
* Helper function to wait until wallet is initialized after constructor call | |
* | |
*/ | |
waitInit = async() => { | |
while(typeof this.wallet == 'undefined') { | |
await new Promise(r => setTimeout(r, 1000)); | |
} | |
return; | |
} | |
/** | |
* Signs a JSON object using this wallet by adding a parameter `sig` with a signature. | |
* | |
* @param {*} json | |
* @returns input json with additional parameter sig | |
*/ | |
signJSON = async (json) => { | |
json.sig = await this.wallet.signMessage(JSON.stringify(json)); | |
return json | |
} | |
/** | |
* Uses blockchain based IPFS Registry to getHash for a given account and retrieves content | |
* | |
* @param {*} account | |
* @returns will return JSON object after retrieval or null | |
*/ | |
retrieveJSON = async (account) => { | |
if(typeof account === "undefined") return null; | |
if((account.length !== 42) || (account == '0x0000000000000000000000000000000000000000')) return null; | |
const lookupService = new ethers.Contract(this.deployment.IPFSRegistry.account, this.deployment.IPFSRegistry.ABI, this.wallet.provider); | |
const rfilter = lookupService.filters.Announce(account, account, null); | |
const blkHigh = await this.wallet.provider.getBlockNumber(); | |
const batchSize = 1024; | |
let blkLow = blkHigh - batchSize; | |
if (blkLow < 1) bloLow = 1; | |
let hash = null; | |
while((hash == null) && (blkLow > 1)) { | |
const rlogs = await lookupService.queryFilter(rfilter,blkLow,blkLow + batchSize); | |
if(rlogs.length >0 ) { | |
rlogs.reverse(); | |
hash = rlogs[0].args.hash; | |
blkLow -= batchSize | |
} | |
if (blkLow < 1) bloLow = 1; | |
} | |
if(hash !== null) { | |
const json = await _getImutable("index.json",hash); | |
return json; | |
} else { | |
return null; | |
} | |
} | |
/** | |
* Returns list of GrünstromTrackers available at the middleware of | |
* Operator (Dienstleister der Datenverarbeitung) | |
*/ | |
getTrackers = async () => { | |
const parent = this; | |
if(typeof parent.delayGetTrackers == 'undefined') parent.delayGetTrackers = 0; | |
const url = this.deployment.REST_API + '/trackers'; | |
let startData = { | |
request: "Sources", | |
account: this.wallet.address, | |
iat: Math.round(new Date().getTime() / 1000) | |
}; | |
let sourcesSig = null; | |
if(typeof window !== null) { | |
let storedSig = window.localStorage.getItem("sources_sig"); | |
if(storedSig !== null) { | |
storedSig = JSON.parse(storedSig); | |
startData.iat = storedSig.iat; | |
sourcesSig = storedSig.sig; | |
} | |
} | |
if(sourcesSig == null) { | |
sourcesSig = await this.signJSON(startData); | |
if(typeof window !== 'undefined') window.localStorage.setItem("sources_sig",JSON.stringify({sig:sourcesSig,iat:startData.iat})); | |
} | |
const doFetch = () => { | |
return new Promise( (resolve) => { | |
fetch(url, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
'x-account': parent.wallet.address | |
}, | |
body: JSON.stringify(sourcesSig) | |
}) | |
.then(response => { | |
if (!response.ok) { | |
if(response.status == 429) { | |
parent.alert({err:"REST API Rate Limit reached",code:429,token:''+parent.wallet.address}) | |
return {}; | |
} else { | |
parent.alert({err:response.statusText,code:response.status}); | |
} | |
} else return response.json(); | |
}) | |
.then(data => { | |
parent.delayGetTrackers = 0; | |
resolve(data); | |
}).catch(function(e) { | |
parent.alert({err:'Fetch trackers failed',code:-5,exception:e}); | |
parent.delayGetTrackers += 500; | |
if(parent.delayGetTrackers > 20000) parent.delayGetTrackers +=20000; | |
if(parent.delayGetTrackers > 900000) parent.delayGetTrackers = 10000; | |
setTimeout(function() { doFetch() ; }, parent.delayGetTrackers); | |
}); | |
}); | |
} | |
return await doFetch(); | |
} | |
/** | |
* Returns the NFT for given ercAddress | |
*/ | |
getNFT = async function(erc20Address) { | |
const parent = this; | |
const contractAddress = parent.deployment.HKNfactory[erc20Address]; | |
const hknfactory = new ethers.Contract(contractAddress, parent.deployment.HKNfactory.ABI, parent.wallet.provider); | |
const coriNFT = hknfactory.nft(); | |
return coriNFT; | |
} | |
/** | |
* Adds tracker to list of available trackers manually at the | |
* middleware of Operator (Dienstleister der Datenverarbeitung) | |
*/ | |
addTracker = function(startData) { | |
const parent = this; | |
return new Promise( async (resolve) => { | |
const url = this.deployment.REST_API + '/addTracker'; | |
fetch(url, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
'x-account': parent.wallet.address | |
}, | |
body: JSON.stringify(await parent.signJSON(startData)) | |
}) | |
.then(response => { | |
if (!response.ok) { | |
if(response.status == 429) { | |
parent.alert({err:"REST API Rate Limit reached",code:429}) | |
return {}; | |
} else { | |
parent.alert({err:response.statusText,code:response.status}); | |
} | |
} else return response.json(); | |
}) | |
.then(async data => { | |
resolve(data); | |
}); | |
}); | |
} | |
/** | |
* Updates meter reading manually at the | |
* middleware of Operator (Dienstleister der Datenverarbeitung) | |
*/ | |
updateTracker = function(updateDID,reading) { | |
const parent = this; | |
return new Promise( async (resolve) => { | |
const url = this.deployment.REST_API + '/reading'; | |
fetch(url, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
'x-account': parent.wallet.address | |
}, | |
body: JSON.stringify(await parent.signJSON({did:updateDID,reading:reading})) | |
}) | |
.then(response => { | |
if (!response.ok) { | |
if(response.status == 429) { | |
parent.alert({err:"REST API Rate Limit reached",code:429}) | |
return {}; | |
} else { | |
parent.alert({err:response.statusText,code:response.status}); | |
} | |
} else return response.json(); | |
}) | |
.then(data2 => { | |
resolve(data2); | |
}); | |
}); | |
} | |
/** | |
* Retrieve balanceOf for given token (alias) | |
*/ | |
balanceOf = async function(tknalias) { | |
const tkn = new ethers.Contract( this.deployment.account[tknalias], this.deployment.ABI, this.wallet.provider); | |
const balance = (await tkn.balanceOf(this.wallet.address)).toString() * 1; | |
return balance; | |
} | |
/** | |
* Gives transaction log of all tokens in system | |
*/ | |
tokenTransactions = async function() { | |
const parent = this; | |
let events = []; | |
for (const [key, value] of Object.entries(parent.deployment.account)) { | |
const tkn = new ethers.Contract( value, this.deployment.ABI, this.wallet.provider); | |
const filter_to = tkn.filters.Transfer(null,this.wallet.address,null); | |
const logs_to = await tkn.queryFilter(filter_to); | |
events = events.concat(logs_to); | |
const filter_from = tkn.filters.Transfer(this.wallet.address,null,null); | |
const logs_from = await tkn.queryFilter(filter_from); | |
events = events.concat(logs_from); | |
} | |
return events; | |
} | |
/** | |
* Retrieves a list of all GSNs (GrünstromNachweise) of account | |
*/ | |
ownedHKN = async function(updateCB,startBlock) { | |
const parent = this; | |
const retrieveByType = async function(ercAddress) { | |
const contractAddress = parent.deployment.HKNfactory[ercAddress]; | |
const hknfactory = new ethers.Contract(contractAddress, parent.deployment.HKNfactory.ABI, parent.wallet.provider); | |
const birth = (await hknfactory.birthBlock(parent.wallet.address)).toString() * 1; | |
const toFilter = hknfactory.filters.ShareHolderChange(null,parent.wallet.address,null); | |
let highBlock = await parent.wallet.provider.getBlockNumber(); | |
let start = birth; | |
if((typeof startBlock !== 'undefined') && (startBlock !== null)) start = startBlock; | |
while(start < highBlock) { | |
const logs = await hknfactory.queryFilter(toFilter,start,start+1000); | |
if(logs.length >0) updateCB(logs); | |
start +=1000; | |
} | |
} | |
for (const [key, value] of Object.entries(parent.deployment.account)) { | |
retrieveByType(value) | |
} | |
} | |
/** | |
* Transfers ownership of a tracker might be used to remove a tracker | |
* Removal of a tracker is done by transfering it to the tracker ID. | |
*/ | |
transferTracker = function(removeDID,transferTo) { | |
const parent = this; | |
return new Promise( async (resolve) => { | |
const url = this.deployment.REST_API + '/transferTracker'; | |
fetch(url, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
'x-account': parent.wallet.address | |
}, | |
body: JSON.stringify(await parent.signJSON({did:removeDID,transferTo:transferTo})) | |
}) | |
.then(response => { | |
if (!response.ok) { | |
if(response.status == 429) { | |
parent.alert({err:"REST API Rate Limit reached",code:429}) | |
return {}; | |
} else { | |
parent.alert({err:response.statusText,code:response.status}); | |
} | |
} else return response.json(); | |
}) | |
.then(data2 => { | |
console.log("Transfer Result",data2); | |
resolve(data2); | |
}); | |
}); | |
} | |
/** | |
* Creates GrünstromIndex from open readings | |
* - retrieve signed DID Presentation first | |
* - Use signed DID Presentation to get securitization. | |
*/ | |
securitization = function(secureDID,progressCallback,errorCallback) { | |
const parent = this; | |
return new Promise( async (resolve) => { | |
try { | |
let vpdata = { | |
action:"Create DID Presentation", | |
did: secureDID, | |
iat: Math.round(new Date().getTime() / 1000) | |
} | |
const vp = await parent.signJSON(vpdata); | |
progressCallback(20); | |
const url = this.deployment.REST_API + '/trackerPresentation'; | |
fetch(url, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
'x-account': parent.wallet.address | |
}, | |
body: JSON.stringify(vp) | |
}) | |
.then(response => { | |
if (!response.ok) { | |
if(response.status == 429) { | |
parent.alert({err:"REST API Rate Limit reached",code:429}) | |
return {}; | |
} else { | |
parent.alert({err:response.statusText,code:response.status}); | |
} | |
} else return response.json(); | |
}) | |
.then(async (data2) => { | |
let pr=30; | |
progressCallback(pr); | |
const url = this.deployment.REST_API + '/securitizationTracker'; | |
let startData = { | |
action:"Request Securization", | |
jwt: data2.jwt, | |
iat: Math.round(new Date().getTime() / 1000) | |
}; | |
const body = JSON.stringify(await parent.signJSON(startData)); | |
let inteval = setInterval(function() { | |
pr+=1; | |
if(pr>95) { | |
clearInterval(inteval); | |
} else { | |
progressCallback(pr); | |
} | |
},350); | |
fetch(url, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
'x-account': parent.wallet.address | |
}, | |
body: body | |
}) | |
.then(response => { | |
if (!response.ok) { | |
if(response.status == 429) { | |
parent.alert({err:"REST API Rate Limit reached",code:429}) | |
return {}; | |
} else { | |
parent.alert({err:response.statusText,code:response.status}); | |
} | |
} else return response.json(); | |
}) | |
.then(data => { | |
pr= 100; | |
progressCallback(pr); | |
clearInterval(inteval); | |
resolve(data); | |
}) | |
.catch(function(e) { | |
errorCallback(e); | |
}) | |
}); | |
} catch(e) { | |
errorCallback(e); | |
} | |
}); | |
} | |
/** | |
* Checks if given filename + hash is available in window.localStorage | |
* if not will retrieve and return object | |
* | |
* @param {*} filename | |
* @param {*} _hknHash | |
* @returns ImutableObject | |
*/ | |
_getImutable = async (filename,_hknHash) => { | |
if((typeof _hknHash == 'undefined') || ( _hknHash == null)) { | |
_hknHash = hknHash; | |
} | |
let cached = {}; | |
if(typeof window !== 'undefined') JSON.parse(window.localStorage.getItem(_hknHash) || '{}'); | |
if (!cached[filename]) { | |
cached[filename] = await _getIPFS(filename,_hknHash); | |
if(typeof window !== 'undefined') window.localStorage.setItem(_hknHash, JSON.stringify(cached)); | |
} | |
return cached[filename]; | |
}; | |
/** | |
* Retrieves given file from hash. Uses list of fallback gateways in case of failure | |
* | |
* @param {*} filename | |
* @param {*} _hknHash | |
* @returns | |
*/ | |
_getIPFS = async (filename,_hknHash) => { | |
if((typeof _hknHash == 'undefined') || ( _hknHash == null)) { | |
_hknHash = hknHash; | |
} | |
let result = null; | |
let ipfsGateways = ["https://storry.tv","https://ipfs.eth.aragon.network","https://api.corrently.io"] | |
while((ipfsGateways.length > 0) && (result == null)) { | |
let ipfsProvider = ipfsGateways.pop(); | |
try { | |
const response = await fetch(`${ipfsProvider}/ipfs/${_hknHash}/${filename}`); | |
result = await response.json(); | |
} catch(e) { | |
console.warn("Failed IPFS Provider:",ipfsProvider); | |
} | |
} | |
return result; | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment