Skip to content

Instantly share code, notes, and snippets.

@zoernert
Created July 11, 2024 11:54
Show Gist options
  • Save zoernert/39548c0a3a86ba08dad6ccf5625663c7 to your computer and use it in GitHub Desktop.
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)
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.
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