diff --git a/contracts/communication/MessageTribute.sol b/contracts/communication/MessageTribute.sol index c0c66a4..221ebf5 100644 --- a/contracts/communication/MessageTribute.sol +++ b/contracts/communication/MessageTribute.sol @@ -25,6 +25,9 @@ contract MessageTribute is Controlled { mapping(address => mapping(address => Fee)) feeCatalog; mapping(address => uint256) balances; + + mapping(bytes32 => uint256) friendIndex; + address[] friends; struct Audience { uint256 blockNum; @@ -33,12 +36,39 @@ contract MessageTribute is Controlled { mapping(address => mapping(address => Audience)) audienceRequested; - function MessageTribute(address _SNT) public { SNT = MiniMeToken(_SNT); } + function addFriends(address[] _friends) public { + uint256 len = _friends.length; + for (uint256 i = 0; i < len; i++) { + bytes32 frHash = keccak256(_friends[i], msg.sender); + if (friendIndex[frHash] == 0) + friendIndex[frHash] = friends.push(_friends[i]); + } + } + + function removeFriends(address[] _friends) public { + uint256 len = _friends.length; + for (uint256 i = 0; i < len; i++) { + bytes32 frHash = keccak256(_friends[i], msg.sender); + require(friendIndex[frHash] > 0); + uint index = friendIndex[frHash] - 1; + delete friendIndex[frHash]; + address replacer = friends[friends.length - 1]; + friends[index] = replacer; + friendIndex[keccak256(replacer, msg.sender)] = index; + friends.length--; + } + } + + function isFriend(address _friend) public view returns(bool) { + return friendIndex[keccak256(_friend, msg.sender)] > 0; + } + function setRequiredTribute(address _to, uint _amount, bool _isTribute, bool _isPermanent) public { + require(friendIndex[keccak256(msg.sender, _to)] == 0); feeCatalog[msg.sender][_to] = Fee(_amount, _isTribute, _isPermanent); } @@ -49,20 +79,11 @@ contract MessageTribute is Controlled { fee = f.amount; tribute = f.tribute; } - - function getFee(address _from) internal - returns (Fee) - { - Fee memory generalFee = feeCatalog[_from][address(0)]; - Fee memory specificFee = feeCatalog[_from][msg.sender]; - return specificFee.amount > 0 ? specificFee : generalFee; - } - function deposit(uint256 _value) public { require(_value > 0); balances[msg.sender] += _value; - require(SNT.transferFrom(msg.sender, this, _value)); + require(SNT.transferFrom(msg.sender, address(this), _value)); } function balance() public view returns (uint256) { @@ -72,7 +93,8 @@ contract MessageTribute is Controlled { function withdraw(uint256 _value) public { require(balances[msg.sender] > 0); require(_value <= balances[msg.sender]); - require(SNT.transferFrom(msg.sender, this, _value)); + balances[msg.sender] -= _value; + require(SNT.transfer(msg.sender, _value)); } event AudienceRequested(address from, address to); @@ -91,6 +113,11 @@ contract MessageTribute is Controlled { balances[msg.sender] -= f.amount; } + function hasPendingAudience(address _from) + public view returns (bool) { + return audienceRequested[_from][msg.sender].blockNum > 0; + } + function cancelAudienceRequest(address _from) public { if (audienceRequested[_from][msg.sender].blockNum > 0) { AudienceCancelled(_from, msg.sender); @@ -99,7 +126,7 @@ contract MessageTribute is Controlled { } } - function grantAudience(address _to, bool _approve) { + function grantAudience(address _to, bool _approve) public { Audience memory aud = audienceRequested[msg.sender][_to]; @@ -132,6 +159,18 @@ contract MessageTribute is Controlled { return getFee(_to).amount <= balances[msg.sender]; } + function getFee(address _from) internal view + returns (Fee) + { + Fee memory generalFee = feeCatalog[_from][address(0)]; + Fee memory specificFee = feeCatalog[_from][msg.sender]; + + if (friendIndex[keccak256(msg.sender, _from)] > 0) + return Fee(0, false, false); + + return specificFee.amount > 0 ? specificFee : generalFee; + } + function clearFee(address _from, address _to) private { if (!feeCatalog[_from][_to].permanent) { feeCatalog[_from][_to].amount = 0; @@ -139,5 +178,7 @@ contract MessageTribute is Controlled { feeCatalog[_from][_to].permanent = false; } } + + } diff --git a/test/messageTribute.js b/test/messageTribute.js new file mode 100644 index 0000000..5174590 --- /dev/null +++ b/test/messageTribute.js @@ -0,0 +1,312 @@ +const TestUtils = require("../utils/testUtils.js"); + +describe('MessageTribute', function() { + + let accounts; + let SNT; + + this.timeout(0); + + before( function(done) { + this.timeout(0); + + EmbarkSpec.deployAll({ + "MiniMeTokenFactory": {}, + "MiniMeToken": { + "args": [ + "$MiniMeTokenFactory", + "0x0", + "0x0", + "Status Test Token", + 18, + "STT", + true + ] + }, + "MessageTribute": { + "args": ["$MiniMeToken"] + } + }, (_accounts) => { + accounts = _accounts; + + SNT = MiniMeToken; + + Promise.all([ + SNT.methods.generateTokens(accounts[0], 5000).send(), + SNT.methods.generateTokens(accounts[1], 5000).send(), + SNT.methods.generateTokens(accounts[2], 5000).send(), + SNT.methods.generateTokens(accounts[3], 5000).send(), + SNT.methods.generateTokens(accounts[4], 5000).send(), + SNT.methods.generateTokens(accounts[5], 5000).send(), + SNT.methods.generateTokens(accounts[6], 5000).send(), + SNT.methods.generateTokens(accounts[7], 5000).send(), + SNT.methods.generateTokens(accounts[8], 5000).send(), + SNT.methods.generateTokens(accounts[9], 5000).send() + ]) + .then(() => { + console.log(" - Added balances"); + done(); + }); + }); + }); + + it("Adding friends", async () => { + await MessageTribute.methods.addFriends([accounts[1], accounts[2]]).send({from: accounts[0]}); + await MessageTribute.methods.addFriends([accounts[3]]).send({from: accounts[1]}); + await MessageTribute.methods.addFriends([accounts[4]]).send({from: accounts[1]}); + await MessageTribute.methods.addFriends([accounts[5]]).send({from: accounts[1]}); + + assert.equal( + await MessageTribute.methods.isFriend(accounts[1]).call({from: accounts[0]}), + true, + accounts[1] + " must be a friend of " + accounts[0]); + + assert.equal( + await MessageTribute.methods.isFriend(accounts[3]).call({from: accounts[1]}), + true, + accounts[3] + " must be a friend of " + accounts[1]); + + assert.equal( + await MessageTribute.methods.isFriend(accounts[4]).call({from: accounts[0]}), + false, + accounts[4] + " must not be a friend of " + accounts[0]); + + assert.equal( + await MessageTribute.methods.isFriend(accounts[2]).call({from: accounts[1]}), + false, + accounts[2] + " must not be a friend of " + accounts[1]); + + }); + + it("Removing friends", async () => { + await MessageTribute.methods.removeFriends([accounts[3]]).send({from: accounts[1]}); + + await MessageTribute.methods.removeFriends([accounts[4], accounts[5]]).send({from: accounts[1]}); + + assert.equal( + await MessageTribute.methods.isFriend(accounts[3]).call({from: accounts[0]}), + false, + accounts[3] + " must not be a friend of " + accounts[0]); + + assert.equal( + await MessageTribute.methods.isFriend(accounts[4]).call({from: accounts[0]}), + false, + accounts[4] + " must not be a friend of " + accounts[0]); + + try { + let tx = await MessageTribute.methods.removeFriends([accounts[5]]).send({from: accounts[1]}); + assert.fail('should have reverted before'); + } catch(error) { + TestUtils.assertJump(error); + } + }); + + it("Should be able to deposit", async() => { + let amount = 2000; + + await SNT.methods.approve(MessageTribute.address, amount).send({from: accounts[1]}); + + let initialBalance = await SNT.methods.balanceOf(accounts[1]).call(); + + await MessageTribute.methods.deposit(amount).send({from: accounts[1]}); + + assert.equal( + await MessageTribute.methods.balance().call({from: accounts[1]}), + amount, + "Deposited balance must be " + amount); + + assert.equal( + await SNT.methods.balanceOf(accounts[1]).call(), + web3.utils.toBN(initialBalance).sub(web3.utils.toBN(amount)).toString(), + accounts[1] + " SNT balance is incorrect"); + + assert.equal( + await SNT.methods.balanceOf(MessageTribute.address).call(), + amount, + "Contract SNT balance is incorrect"); + + }); + + it("Should be able to withdraw", async() => { + let amount = 2000; + + let initialBalance = await SNT.methods.balanceOf(accounts[1]).call(); + + await MessageTribute.methods.withdraw(amount).send({from: accounts[1]}); + + assert.equal( + await SNT.methods.balanceOf(accounts[1]).call(), + web3.utils.toBN(initialBalance).add(web3.utils.toBN(amount)).toString(), + "SNT Balance must be " + initialBalance + amount); + + assert.equal( + await SNT.methods.balanceOf(MessageTribute.address).call(), + 0, + "Contract SNT balance is incorrect"); + + assert.equal( + await MessageTribute.methods.balance().call({from: accounts[1]}), + 0, + "Deposited balance must be 0"); + + }); + + it("Requesting audience without requiring deposit", async () => { + assert.equal( + await MessageTribute.methods.balance().call({from: accounts[9]}), + 0, + "Deposited balance must be 0"); + + let tx = await MessageTribute.methods.requestAudience(accounts[0]).send({from: accounts[9]}); + + assert.notEqual(tx.events.AudienceRequested, undefined, "AudienceRequested wasn't triggered"); + + }); + + it("Requesting audience requiring deposit and no funds deposited", async () => { + await MessageTribute.methods.setRequiredTribute(accounts[9], 100, false, true).send({from: accounts[0]}); + + assert.equal( + await MessageTribute.methods.balance().call({from: accounts[9]}), + 0, + "Deposited balance must be 0"); + + assert.equal( + await MessageTribute.methods.hasEnoughFundsToTalk(accounts[0]).call({from: accounts[9]}), + false, + "Must return false"); + + try { + let tx = await MessageTribute.methods.requestAudience(accounts[0]).send({from: accounts[9]}); + assert.fail('should have reverted before'); + } catch(error) { + TestUtils.assertJump(error); + } + }); + + it("Requesting an audience as a friend", async() => { + assert.equal( + await MessageTribute.methods.isFriend(accounts[2]).call({from: accounts[0]}), + true, + "Should be friends"); + + assert.equal( + await MessageTribute.methods.balance().call({from: accounts[2]}), + 0, + "Deposited balance must be 0"); + + assert.equal( + await MessageTribute.methods.hasEnoughFundsToTalk(accounts[0]).call({from: accounts[2]}), + true, + "Must return true"); + + let tx = await MessageTribute.methods.requestAudience(accounts[0]).send({from: accounts[2]}); + + assert.notEqual(tx.events.AudienceRequested, undefined, "AudienceRequested wasn't triggered"); + }); + + + it("Request audience requiring deposit, not having funds at the beginning, deposit funds and request audience", async () => { + await MessageTribute.methods.setRequiredTribute(accounts[8], 200, false, true).send({from: accounts[0]}); + + await SNT.methods.approve(MessageTribute.address, 100).send({from: accounts[8]}); + await MessageTribute.methods.deposit(100).send({from: accounts[8]}); + + assert.equal( + await MessageTribute.methods.balance().call({from: accounts[8]}), + 100, + "Deposited balance must be 100"); + + assert.equal( + await MessageTribute.methods.hasEnoughFundsToTalk(accounts[0]).call({from: accounts[8]}), + false, + "Must return false"); + + try { + let tx = await MessageTribute.methods.requestAudience(accounts[0]).send({from: accounts[8]}); + assert.fail('should have reverted before'); + } catch(error) { + TestUtils.assertJump(error); + } + + await SNT.methods.approve(MessageTribute.address, 100).send({from: accounts[8]}); + await MessageTribute.methods.deposit(100).send({from: accounts[8]}); + + assert.equal( + await MessageTribute.methods.balance().call({from: accounts[8]}), + 200, + "Deposited balance must be 200"); + + assert.equal( + await MessageTribute.methods.hasEnoughFundsToTalk(accounts[0]).call({from: accounts[8]}), + true, + "Must return true"); + + let tx = await MessageTribute.methods.requestAudience(accounts[0]).send({from: accounts[8]}); + + assert.notEqual(tx.events.AudienceRequested, undefined, "AudienceRequested wasn't triggered"); + }); + + + it("Requesting tribute from specific account", async() => { + await MessageTribute.methods.setRequiredTribute(accounts[7], 100, true, true).send({from: accounts[0]}); + + await SNT.methods.approve(MessageTribute.address, 200).send({from: accounts[7]}); + await MessageTribute.methods.deposit(200).send({from: accounts[7]}); + + assert.equal( + await MessageTribute.methods.balance().call({from: accounts[7]}), + 200, + "Deposited balance must be 200"); + + let tx = await MessageTribute.methods.requestAudience(accounts[0]).send({from: accounts[7]}); + + assert.notEqual(tx.events.AudienceRequested, undefined, "AudienceRequested wasn't triggered"); + + assert.equal( + await MessageTribute.methods.balance().call({from: accounts[7]}), + 100, + "Deposited balance must be 100"); + }); + + it("Missing tests", async () => { + // Cancelling Audience - account8 + + // Granting Audience - account7 + + // Deniying Audience + + /* + // Requiring 200 tribute from account6 non permanent + await MessageTribute.methods.setRequiredTribute(accounts[4], 200, true, false).send({from: accounts[0]}); + + // Requiring 100 deposit from everyone + await MessageTribute.methods.setRequiredTribute("0x0", 100, false, true).send({from: accounts[0]}); + + // Requiring 100 tribute from everyone + await MessageTribute.methods.setRequiredTribute("0x0", 100, true, true).send({from: accounts[1]}); +*/ + }); + + it("Requesting a non permanent tribute from specific account", async() => { + await MessageTribute.methods.setRequiredTribute(accounts[6], 100, true, false).send({from: accounts[0]}); + + await SNT.methods.approve(MessageTribute.address, 200).send({from: accounts[6]}); + await MessageTribute.methods.deposit(200).send({from: accounts[6]}); + + let tx = await MessageTribute.methods.requestAudience(accounts[0]).send({from: accounts[6]}); + + assert.notEqual(tx.events.AudienceRequested, undefined, "AudienceRequested wasn't triggered"); + + assert.equal( + await MessageTribute.methods.balance().call({from: accounts[6]}), + 100, + "Deposited balance must be 100"); + + // TODO grant audience + // TODO request another audience + }); + + + +}); \ No newline at end of file