Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Colony metrics route #25

Merged
merged 6 commits into from Jul 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
43 changes: 43 additions & 0 deletions Architecture and Vision.md
Expand Up @@ -124,6 +124,49 @@ but in the future I think it would be worthwhile creating a DSL (Domain Specific
It would also be worthwhile to investigate how other integration software approaches this problem like Home
Assistant.

## Blockchain Integrations

### Colony

[Colony](https://colony.io/) is a Decentralized Application which helps user build, deploy, and manage DAOs on [Gnosis Chain](https://gnosis.io/).

Heifer has deployed their own test DAO which can be found [here](https://xdai.colony.io/colony/heifertest).

To execute transactions using the HeiferDAO smart contract, OpenHarvest uses:
- [EthersJS](https://docs.ethers.io/v5/) to create an RPC connection to Gnosis
- [Colonly SDK](https://github.com/JoinColony/colonySDK) to run Colony DAO methods such as `getReputation`, `pay`, etc.
- [AWS SDK (KMS)](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/KMS.html) to create CMKs, manage key permissions, and generate cryptographic signatures for Gnosis transactions.

Current workflow:
1. Farmer is added to OpenHarvest and a CMK is created and assigned to that farmer.
2. Farmer completes a recommended action and OpenHarvest Gnosis account calculates the amount of HX (ERC20 token associated with the HeiferDAO)the farmer should receive.
3. OpenHarvest application make a `pay` call to send/reward the Farmer with the HX tokens.

### IBM Food Trust

The purpose of the integration is to track a crop's lifecycle events via Hyperledger Fabric. Read more about [EPCIS events here](https://www.ibm.com/docs/en/food-trust?topic=reference-epcis-events). API requests to the endpoints described below will be triggered upon a farmer's completion of a recommended action.

OpenHarvest has been integrated with [IBM Food Trust](https://www.ibm.com/blockchain/solutions/food-trust)'s sandbox instance for the pilot phase and will eventually be pointed to the Production environment.

**Current Groundnut crop lifecycle:**
1. Perform Drying Method 1 `OBSERVATION`
2. Perform Drying Method 2 `OBSERVATION`
3. Perform Drying Method 3 `OBSERVATION`
4. Perform Drying Method 4 `OBSERVATION`
5. Perform Drying Method 5 `OBSERVATION`
6. Storage 6 `OBSERVATION`
7. Package for Transport `OBSERVATION`
8. Transport to Retailer `TRANSFORMATION`
9. Unpack at retailer `OBSERVATION`

**Food Trust Express Routes & Sub-routes:**
- /routes/food-trust-route
- /foodTrustProducts `POST`
- /foodTrustProductByDesciption `GET`
- /foodTrustLocations `POST`
- /foodTrustLocationsByOrg `GET`
- /issueIFTTransaction `POST`
- /getEventByAssetId `GET`
## Events

Events are an important aspect of the OpenHarvest integration platform and they serve both users and other
Expand Down
6 changes: 5 additions & 1 deletion README.md
Expand Up @@ -52,9 +52,13 @@ flowchart LR
react<-->node
node<-->MongoDB
node<-->AfricasTalking
node<-->Twilio
node<-->IBMFoodTrust

User<-->react
Integrations<-->node
EthersJS<-->node
Colony<-->node
AWS-KMS<-->node
```

### Infrastructure / deployment stack
Expand Down
Expand Up @@ -8,4 +8,11 @@ export const kmsAuth = () => {
});
const kms = new AWS.KMS();
return kms;
}

// connect to Gnosis network
export const gnosisConnection = () => {
const ethers = require('ethers');
const provider = new ethers.providers.JsonRpcProvider(process.env.GNOSIS_RPC_URL);
return provider;
}
Expand Up @@ -2,7 +2,7 @@ import { ethers } from "ethers";
import { KMS } from "aws-sdk";
import * as asn1 from "asn1.js";
import BN from "bn.js";
import { kmsAuth } from "./aws-sdk-authentication";
import { kmsAuth } from "./authentication-functions";

const kms = kmsAuth();
/* this asn1.js library has some funky things going on */
Expand Down
73 changes: 73 additions & 0 deletions backend/src/integrations/Blockchain/web3/helper-functions.ts
@@ -0,0 +1,73 @@
import { CropTemplate} from "../../../db/entities/cropTemplate";
import { Field, SubField, SubFieldCrop} from "../../../db/entities/field"

// actionWeight * MaxPayout = NumOfHXToPay

export const calculatePayment = (actionWeight, maxPayout) => {
return Math.floor((actionWeight/100) * maxPayout);
}

// this function should receive a cropId and farmerId which can be used to find the proper SubFieldCrop
// once found, we can set the action value to that of actionStatus arg.
export function UpdateReputationActions(
field: Field,
cropId: string,
farmer: string,
actionName: string,
actionStatus: boolean): [Field, CropTemplate, Boolean]{
let cropTemplate;
let statusUpdated = false;
const subFieldsArray: SubField[] = field.subFields;
for(let subFieldIndex in subFieldsArray){
const subFieldCropsArray: SubFieldCrop[] = subFieldsArray[subFieldIndex].properties.crops;
for(let subFieldCropIndex in subFieldCropsArray){
if(subFieldCropsArray[subFieldCropIndex].crop._id?.toString() === cropId &&
subFieldCropsArray[subFieldCropIndex].farmer === farmer &&
subFieldCropsArray[subFieldCropIndex].reputation_actions
){
statusUpdated = (subFieldCropsArray[subFieldCropIndex].reputation_actions![actionName] != actionStatus);
subFieldCropsArray[subFieldCropIndex].reputation_actions![actionName] = actionStatus;
cropTemplate = subFieldCropsArray[subFieldCropIndex].crop_template!;
}
}
}
return [field, cropTemplate, statusUpdated];
}

const calculateReputationEarned = (reputationActions: Record<string,boolean>, reputationWeights: Record<string, string>) => {
let totalRepEarned = 0;
for(const key in reputationActions){
totalRepEarned += calculatePayment(reputationActions[key], reputationWeights[key]);
}
return totalRepEarned;
}

/*
given a field, return the max payout and total reputation earned per subfield
[
crop_name : {
id: ...
max_reputation: ...
reputation_earned: ...
},
...
]
*/
export function getReputationReport(field: Field): any{
let reputationReportList: Record<string, any>[] = [];
const subFieldsArray: SubField[] = field.subFields;

for(let subFieldIndex in subFieldsArray){
const subFieldCropsArray: SubFieldCrop[] = subFieldsArray[subFieldIndex].properties.crops;
for(let subFieldCropIndex in subFieldCropsArray){
reputationReportList.push({
[subFieldCropsArray[subFieldCropIndex].crop.name] : {
id : subFieldCropsArray[subFieldCropIndex].crop._id!.toString(),
max_reputation : subFieldCropsArray[subFieldCropIndex].crop.crop_template!.max_payout,
reputation_earned : calculateReputationEarned(subFieldCropsArray[subFieldCropIndex].reputation_actions!, subFieldCropsArray[subFieldCropIndex].crop.crop_template!.action_weights!)
}
});
}
}
return reputationReportList;
}
2 changes: 2 additions & 0 deletions backend/src/main.ts
Expand Up @@ -22,6 +22,7 @@ import messageLogRoutes from "./routes/messaging-route";
import smsRoutes from "./routes/sms-route";
import foodTrustRoutes from "./routes/food-trust-route";
import cropTemplateRoutes from "./routes/crop-template-route";
import colony from "./routes/colony-route"

import { SocketIOManager, SocketIOManagerInstance } from "./sockets/socket.io";
import { Server } from "http";
Expand Down Expand Up @@ -71,6 +72,7 @@ app.use("/api/sms", smsRoutes);
// blockchain related routes
app.use("/api/foodtrust", foodTrustRoutes)
app.use("/api/cropTemplates", cropTemplateRoutes)
app.use("/api/colony", colony)

// Static Files
const publicPath = path.resolve("public");
Expand Down
46 changes: 46 additions & 0 deletions backend/src/routes/colony-route.ts
@@ -0,0 +1,46 @@
import { Request, Response, Router } from "express";
import { gnosisConnection } from "../integrations/Blockchain/web3/authentication-functions";
import { AwsKmsSigner } from "../integrations/Blockchain/web3/AwsKmsSigner";
import { ColonyNetwork } from '@colony/sdk';
import { getReputationReport } from "../integrations/Blockchain/web3/helper-functions";
import { FarmerModel } from "../db/entities/farmer";

var router = Router();

router.post("/", getReputationForFarmer);

// This route expects a Farmer ID string. The farmer Id will be used to retrieve the Farmer object.
// The farmer object's ethKeyID will be used to get the farmer's ethAddress and reputation.
async function getReputationForFarmer(req: Request, res: Response){
try{
// get farmer object
const farmer = await FarmerModel.findById(req.body.farmer_id).lean().exec();

if(!farmer){
throw new Error("Farmer not found.");
}else{
// connect to Gnosis network with Farmer account
const provider = gnosisConnection();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to create extra work for you, but would it possible to wrap anything related to colony into it's own class? It would make it easier to call from other places but also test and mock colony data. It would really come down to being another class in the web3 folder that wraps around colony's api calls.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's discuss further in a technical call and then I will create a new branch for this if needed. The reason why I'm not sure if its required is because the Colony SDK is already like a wrapper class which makes it easier to interact with colony so another class would be like a wrapper class on top of another wrapper class.

const farmerSigner = new AwsKmsSigner(farmer!.ethKeyID, provider);
const farmerEthAddress = await farmerSigner.getAddress();

// connect to Heifer colony and retrieve farmer rep and HX balance
const colonyNetwork = new ColonyNetwork(farmerSigner);
const colony = await colonyNetwork.getColony(process.env.HEIFER_COLONY_CONTRACT_ADDRESS!);
const balance = await colony.getBalance();
const reputation = await colony.getReputation(farmerEthAddress);
res.json({
colony_balance : balance.toString(),
reputation : reputation.toString(),
farmer_eth_address: farmerEthAddress,
reputation_report : getReputationReport(req.body)
});
}

}catch(e){
console.error(e);
res.status(500).json(e);
}
}

export default router;
19 changes: 8 additions & 11 deletions backend/src/routes/crop-template-route.ts
@@ -1,13 +1,10 @@
import { Router, Request, Response } from "express";
import { CropTemplateModel } from "../db/entities/cropTemplate";
import { FieldModel } from "../db/entities/field"
import { Crop } from "../db/entities/crop"
import { FarmerModel } from "../db/entities/farmer"
import { calculatePayment, UpdateReputationActions } from "../web3/helper-functions"
// import { Schema, model, ObjectId, Types, isObjectIdOrHexString } from 'mongoose';
// import { erc20ABI } from "../web3/utils/erc20-abi"

import { AwsKmsSigner } from "../web3/AwsKmsSigner";
import { FieldModel } from "../db/entities/field";
import { FarmerModel } from "../db/entities/farmer";
import { calculatePayment, UpdateReputationActions } from "../integrations/Blockchain/web3/helper-functions";
import { gnosisConnection } from "../integrations/Blockchain/web3/authentication-functions";
import { AwsKmsSigner } from "../integrations/Blockchain/web3/AwsKmsSigner";
import { ColonyNetwork } from '@colony/sdk';
const router = Router();

Expand Down Expand Up @@ -126,7 +123,7 @@ async function updateRepActions(req: Request, res: Response) {

// connect to Gnosis network
const ethers = require('ethers');
const provider = new ethers.providers.JsonRpcProvider(process.env.GNOSIS_RPC_URL);
const provider = gnosisConnection();
const openHarvestSigner = new AwsKmsSigner(process.env.OPEN_HARVEST_KEY_ID!, provider);

// connect OH account to Heifer colony
Expand All @@ -141,7 +138,7 @@ async function updateRepActions(req: Request, res: Response) {

res.json(docs);
}else{
throw new Error('The request must be made with a different action status than what is existing.')
throw new Error('The request must be made with a different action status than what is existing.');
}
}catch (e){
console.error(e);
Expand Down Expand Up @@ -175,7 +172,7 @@ router.put("/updateRepActions", updateRepActions);
router.put("/addCropTemplateToField", addCropTemplateToField);

// return all Fields which have the given crop_id in their subFields.properties.crops
router.get("/getActionsForField/:field_id", async (req: Request, res: Response) => {
router.get("/getField/:field_id", async (req: Request, res: Response) => {
try {
const docs = await FieldModel.findById(req.params.field_id)
.then(doc => {
Expand Down
2 changes: 1 addition & 1 deletion backend/src/routes/farmer-route.ts
Expand Up @@ -5,7 +5,7 @@ import { Farmer, FarmerModel } from "../db/entities/farmer";
import { Field, FieldModel } from "../db/entities/field";
import { center, bbox, area, bboxPolygon } from "@turf/turf";
import { FeatureCollection } from "geojson";
import { kmsAuth } from "../web3/aws-sdk-authentication";
import { kmsAuth } from "../integrations/Blockchain/web3/authentication-functions";

const EISKey = process.env.EIS_apiKey;

Expand Down
2 changes: 1 addition & 1 deletion backend/src/routes/food-trust-route.ts
@@ -1,6 +1,6 @@
// import dependencies and initialize the express router
import { Router } from "express";
import { FoodTrustAPI } from "../integrations/food-trust/food-trust-api.service";
import { FoodTrustAPI } from "../integrations/Blockchain/food-trust/food-trust-api.service";

const router = Router();

Expand Down
1 change: 0 additions & 1 deletion backend/src/services/crop.service.ts
Expand Up @@ -31,7 +31,6 @@ export default class CropService {
crop_template: crop.crop_template
}
const response = CropModel.updateOne({_id: crop._id}, cropBody);
console.log("response from server: ", response)
return response;
}

Expand Down
35 changes: 0 additions & 35 deletions backend/src/web3/helper-functions.ts

This file was deleted.

16 changes: 11 additions & 5 deletions react-app/src/components/Crops/CropTemplate.tsx
Expand Up @@ -4,12 +4,14 @@ import {Add16} from '@carbon/icons-react';
import { CropTemplate, CropTemplateAPI} from "../../services/cropTemplate";
import { CropService } from "../../services/CropService";
import { Crop } from "../../services/crops";
import { getFarmer } from "../../services/farmers";
import { getFarmer, Farmer } from "../../services/farmers";
import { Field, SubField, SubFieldCrop} from "../../../../backend/src/db/entities/field"
import { UpdateSubFieldWithCropTemplate, OrganizeReputationActions } from './helperFunctions'
import { UpdateSubFieldWithCropTemplate, OrganizeReputationActions } from './helperFunctions';
import { ColonyAPI } from '../../services/colony';

const cropTemplateAPI = new CropTemplateAPI();
const cropService = new CropService();
const colonyService = new ColonyAPI();

const rowStyle = {
// justifyContent:"center",
Expand Down Expand Up @@ -130,14 +132,14 @@ const CropTemplateSelector = () => {
const cropToUpdate: Crop = await cropService.getCrop(selectedCrop);
cropToUpdate.crop_template = templateForSubmission;
await cropService.updateCrop(cropToUpdate); //should check to see if the crop already has an associated template first... but can add that later
//2. add cropTemplate and rep actions to fields: search for existing Field.Subfield[] with Crop_id and update to add cropTemplate object
// 2. add cropTemplate and rep actions to fields: search for existing Field.Subfield[] with Crop_id and update to add cropTemplate object
const fields: Field[] = await cropTemplateAPI.getFieldsforCropId(selectedCrop);
UpdateSubFieldWithCropTemplate(fields, cropTemplateAPI, selectedCrop, templateForSubmission);

// ******** UI display ********

//get all reputation Actions for a field and then put them in a list (usefull for front end display)
// const res = await cropTemplateAPI.getActionsForField("62bdf886ff4ab905c24225a6");
// const res = await cropTemplateAPI.getField("62bdf886ff4ab905c24225a6");
// console.log(OrganizeReputationActions(res))

// ******** updating reputation action ********
Expand All @@ -148,8 +150,12 @@ const CropTemplateSelector = () => {
// 4. Use the OpenHarvest's ethKeyID to instantiate a new AWSSigner with OpenHarvest's ethereum Address
// 5. Use the farmer's ethKeyID to instantiate a new AWSSigner with the famer's ethereum Address
// 6. Calculate the payout amount and send from OpenHarvest address using colony SDK's pay() function
// const field: Field = await cropTemplateAPI.getActionsForField("62bdf886ff4ab905c24225a6");
// const field: Field = await cropTemplateAPI.getField("62c87e05e468c7b44ad4af96");
// await cropTemplateAPI.updateRepActions(field, selectedCrop, "", "MyAction", true);

// ******** get reputation data for farmer object ********
//await colonyService.getReputationForFarmer(field);

}

// update state for addCropTemplateName
Expand Down
12 changes: 12 additions & 0 deletions react-app/src/services/colony.tsx
@@ -0,0 +1,12 @@
import axios from 'axios';
import { Field } from '../../../backend/src/db/entities/field';

export class ColonyAPI{
APIBase = "/api/colony/";

async getReputationForFarmer(field: Field): Promise<any> {
const data = await axios.post(this.APIBase, field);
console.log(data.data)
return data.data;
}
}