getCampsInvestedByUser(String token) async {
124 | var headers = {
125 | 'x-api-key':
126 | '8\$dsfsfgreb6&4w5fsdjdjkje#\$54757jdskjrekrm@#\$@\$%&8fdddg*&*ffdsds',
127 | 'Content-Type': 'application/json',
128 | 'Authorization': token
129 | };
130 |
131 | var request = http.Request(
132 | 'GET', Uri.parse('http://3.135.1.141/api/getCampsInvestedByUser'));
133 |
134 | request.headers.addAll(headers);
135 |
136 | http.StreamedResponse response = await request.send();
137 |
138 | return await jsonDecode(await response.stream.bytesToString());
139 | }
140 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Collective
2 |
3 |
4 |
5 | An equity crowdfunding app managed with smart contracts that allows users to invest in projects with crypto in return of equity.
6 |
7 |
8 |
9 |
10 | Abstract
11 | At present, Crowdfunding source of raising funds typically for startups or projects has gained popularity with most startups resorting to the use of Crowdfunding platforms to raise funds in exchange of equity because it is relatively inexpensive and uncomplicated in nature. In the existing model, Pool of people contribute small amounts of money towards a project or cause and expect some financial returns. The call for a solution to issues related to security, investor abuse and, illegal transactions that could plague crowdfunding has lead me to investigate the implications of blockchain in Crowdfunding.
12 |
13 |
14 |
15 |
16 |
17 | Introduction
18 |
19 | - A global challenge facing start-ups is the raising of the required funds. Although there are many sources of funds available to entrepreneurs who wish to start new businesses or expand existing ones, the challenge of getting inexpensive funding at the right time still remains a challenge in the small business domain. The emergence of crowdfunding as a brainchild of crowd-sourcing provides an alternative form of funding for start-ups.
20 |
21 |
22 | - Collective is an online equity crowdfunding platform managed by smart contracts on Ethereum. The platform enables start-ups and projects to raise funding in return of equity. Individual can invest a relatively small amount of money in-order to receive stake in a company at an early stage hoping to get good returns in the long-term as the startup/project grows. Along with funding, Collective also enables startups/projects to find the required talent for their projects.
23 |
24 | - What makes Collective truly unique is its decentralized and autonomous approach towards crowdfunding using smart contract that are deployed on the Ethereum blockchain and a mobile app created using Google’s Flutter which can be used on both Android and IOS.
25 |
26 | - Startups on the platform can create camps in which normal users can purchase equity by investing CTV ( Collective token ) a ERC20 fungible token exclusive only to the Collective platform. This platform exclusive ERC20 token along with smart contracts enables Collective to tackle the issue of trust and security that plagues all the existing crowdfunding platforms.
27 |
28 |
29 |
30 |
31 |
32 | Tech Stack
33 | Flutter - Android / IOS app
34 | Node.js - Backend server with MVC architecture hosted on AWS EC2 along with NGINX reverse proxy
35 | MongoDB - Database hosted on cloud atlas
36 | Blockchain - Ethereum
37 | CTV token - ERC20 token
38 | Mocha - Testing
39 |
40 |
41 |
42 | App walkthrough video
43 |
44 | https://youtu.be/dtBWU9CF_cI
45 |
46 |
47 |
48 |
49 | Collective Screens
50 |
51 | 
52 |
53 |
54 | 
55 |
56 | 
57 |
58 | 
59 |
60 | 
61 |
62 | 
63 |
64 | 
65 |
66 | 
67 |
68 |
69 |
70 | High level system design
71 |
72 |
73 |
74 |
75 |
76 | How equity crowdfunding works
77 |
78 |
79 |
80 |
81 |
82 |
83 | How collaboration works
84 |
85 |
86 |
87 |
88 |
89 | Future enhancement
90 |
91 |
92 | - Migrating to a micro-service based architecture for the backend using RabbitMQ.
93 | - Moving all the camp images to IPFS or AWS S3 bucket.
94 | - Improving the transaction speeds by migrating to Hedera hashgraph and implementing HTS and HSC 2.0.
95 |
96 |
97 |
98 |
99 | Thank you for your interest !!
100 |
--------------------------------------------------------------------------------
/frontend/lib/screens/UsersCollabScreen.dart:
--------------------------------------------------------------------------------
1 | import 'package:collective/widgets/appBarGoBack.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:shared_preferences/shared_preferences.dart';
4 | import 'package:flutter_spinkit/flutter_spinkit.dart';
5 | import 'dart:async';
6 | import 'dart:convert';
7 | import 'package:http/http.dart' as http;
8 |
9 | class UsersCollabScreen extends StatefulWidget {
10 | static String routeName = '/userCollabScreen';
11 |
12 | @override
13 | _UsersCollabScreenState createState() => _UsersCollabScreenState();
14 | }
15 |
16 | class _UsersCollabScreenState extends State {
17 | String token;
18 | String username;
19 | Future collabsList;
20 |
21 | @override
22 | void initState() {
23 | super.initState();
24 |
25 | setToken();
26 | }
27 |
28 | Future setToken() async {
29 | SharedPreferences.getInstance().then((prefValue) {
30 | token = prefValue.getString('token');
31 | username = prefValue.getString('username');
32 | collabsList = getUsersCollaboration(token);
33 | setState(() {});
34 | });
35 | }
36 |
37 | @override
38 | Widget build(BuildContext context) {
39 | return SafeArea(
40 | child: Scaffold(
41 | appBar: PreferredSize(
42 | preferredSize: const Size.fromHeight(55),
43 | child: AppBarGoBack(),
44 | ),
45 | body: Container(
46 | color: Colors.white,
47 | padding: EdgeInsets.all(16),
48 | width: MediaQuery.of(context).size.width,
49 | height: MediaQuery.of(context).size.height -
50 | AppBar().preferredSize.height -
51 | 35,
52 | child: FutureBuilder(
53 | future: collabsList,
54 | builder: (BuildContext context, AsyncSnapshot snapshot) {
55 | if (snapshot.connectionState == ConnectionState.waiting ||
56 | snapshot.connectionState == ConnectionState.none) {
57 | return Padding(
58 | padding: const EdgeInsets.only(top: 270),
59 | child: Column(
60 | children: [
61 | SpinKitThreeBounce(
62 | color: Theme.of(context).primaryColor,
63 | size: 35.0,
64 | ),
65 | ],
66 | ),
67 | );
68 | }
69 | if (snapshot.data['result'] == false) {
70 | return Center(
71 | child: Padding(
72 | padding: const EdgeInsets.only(
73 | left: 60.0,
74 | right: 60.0,
75 | top: 0,
76 | ),
77 | child: Text(
78 | 'No collaborations found',
79 | textAlign: TextAlign.center,
80 | style: TextStyle(fontSize: 16),
81 | ),
82 | ),
83 | );
84 | }
85 | if (snapshot.data['result'] == true &&
86 | snapshot.data['details'].length == 0) {
87 | return Center(
88 | child: Padding(
89 | padding: const EdgeInsets.only(
90 | left: 60.0,
91 | right: 60.0,
92 | top: 0,
93 | ),
94 | child: Text(
95 | 'No collaborations found',
96 | textAlign: TextAlign.center,
97 | style: TextStyle(fontSize: 16),
98 | ),
99 | ),
100 | );
101 | }
102 | return ListView.builder(
103 | itemCount: snapshot.data['details'].length,
104 | itemBuilder: (context, index) {
105 | return Container(
106 | margin: EdgeInsets.only(bottom: 25),
107 | padding: EdgeInsets.all(15),
108 | decoration: BoxDecoration(
109 | borderRadius: BorderRadius.circular(15),
110 | color: Color.fromRGBO(240, 240, 240, 1),
111 | ),
112 | child: Column(
113 | children: [
114 | Row(
115 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
116 | children: [
117 | Text('Role',
118 | style:
119 | TextStyle(fontWeight: FontWeight.bold)),
120 | Text(
121 | snapshot.data['details'][index]
122 | ['collabTitle'],
123 | style: TextStyle(
124 | color: Theme.of(context).primaryColor))
125 | ],
126 | ),
127 | Divider(),
128 | Row(
129 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
130 | children: [
131 | Text('Budget',
132 | style:
133 | TextStyle(fontWeight: FontWeight.bold)),
134 | Text(
135 | snapshot.data['details'][index]
136 | ['collabAmount']
137 | .toString() +
138 | " CTV",
139 | style: TextStyle(
140 | color: Theme.of(context).primaryColor))
141 | ],
142 | ),
143 | ],
144 | ),
145 | );
146 | });
147 | },
148 | ),
149 | ),
150 | ),
151 | );
152 | }
153 | }
154 |
155 | Future getUsersCollaboration(String token) async {
156 | var headers = {
157 | 'x-api-key':
158 | '8\$dsfsfgreb6&4w5fsdjdjkje#\$54757jdskjrekrm@#\$@\$%&8fdddg*&*ffdsds',
159 | 'Content-Type': 'application/json',
160 | 'Authorization': token
161 | };
162 | var request =
163 | http.Request('GET', Uri.parse('http://3.135.1.141/api/getUsersCollabs'));
164 |
165 | request.headers.addAll(headers);
166 |
167 | http.StreamedResponse response = await request.send();
168 |
169 | return await jsonDecode(await response.stream.bytesToString());
170 | }
171 |
--------------------------------------------------------------------------------
/frontend/lib/widgets/CampScreenWidgets/CampCollaboratorsWidget.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:shared_preferences/shared_preferences.dart';
3 | import 'package:flutter_spinkit/flutter_spinkit.dart';
4 | import 'dart:async';
5 | import 'dart:convert';
6 | import 'package:http/http.dart' as http;
7 |
8 | class CampCollaboratorsWidget extends StatefulWidget {
9 | final Map selectedCamp;
10 |
11 | CampCollaboratorsWidget(this.selectedCamp);
12 | @override
13 | _CampCollaboratorsWidgetState createState() =>
14 | _CampCollaboratorsWidgetState();
15 | }
16 |
17 | class _CampCollaboratorsWidgetState extends State {
18 | String token;
19 | String username;
20 | Future collabJobsList;
21 |
22 | @override
23 | void initState() {
24 | super.initState();
25 |
26 | setToken();
27 | }
28 |
29 | Future setToken() async {
30 | SharedPreferences.getInstance().then((prefValue) {
31 | token = prefValue.getString('token');
32 | username = prefValue.getString('username');
33 | collabJobsList = getAllAcceptedCollabJobs(
34 | token, widget.selectedCamp['selectedCamp']['campAddress']);
35 | setState(() {});
36 | });
37 | }
38 |
39 | @override
40 | Widget build(BuildContext context) {
41 | return Container(
42 | width: MediaQuery.of(context).size.width,
43 | child: FutureBuilder(
44 | future: collabJobsList,
45 | builder: (BuildContext context, AsyncSnapshot snapshot) {
46 | if (snapshot.connectionState == ConnectionState.waiting ||
47 | snapshot.connectionState == ConnectionState.none) {
48 | return Padding(
49 | padding: const EdgeInsets.only(top: 270),
50 | child: Column(
51 | children: [
52 | SpinKitThreeBounce(
53 | color: Theme.of(context).primaryColor,
54 | size: 35.0,
55 | ),
56 | ],
57 | ),
58 | );
59 | }
60 | if (snapshot.data['result'] == false) {
61 | return Center(
62 | child: Padding(
63 | padding: const EdgeInsets.only(
64 | left: 60.0,
65 | right: 60.0,
66 | top: 0,
67 | ),
68 | child: Text(
69 | 'No collaborators found',
70 | textAlign: TextAlign.center,
71 | style: TextStyle(fontSize: 16),
72 | ),
73 | ),
74 | );
75 | }
76 | if (snapshot.data['result'] == true &&
77 | snapshot.data['curatedCollabJobDetails'].length == 0) {
78 | return Center(
79 | child: Padding(
80 | padding: const EdgeInsets.only(
81 | left: 60.0,
82 | right: 60.0,
83 | top: 0,
84 | ),
85 | child: Text(
86 | 'No collaborators found',
87 | textAlign: TextAlign.center,
88 | style: TextStyle(fontSize: 16),
89 | ),
90 | ),
91 | );
92 | }
93 |
94 | return ListView.builder(
95 | itemCount: snapshot.data['curatedCollabJobDetails'].length,
96 | itemBuilder: (context, index) {
97 | return Container(
98 | margin: EdgeInsets.only(bottom: 25, left: 16, right: 16),
99 | padding: EdgeInsets.all(15),
100 | decoration: BoxDecoration(
101 | borderRadius: BorderRadius.circular(15),
102 | color: Color.fromRGBO(240, 240, 240, 1),
103 | ),
104 | child: Column(
105 | children: [
106 | Row(
107 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
108 | children: [
109 | Text(
110 | 'Username',
111 | style: TextStyle(fontWeight: FontWeight.bold),
112 | ),
113 | Text(
114 | snapshot.data['curatedCollabJobDetails'][index]
115 | ['username'],
116 | style: TextStyle(
117 | color: Theme.of(context).primaryColor))
118 | ],
119 | ),
120 | Divider(),
121 | Row(
122 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
123 | children: [
124 | Text('Email',
125 | style: TextStyle(fontWeight: FontWeight.bold)),
126 | Text(
127 | snapshot.data['curatedCollabJobDetails'][index]
128 | ['email'],
129 | style: TextStyle(
130 | color: Theme.of(context).primaryColor))
131 | ],
132 | ),
133 | Divider(),
134 | Row(
135 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
136 | children: [
137 | Text('Role',
138 | style: TextStyle(fontWeight: FontWeight.bold)),
139 | Text(
140 | snapshot.data['curatedCollabJobDetails'][index]
141 | ['position'],
142 | style: TextStyle(
143 | color: Theme.of(context).primaryColor))
144 | ],
145 | ),
146 | Divider(),
147 | Row(
148 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
149 | children: [
150 | Text('Budget',
151 | style: TextStyle(fontWeight: FontWeight.bold)),
152 | Text(
153 | snapshot.data['curatedCollabJobDetails'][index]
154 | ['amount'],
155 | style: TextStyle(
156 | color: Theme.of(context).primaryColor))
157 | ],
158 | ),
159 | ],
160 | ),
161 | );
162 | });
163 | }),
164 | );
165 | }
166 | }
167 |
168 | Future getAllAcceptedCollabJobs(
169 | String token, String campAddress) async {
170 | var headers = {
171 | 'x-api-key':
172 | '8\$dsfsfgreb6&4w5fsdjdjkje#\$54757jdskjrekrm@#\$@\$%&8fdddg*&*ffdsds',
173 | 'Content-Type': 'application/json',
174 | 'Authorization': token
175 | };
176 | var request = http.Request(
177 | 'GET',
178 | Uri.parse(
179 | 'http://3.135.1.141/api/getCollabAcceptedRequest/' + campAddress));
180 | request.headers.addAll(headers);
181 |
182 | http.StreamedResponse response = await request.send();
183 |
184 | return await jsonDecode(await response.stream.bytesToString());
185 | }
186 |
--------------------------------------------------------------------------------
/backend/controllers/authControllers/authController.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require("bcryptjs");
2 | const Web3 = require('web3');
3 | const moment = require('moment-timezone');
4 | const CryptoJS = require("crypto-js");
5 | const UserAuthModel = require("../../models/userAuthModel")
6 | const UserDetailsModel = require("../../models/userDetailsModel");
7 | const { validationResult } = require("express-validator");
8 | const {generateToken} = require("../../middleware/authHelper");
9 |
10 |
11 | require('dotenv').config();
12 |
13 | const rpcURL = 'https://ropsten.infura.io/v3/7a0de82adffe468d8f3c1e2183b37c39';
14 |
15 | const web3 = new Web3(rpcURL);
16 |
17 |
18 | const userRegister = async(req,res) => {
19 | try{
20 | const errors = validationResult(req);
21 | if (!errors.isEmpty()) {
22 | return res.status(422).json({
23 | error: errors.array()[0],
24 | result:false
25 | });
26 | }
27 |
28 | //Check if the username is within the length bracket
29 | if(req.body.username.length>50 || req.body.username.length<5){
30 | return res.status(401).json({
31 | error:'Username cannot be more than 50 characters and smaller than 5 characters',
32 | result:false
33 | });
34 | }
35 |
36 | //Check if the password is smaller than 5
37 | if(req.body.password.length<5){
38 | return res.status(401).json({
39 | error:'Password cannot be smaller than 5 characters',
40 | result:false
41 | });
42 | }
43 |
44 | //Hashing password
45 | const salt = await bcrypt.genSalt(10);
46 | const hash = await bcrypt.hash(req.body.password, salt);
47 |
48 |
49 | const ethAccount = await web3.eth.accounts.create();
50 |
51 | if(!ethAccount){
52 | return res.status(400).json({
53 | msg:"There was a problem creating ETH account for the user",
54 | result:false
55 | });
56 | }
57 |
58 | // Using AES to encrypt the Ethereum private key
59 |
60 | let ciphertext = CryptoJS.AES.encrypt(ethAccount.privateKey,process.env.master_key).toString();
61 |
62 | //Saving user's auth details
63 |
64 | const userDetails = {
65 | email:req.body.email,
66 | username:req.body.username,
67 | password:hash,
68 | timestamp:moment().format('MMMM Do YYYY, h:mm:ss a'),
69 | eth_address:ethAccount.address,
70 | eth_private_key:ciphertext
71 | }
72 |
73 | const newUser = await UserAuthModel.create(userDetails)
74 |
75 | //Saving user's extra details
76 |
77 | const userPersonalDetails = {email:req.body.email,username:req.body.username}
78 |
79 | const newUserDetails = await UserDetailsModel.create(userPersonalDetails)
80 |
81 | if(newUser && newUserDetails){
82 |
83 | // Generating Token on signup
84 | let payload = {
85 | email : newUserDetails.email,
86 | username : newUserDetails.username,
87 | id : newUserDetails._id,
88 | eth_address:ethAccount.address,
89 | eth_private_key:ciphertext
90 | };
91 |
92 | const token = generateToken(payload);
93 |
94 | if(token.length>0){
95 | res.status(200).json({
96 | result:true,
97 | msg: "User registered successfully",
98 | details:newUserDetails,
99 | token:token
100 | });
101 | }
102 | else{
103 | res.status(500).json({
104 | msg:"There was a problem creating token for the new user",
105 | result:false
106 | })
107 | }
108 |
109 | }
110 | else{
111 | res.status(400).json(
112 | {
113 | result:false,
114 | msg: "There was a problem creating a new user",
115 | });
116 | }
117 | }
118 | catch(err){
119 | console.log(err);
120 | if(err.name === 'MongoError' && err.code === 11000){
121 | console.log(err);
122 | res.status(401).json({
123 | result:false,
124 | msg:"Duplicate field",
125 | field:err.keyValue
126 | });
127 | }
128 | else{
129 | res.status(401).json(err);
130 | }
131 | }
132 |
133 | }
134 |
135 |
136 |
137 | const userLogin = async (req,res)=>{
138 | try{
139 |
140 | const errors = validationResult(req);
141 | if (!errors.isEmpty()) {
142 | return res.status(422).json({
143 | error: errors.array()[0],
144 | result:false
145 | });
146 | }
147 |
148 | const {email_username,password} = req.body;
149 |
150 | const userAuth = await UserAuthModel.find({ $or:[ {email:email_username},{username:email_username} ]});
151 | if(userAuth.length == 0){
152 | return res.status(401).json({
153 | msg:"User doesn't exist",
154 | result:false
155 | });
156 | }
157 |
158 | const passwordCheck = await bcrypt.compare(req.body.password,userAuth[0].password);
159 | if(!passwordCheck){
160 | return res.status(401).json({
161 | msg:"Wrong password",
162 | result:false
163 | })
164 | }
165 |
166 | const userDetails = await UserDetailsModel.find({$or:[ {email:email_username},{username:email_username} ]});
167 | if(!userDetails){
168 | res.status(404).json({
169 | msg:"UserDetails not found",
170 | result:false
171 | })
172 | }
173 |
174 | // Create a payload for JWT
175 |
176 | let payload = {
177 | email: userDetails[0].email,
178 | username:userDetails[0].username,
179 | id:userDetails[0]._id,
180 | eth_address:userAuth[0].eth_address,
181 | eth_private_key:userAuth[0].eth_private_key
182 | };
183 |
184 | // Create a JWT token
185 |
186 | const token = generateToken(payload);
187 |
188 | return res.status(200).json({
189 | email: userDetails[0].email,
190 | username:userDetails[0].username,
191 | id:userDetails[0]._id,
192 | token:token,
193 | result:true
194 | });
195 |
196 | }
197 | catch(err){
198 | console.log(err);
199 | res.status(500).json({
200 | msg:"There was an issue in login",
201 | result:false
202 | })
203 | }
204 |
205 | }
206 |
207 |
208 | const userVerify = async (req,res)=>{
209 | try{
210 | const userData = await UserAuthModel.find({ $or:[ {email:req.decoded.email},{username:req.decoded.username}]});
211 | if(userData.length>0){
212 | console.log(req.decoded);
213 | res.status(200).json({
214 | msg:"User is a valid registered Collective user",
215 | result:true
216 | })
217 | }
218 | else{
219 | res.status(404).json({
220 | error:"User doesn't exist",
221 | result:false
222 | })
223 | }
224 | }catch(err){
225 | return res.status(500).json({
226 | msg:"Failed to verify user.",
227 | result:false,
228 | err
229 | });
230 | }
231 | }
232 |
233 | module.exports = {
234 | userRegister,
235 | userLogin,
236 | userVerify
237 | }
--------------------------------------------------------------------------------
/frontend/lib/widgets/CampListViewWidget.dart:
--------------------------------------------------------------------------------
1 | import 'package:collective/screens/CampScreen.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:progressive_image/progressive_image.dart';
4 |
5 | class CampListViewWidget extends StatelessWidget {
6 | final AsyncSnapshot snapshot;
7 |
8 | CampListViewWidget(this.snapshot);
9 |
10 | @override
11 | Widget build(BuildContext context) {
12 | return ListView.builder(
13 | padding: EdgeInsets.only(top: 10, left: 6, right: 6, bottom: 200),
14 | itemBuilder: (context, index) {
15 | return Container(
16 | margin: EdgeInsets.only(bottom: 25),
17 | decoration: BoxDecoration(
18 | borderRadius: BorderRadius.circular(15),
19 | color: Color.fromRGBO(240, 240, 240, 1),
20 | boxShadow: [
21 | BoxShadow(
22 | color: Colors.grey,
23 | blurRadius: 0.0,
24 | ),
25 | ],
26 | ),
27 | height: 360,
28 | width: MediaQuery.of(context).size.width,
29 | child: Column(
30 | children: [
31 | Row(
32 | children: [
33 | Container(
34 | height: 180,
35 | width: MediaQuery.of(context).size.width - 32,
36 | child: ClipRRect(
37 | borderRadius: BorderRadius.only(
38 | topLeft: Radius.circular(15),
39 | topRight: Radius.circular(15)),
40 | child: ProgressiveImage(
41 | placeholder:
42 | AssetImage('assets/images/placeholder.jpg'),
43 | thumbnail: AssetImage('assets/images/placeholder.jpg'),
44 | image: NetworkImage(
45 | snapshot.data['details'][index]['camp_image']),
46 | fit: BoxFit.cover,
47 | height: 180,
48 | width: MediaQuery.of(context).size.width - 32,
49 | ),
50 | ),
51 | ),
52 | ],
53 | ),
54 | Row(
55 | children: [
56 | Container(
57 | padding: EdgeInsets.all(15),
58 | height: 180,
59 | width: MediaQuery.of(context).size.width - 32,
60 | decoration: BoxDecoration(
61 | borderRadius: BorderRadius.only(
62 | bottomLeft: Radius.circular(15),
63 | bottomRight: Radius.circular(15),
64 | ),
65 | color: Color.fromRGBO(250, 250, 250, 1),
66 | ),
67 | child: Column(
68 | children: [
69 | Container(
70 | height: 30,
71 | width: 326,
72 | child: Row(
73 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
74 | children: [
75 | Text(
76 | snapshot.data['details'][index]['name'],
77 | style: TextStyle(
78 | fontWeight: FontWeight.bold,
79 | fontSize: 20,
80 | ),
81 | ),
82 | Padding(
83 | padding: const EdgeInsets.only(right: 0),
84 | child: Text(
85 | '- ' +
86 | snapshot.data['details'][index]
87 | ['category'],
88 | style: TextStyle(
89 | fontSize: 14,
90 | color: Theme.of(context).primaryColor),
91 | ),
92 | ),
93 | ],
94 | ),
95 | ),
96 | Container(
97 | height: 64,
98 | width: 326,
99 | child: Padding(
100 | padding: const EdgeInsets.only(top: 2.0),
101 | child: Text(
102 | snapshot.data['details'][index]
103 | ['camp_description'],
104 | textAlign: TextAlign.start,
105 | style: TextStyle(
106 | fontSize: 15.5,
107 | color: Colors.grey[600],
108 | ),
109 | ),
110 | ),
111 | ),
112 | Divider(),
113 | Container(
114 | padding: EdgeInsets.only(top: 6),
115 | height: 40,
116 | child: Row(
117 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
118 | children: [
119 | Row(
120 | children: [
121 | Image.asset(
122 | 'assets/images/LogoNoPadding.png',
123 | width: 27,
124 | height: 27,
125 | ),
126 | Padding(
127 | padding:
128 | const EdgeInsets.only(top: 4, left: 3),
129 | child: Text(
130 | snapshot.data['details'][index]['target']
131 | .toString()
132 | .replaceAllMapped(
133 | new RegExp(
134 | r'(\d{1,3})(?=(\d{3})+(?!\d))'),
135 | (Match m) => '${m[1]},'),
136 | style: TextStyle(
137 | fontSize: 20,
138 | fontWeight: FontWeight.bold,
139 | color:
140 | Theme.of(context).primaryColor),
141 | ),
142 | ),
143 | ],
144 | ),
145 | ElevatedButton(
146 | style: ElevatedButton.styleFrom(
147 | elevation: 0,
148 | primary: Theme.of(context).primaryColor,
149 | shape: RoundedRectangleBorder(
150 | borderRadius:
151 | BorderRadius.circular(60)),
152 | minimumSize: Size(100, 45)),
153 | onPressed: () {
154 | Navigator.of(context).pushNamed(
155 | CampScreen.routeName,
156 | arguments: {
157 | 'campAddress': snapshot.data['details']
158 | [index]['address']
159 | });
160 | },
161 | child: Padding(
162 | padding: const EdgeInsets.only(top: 2),
163 | child: Text(
164 | 'Checkout',
165 | style: TextStyle(
166 | fontWeight: FontWeight.normal,
167 | fontSize: 16,
168 | ),
169 | ),
170 | ),
171 | ),
172 | ],
173 | ),
174 | )
175 | ],
176 | ),
177 | ),
178 | ],
179 | ),
180 | ],
181 | ),
182 | );
183 | },
184 | itemCount: snapshot.data['details'].length,
185 | );
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/frontend/lib/screens/HomeScreen.dart:
--------------------------------------------------------------------------------
1 | import 'package:bottom_navy_bar/bottom_navy_bar.dart';
2 | import 'package:collective/screens/CreateCampHomeScreen.dart';
3 | import 'package:collective/screens/SearchCampScreen.dart';
4 | import 'package:collective/screens/SupportEmailScreen.dart';
5 | import 'package:collective/screens/UserDetailsScreen.dart';
6 | import 'package:collective/screens/UserInvestmentScreen.dart';
7 | import 'package:collective/screens/UsersCampScreen.dart';
8 | import 'package:collective/screens/UsersCollabScreen.dart';
9 | import 'package:collective/widgets/HomeScreenWidget.dart';
10 | import 'package:collective/widgets/keepAlivePage.dart';
11 | import 'package:flutter/material.dart';
12 | import 'package:flutter/services.dart';
13 |
14 | class HomeScreen extends StatefulWidget {
15 | static const routeName = '/homeScreen';
16 |
17 | @override
18 | _HomeScreenState createState() => _HomeScreenState();
19 | }
20 |
21 | class _HomeScreenState extends State {
22 | int _currentIndex = 1;
23 | PageController _pageController;
24 |
25 | final GlobalKey _scaffoldKey = GlobalKey();
26 |
27 | @override
28 | void initState() {
29 | super.initState();
30 | _pageController = PageController(initialPage: 1);
31 | }
32 |
33 | @override
34 | void dispose() {
35 | _pageController.dispose();
36 | super.dispose();
37 | }
38 |
39 | @override
40 | Widget build(BuildContext context) {
41 | SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
42 | statusBarColor: Colors.white,
43 | statusBarIconBrightness: Brightness.light,
44 | ));
45 | return Scaffold(
46 | key: _scaffoldKey,
47 | endDrawerEnableOpenDragGesture: false,
48 | backgroundColor: Colors.white,
49 | appBar: AppBar(
50 | backgroundColor: Colors.white,
51 | leading: IconButton(
52 | icon: const Icon(Icons.menu),
53 | color: Colors.black,
54 | iconSize: 25,
55 | onPressed: () {
56 | _scaffoldKey.currentState.openDrawer();
57 | },
58 | ),
59 | elevation: 2,
60 | centerTitle: true,
61 | title: Container(
62 | width: 155,
63 | child: Row(
64 | children: [
65 | Image.asset(
66 | 'assets/images/Logo.png',
67 | width: 32,
68 | height: 32,
69 | ),
70 | Padding(
71 | padding: const EdgeInsets.only(top: 4.5, left: 3),
72 | child: Text(
73 | 'Collective',
74 | style: TextStyle(
75 | color: Theme.of(context).primaryColor,
76 | fontWeight: FontWeight.bold,
77 | fontSize: 25,
78 | ),
79 | ),
80 | ),
81 | ],
82 | ),
83 | ),
84 | actions: [
85 | IconButton(
86 | icon: Icon(
87 | Icons.person,
88 | color: Colors.black,
89 | ),
90 | onPressed: () {
91 | Navigator.of(context).pushNamed(UserDetailsScreen.routeName);
92 | })
93 | ],
94 | ),
95 | drawer: Drawer(
96 | child: Container(
97 | child: ListView(
98 | children: [
99 | SizedBox(
100 | height: 64,
101 | child: DrawerHeader(
102 | decoration: BoxDecoration(
103 | color: Color.fromRGBO(240, 240, 240, 1),
104 | ),
105 | child: Row(
106 | children: [
107 | Padding(
108 | padding: const EdgeInsets.only(top: 0.0, left: 0.0),
109 | child: Text(
110 | 'Collective',
111 | style: TextStyle(
112 | color: Theme.of(context).primaryColor,
113 | fontSize: 24,
114 | fontWeight: FontWeight.bold,
115 | ),
116 | ),
117 | ),
118 | ],
119 | ),
120 | ),
121 | ),
122 | ListTile(
123 | leading: Icon(
124 | Icons.person,
125 | color: Theme.of(context).primaryColor,
126 | ),
127 | title: Text('Account'),
128 | onTap: () {
129 | Navigator.of(context).pushNamed(UserDetailsScreen.routeName);
130 | },
131 | ),
132 | Divider(),
133 | ListTile(
134 | leading: Icon(
135 | Icons.campaign,
136 | color: Theme.of(context).primaryColor,
137 | ),
138 | title: Text('Camps owned'),
139 | onTap: () {
140 | Navigator.of(context).pushNamed(UsersCampScreen.routeName);
141 | },
142 | ),
143 | Divider(),
144 | ListTile(
145 | leading: Icon(
146 | Icons.monetization_on,
147 | color: Theme.of(context).primaryColor,
148 | ),
149 | title: Text('Investments'),
150 | onTap: () {
151 | Navigator.of(context)
152 | .pushNamed(UserInvestmentScreen.routeName);
153 | }),
154 | Divider(),
155 | ListTile(
156 | leading: Icon(
157 | Icons.group,
158 | color: Theme.of(context).primaryColor,
159 | ),
160 | title: Text('Collaborations'),
161 | onTap: () {
162 | Navigator.of(context).pushNamed(UsersCollabScreen.routeName);
163 | },
164 | ),
165 | Divider(),
166 | ListTile(
167 | leading: Icon(
168 | Icons.help_rounded,
169 | color: Theme.of(context).primaryColor,
170 | ),
171 | title: Text('Support'),
172 | onTap: () {
173 | Navigator.of(context).pushNamed(SupportEmailScreen.routeName);
174 | },
175 | ),
176 | Divider()
177 | ],
178 | ),
179 | ),
180 | ),
181 | body: PageView(
182 | controller: _pageController,
183 | onPageChanged: (index) {
184 | setState(() => _currentIndex = index);
185 | },
186 | children: [
187 | KeepAlivePage(child: SearchCampScreen()),
188 | KeepAlivePage(child: HomeScreenWidget()),
189 | CreateCampHomeScreen(),
190 | ],
191 | ),
192 | bottomNavigationBar: BottomNavyBar(
193 | selectedIndex: _currentIndex,
194 | showElevation: true,
195 | itemCornerRadius: 24,
196 | containerHeight: 47,
197 | mainAxisAlignment: MainAxisAlignment.spaceAround,
198 | curve: Curves.easeIn,
199 | backgroundColor: Colors.white,
200 | onItemSelected: (index) {
201 | setState(() => _currentIndex = index);
202 | _pageController.jumpToPage(index);
203 | },
204 | items: [
205 | BottomNavyBarItem(
206 | icon: Icon(
207 | Icons.search,
208 | color: Theme.of(context).primaryColor,
209 | ),
210 | title: Padding(
211 | padding: const EdgeInsets.only(top: 1.0),
212 | child: Text(
213 | 'Search',
214 | style: TextStyle(
215 | fontSize: 16, color: Theme.of(context).primaryColor),
216 | ),
217 | ),
218 | inactiveColor: Colors.black26,
219 | activeColor: Colors.grey[400],
220 | textAlign: TextAlign.center,
221 | ),
222 | BottomNavyBarItem(
223 | icon: Icon(
224 | Icons.campaign,
225 | color: Theme.of(context).primaryColor,
226 | ),
227 | title: Padding(
228 | padding: const EdgeInsets.only(top: 1.0),
229 | child: Text(
230 | 'Camps',
231 | style: TextStyle(
232 | fontSize: 16, color: Theme.of(context).primaryColor),
233 | ),
234 | ),
235 | inactiveColor: Colors.black26,
236 | activeColor: Colors.grey[400],
237 | textAlign: TextAlign.center,
238 | ),
239 | BottomNavyBarItem(
240 | icon: Icon(
241 | Icons.add_circle_rounded,
242 | color: Theme.of(context).primaryColor,
243 | ),
244 | title: Padding(
245 | padding: const EdgeInsets.only(top: 1.0),
246 | child: Text(
247 | 'Create',
248 | style: TextStyle(
249 | fontSize: 16, color: Theme.of(context).primaryColor),
250 | ),
251 | ),
252 | inactiveColor: Colors.black26,
253 | activeColor: Colors.grey[400],
254 | textAlign: TextAlign.center,
255 | ),
256 | ],
257 | ),
258 | );
259 | }
260 | }
261 |
--------------------------------------------------------------------------------
/backend/test/Camps.test.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const ganache = require('ganache-cli');
3 | const Web3 = require('web3');
4 |
5 | const web3 = new Web3(ganache.provider());
6 |
7 |
8 | ///////////////////////////
9 | // Contract setup
10 | ///////////////////////////
11 |
12 |
13 | const Camps = require('../../backend/build/contracts/Camps.json');
14 |
15 | const abi = Camps.abi;
16 |
17 | const bytecode = Camps.bytecode;
18 |
19 | let accounts;
20 |
21 | let contract;
22 |
23 |
24 | // A normal test for creating a new camp and user investing in it
25 |
26 | describe('Create camp,buy equity and get details', ()=>{
27 | before(async()=>{
28 | accounts = await web3.eth.getAccounts();
29 | contract = await new web3.eth.Contract(abi)
30 | .deploy({data:bytecode})
31 | .send({
32 | from:accounts[0],
33 | gas:'2500000',
34 | });
35 |
36 | })
37 |
38 | it('Create a new camp',async()=>{
39 | const camp = await contract.methods.createCamp(accounts[2],100,10).send({
40 | from:accounts[1],
41 | to:contract.options.address,
42 | gas:'2500000',
43 | })
44 | assert.ok(camp);
45 | })
46 |
47 | it('Buy equity',async()=>{
48 | const camp = await contract.methods.buyEquity(accounts[1],accounts[2],10).send({
49 | from:accounts[1],
50 | gas:'2500000',
51 | })
52 | assert.ok(camp);
53 | })
54 |
55 | it('Get camp details',async()=>{
56 | const campDetails = await contract.methods.camps(accounts[2]).call({});
57 | assert.strictEqual(
58 | '10',campDetails[1]
59 | );
60 | })
61 | });
62 |
63 |
64 | // Third transaction should not succeed and throw error
65 |
66 | describe('Buying over the target', ()=>{
67 | before(async()=>{
68 | accounts = await web3.eth.getAccounts();
69 | contract = await new web3.eth.Contract(abi)
70 | .deploy({data:bytecode})
71 | .send({
72 | from:accounts[0],
73 | gas:'2500000',
74 | });
75 |
76 | })
77 |
78 | it('Create a new camp',async()=>{
79 | const camp = await contract.methods.createCamp(accounts[2],50,10).send({
80 | from:accounts[1],
81 | to:contract.options.address,
82 | gas:'2500000',
83 | })
84 | assert.ok(camp);
85 | })
86 |
87 | it('Buy equity - 30',async()=>{
88 | const camp = await contract.methods.buyEquity(accounts[1],accounts[2],30).send({
89 | from:accounts[1],
90 | gas:'2500000',
91 | })
92 | assert.ok(camp);
93 | });
94 |
95 |
96 | it('Buy equity - 20',async()=>{
97 | const camp = await contract.methods.buyEquity(accounts[3],accounts[2],20).send({
98 | from:accounts[3],
99 | gas:'2500000',
100 | })
101 | assert.ok(camp);
102 | });
103 |
104 | it('Buy equity - 10 (Should not go through)',async()=>{
105 | try{
106 | await contract.methods.buyEquity(accounts[4],accounts[2],10).send({
107 | from:accounts[4],
108 | gas:'2500000',
109 | })
110 | }
111 | catch(e){
112 | assert.strictEqual(e.message,'VM Exception while processing transaction: revert Camp not found');
113 | }
114 | });
115 |
116 |
117 | it('Checking the amount raised',async()=>{
118 | const campDetails = await contract.methods.camps(accounts[2]).call({});
119 | assert.strictEqual(
120 | '50',campDetails[1]
121 | );
122 | })
123 | });
124 |
125 |
126 |
127 | // Checking the Angels and the numbers of Angels in the AngelList
128 |
129 | describe('Checking the Angels and the numbers of Angels in the AngelList', ()=>{
130 | before(async()=>{
131 | accounts = await web3.eth.getAccounts();
132 | contract = await new web3.eth.Contract(abi)
133 | .deploy({data:bytecode})
134 | .send({
135 | from:accounts[0],
136 | gas:'2500000',
137 | });
138 |
139 | })
140 |
141 | it('Create a new camp',async()=>{
142 | const camp = await contract.methods.createCamp(accounts[2],50,10).send({
143 | from:accounts[1],
144 | to:contract.options.address,
145 | gas:'2500000',
146 | })
147 | assert.ok(camp);
148 | })
149 |
150 | it('Buy equity - 20',async()=>{
151 | const camp = await contract.methods.buyEquity(accounts[1],accounts[2],20).send({
152 | from:accounts[1],
153 | gas:'2500000',
154 | })
155 | assert.ok(camp);
156 | });
157 |
158 |
159 | it('Buy equity - 20',async()=>{
160 | const camp = await contract.methods.buyEquity(accounts[3],accounts[2],20).send({
161 | from:accounts[3],
162 | gas:'2500000',
163 | })
164 | assert.ok(camp);
165 | });
166 |
167 | it('Buy equity - 10',async()=>{
168 |
169 | const camp = await contract.methods.buyEquity(accounts[4],accounts[2],10).send({
170 | from:accounts[4],
171 | gas:'2500000',
172 | })
173 |
174 | assert.ok(camp);
175 |
176 | });
177 |
178 |
179 | it('Checking the amount raised',async()=>{
180 | const campDetails = await contract.methods.camps(accounts[2]).call({});
181 | assert.strictEqual(
182 | '50',campDetails[1]
183 | );
184 | })
185 |
186 | it('Checking the angel list length',async()=>{
187 | const angelListLength = await contract.methods.getAngelListLength(accounts[2]).call({});
188 | assert.notStrictEqual(2,angelListLength);
189 | });
190 |
191 |
192 | it('Checking the angel address in the angel list',async()=>{
193 | const angelList = await contract.methods.getAngelList(accounts[2]).call({});
194 | assert.notStrictEqual([accounts[1],accounts[3],accounts[4]],angelList);
195 | });
196 | });
197 |
198 |
199 |
200 | // Checking the funding amount for a angel
201 |
202 | describe('Checking the funding amount for a angel', ()=>{
203 | before(async()=>{
204 | accounts = await web3.eth.getAccounts();
205 | contract = await new web3.eth.Contract(abi)
206 | .deploy({data:bytecode})
207 | .send({
208 | from:accounts[0],
209 | gas:'2500000',
210 | });
211 |
212 | })
213 |
214 | it('Create a new camp',async()=>{
215 | const camp = await contract.methods.createCamp(accounts[2],50,10).send({
216 | from:accounts[1],
217 | to:contract.options.address,
218 | gas:'2500000',
219 | })
220 | assert.ok(camp);
221 | })
222 |
223 | it('Buy equity - 20',async()=>{
224 | const camp = await contract.methods.buyEquity(accounts[1],accounts[2],20).send({
225 | from:accounts[1],
226 | gas:'2500000',
227 | })
228 | assert.ok(camp);
229 | });
230 |
231 |
232 | it('Buy equity - 25',async()=>{
233 | const camp = await contract.methods.buyEquity(accounts[3],accounts[2],25).send({
234 | from:accounts[3],
235 | gas:'2500000',
236 | })
237 | assert.ok(camp);
238 | });
239 |
240 |
241 | it('Checking the amount raised',async()=>{
242 | const campDetails = await contract.methods.camps(accounts[2]).call({});
243 | assert.strictEqual(
244 | '45',campDetails[1]
245 | );
246 | });
247 |
248 | it('Checking the funding amount for a angel',async()=>{
249 | const fundingDetails = await contract.methods.funding(accounts[2],accounts[3]).call();
250 | assert.strictEqual('25',fundingDetails)
251 | });
252 |
253 | });
254 |
255 |
256 | // Collaboration in camp
257 |
258 | describe('Camp Collaboration', ()=>{
259 | before(async()=>{
260 | accounts = await web3.eth.getAccounts();
261 | contract = await new web3.eth.Contract(abi)
262 | .deploy({data:bytecode})
263 | .send({
264 | from:accounts[0],
265 | gas:'2500000',
266 | });
267 |
268 | })
269 |
270 | it('Create a new camp',async()=>{
271 | const camp = await contract.methods.createCamp(accounts[2],50,10).send({
272 | from:accounts[1],
273 | to:contract.options.address,
274 | gas:'2500000',
275 | })
276 | assert.ok(camp);
277 | })
278 |
279 | it('Buy equity - 20',async()=>{
280 | const camp = await contract.methods.buyEquity(accounts[1],accounts[2],20).send({
281 | from:accounts[1],
282 | gas:'2500000',
283 | })
284 | assert.ok(camp);
285 | });
286 |
287 |
288 |
289 |
290 | it('Checking the amount raised',async()=>{
291 | const campDetails = await contract.methods.camps(accounts[2]).call({});
292 | assert.strictEqual(
293 | '20',campDetails[1]
294 | );
295 | });
296 |
297 | it('Adding First collaborator with amount',async()=>{
298 | const collab = await contract.methods.collab(accounts[5],accounts[2],'Software developer',10).send({
299 | from:accounts[5],
300 | gas:'2500000',
301 | })
302 |
303 | assert.ok(collab);
304 | });
305 |
306 |
307 | it('Adding Second collaborator with amount',async()=>{
308 | const collab = await contract.methods.collab(accounts[6],accounts[2],'Blockchain developer',20).send({
309 | from:accounts[6],
310 | gas:'2500000',
311 | })
312 |
313 | assert.ok(collab);
314 | });
315 |
316 | it('Fetching and checking camp collaborator',async()=>{
317 | const collabDetails = await contract.methods.getCollabDetails(accounts[2]).call();
318 | assert.strictEqual(accounts[6],collabDetails[1][0]);
319 | })
320 |
321 | });
322 |
--------------------------------------------------------------------------------
/backend/controllers/collectiveControllers/collectiveController.js:
--------------------------------------------------------------------------------
1 | const Tx = require('ethereumjs-tx').Transaction;
2 | const Web3 = require('web3');
3 | const moment = require('moment-timezone');
4 | const CryptoJS = require("crypto-js");
5 | const axios = require("axios");
6 | const nodemailer = require('nodemailer');
7 |
8 | const CampModel = require("../../models/campDetailsModel");
9 | const UserDetailsModel = require("../../models/userDetailsModel");
10 |
11 | const { validationResult } = require("express-validator");
12 | require('dotenv').config();
13 |
14 |
15 | ///////////////////////////
16 | //Web3 and contract setup
17 | ///////////////////////////
18 |
19 | const rpcURL = 'https://ropsten.infura.io/v3/7a0de82adffe468d8f3c1e2183b37c39';
20 |
21 | const web3 = new Web3(rpcURL);
22 |
23 | const CTVToken = require('../../build/contracts/CollectiveToken.json');
24 |
25 | const ctv_contract_address = process.env.ctv_contract_address;
26 |
27 | const ctvabi = CTVToken.abi;
28 |
29 | const ctv_contract = new web3.eth.Contract(ctvabi,ctv_contract_address);
30 |
31 | ////////////////////////////////////
32 | // Account addresses & Private keys
33 | ////////////////////////////////////
34 |
35 | //Main account with which contract is deployed
36 |
37 | const account_address_1 = process.env.account_1;
38 |
39 | // Main private key - token generation
40 |
41 | const privateKey1 = Buffer.from(process.env.privateKey_1,'hex');
42 |
43 |
44 | const getUserDetails = async (req,res)=>{
45 | try{
46 |
47 | const userData = await UserDetailsModel.find({username:req.decoded.username});
48 | if(userData.length>0){
49 | res.status(200).json({
50 | msg:"User is a valid registered Collective user",
51 | result:true,
52 | userData:userData[0],
53 | userAuthData:req.decoded
54 | })
55 | }
56 | else{
57 | res.status(404).json({
58 | error:"User doesn't exist",
59 | result:false
60 | })
61 | }
62 | }catch(err){
63 | return res.status(500).json({
64 | msg:"Failed to fetch user data.",
65 | result:false,
66 | err
67 | });
68 | }
69 | }
70 |
71 |
72 | const withdrawAmount = async (req,res)=>{
73 | try{
74 |
75 | //Input field validation
76 | const errors = validationResult(req);
77 | if (!errors.isEmpty()) {
78 | return res.status(422).json({
79 | error: errors.array()[0],result:false
80 | });
81 | }
82 |
83 | const owner_address = req.body.owner_address;
84 | const camp_private_key = req.body.owner_private_key;
85 | const transfer_address = req.decoded.eth_address;
86 | const amount = req.body.amount;
87 |
88 |
89 | let bytes = CryptoJS.AES.decrypt(camp_private_key, process.env.master_key);
90 | let bytes_key = bytes.toString(CryptoJS.enc.Utf8).slice(2);
91 | let owner_private_key = Buffer.from(bytes_key,'hex');
92 | const estGasPrice = await web3.eth.getGasPrice()*2;
93 |
94 | res.status(200).json({
95 | msg:"Amount withdrawal in-progress",
96 | result:true,
97 | });
98 |
99 | ///////////////////////////////////////////////////////////////
100 | // Transferring ETH(gas) required for the transaction to owner
101 | ///////////////////////////////////////////////////////////////
102 |
103 | const txCount = await web3.eth.getTransactionCount(account_address_1);
104 | if(!txCount){
105 | return res.status(500).json({
106 | result:false,
107 | msg:'There was a problem transferring ETH - gas for the transaction'
108 | })
109 | }
110 |
111 | // Build the transaction
112 | const txObject1 = {
113 | nonce: web3.utils.toHex(txCount),
114 | to: owner_address,
115 | value: web3.utils.toHex(web3.utils.toWei('100000000', 'gwei')),
116 | gasLimit: web3.utils.toHex(500000),
117 | gasPrice: web3.utils.toHex(estGasPrice),
118 | }
119 |
120 | // Sign the transaction
121 | const tx1 = new Tx(txObject1,{chain:3})
122 | tx1.sign(privateKey1)
123 |
124 | const serializedTx1 = tx1.serialize()
125 | const raw1 = '0x' + serializedTx1.toString('hex')
126 |
127 | // Broadcast the transaction
128 | const sendTransaction = await web3.eth.sendSignedTransaction(raw1);
129 | if(!sendTransaction){
130 | return res.status(500).json({
131 | result:false,
132 | msg:'There was a problem transferring ETH - gas for the transaction'
133 | })
134 | }
135 | console.log('\nETH transfered for the transaction');
136 |
137 |
138 |
139 | //////////////////////////////////////////////////////////////
140 | // Getting approval for the transaction
141 | //////////////////////////////////////////////////////////////
142 |
143 | const ownertxCount = await web3.eth.getTransactionCount(owner_address);
144 | console.log("Approval txCount : "+ownertxCount);
145 |
146 | // Build the transaction
147 | const txObject2 = {
148 | nonce: web3.utils.toHex(ownertxCount),
149 | to: ctv_contract_address,
150 | gasLimit: web3.utils.toHex(50000),
151 | gasPrice: web3.utils.toHex(estGasPrice),
152 | data: ctv_contract.methods.increaseAllowance(owner_address,amount).encodeABI()
153 | }
154 |
155 | // Sign the transaction
156 | const tx2 = new Tx(txObject2,{chain:3})
157 | tx2.sign(owner_private_key)
158 |
159 | const serializedTx2 = tx2.serialize()
160 | const raw2 = '0x' + serializedTx2.toString('hex')
161 |
162 | // Broadcast the transaction
163 | const approvalHash = await web3.eth.sendSignedTransaction(raw2);
164 |
165 | if(!approvalHash){
166 | return res.status(500).json({
167 | result:false,
168 | msg:'There was a problem getting approval for transaction'
169 | })
170 | }
171 |
172 | console.log("\nTransfer approved");
173 |
174 |
175 |
176 | /////////////////////////////////
177 | // Transfering CTV between users
178 | /////////////////////////////////
179 |
180 |
181 | const ownertxCountUpdated = await web3.eth.getTransactionCount(owner_address);
182 | console.log("Transfer txCount : "+ownertxCountUpdated);
183 |
184 |
185 | // Build the transaction
186 | const txObject3 = {
187 | nonce: web3.utils.toHex(ownertxCountUpdated),
188 | to: ctv_contract_address,
189 | gasLimit: web3.utils.toHex(100000),
190 | gasPrice: web3.utils.toHex(estGasPrice),
191 | data: ctv_contract.methods.transferFrom(owner_address,transfer_address,amount).encodeABI()
192 | }
193 |
194 |
195 | // Sign the transaction2
196 | const tx3 = new Tx(txObject3,{chain:3})
197 | tx3.sign(owner_private_key)
198 |
199 | const serializedTx3 = tx3.serialize()
200 | const raw3 = '0x' + serializedTx3.toString('hex')
201 |
202 | // Broadcast the transaction
203 | const finalTransactionHash = await web3.eth.sendSignedTransaction(raw3);
204 | if(!finalTransactionHash){
205 | return res.status(500).json({
206 | result:false,
207 | msg:'There was a problem transferring CTV between users.'
208 | })
209 | }
210 |
211 | console.log(`\nCTV transfered between accounts : ${amount} CTV`);
212 |
213 | const userData = await CampModel.findOneAndUpdate({address:owner_address},{amountWithdrawn:true});
214 | if(userData){
215 | console.log("Amount withdrawal successful");
216 | }
217 | else{
218 | console.log("Camp doesnt exits!");
219 | }
220 |
221 |
222 |
223 | }catch(err){
224 | console.log(err);
225 | return res.status(500).json({
226 | msg:"Failed to withdraw amount!",
227 | result:false,
228 | });
229 | }
230 | }
231 |
232 |
233 | // Support/Help email API
234 |
235 | const transporter = nodemailer.createTransport({
236 | service: 'gmail',
237 | auth: {
238 | user: process.env.email_id,
239 | pass: process.env.email_password
240 | },
241 | tls: {
242 | rejectUnauthorized: false
243 | }
244 | });
245 |
246 |
247 | const supportEmail = async(req,res)=>{
248 | try{
249 | //Input field validation
250 | const errors = validationResult(req);
251 | if (!errors.isEmpty()) {
252 | return res.status(422).json({
253 | error: errors.array()[0],result:false
254 | });
255 | }
256 |
257 | let { email_subject,email_message } = req.body;
258 | let email_sender = req.decoded.email;
259 |
260 | // Using nodemailer to send support email
261 |
262 | const mailOptions = {
263 | from: email_sender,
264 | to: 'collectivecfplatform@gmail.com',
265 | subject: email_sender+" - "+email_subject,
266 | text: email_sender+" - "+email_message
267 | };
268 |
269 | transporter.sendMail(mailOptions, function(error, info){
270 | if (error) {
271 | console.log(error);
272 | return res.status(500).json({
273 | msg:"There was a problem sending the email",
274 | result:false,
275 | error
276 | });
277 | } else {
278 | return res.status(200).json({
279 | msg:"Email sent, please wait for the response from our support team",
280 | result:true
281 | })
282 | }
283 | });
284 |
285 | }
286 | catch(err){
287 | console.log(err);
288 | return res.status(500).json({
289 | msg:"There was an error sending a message to our team, Please try again.",
290 | result:false,
291 | });
292 | }
293 | }
294 |
295 |
296 | module.exports = {
297 | getUserDetails,
298 | withdrawAmount,
299 | supportEmail
300 | }
--------------------------------------------------------------------------------
/frontend/lib/screens/LoginScreen.dart:
--------------------------------------------------------------------------------
1 | import 'dart:ui';
2 |
3 | import 'package:collective/screens/HomeScreen.dart';
4 | import 'package:collective/screens/RegisterScreen.dart';
5 | import 'package:flutter/material.dart';
6 | import 'package:flutter/services.dart';
7 |
8 | import 'dart:async';
9 | import 'dart:convert';
10 | import 'package:http/http.dart' as http;
11 | import 'package:form_field_validator/form_field_validator.dart';
12 | import 'package:shared_preferences/shared_preferences.dart';
13 |
14 | class LoginScreen extends StatefulWidget {
15 | static const routeName = '/loginScreen';
16 |
17 | @override
18 | _LoginScreenState createState() => _LoginScreenState();
19 | }
20 |
21 | class _LoginScreenState extends State {
22 | TextEditingController emailUsernameController = new TextEditingController();
23 | TextEditingController passwordController = new TextEditingController();
24 |
25 | final _formKey = GlobalKey();
26 |
27 | @override
28 | void dispose() {
29 | emailUsernameController.dispose();
30 | passwordController.dispose();
31 | super.dispose();
32 | }
33 |
34 | @override
35 | Widget build(BuildContext context) {
36 | SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
37 | statusBarColor: Colors.white,
38 | statusBarIconBrightness: Brightness.dark,
39 | ));
40 | return Scaffold(
41 | backgroundColor: Theme.of(context).backgroundColor,
42 | body: SafeArea(
43 | child: SingleChildScrollView(
44 | child: Column(
45 | children: [
46 | Container(
47 | height: 90,
48 | padding: EdgeInsets.only(top: 0),
49 | decoration: BoxDecoration(
50 | color: Colors.white,
51 | ),
52 | child: Row(
53 | mainAxisAlignment: MainAxisAlignment.center,
54 | children: [
55 | Image.asset(
56 | 'assets/images/LogoNoPadding.png',
57 | height: 32,
58 | width: 32,
59 | ),
60 | Padding(
61 | padding: const EdgeInsets.only(left: 3, top: 5),
62 | child: Text(
63 | 'Collective',
64 | style: TextStyle(
65 | color: Theme.of(context).primaryColor,
66 | fontWeight: FontWeight.bold,
67 | fontSize: 30),
68 | ),
69 | )
70 | ],
71 | ),
72 | ),
73 | Container(
74 | height: 300,
75 | padding: const EdgeInsets.only(
76 | top: 35,
77 | left: 35,
78 | right: 35,
79 | ),
80 | child: Form(
81 | key: _formKey,
82 | child: ListView(
83 | children: [
84 | Container(
85 | child: TextFormField(
86 | decoration: InputDecoration(
87 | labelText: 'Email / Username',
88 | filled: true,
89 | fillColor: Colors.grey[50],
90 | focusedBorder: OutlineInputBorder(
91 | borderRadius: BorderRadius.circular(30),
92 | borderSide: BorderSide(
93 | color: Theme.of(context).primaryColor,
94 | width: 1.5),
95 | ),
96 | enabledBorder: OutlineInputBorder(
97 | borderRadius: BorderRadius.circular(30),
98 | borderSide:
99 | BorderSide(color: Colors.grey[300], width: 1),
100 | ),
101 | ),
102 | controller: emailUsernameController,
103 | validator: RequiredValidator(
104 | errorText: 'Please provide a username or email'),
105 | ),
106 | ),
107 | Container(
108 | margin: EdgeInsets.only(top: 20),
109 | child: TextFormField(
110 | decoration: InputDecoration(
111 | labelText: 'Password',
112 | filled: true,
113 | fillColor: Colors.grey[50],
114 | focusedBorder: OutlineInputBorder(
115 | borderRadius: BorderRadius.circular(30),
116 | borderSide: BorderSide(
117 | color: Theme.of(context).primaryColor,
118 | width: 1.5),
119 | ),
120 | enabledBorder: OutlineInputBorder(
121 | borderRadius: BorderRadius.circular(30),
122 | borderSide:
123 | BorderSide(color: Colors.grey[300], width: 1),
124 | ),
125 | ),
126 | obscureText: true,
127 | controller: passwordController,
128 | validator: RequiredValidator(
129 | errorText: 'Please provide a password'),
130 | ),
131 | ),
132 | Container(
133 | margin: EdgeInsets.only(top: 25),
134 | child: ElevatedButton(
135 | onPressed: () {
136 | _formKey.currentState.validate()
137 | ? userLogin(emailUsernameController.text,
138 | passwordController.text)
139 | .then(
140 | (data) async {
141 | if (data['result'] == false) {
142 | ScaffoldMessenger.of(context)
143 | .showSnackBar(SnackBar(
144 | content: Text(
145 | "Invalid credentials",
146 | textAlign: TextAlign.center,
147 | ),
148 | ));
149 | } else if (data['result'] == true) {
150 | SharedPreferences prefs =
151 | await SharedPreferences
152 | .getInstance();
153 | prefs.setString('email', data['email']);
154 | prefs.setString(
155 | 'username', data['username']);
156 | prefs.setString('id', data['id']);
157 | prefs.setString('token', data['token']);
158 | Navigator.of(context)
159 | .pushReplacementNamed(
160 | HomeScreen.routeName);
161 | ScaffoldMessenger.of(context)
162 | .showSnackBar(SnackBar(
163 | backgroundColor:
164 | Theme.of(context).primaryColor,
165 | content: Text(
166 | "Welcome to Collective",
167 | textAlign: TextAlign.center,
168 | style: TextStyle(
169 | color: Colors.white,
170 | ),
171 | ),
172 | ));
173 | }
174 | },
175 | )
176 | : print('Invalid credentials in from field');
177 | },
178 | style: ElevatedButton.styleFrom(
179 | elevation: 0,
180 | primary: Theme.of(context).primaryColor,
181 | shape: RoundedRectangleBorder(
182 | borderRadius: BorderRadius.circular(60)),
183 | minimumSize: Size(double.infinity, 52),
184 | ),
185 | child: Text(
186 | 'LOGIN',
187 | style: TextStyle(
188 | fontWeight: FontWeight.bold,
189 | fontSize: 17,
190 | ),
191 | ),
192 | ),
193 | )
194 | ],
195 | ),
196 | ),
197 | ),
198 | Container(
199 | child: TextButton(
200 | onPressed: () {
201 | Navigator.of(context)
202 | .pushReplacementNamed(RegisterScreen.routeName);
203 | },
204 | child: Text(
205 | 'New User ? register here',
206 | style: TextStyle(
207 | color: Theme.of(context).primaryColor,
208 | fontSize: 16,
209 | ),
210 | ),
211 | ),
212 | ),
213 | Container(
214 | margin: EdgeInsets.only(top: 10),
215 | decoration: BoxDecoration(
216 | color: Colors.white,
217 | ),
218 | height: 270,
219 | child: Image.asset(
220 | 'assets/images/LoginScreen.png',
221 | fit: BoxFit.cover,
222 | ),
223 | ),
224 | ],
225 | ),
226 | ),
227 | ),
228 | );
229 | }
230 | }
231 |
232 | Future userLogin(String emailusername, String password) async {
233 | var headers = {
234 | 'x-api-key':
235 | '8\$dsfsfgreb6&4w5fsdjdjkje#\$54757jdskjrekrm@#\$@\$%&8fdddg*&*ffdsds',
236 | 'Content-Type': 'application/json'
237 | };
238 | var request =
239 | http.Request('POST', Uri.parse('http://3.135.1.141/api/userLogin'));
240 | request.body =
241 | json.encode({"email_username": emailusername, "password": password});
242 | request.headers.addAll(headers);
243 |
244 | http.StreamedResponse response = await request.send();
245 |
246 | return jsonDecode(await response.stream.bytesToString());
247 | }
248 |
--------------------------------------------------------------------------------
/backend/contracts/ERC.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.0;
3 |
4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5 | import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
6 | import "@openzeppelin/contracts/utils/Context.sol";
7 |
8 | /**
9 | * @dev Implementation of the {IERC20} interface.
10 | *
11 | * This implementation is agnostic to the way tokens are created. This means
12 | * that a supply mechanism has to be added in a derived contract using {_mint}.
13 | * For a generic mechanism see {ERC20PresetMinterPauser}.
14 | *
15 | * TIP: For a detailed writeup see our guide
16 | * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How
17 | * to implement supply mechanisms].
18 | *
19 | * We have followed general OpenZeppelin guidelines: functions revert instead
20 | * of returning `false` on failure. This behavior is nonetheless conventional
21 | * and does not conflict with the expectations of ERC20 applications.
22 | *
23 | * Additionally, an {Approval} event is emitted on calls to {transferFrom}.
24 | * This allows applications to reconstruct the allowance for all accounts just
25 | * by listening to said events. Other implementations of the EIP may not emit
26 | * these events, as it isn't required by the specification.
27 | *
28 | * Finally, the non-standard {decreaseAllowance} and {increaseAllowance}
29 | * functions have been added to mitigate the well-known issues around setting
30 | * allowances. See {IERC20-approve}.
31 | */
32 | contract ERC is Context, IERC20, IERC20Metadata {
33 | mapping (address => uint256) private _balances;
34 |
35 | mapping (address => mapping (address => uint256)) private _allowances;
36 |
37 | uint256 private _totalSupply;
38 |
39 | string private _name;
40 | string private _symbol;
41 |
42 | /**
43 | * @dev Sets the values for {name} and {symbol}.
44 | *
45 | * The defaut value of {decimals} is 18. To select a different value for
46 | * {decimals} you should overload it.
47 | *
48 | * All two of these values are immutable: they can only be set once during
49 | * construction.
50 | */
51 | constructor (string memory name_, string memory symbol_) {
52 | _name = name_;
53 | _symbol = symbol_;
54 | }
55 |
56 | /**
57 | * @dev Returns the name of the token.
58 | */
59 | function name() public view virtual override returns (string memory) {
60 | return _name;
61 | }
62 |
63 | /**
64 | * @dev Returns the symbol of the token, usually a shorter version of the
65 | * name.
66 | */
67 | function symbol() public view virtual override returns (string memory) {
68 | return _symbol;
69 | }
70 |
71 | /**
72 | * @dev Returns the number of decimals used to get its user representation.
73 | * For example, if `decimals` equals `2`, a balance of `505` tokens should
74 | * be displayed to a user as `5,05` (`505 / 10 ** 2`).
75 | *
76 | * Tokens usually opt for a value of 18, imitating the relationship between
77 | * Ether and Wei. This is the value {ERC20} uses, unless this function is
78 | * overridden;
79 | *
80 | * NOTE: This information is only used for _display_ purposes: it in
81 | * no way affects any of the arithmetic of the contract, including
82 | * {IERC20-balanceOf} and {IERC20-transfer}.
83 | */
84 | function decimals() public view virtual override returns (uint8) {
85 | return 18;
86 | }
87 |
88 | /**
89 | * @dev See {IERC20-totalSupply}.
90 | */
91 | function totalSupply() public view virtual override returns (uint256) {
92 | return _totalSupply;
93 | }
94 |
95 | /**
96 | * @dev See {IERC20-balanceOf}.
97 | */
98 | function balanceOf(address account) public view virtual override returns (uint256) {
99 | return _balances[account];
100 | }
101 |
102 | /**
103 | * @dev See {IERC20-transfer}.
104 | *
105 | * Requirements:
106 | *
107 | * - `recipient` cannot be the zero address.
108 | * - the caller must have a balance of at least `amount`.
109 | */
110 | function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
111 | _transfer(_msgSender(), recipient, amount);
112 | return true;
113 | }
114 |
115 | /**
116 | * @dev See {IERC20-allowance}.
117 | */
118 | function allowance(address owner, address spender) public view virtual override returns (uint256) {
119 | return _allowances[owner][spender];
120 | }
121 |
122 | /**
123 | * @dev See {IERC20-approve}.
124 | *
125 | * Requirements:
126 | *
127 | * - `spender` cannot be the zero address.
128 | */
129 | function approve(address spender, uint256 amount) public virtual override returns (bool) {
130 | _approve(_msgSender(), spender, amount);
131 | return true;
132 | }
133 |
134 | /**
135 | * @dev See {IERC20-transferFrom}.
136 | *
137 | * Emits an {Approval} event indicating the updated allowance. This is not
138 | * required by the EIP. See the note at the beginning of {ERC20}.
139 | *
140 | * Requirements:
141 | *
142 | * - `sender` and `recipient` cannot be the zero address.
143 | * - `sender` must have a balance of at least `amount`.
144 | * - the caller must have allowance for ``sender``'s tokens of at least
145 | * `amount`.
146 | */
147 | function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) {
148 | _transfer(sender, recipient, amount);
149 |
150 | uint256 currentAllowance = _allowances[sender][_msgSender()];
151 | require(currentAllowance >= amount, "ERC20: transfer amount exceeds allowance");
152 | _approve(sender, _msgSender(), currentAllowance - amount);
153 |
154 | return true;
155 | }
156 |
157 | /**
158 | * @dev Atomically increases the allowance granted to `spender` by the caller.
159 | *
160 | * This is an alternative to {approve} that can be used as a mitigation for
161 | * problems described in {IERC20-approve}.
162 | *
163 | * Emits an {Approval} event indicating the updated allowance.
164 | *
165 | * Requirements:
166 | *
167 | * - `spender` cannot be the zero address.
168 | */
169 | function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
170 | _approve(_msgSender(), spender, _allowances[_msgSender()][spender] + addedValue);
171 | return true;
172 | }
173 |
174 | /**
175 | * @dev Atomically decreases the allowance granted to `spender` by the caller.
176 | *
177 | * This is an alternative to {approve} that can be used as a mitigation for
178 | * problems described in {IERC20-approve}.
179 | *
180 | * Emits an {Approval} event indicating the updated allowance.
181 | *
182 | * Requirements:
183 | *
184 | * - `spender` cannot be the zero address.
185 | * - `spender` must have allowance for the caller of at least
186 | * `subtractedValue`.
187 | */
188 | function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
189 | uint256 currentAllowance = _allowances[_msgSender()][spender];
190 | require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
191 | _approve(_msgSender(), spender, currentAllowance - subtractedValue);
192 |
193 | return true;
194 | }
195 |
196 | /**
197 | * @dev Moves tokens `amount` from `sender` to `recipient`.
198 | *
199 | * This is internal function is equivalent to {transfer}, and can be used to
200 | * e.g. implement automatic token fees, slashing mechanisms, etc.
201 | *
202 | * Emits a {Transfer} event.
203 | *
204 | * Requirements:
205 | *
206 | * - `sender` cannot be the zero address.
207 | * - `recipient` cannot be the zero address.
208 | * - `sender` must have a balance of at least `amount`.
209 | */
210 | function _transfer(address sender, address recipient, uint256 amount) internal virtual {
211 | require(sender != address(0), "ERC20: transfer from the zero address");
212 | require(recipient != address(0), "ERC20: transfer to the zero address");
213 |
214 | _beforeTokenTransfer(sender, recipient, amount);
215 |
216 | uint256 senderBalance = _balances[sender];
217 | require(senderBalance >= amount, "ERC20: transfer amount exceeds balance");
218 | _balances[sender] = senderBalance - amount;
219 | _balances[recipient] += amount;
220 |
221 | emit Transfer(sender, recipient, amount);
222 | }
223 |
224 | /** @dev Creates `amount` tokens and assigns them to `account`, increasing
225 | * the total supply.
226 | *
227 | * Emits a {Transfer} event with `from` set to the zero address.
228 | *
229 | * Requirements:
230 | *
231 | * - `to` cannot be the zero address.
232 | */
233 | function _mint(address account, uint256 amount) internal virtual {
234 | require(account != address(0), "ERC20: mint to the zero address");
235 |
236 | _beforeTokenTransfer(address(0), account, amount);
237 |
238 | _totalSupply += amount;
239 | _balances[account] += amount;
240 | emit Transfer(address(0), account, amount);
241 | }
242 |
243 | /**
244 | * @dev Destroys `amount` tokens from `account`, reducing the
245 | * total supply.
246 | *
247 | * Emits a {Transfer} event with `to` set to the zero address.
248 | *
249 | * Requirements:
250 | *
251 | * - `account` cannot be the zero address.
252 | * - `account` must have at least `amount` tokens.
253 | */
254 | function _burn(address account, uint256 amount) internal virtual {
255 | require(account != address(0), "ERC20: burn from the zero address");
256 |
257 | _beforeTokenTransfer(account, address(0), amount);
258 |
259 | uint256 accountBalance = _balances[account];
260 | require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
261 | _balances[account] = accountBalance - amount;
262 | _totalSupply -= amount;
263 |
264 | emit Transfer(account, address(0), amount);
265 | }
266 |
267 | /**
268 | * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens.
269 | *
270 | * This internal function is equivalent to `approve`, and can be used to
271 | * e.g. set automatic allowances for certain subsystems, etc.
272 | *
273 | * Emits an {Approval} event.
274 | *
275 | * Requirements:
276 | *
277 | * - `owner` cannot be the zero address.
278 | * - `spender` cannot be the zero address.
279 | */
280 | function _approve(address owner, address spender, uint256 amount) internal virtual {
281 | require(owner != address(0), "ERC20: approve from the zero address");
282 | require(spender != address(0), "ERC20: approve to the zero address");
283 |
284 | _allowances[owner][spender] = amount;
285 | emit Approval(owner, spender, amount);
286 | }
287 |
288 | /**
289 | * @dev Hook that is called before any transfer of tokens. This includes
290 | * minting and burning.
291 | *
292 | * Calling conditions:
293 | *
294 | * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
295 | * will be to transferred to `to`.
296 | * - when `from` is zero, `amount` tokens will be minted for `to`.
297 | * - when `to` is zero, `amount` of ``from``'s tokens will be burned.
298 | * - `from` and `to` are never both zero.
299 | *
300 | * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
301 | */
302 | function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }
303 | }
304 |
--------------------------------------------------------------------------------