Skip to content

Instantly share code, notes, and snippets.

@rob-Hitchens
Last active February 5, 2018 11:58
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save rob-Hitchens/954f8e410bbefe021337551cb0ccc25d to your computer and use it in GitHub Desktop.
Save rob-Hitchens/954f8e410bbefe021337551cb0ccc25d to your computer and use it in GitHub Desktop.
MODULE 5 - START, Hub Contracts and UI
h3 {
font-weight:; normal;
color: #777;
font-size: 24px;
}
.black {
color: black;
}
.red {
color: red;
}
.green {
color: green;
}
var app = angular.module('HubApp', []);
app.config(function( $locationProvider) {
$locationProvider.html5Mode({
enabled: true,
requireBase: false
});
});
app.controller("HubController",
[ '$scope', '$location', '$http', '$q', '$window', '$timeout',
function($scope, $location, $http, $q, $window, $timeout) {
var hub;
Hub.deployed().then(function(instance) {
hub = instance;
newCampaignWatcher = watchForNewCampaigns();
});
txn = {}; // workaround for repetitive event emission (testRPC)
$scope.campaigns=[]; // array of structs
$scope.campaignIndex={}; // row pointers
$scope.campaignLog=[]; // verbose on-screen display of happenings
$scope.new = {}; // new campaign
$scope.campaignSelected; // campaign selector
$scope.contribution; // contribution field
// INTERACTIONS
// select account
$scope.setAccount = function() {
$scope.account = $scope.accountSelected;
$scope.balance = web3.eth.getBalance($scope.account).toString(10);
var countCampaigns = $scope.campaigns.length;
// the "User Contributed" col needs a new context, so refresh them
for(i=0; i<countCampaigns; i++) {
upsertCampaign($scope.campaigns[i].campaign);
}
console.log('Using account',$scope.account);
}
// new campaign
$scope.newCampaign = function() {
if(parseInt($scope.new.goal) > 0 && parseInt($scope.new.duration) > 0) {
hub.createCampaign($scope.new.duration, $scope.new.goal, {from: $scope.account, gas: 4000000})
.then(function(txn) {
$scope.new.goal = "";
$scope.new.duration = "";
});
} else {
alert('Integers over Zero, please');
}
}
// contribute to campaign
$scope.contribute = function() {
if($scope.campaignSelected=="") return;
if(parseInt($scope.contribution)<=0) return;
var campaign = Campaign.at($scope.campaignSelected);
var amount = $scope.contribution;
$scope.contribution = "";
campaign.contribute({from: $scope.account, value: parseInt(amount), gas: 4000000})
.then(function(txn) {
return;
});
}
// claim a refund
$scope.refund = function(campaign) {
var campaign = Campaign.at(campaign);
return campaign.requestRefund({from: $scope.account, gas: 4000000})
.then(function(txn) {
// an event will arrive
});
}
// DISPLAY
// watch hub campaigns created. UI starts here.
function watchForNewCampaigns() {
hub.LogNewCampaign( {}, {fromBlock: 0})
.watch(function(err,newCampaign) {
if(err)
{
console.error("Campaign Error:",err);
} else {
// normalizing data for output purposes
console.log("New Campaign", newCampaign);
newCampaign.args.user = newCampaign.args.sponsor;
newCampaign.args.amount = newCampaign.args.goal.toString(10);
// only if non-repetitive (testRPC)
if(typeof(txn[newCampaign.transactionHash])=='undefined')
{
$scope.campaignLog.push(newCampaign);
txn[newCampaign.transactionHash]=true;
upsertCampaign(newCampaign.args.campaign);
}
}
})
};
// watch functions for each campaign we know about
// watch receipts
function watchReceived(address) {
var campaign = Campaign.at(address);
var watcher = campaign.LogContribution( {}, {fromBlock: 0})
.watch(function(err,received) {
if(err)
{
console.error('Received Error', address, err);
} else {
console.log("Contribution", received);
if(typeof(txn[received.transactionHash+'rec'])=='undefined')
{
received.args.user = received.args.sender;
received.args.amount = parseInt(received.args.amount);
received.args.campaign = address;
$scope.campaignLog.push(received);
upsertCampaign(address);
txn[received.transactionHash+'rec']=true;
}
}
});
}
// watch refunds
function watchRefunded(address) {
var campaign = Campaign.at(address);
var watcher = campaign.LogRefundSent( {}, {fromBlock: 0})
.watch(function(err,refunded) {
if(err)
{
console.error('Refunded Error', address, err);
} else {
console.log("Refund", refunded);
if(typeof(txn[refunded.transactionHash+'ref'])=='undefined')
{
refunded.args.user = refunded.args.funder;
refunded.args.amount = parseInt(refunded.args.amount);
refunded.args.campaign = address;
$scope.campaignLog.push(refunded);
upsertCampaign(address);
txn[refunded.transactionHash+'ref']=true;
}
}
});
}
// update display (row) and instantiate campaign watchers
// safe to call for newly discovered and existing campaigns that may have changed in some way
function upsertCampaign(address) {
console.log("Upserting campaign", address);
var campaign = Campaign.at(address);
// console.log("Campaign", campaign);
var campaignDeadline;
var campaignGoal;
var campaignFundsRaised;
var campaignIsSuccess;
var campaignHasFailed;
return campaign.deadline.call({from: $scope.account})
.then(function(_deadline) {
campaignDeadline = _deadline;
//console.log("Deadline", campaignDeadline);
return campaign.goal.call({from: $scope.account});
})
.then(function(_goal) {
campaignGoal = _goal;
//console.log("Goal", campaignGoal);
return campaign.fundsRaised.call({from: $scope.account});
})
.then(function(_fundsRaised) {
campaignFundsRaised = _fundsRaised;
//console.log("Funds Raised", campaignFundsRaised);
return campaign.withdrawn.call({from: $scope.account});
})
.then(function(_withdrawn) {
campaignWithdrawn = _withdrawn;
//console.log("Withdrawn", _withdrawn);
return campaign.sponsor.call({from: $scope.account});
})
.then(function(_sponsor) {
campaignSponsor = _sponsor;
//console.log("Sponsor", campaignSponsor);
return campaign.isSuccess.call({from: $scope.account});
})
.then(function(_isSuccess) {
campaignIsSuccess = _isSuccess;
//console.log("is Success", campaignIsSuccess);
return campaign.hasFailed.call({from: $scope.account});
})
.then(function(_hasFailed) {
campaignHasFailed = _hasFailed;
//console.log("has Failed", campaignHasFailed);
// build a row step-by-step
var c = {};
c.campaign = address;
c.sponsor = campaignSponsor;
c.goal = campaignGoal.toString(10);
c.deadline = parseInt(campaignDeadline.toString(10));
c.accepted = parseInt(campaignFundsRaised.toString(10));
c.withdrawn = parseInt(campaignWithdrawn.toString(10));
c.isSuccess = campaignIsSuccess;
c.hasFailed = campaignHasFailed;
c.status = "open";
if(c.isSuccess) c.status = "success";
if(c.hasFailed) c.status = "failed";
if(typeof($scope.campaignIndex[address]) == 'undefined')
{
$scope.campaignIndex[c.campaign]=$scope.campaigns.length;
$scope.campaigns.push(c);
var receiveWatcher = watchReceived(address);
var refundWatcher = watchRefunded(address);
$scope.$apply();
} else {
var index = $scope.campaignIndex[c.campaign];
$scope.campaigns[index].accepted = c.accepted;
$scope.campaigns[index].refunded = c.refunded;
$scope.campaigns[index].withdrawn = c.withdrawn;
$scope.campaigns[index].isSuccess = c.isSuccess;
$scope.campaigns[index].hasFailed = c.hasFailed;
$scope.$apply();
}
return getFunder(address);
});
}
// Check contributions from the current user
function getFunder(address) {
var campaign = Campaign.at(address);
var index = $scope.campaignIndex[address];
return campaign.funderStructs.call($scope.account, {from: $scope.account})
.then(function(funder) {
// when a function returns multiple values, we get an array
$scope.campaigns[index].userAccepted = parseInt(funder[0].toString(10));
$scope.campaigns[index].userRefunded = parseInt(funder[1].toString(10));
$scope.$apply();
return true;;
})
}
// work with the first account.
web3.eth.getAccounts(function(err, accs) {
if (err != null) {
alert("There was an error fetching your accounts.");
return;
}
if (accs.length == 0) {
alert("Couldn't get any accounts! Make sure your Ethereum client is configured correctly.");
return;
}
$scope.accounts = accs;
$scope.account = $scope.accounts[0];
$scope.balance = web3.eth.getBalance($scope.account).toString(10);
console.log('Using account',$scope.account);
});
}]);
//////////////////////////////////////////////////////////
// For training purposes.
// Solidity Contract Factory
// Module 5 - START
// Copyright (c) 2017, Rob Hitchens, all rights reserved.
// Not suitable for actual use
//////////////////////////////////////////////////////////
pragma solidity ^0.4.6;
import "./Stoppable.sol";
contract Campaign is Stoppable {
address public sponsor;
uint public deadline;
uint public goal;
uint public fundsRaised;
uint public withdrawn;
struct FunderStruct {
uint amountContributed;
uint amountRefunded;
}
mapping (address => FunderStruct) public funderStructs;
modifier onlySponsor {
if(msg.sender != sponsor) throw;
_;
}
event LogContribution(address sender, uint amount);
event LogRefundSent(address funder, uint amount);
event LogWithdrawal(address beneficiary, uint amount);
function Campaign(address campaignSponsor, uint campaignDuration, uint campaignGoal) {
sponsor = campaignSponsor;
deadline = block.number + campaignDuration;
goal = campaignGoal;
}
function isSuccess()
public
constant
returns(bool isIndeed)
{
return(fundsRaised >= goal);
}
function hasFailed()
public
constant
returns(bool hasIndeed)
{
return(fundsRaised < goal && block.number > deadline);
}
function contribute()
public
onlyIfRunning
payable
returns(bool success)
{
if(msg.value==0) throw;
if(isSuccess()) throw;
if(hasFailed()) throw;
fundsRaised += msg.value;
funderStructs[msg.sender].amountContributed += msg.value;
LogContribution(msg.sender, msg.value);
return true;
}
function withdrawFunds()
onlySponsor
onlyIfRunning
returns(bool success)
{
if(!isSuccess()) throw;
uint amount = fundsRaised - withdrawn;
withdrawn += amount;
if(!owner.send(amount)) throw;
LogWithdrawal(owner, amount);
return true;
}
function requestRefund()
public
onlyIfRunning
returns(bool success)
{
uint amountOwed = funderStructs[msg.sender].amountContributed - funderStructs[msg.sender].amountRefunded;
if(amountOwed == 0) throw;
if(!hasFailed()) throw;
funderStructs[msg.sender].amountRefunded += amountOwed;
if(!msg.sender.send(amountOwed)) throw;
LogRefundSent(msg.sender, amountOwed);
return true;
}
}
//////////////////////////////////////////////////////////
// For training purposes.
// Solidity Contract Factory
// Module 5
// Copyright (c) 2017, Rob Hitchens, all rights reserved.
// Not suitable for actual use
//////////////////////////////////////////////////////////
pragma solidity ^0.4.6;
import "./Campaign.sol";
contract Hub is Stoppable {
address[] public campaigns;
mapping(address => bool) campaignExists;
modifier onlyIfCampaign(address campaign) {
if(campaignExists[campaign] != true) throw;
_;
}
event LogNewCampaign(address sponsor, address campaign, uint duration, uint goal);
event LogCampaignStopped(address sender, address campaign);
event LogCampaignStarted(address sender, address campaign);
event LogCampaignNewOwner(address sender, address campaign, address newOwner);
function getCampaignCount()
public
constant
returns(uint campaignCount)
{
return campaigns.length;
}
function createCampaign(uint campaignDuration, uint campaignGoal)
public
returns(address campaignContract)
{
Campaign trustedCampaign = new Campaign(msg.sender,campaignDuration, campaignGoal);
campaigns.push(trustedCampaign);
campaignExists[trustedCampaign] = true;
LogNewCampaign(msg.sender, trustedCampaign, campaignDuration, campaignGoal);
return trustedCampaign;
}
// Pass-through Admin Controls
function stopCampaign(address campaign)
onlyOwner
onlyIfCampaign(campaign)
returns(bool success)
{
Campaign trustedCampaign = Campaign(campaign);
LogCampaignStopped(msg.sender, campaign);
return(trustedCampaign.runSwitch(false));
}
function startCampaign(address campaign)
onlyOwner
onlyIfCampaign(campaign)
returns(bool success)
{
Campaign trustedCampaign = Campaign(campaign);
LogCampaignStarted(msg.sender, campaign);
return(trustedCampaign.runSwitch(true));
}
function changeCampaignOwner(address campaign, address newOwner)
onlyOwner
onlyIfCampaign(campaign)
returns(bool success)
{
Campaign trustedCampaign = Campaign(campaign);
LogCampaignNewOwner(msg.sender, campaign, newOwner);
return(trustedCampaign.changeOwner(newOwner));
}
}
<!DOCTYPE html>
<html>
<head>
<base href="." />
<title>FundingHub</title>
<!-- Latest compiled and minified bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<!-- Optional bootstrap theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
<!-- JQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<!-- Latest compiled and minified bootstrap JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<link href="./app.css" rel='stylesheet' type='text/css'>
<!-- app controller -->
<script src="./app.js"></script>
</head>
<body ng-cloak ng-controller="HubController">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<h3>Funding<span class="black">Hub </span>
<table>
<tr>
<td>
<form id="campaignForm" ng-submit="newCampaign()">
<input type="text" name="goal" ng-model="new.goal" placeholder="goal in Wei" size="10" required />
<input type="text" name="duration" ng-model="new.duration" placeholder="duration in Blocks" required />
<input type="submit" class="btn btn-primary addnew" value="New Campaign"></h3>
</form>
</td>
<td>
<h3>
<form id="accountForm" ng-submit="setAccount()">
&nbsp;
<select name="account" ng-model="accountSelected" required>
<option ng-repeat="account in accounts" value="{{ account }}">
{{ account }}
</option>
</select>
<input type="submit" class="btn btn-primary addnew" value="Select Account"></h3>
</form>
</h3>
</td>
</tr>
</table>
<h6>Your Balance (wei): {{ balance }} <span style="color: #777;">(account: {{ account }})</span></h6>
<div class="panel panel-default">
<div class="panel-body">
<form id="contributeForm" ng-submit="contribute()">
<table class="table table-striped table-bordered">
<tr>
<th>Choose Campaign to Contribute</th>
<th>Contribution in Wei</th>
<th>&nbsp;</th>
</tr>
<tr>
<td>
<select name="campaign" ng-model="campaignSelected" required>
<option></option>
<option ng-repeat="campaign in campaigns track by campaign.campaign" value="{{ campaign.campaign }}">
{{ campaign.campaign }}
</option>
</select>
</td>
<td><input type="text" class="form-control" ng-model="contribution" placeholder="0 in Wei" required/></td>
<td><input type="submit" class="btn btn-primary addnew pull-right" value="Contribute"></td>
</tr>
</table>
</form>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Campaign</th>
<th>Campaign Goal</th>
<th>Campaign Accepted</th>
<th>Campaign Withdrawn</th>
<th>Deadline</th>
<th>User Contributed</th>
<th>User Refunded</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="campaign in campaigns track by campaign.campaign">
<td>{{ campaign.campaign }}</td>
<td>{{ campaign.goal }}</td>
<td>{{ campaign.accepted }}</td>
<td>{{ campaign.withdrawn }}</td>
<td>{{ campaign.deadline }}</td>
<td>{{ campaign.userAccepted }}</td>
<td>{{ campaign.userRefunded }}</td>
<td>{{ campaign.status }}</td>
<td>
<form>
<input
ng-if= 'campaign.status == "failed" && campaign.userAccepted - campaign.userRefunded > 0'
type="submit" class="btn btn-success addnew pull-right" value="Refund"
ng-click="refund(campaign.campaign)">
<input
ng-if= 'campaign.status != "failed" || campaign.userAccepted - campaign.userRefunded <= 0'
type="submit" class="btn btn-success addnew pull-right
disabled" value="Refund">
</form>
</td>
</tr>
</tbody>
</table>
<table class="table table-striped table-bordered">
<tr>
<th>Event</th>
<th>Amount</th>
<th>Campaign</th>
<th>User</th>
</tr>
<tr ng-repeat="campaignInfo in campaignLog">
<td>{{ campaignInfo.event }}</td>
<td>{{ campaignInfo.args.amount }}</td>
<td>{{ campaignInfo.args.campaign }}</td>
<td>{{ campaignInfo.args.user }}</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
<script>
window.addEventListener('load', function() {
angular.bootstrap(document, ['HubApp'])
});
</script>
</body>
</html>
//////////////////////////////////////////////////////////
// For training purposes.
// Solidity Contract Factory
// Module 5 - START
// Copyright (c) 2017, Rob Hitchens, all rights reserved.
// Not suitable for actual use
//////////////////////////////////////////////////////////
pragma solidity ^0.4.6;
contract Owned {
address public owner;
event LogNewOwner(address sender, address oldOwner, address newOwner);
modifier onlyOwner {
if(msg.sender != owner) throw;
_;
}
function Owned() {
owner = msg.sender;
}
function changeOwner(address newOwner)
onlyOwner
returns(bool success)
{
if(newOwner == 0) throw;
LogNewOwner(msg.sender, owner, newOwner);
owner = newOwner;
return true;
}
}
//////////////////////////////////////////////////////////
// For training purposes.
// Solidity Contract Factory
// Module 5 - START
// Copyright (c) 2017, Rob Hitchens, all rights reserved.
// Not suitable for actual use
//////////////////////////////////////////////////////////
pragma solidity ^0.4.6;
import "./Owned.sol";
contract Stoppable is Owned {
bool public running;
event LogRunSwitch(address sender, bool switchSetting);
modifier onlyIfRunning {
if(!running) throw;
_;
}
function Stoppable() {
running = true;
}
function runSwitch(bool onOff)
onlyOwner
returns(bool success)
{
running = onOff;
LogRunSwitch(msg.sender, onOff);
return true;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment