Skip to content

Commit

Permalink
Added KMS signer, colony SDK integration, max payout field to crop te…
Browse files Browse the repository at this point in the history
…mplate, new endpoints for cropTemplate management
  • Loading branch information
JonathanScialpi committed Jun 30, 2022
1 parent ae7d391 commit bf40e31
Show file tree
Hide file tree
Showing 20 changed files with 674 additions and 313 deletions.
7 changes: 6 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,29 @@
"author": "",
"license": "ISC",
"dependencies": {
"@colony/sdk": "^0.5.0",
"@turf/turf": "^6.5.0",
"@types/geojson": "^7946.0.8",
"@types/uuid": "^8.3.4",
"aws-sdk": "^2.1159.0",
"axios": "^0.26.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.0.0",
"ethers": "^5.6.9",
"express": "^4.17.2",
"express-session": "^1.17.2",
"geojson": "^0.5.0",
"jsonwebtoken": "^8.5.1",
"mongoose": "^6.2.1",
"npm": "^8.13.1",
"passport": "^0.5.2",
"passport-ci-oidc": "^2.0.5",
"passport-jwt": "^4.0.0",
"socket.io": "^4.4.1",
"twilio": "^3.75.1",
"uuid": "^8.3.2"
"uuid": "^8.3.2",
"web3": "^1.7.4"
},
"devDependencies": {
"@types/cors": "^2.8.12",
Expand Down
2 changes: 2 additions & 0 deletions backend/src/db/entities/cropTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface CropTemplate{
_id?: string,
action_weights: Record<string, string>,
crop_template_name: string,
max_payout: number
}

export const CropTemplateSchema = new Schema({
Expand All @@ -27,6 +28,7 @@ export const CropTemplateSchema = new Schema({
default: {}
},
crop_template_name: String,
max_payout: Number
});

export const CropTemplateModel = model<CropTemplate>("cropTemplate", CropTemplateSchema);
4 changes: 3 additions & 1 deletion backend/src/db/entities/farmer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface Farmer {
coopOrganisations: string[],
fieldCount: number;
field?: Field;
ethKeyID: string;
}

export const FarmerSchema = new Schema({
Expand All @@ -23,7 +24,8 @@ export const FarmerSchema = new Schema({
mobile: String,
address: String,
coopOrganisations: [String],
fieldCount: Number
fieldCount: Number,
ethKeyID: String
});

export const FarmerModel = model<Farmer>("farmer", FarmerSchema);
2 changes: 1 addition & 1 deletion backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import organisationRoutes from "./routes/organisation-route";
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 cropTemplateRoutes from "./routes/crop-template-route";

import { SocketIOManager, SocketIOManagerInstance } from "./sockets/socket.io";
import { Server } from "http";
Expand Down
1 change: 1 addition & 0 deletions backend/src/routes/crop-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ async function createOrUpdateCrop(req: Request, res: Response) {
const response = await cropService.saveOrUpdate(crop);
res.json(response);
} catch (e) {
console.log(e);
res.status(500).json(e);
}
}
Expand Down
57 changes: 54 additions & 3 deletions backend/src/routes/crop-template-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ 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 { Schema, model, ObjectId, Types, isObjectIdOrHexString } from 'mongoose';
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 { ColonyNetwork } from '@colony/sdk';
const router = Router();

async function createOrUpdateCropTemplates(req: Request, res: Response) {
Expand Down Expand Up @@ -102,7 +107,49 @@ router.get("/getFieldsforCropId/:crop_id", async (req: Request, res: Response) =

// first lookup the field ID and if found replace its subfields with subfields
// data from the incoming payload. This will set the crop_template and reputation actions
async function createOrUpdateField(req: Request, res: Response) {
async function updateRepActions(req: Request, res: Response) {
try{
const [updatedField, cropTemplate, statusUpdated] = UpdateReputationActions(req.body.field,req.body.cropId,req.body.farmer,req.body.actionName,req.body.actionStatus);
if(statusUpdated){
const query = {"_id": updatedField._id};
const update = {$set: {'subFields': updatedField.subFields}}
const docs = await FieldModel.updateOne(query, update)
.then(doc => {
return doc;
})
.catch(err => {
return err;
})

// Create Payload by calculating the amount of reputation tokens to pay
const payment = calculatePayment(cropTemplate.action_weights[req.body.actionName], cropTemplate.max_payout);

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

// connect OH account to Heifer colony
const colonyNetwork = new ColonyNetwork(openHarvestSigner);
const colony = await colonyNetwork.getColony(process.env.HEIFER_COLONY_CONTRACT_ADDRESS!);

// get farmer ethereum account form KMS
const farmer = await FarmerModel.findById(updatedField.farmer_id).lean().exec();
const farmerSigner = new AwsKmsSigner(farmer!.ethKeyID);
const farmerEthAddress = await farmerSigner.getAddress();
await colony.pay(farmerEthAddress, ethers.utils.parseUnits(payment.toString()));

res.json(docs);
}else{
throw new Error('The request must be made with a different action status than what is existing.')
}
}catch (e){
console.error(e);
res.status(500).json(e);
}
}

async function addCropTemplateToField(req: Request, res: Response) {
try{
const query = {"_id": req.body._id};
const update = {$set: {'subFields': req.body.subFields}}
Expand All @@ -113,6 +160,7 @@ async function createOrUpdateField(req: Request, res: Response) {
.catch(err => {
return err;
})

res.json(docs);
}catch (e){
console.error(e);
Expand All @@ -121,7 +169,10 @@ async function createOrUpdateField(req: Request, res: Response) {
}

//api to create or update weights
router.put("/updateField", createOrUpdateField);
router.put("/updateRepActions", updateRepActions);

//api to create or update weights
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) => {
Expand Down
39 changes: 37 additions & 2 deletions backend/src/routes/farmer-route.ts
Original file line number Diff line number Diff line change
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";

const EISKey = process.env.EIS_apiKey;

Expand Down Expand Up @@ -111,6 +111,35 @@ export interface FarmerAddDTO {
field: Field
}

// Create Pub/Priv key on AWS KMS using the FarmerId as alias
const createEthAccount = async(farmerId: String) => {

//Authenticate
const kms = kmsAuth();

//Create Key
const cmk = await kms.createKey({
KeyUsage : 'SIGN_VERIFY',
KeySpec : 'ECC_SECG_P256K1',
}).promise();

// Assign farmerId as alias of created key
const keyId = cmk.KeyMetadata.KeyId;
const aliasResponse = await kms.createAlias({
AliasName : "alias/" + farmerId,
TargetKeyId : keyId
}).promise();

//Grant the application user IAM role permissions on the new key
const grantResponse = await kms.createGrant({
KeyId : keyId,
GranteePrincipal : process.env.OPEN_HARVEST_APPLICATION_USER_ARN,
Operations : ['GetPublicKey', 'Sign']
}).promise();

return keyId;
}

router.post("/add", async(req: Request, res: Response) => {
const {farmer, field}: FarmerAddDTO = req.body;
if (farmer == undefined) {
Expand All @@ -123,7 +152,7 @@ router.post("/add", async(req: Request, res: Response) => {
}
// First we'll create the farmer
const farmerDoc = new FarmerModel(farmer);
const newFarmer = await farmerDoc.save();
let newFarmer = await farmerDoc.save();

if (newFarmer._id == undefined) {
throw new Error("Farmer ID is not defined after saving!")
Expand All @@ -132,6 +161,12 @@ router.post("/add", async(req: Request, res: Response) => {
// Then we'll create the Field
field.farmer_id = newFarmer._id.toHexString();

//Create and set ethAccount keyID to the farmer object
const farmerKeyId = await createEthAccount(field.farmer_id);
farmerDoc.ethKeyID = farmerKeyId;
await farmerDoc.save();


// Lets populate some of the field information

// Subfield area, centre and Bbox
Expand Down
13 changes: 12 additions & 1 deletion backend/src/services/crop.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,18 @@ export default class CropService {

async saveOrUpdate(crop: Crop) {
if (crop._id) {
return CropModel.updateOne(crop);
const cropBody = {
type: crop.type,
name: crop.name,
planting_season: crop.planting_season,
time_to_harvest: crop.time_to_harvest,
is_ongoing: crop.is_ongoing,
yield_per_sqm: crop.yield_per_sqm,
crop_template: crop.crop_template
}
const response = CropModel.updateOne({_id: crop._id}, cropBody);
console.log("response from server: ", response)
return response;
}

return await new CropModel(crop).save();
Expand Down
48 changes: 48 additions & 0 deletions backend/src/web3/AwsKmsSigner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ethers, UnsignedTransaction } from "ethers";
import { getPublicKey, getEthereumAddress, requestKmsSignature, determineCorrectV } from "./aws-kms-utils";

export class AwsKmsSigner extends ethers.Signer {
keyId: string;
ethereumAddress: string;

constructor(keyId: string, provider?: ethers.providers.Provider) {
super();
ethers.utils.defineReadOnly(this, "provider", provider);
ethers.utils.defineReadOnly(this, "keyId", keyId);
}

async getAddress(): Promise<string> {
if (this.ethereumAddress === undefined) {
const key = await getPublicKey(this.keyId);
this.ethereumAddress = getEthereumAddress(key.PublicKey as Buffer);
}
return Promise.resolve(this.ethereumAddress);
}

async _signDigest(digestString: string): Promise<string> {
const digestBuffer = Buffer.from(ethers.utils.arrayify(digestString));
const sig = await requestKmsSignature(digestBuffer, this.keyId);
const ethAddr = await this.getAddress();
const { v } = determineCorrectV(digestBuffer, sig.r, sig.s, ethAddr);
return ethers.utils.joinSignature({
v,
r: `0x${sig.r.toString("hex")}`,
s: `0x${sig.s.toString("hex")}`,
});
}

async signMessage(message: string | ethers.utils.Bytes): Promise<string> {
return this._signDigest(ethers.utils.hashMessage(message));
}

async signTransaction(transaction: ethers.utils.Deferrable<ethers.providers.TransactionRequest>): Promise<string> {
const unsignedTx = await ethers.utils.resolveProperties(transaction);
const serializedTx = ethers.utils.serializeTransaction(<UnsignedTransaction>unsignedTx);
const transactionSignature = await this._signDigest(ethers.utils.keccak256(serializedTx));
return ethers.utils.serializeTransaction(<UnsignedTransaction>unsignedTx, transactionSignature);
}

connect(provider: ethers.providers.Provider): AwsKmsSigner {
return new AwsKmsSigner(this.keyId, provider);
}
}

0 comments on commit bf40e31

Please sign in to comment.