diff --git a/contracts/DecentralisedPaymentProcessor.sol b/contracts/DecentralisedPaymentProcessor.sol new file mode 100644 index 0000000..e9df9aa --- /dev/null +++ b/contracts/DecentralisedPaymentProcessor.sol @@ -0,0 +1,428 @@ +pragma solidity ^0.4.24; + +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "openzeppelin-solidity/contracts/lifecycle/Pausable.sol"; +import "openzeppelin-solidity/contracts/lifecycle/Destructible.sol"; +import "openzeppelin-solidity/contracts/ownership/Contactable.sol"; +import "monetha-utility-contracts/contracts/Restricted.sol"; +import "./MonethaGateway.sol"; +import "./MerchantDealsHistory.sol"; +import "./MerchantWallet.sol"; +import "./GenericERC20.sol"; +import "./MonethaSupportedTokens.sol"; + + +/** + * @title DecentralisedPaymentProcessor + * Each Merchant has one DecentralisedPaymentProcessor that ensure payment and order processing with Trust and Reputation + * + * Payment Processor State Transitions: + * Null -(addOrder) -> Created + * Created -(securePay) -> Paid + * Created -(cancelOrder) -> Cancelled + * Paid -(refundPayment) -> Refunding + * Paid -(processPayment) -> Finalized + * Refunding -(withdrawRefund) -> Refunded + */ + + +contract DecentralisedPaymentProcessor is Pausable, Destructible, Contactable, Restricted { + + using SafeMath for uint256; + + string constant VERSION = "0.7"; + + /** + * Fee permille of Monetha fee. + * 1 permille = 0.1 % + * 15 permille = 1.5% + */ + uint public constant FEE_PERMILLE = 15; + + /** + * Payback permille. + * 1 permille = 0.1 % + */ + uint public constant PAYBACK_PERMILLE = 2; // 0.2% + + uint public constant PERMILLE_COEFFICIENT = 1000; + + /// MonethaSupportedTokens contract for validating tokens + MonethaSupportedTokens public supportedTokens; + + /// MonethaGateway contract for payment processing + MonethaGateway public monethaGateway; + + /// MerchantDealsHistory contract of acceptor's merchant + MerchantDealsHistory public merchantHistory; + + /// Address of MerchantWallet, where merchant reputation and funds are stored + MerchantWallet public merchantWallet; + + /// Merchant identifier hash, that associates with the acceptor + bytes32 public merchantIdHash; + + enum State {Null, Created, Paid, Finalized, Refunding, Refunded, Cancelled} + + struct Order { + State state; + uint price; + uint fee; + address paymentAcceptor; + address originAddress; + address tokenAddress; + uint vouchersApply; + uint discount; + } + + mapping(uint => Order) public orders; + + /** + * Asserts current state. + * @param _state Expected state + * @param _orderId Order Id + */ + modifier atState(uint _orderId, State _state) { + require(_state == orders[_orderId].state); + _; + } + + /** + * Performs a transition after function execution. + * @param _state Next state + * @param _orderId Order Id + */ + modifier transition(uint _orderId, State _state) { + _; + orders[_orderId].state = _state; + } + + /** + * payment Processor sets Monetha Gateway + * @param _merchantId Merchant of the acceptor + * @param _merchantHistory Address of MerchantDealsHistory contract of acceptor's merchant + * @param _monethaGateway Address of MonethaGateway contract for payment processing + * @param _merchantWallet Address of MerchantWallet, where merchant reputation and funds are stored + */ + constructor( + address _tokenAddr, + string _merchantId, + MerchantDealsHistory _merchantHistory, + MonethaGateway _monethaGateway, + MerchantWallet _merchantWallet + ) + public + { + require(bytes(_merchantId).length > 0); + + supportedTokens = MonethaSupportedTokens(_tokenAddr); + merchantIdHash = keccak256(abi.encodePacked(_merchantId)); + + setMonethaGateway(_monethaGateway); + setMerchantWallet(_merchantWallet); + setMerchantDealsHistory(_merchantHistory); + } + + /** + * Assigns the acceptor to the order (when client initiates order). + * @param _orderId Identifier of the order + * @param _price Price of the order + * @param _paymentAcceptor order payment acceptor + * @param _originAddress buyer address + */ + function addOrder( + uint _orderId, + uint _price, + address _paymentAcceptor, + address _originAddress, + address _tokenAddress, + uint _vouchersApply + ) external whenNotPaused atState(_orderId, State.Null) + { + require(_orderId > 0); + require(_price > 0); + require(_paymentAcceptor != address(0)); + require(_originAddress != address(0)); + require(orders[_orderId].price == 0 && orders[_orderId].fee == 0); + if (_tokenAddress != address(0)) { + require(supportedTokens.isTokenValid(_tokenAddress) == true, "token not supported"); + } + + // Monetha fee will always be 1.5% of price + uint _fee = FEE_PERMILLE.mul(_price).div(PERMILLE_COEFFICIENT); + + orders[_orderId] = Order({ + state : State.Created, + price : _price, + fee : _fee, + paymentAcceptor : _paymentAcceptor, + originAddress : _originAddress, + tokenAddress : _tokenAddress, + vouchersApply : _vouchersApply, + discount: 0 + }); + } + + /** + * securePay can be used by client if he wants to securely set client address for refund together with payment. + * This function require more gas, then fallback function. + * @param _orderId Identifier of the order + */ + function securePay(uint _orderId) + external payable whenNotPaused + atState(_orderId, State.Created) transition(_orderId, State.Paid) + { + Order storage order = orders[_orderId]; + + require(order.tokenAddress == address(0)); + require(msg.sender == order.paymentAcceptor); + require(msg.value == order.price); + } + + /** + * secureTokenPay can be used by client if he wants to securely set client address for token refund together with token payment. + * This call requires that token's approve method has been called prior to this. + * @param _orderId Identifier of the order + */ + function secureTokenPay(uint _orderId) + external whenNotPaused + atState(_orderId, State.Created) transition(_orderId, State.Paid) + { + Order storage order = orders[_orderId]; + + require(msg.sender == order.paymentAcceptor); + require(order.tokenAddress != address(0)); + + GenericERC20(order.tokenAddress).transferFrom(msg.sender, address(this), order.price); + } + + /** + * cancelOrder is used when client doesn't pay and order need to be cancelled. + * @param _orderId Identifier of the order + * @param _clientReputation Updated reputation of the client + * @param _merchantReputation Updated reputation of the merchant + * @param _dealHash Hashcode of the deal, describing the order (used for deal verification) + * @param _cancelReason Order cancel reason + */ + function cancelOrder( + uint _orderId, + uint32 _clientReputation, + uint32 _merchantReputation, + uint _dealHash, + string _cancelReason + ) + external onlyMonetha whenNotPaused + atState(_orderId, State.Created) transition(_orderId, State.Cancelled) + { + require(bytes(_cancelReason).length > 0); + + Order storage order = orders[_orderId]; + + updateDealConditions( + _orderId, + _clientReputation, + _merchantReputation, + false, + _dealHash + ); + + merchantHistory.recordDealCancelReason( + _orderId, + order.originAddress, + _clientReputation, + _merchantReputation, + _dealHash, + _cancelReason + ); + } + + /** + * refundPayment used in case order cannot be processed. + * This function initiate process of funds refunding to the client. + * @param _orderId Identifier of the order + * @param _clientReputation Updated reputation of the client + * @param _merchantReputation Updated reputation of the merchant + * @param _dealHash Hashcode of the deal, describing the order (used for deal verification) + * @param _refundReason Order refund reason, order will be moved to State Cancelled after Client withdraws money + */ + function refundPayment( + uint _orderId, + uint32 _clientReputation, + uint32 _merchantReputation, + uint _dealHash, + string _refundReason + ) + external onlyMonetha whenNotPaused + atState(_orderId, State.Paid) transition(_orderId, State.Refunding) + { + require(bytes(_refundReason).length > 0); + + Order storage order = orders[_orderId]; + + updateDealConditions( + _orderId, + _clientReputation, + _merchantReputation, + false, + _dealHash + ); + + merchantHistory.recordDealRefundReason( + _orderId, + order.originAddress, + _clientReputation, + _merchantReputation, + _dealHash, + _refundReason + ); + } + + /** + * withdrawRefund performs fund transfer to the client's account. + * @param _orderId Identifier of the order + */ + function withdrawRefund(uint _orderId) + external whenNotPaused + atState(_orderId, State.Refunding) transition(_orderId, State.Refunded) + { + Order storage order = orders[_orderId]; + require(order.tokenAddress == address(0)); + + order.originAddress.transfer(order.price.sub(order.discount)); + } + + /** + * withdrawTokenRefund performs token transfer to the client's account. + * @param _orderId Identifier of the order + */ + function withdrawTokenRefund(uint _orderId) + external whenNotPaused + atState(_orderId, State.Refunding) transition(_orderId, State.Refunded) + { + require(orders[_orderId].tokenAddress != address(0)); + + GenericERC20(orders[_orderId].tokenAddress).transfer(orders[_orderId].originAddress, orders[_orderId].price); + } + + /** + * processPayment transfer funds/tokens to MonethaGateway and completes the order. + * @param _orderId Identifier of the order + * @param _clientReputation Updated reputation of the client + * @param _merchantReputation Updated reputation of the merchant + * @param _dealHash Hashcode of the deal, describing the order (used for deal verification) + */ + function processPayment( + uint _orderId, + uint32 _clientReputation, + uint32 _merchantReputation, + uint _dealHash + ) + external onlyMonetha whenNotPaused + atState(_orderId, State.Paid) transition(_orderId, State.Finalized) + { + Order storage order = orders[_orderId]; + address fundAddress = merchantWallet.merchantFundAddress(); + + if (order.tokenAddress != address(0)) { + if (fundAddress != address(0)) { + GenericERC20(order.tokenAddress).transfer(address(monethaGateway), order.price); + monethaGateway.acceptTokenPayment(fundAddress, order.fee, order.tokenAddress, order.price); + } else { + GenericERC20(order.tokenAddress).transfer(address(monethaGateway), order.price); + monethaGateway.acceptTokenPayment(merchantWallet, order.fee, order.tokenAddress, order.price); + } + } else { + uint discountWei = 0; + if (fundAddress != address(0)) { + discountWei = monethaGateway.acceptPayment.value(order.price)( + fundAddress, + order.fee, + order.originAddress, + order.vouchersApply, + PAYBACK_PERMILLE); + } else { + discountWei = monethaGateway.acceptPayment.value(order.price)( + merchantWallet, + order.fee, + order.originAddress, + order.vouchersApply, + PAYBACK_PERMILLE); + } + + if (discountWei > 0) { + order.discount = discountWei; + } + } + + updateDealConditions( + _orderId, + _clientReputation, + _merchantReputation, + true, + _dealHash + ); + } + + /** + * setMonethaGateway allows owner to change address of MonethaGateway. + * @param _newGateway Address of new MonethaGateway contract + */ + function setMonethaGateway(MonethaGateway _newGateway) public onlyOwner { + require(address(_newGateway) != 0x0); + + monethaGateway = _newGateway; + } + + /** + * setMerchantWallet allows owner to change address of MerchantWallet. + * @param _newWallet Address of new MerchantWallet contract + */ + function setMerchantWallet(MerchantWallet _newWallet) public onlyOwner { + require(address(_newWallet) != 0x0); + require(_newWallet.merchantIdHash() == merchantIdHash); + + merchantWallet = _newWallet; + } + + /** + * setMerchantDealsHistory allows owner to change address of MerchantDealsHistory. + * @param _merchantHistory Address of new MerchantDealsHistory contract + */ + function setMerchantDealsHistory(MerchantDealsHistory _merchantHistory) public onlyOwner { + require(address(_merchantHistory) != 0x0); + require(_merchantHistory.merchantIdHash() == merchantIdHash); + + merchantHistory = _merchantHistory; + } + + /** + * updateDealConditions record finalized deal and updates merchant reputation + * in future: update Client reputation + * @param _orderId Identifier of the order + * @param _clientReputation Updated reputation of the client + * @param _merchantReputation Updated reputation of the merchant + * @param _isSuccess Identifies whether deal was successful or not + * @param _dealHash Hashcode of the deal, describing the order (used for deal verification) + */ + function updateDealConditions( + uint _orderId, + uint32 _clientReputation, + uint32 _merchantReputation, + bool _isSuccess, + uint _dealHash + ) + internal + { + merchantHistory.recordDeal( + _orderId, + orders[_orderId].originAddress, + _clientReputation, + _merchantReputation, + _isSuccess, + _dealHash + ); + + //update parties Reputation + merchantWallet.setCompositeReputation("total", _merchantReputation); + } +} diff --git a/contracts/MonethaSupportedTokens.sol b/contracts/MonethaSupportedTokens.sol index b9eb4a0..b2b6c78 100644 --- a/contracts/MonethaSupportedTokens.sol +++ b/contracts/MonethaSupportedTokens.sol @@ -17,6 +17,7 @@ contract MonethaSupportedTokens is Restricted { } mapping (uint => Token) public tokens; + mapping (address => bool) public validToken; uint public tokenId; @@ -34,12 +35,15 @@ contract MonethaSupportedTokens is Restricted { }); allAddresses.push(_tokenAddress); allAccronym.push(bytes32(_tokenAcronym)); + validToken[_tokenAddress] = true; } function deleteToken(uint _tokenId) external onlyMonetha { - + address _tokenAddress = tokens[_tokenId].token_address; + validToken[_tokenAddress] = false; + tokens[_tokenId].token_address = tokens[tokenId].token_address; tokens[_tokenId].token_acronym = tokens[tokenId].token_acronym; @@ -52,9 +56,11 @@ contract MonethaSupportedTokens is Restricted { tokenId--; } - function getAll() external view returns (address[], bytes32[]) - { + function getAll() external view returns (address[], bytes32[]) { return (allAddresses, allAccronym); } + function isTokenValid(address _tokenAddr) external view returns (bool) { + return validToken[_tokenAddr]; + } } diff --git a/contracts/PaymentProcessor.sol b/contracts/PaymentProcessor.sol index 7d75854..4f93786 100644 --- a/contracts/PaymentProcessor.sol +++ b/contracts/PaymentProcessor.sol @@ -9,6 +9,7 @@ import "./MonethaGateway.sol"; import "./MerchantDealsHistory.sol"; import "./MerchantWallet.sol"; import "./GenericERC20.sol"; +import "./MonethaSupportedTokens.sol"; /** @@ -46,6 +47,9 @@ contract PaymentProcessor is Pausable, Destructible, Contactable, Restricted { uint public constant PERMILLE_COEFFICIENT = 1000; + /// MonethaSupportedTokens contract for validating tokens + MonethaSupportedTokens public supportedTokens; + /// MonethaGateway contract for payment processing MonethaGateway public monethaGateway; @@ -101,6 +105,7 @@ contract PaymentProcessor is Pausable, Destructible, Contactable, Restricted { * @param _merchantWallet Address of MerchantWallet, where merchant reputation and funds are stored */ constructor( + address _tokenAddr, string _merchantId, MerchantDealsHistory _merchantHistory, MonethaGateway _monethaGateway, @@ -110,6 +115,7 @@ contract PaymentProcessor is Pausable, Destructible, Contactable, Restricted { { require(bytes(_merchantId).length > 0); + supportedTokens = MonethaSupportedTokens(_tokenAddr); merchantIdHash = keccak256(abi.encodePacked(_merchantId)); setMonethaGateway(_monethaGateway); @@ -142,7 +148,10 @@ contract PaymentProcessor is Pausable, Destructible, Contactable, Restricted { require(_paymentAcceptor != address(0)); require(_originAddress != address(0)); require(orders[_orderId].price == 0 && orders[_orderId].fee == 0); - + if (_tokenAddress != address(0)) { + require(supportedTokens.isTokenValid(_tokenAddress) == true, "token not supported"); + } + orders[_orderId] = Order({ state : State.Created, price : _price, diff --git a/contracts/PrivatePaymentProcessor.sol b/contracts/PrivatePaymentProcessor.sol index e1a1d37..b366b3f 100644 --- a/contracts/PrivatePaymentProcessor.sol +++ b/contracts/PrivatePaymentProcessor.sol @@ -8,6 +8,7 @@ import "monetha-utility-contracts/contracts/Restricted.sol"; import "./MonethaGateway.sol"; import "./MerchantWallet.sol"; import "./GenericERC20.sol"; +import "./MonethaSupportedTokens.sol"; contract PrivatePaymentProcessor is Pausable, Destructible, Contactable, Restricted { @@ -60,6 +61,9 @@ contract PrivatePaymentProcessor is Pausable, Destructible, Contactable, Restric uint amount ); + /// MonethaSupportedTokens contract for validating tokens + MonethaSupportedTokens public supportedTokens; + /// MonethaGateway contract for payment processing MonethaGateway public monethaGateway; @@ -87,6 +91,7 @@ contract PrivatePaymentProcessor is Pausable, Destructible, Contactable, Restric * @param _merchantWallet Address of MerchantWallet, where merchant reputation and funds are stored */ constructor( + address _tokenAddr, string _merchantId, MonethaGateway _monethaGateway, MerchantWallet _merchantWallet @@ -97,6 +102,7 @@ contract PrivatePaymentProcessor is Pausable, Destructible, Contactable, Restric merchantIdHash = keccak256(abi.encodePacked(_merchantId)); + supportedTokens = MonethaSupportedTokens(_tokenAddr); setMonethaGateway(_monethaGateway); setMerchantWallet(_merchantWallet); } @@ -165,7 +171,8 @@ contract PrivatePaymentProcessor is Pausable, Destructible, Contactable, Restric require(_originAddress != 0x0); require(_orderValue > 0); require(_tokenAddress != address(0)); - + require(supportedTokens.isTokenValid(_tokenAddress) == true, "token not supported"); + address fundAddress; fundAddress = merchantWallet.merchantFundAddress(); diff --git a/test/DecentralisedPaymentProcessorTests.js b/test/DecentralisedPaymentProcessorTests.js new file mode 100644 index 0000000..4c86268 --- /dev/null +++ b/test/DecentralisedPaymentProcessorTests.js @@ -0,0 +1,345 @@ +import Revert from "./helpers/VMExceptionRevert"; +const {BigNumber} = require('./helpers/setup'); +const PaymentProcessor = artifacts.require("DecentralisedPaymentProcessor") +const MerchantDealsHistory = artifacts.require("MerchantDealsHistory") +const MonethaGateway = artifacts.require("MonethaGateway") +const MerchantWallet = artifacts.require("MerchantWallet") +const Token = artifacts.require("ERC20Mintable") +const MonethaSupportedTokens = artifacts.require("MonethaSupportedTokens"); +let merchantId; + +contract('DecentralisedPaymentProcessor', function (accounts) { + + const State = { + Null: 0, + Created: 1, + Paid: 2, + Finalized: 3, + Refunding: 4, + Refunded: 5, + Cancelled: 6 + } + + const OWNER = accounts[0] + const PROCESSOR = accounts[1] + const CLIENT = accounts[2] + const ADMIN = accounts[3] + const GATEWAY_2 = accounts[4] + const UNKNOWN = accounts[5] + const ORIGIN = accounts[6] + const ACCEPTOR = accounts[7] + const VAULT = accounts[8] + const MERCHANT = accounts[9] + const FUND_ADDRESS = accounts[2] + var TOKEN_ADDRESS = "0x0000000000000000000000000000000000000000" + const PRICE = 1000 + const FEE = 15 + const ORDER_ID = 123 + const ORDER_ID2 = 456 + const ORDER_ID3 = 789 + const MONETHA_VOUCHER_CONTRACT = "0x0000000000000000000000000000000000000000" // TODO: replace with mock or actual contract + const VOUCHERS_APPLY = 0 + + let processor, gateway, wallet, history, token, supportedToken + + before(async () => { + gateway = await MonethaGateway.new(VAULT, ADMIN, MONETHA_VOUCHER_CONTRACT) + wallet = await MerchantWallet.new(MERCHANT, "merchantId", FUND_ADDRESS) + history = await MerchantDealsHistory.new("merchantId") + supportedToken = await MonethaSupportedTokens.new() + await supportedToken.setMonethaAddress(ADMIN, true) + + processor = await PaymentProcessor.new( + supportedToken.address, + "merchantId", + history.address, + gateway.address, + wallet.address + ) + + await gateway.setMonethaAddress(processor.address, true, { from: ADMIN }) + await wallet.setMonethaAddress(processor.address, true) + await history.setMonethaAddress(processor.address, true) + token = await Token.new() + await token.mint(ACCEPTOR, PRICE) + await token.approve(processor.address, PRICE, { from: ACCEPTOR }) + + await supportedToken.addToken("abc", token.address,{from: ADMIN}) + }) + + it('should set Monetha address correctly', async () => { + await processor.setMonethaAddress(PROCESSOR, true, { from: OWNER }) + + const res = await processor.isMonethaAddress(PROCESSOR) + res.should.be.true + }) + + + it('should accept secure token payment correctly', async () => { + await processor.addOrder(ORDER_ID, PRICE, ACCEPTOR, ORIGIN, token.address, VOUCHERS_APPLY, { from: PROCESSOR }) + var order = await processor.orders(ORDER_ID) + await processor.secureTokenPay(ORDER_ID, { from: ACCEPTOR }) + + var balance = await token.balanceOf(processor.address) + balance.toNumber().should.equal(PRICE) + + await checkState(processor, ORDER_ID, State.Paid) + }) + + it('should refund payment correctly for tokens', async () => { + const clientReputation = randomReputation() + const merchantReputation = randomReputation() + + const result = await processor.refundPayment( + ORDER_ID, + clientReputation, + merchantReputation, + 0x1234, + "refundig from tests", + { from: PROCESSOR } + ) + + await checkReputation( + wallet, + clientReputation, + merchantReputation + ) + await checkState(processor, ORDER_ID, State.Refunding) + + const order = await processor.orders(ORDER_ID) + }) + + it('should not allow to withdraw ether if paid via tokens', async () => { + await processor.withdrawRefund(ORDER_ID, { from: UNKNOWN }).should.be.rejectedWith(Revert); + }) + + it('should withdraw token refund correctly', async () => { + var clientBalance1 = await token.balanceOf(ORIGIN) + clientBalance1.toNumber() + var processorBalance1 = await token.balanceOf(processor.address) + processorBalance1.toNumber() + + await processor.withdrawTokenRefund(ORDER_ID, { from: UNKNOWN }) + + var clientBalance2 = await token.balanceOf(ORIGIN) + clientBalance2.toNumber() + var processorBalance2 = await token.balanceOf(processor.address) + processorBalance2.toNumber() + + var processorBalanceDiff = processorBalance2 - processorBalance1 + var clientBalanceDiff = clientBalance2 - clientBalance1 + + processorBalanceDiff.should.equal(-PRICE) + clientBalanceDiff.should.equal(PRICE) + + await checkState(processor, ORDER_ID, State.Refunded) + }) + + it('should add order correctly', async () => { + await processor.addOrder(ORDER_ID2, PRICE, ACCEPTOR, ORIGIN, TOKEN_ADDRESS, VOUCHERS_APPLY, { from: PROCESSOR }) + + const order = await processor.orders(ORDER_ID2) + new BigNumber(order[0]).should.bignumber.equal(State.Created) + new BigNumber(order[1]).should.bignumber.equal(PRICE) + order[3].should.equal(ACCEPTOR) + order[4].should.equal(ORIGIN) + }) + + it("should not add order if the token is not supported", async function () { + await processor.addOrder(ORDER_ID2, PRICE, ACCEPTOR, ORIGIN, CLIENT, VOUCHERS_APPLY, { from: PROCESSOR }).should.be.rejectedWith(Revert); + }); + + it('should not accept secure payment if token address is present', async () => { + await processor.addOrder(ORDER_ID3, PRICE, ACCEPTOR, ORIGIN, token.address, VOUCHERS_APPLY, { from: PROCESSOR }) + + const order = await processor.orders(ORDER_ID3) + await processor.securePay(ORDER_ID3, { from: ACCEPTOR, value: PRICE }).should.be.rejectedWith(Revert); + }) + + it('should accept secure payment correctly', async () => { + const order = await processor.orders(ORDER_ID2) + + await processor.securePay(ORDER_ID2, { from: ACCEPTOR, value: PRICE }) + + const balance = new BigNumber(web3.eth.getBalance(processor.address)) + balance.should.bignumber.equal(PRICE) + + await checkState(processor, ORDER_ID2, State.Paid) + }) + + it('should refund payment correctly', async () => { + const clientReputation = randomReputation() + const merchantReputation = randomReputation() + + const result = await processor.refundPayment( + ORDER_ID2, + clientReputation, + merchantReputation, + 0x1234, + "refundig from tests", + { from: PROCESSOR } + ) + + await checkReputation( + wallet, + clientReputation, + merchantReputation + ) + await checkState(processor, ORDER_ID2, State.Refunding) + + const order = await processor.orders(ORDER_ID2) + }) + + it('should not allow to withdraw tokens if paid via ether', async () => { + await processor.withdrawTokenRefund(ORDER_ID2, { from: UNKNOWN }).should.be.rejectedWith(Revert); + }) + + it('should withdraw refund correctly', async () => { + const clientBalance1 = new BigNumber(web3.eth.getBalance(ORIGIN)) + const processorBalance1 = new BigNumber(web3.eth.getBalance(processor.address)) + + await processor.withdrawRefund(ORDER_ID2, { from: UNKNOWN }) + + const clientBalance2 = new BigNumber(web3.eth.getBalance(ORIGIN)) + const processorBalance2 = new BigNumber(web3.eth.getBalance(processor.address)) + + processorBalance2.minus(processorBalance1).should.bignumber.equal(-PRICE) + clientBalance2.minus(clientBalance1).should.bignumber.equal(PRICE) + + await checkState(processor, ORDER_ID2, State.Refunded) + }) + + it('should set Monetha gateway correctly', async () => { + await processor.setMonethaGateway(GATEWAY_2, { from: OWNER }) + + const gateway = await processor.monethaGateway() + gateway.should.equal(GATEWAY_2) + }) + + it('should cancel order correctly', async () => { + const contracts = await setupNewWithOrder() + + await contracts.processor.cancelOrder(ORDER_ID, 1234, 1234, 0, "cancel from test", { from: PROCESSOR }) + + const order = await contracts.processor.orders(ORDER_ID) + await checkState(contracts.processor, ORDER_ID, State.Cancelled) + }) + + it('should not allow to send invalid amount of money', () => { + return setupNewWithOrder() + .then(a => a.processor.securePay(ORDER_ID, { from: ACCEPTOR, value: PRICE - 1 })) + .should.be.rejected + }) + + it('should not allow to pay twice', async () => { + const contracts = await setupNewWithOrder() + await contracts.processor.securePay(ORDER_ID, { from: ACCEPTOR, value: PRICE }) + const res = contracts.processor.securePay(ORDER_ID, { from: ACCEPTOR, value: PRICE }) + + return res.should.be.rejected + }) + + it('should process payment correctly', async () => { + const clientReputation = randomReputation() + const merchantReputation = randomReputation() + + const created = await setupNewWithOrder() + const processor = created.processor + + await processor.securePay(ORDER_ID, { from: ACCEPTOR, value: PRICE }) + + const processorBalance1 = new BigNumber(web3.eth.getBalance(processor.address)) + + const result = await processor.processPayment( + ORDER_ID, + clientReputation, + merchantReputation, + 0x1234, + { from: PROCESSOR } + ) + + const processorBalance2 = new BigNumber(web3.eth.getBalance(processor.address)) + processorBalance1.minus(processorBalance2).should.bignumber.equal(PRICE) + + await checkReputation( + created.wallet, + clientReputation, + merchantReputation + ) + await checkState(processor, ORDER_ID, State.Finalized) + }) + + it('should set Merchant Deals History correctly', async () => { + history = await MerchantDealsHistory.new("merchantId") + const created = await setupNewWithOrder() + + await created.processor.setMerchantDealsHistory(history.address, { from: OWNER }) + + const historyAddress = await created.processor.merchantHistory() + historyAddress.should.equal(history.address) + }) + + it('should not set Merchant Deals History for different merchant id', async () => { + const history2 = await MerchantDealsHistory.new("merchant2") + + const created = await setupNewWithOrder("merchant1") + + await created.processor.setMerchantDealsHistory(history2.address, { from: OWNER }).should.be.rejected + }) + + it('should not add order when contract is paused', async () => { + const ORDER_ID = randomReputation() + + await processor.pause({ from: OWNER }) + + await processor.addOrder(ORDER_ID, PRICE, ACCEPTOR, ORIGIN, TOKEN_ADDRESS, VOUCHERS_APPLY, { from: PROCESSOR }).should.be.rejected + }) + + async function checkState(processor, orderID, expected) { + const order = await processor.orders(orderID) + new BigNumber(order[0]).should.bignumber.equal(expected) + } + + async function checkReputation( + merchantWallet, + expectedClientReputation, + expectedMerchantReputation + ) { + //TODO: add client reputation check, once client wallet will be implemented + + const merchRep = new BigNumber(await merchantWallet.compositeReputation("total")) + merchRep.should.bignumber.equal(expectedMerchantReputation) + } + + + async function setupNewWithOrder(_merchantId) { + merchantId = _merchantId || "merchantId"; + let gateway = await MonethaGateway.new(VAULT, ADMIN, MONETHA_VOUCHER_CONTRACT) + let wallet = await MerchantWallet.new(MERCHANT, merchantId, FUND_ADDRESS) + let history = await MerchantDealsHistory.new(merchantId) + let supportedToken = await MonethaSupportedTokens.new() + await supportedToken.setMonethaAddress(ADMIN, true) + + let processor = await PaymentProcessor.new( + supportedToken.address, + merchantId, + history.address, + gateway.address, + wallet.address + ) + + await processor.setMonethaAddress(PROCESSOR, true) + await gateway.setMonethaAddress(processor.address, true, { from: ADMIN }) + await wallet.setMonethaAddress(processor.address, true) + await history.setMonethaAddress(processor.address, true) + await supportedToken.addToken("abc", token.address,{from: ADMIN}) + await processor.addOrder(ORDER_ID, PRICE, ACCEPTOR, ORIGIN, TOKEN_ADDRESS, VOUCHERS_APPLY, { from: PROCESSOR }) + + return { processor, wallet } + } + + function randomReputation() { + return Math.floor(Math.random() * 100) + } + +}) diff --git a/test/MonethaSupportedTokensTests.js b/test/MonethaSupportedTokensTests.js index 6cb555d..d5ccad0 100644 --- a/test/MonethaSupportedTokensTests.js +++ b/test/MonethaSupportedTokensTests.js @@ -120,5 +120,36 @@ contract("MonethaSupportedTokens", function (accounts) { }); + describe("isTokenValid", function () { + it("should return if token is supported or not", async function () { + + const tx = await monethaSupportedToken.addToken( + token_acronym, + token_address, + { + from: accounts[0] + } + ).should.be.fulfilled; + + const tx1 = await monethaSupportedToken.isTokenValid( + token_address, + { + from: accounts[0] + } + ).should.be.fulfilled; + tx1.should.be.equal(true); + + const tx2 = await monethaSupportedToken.isTokenValid( + token_address2, + { + from: accounts[0] + } + ).should.be.fulfilled; + + tx2.should.be.equal(false); + }); + + }); + }); diff --git a/test/PaymentProcessorTests.js b/test/PaymentProcessorTests.js index 9c310f8..86f1934 100644 --- a/test/PaymentProcessorTests.js +++ b/test/PaymentProcessorTests.js @@ -5,6 +5,7 @@ const MerchantDealsHistory = artifacts.require("MerchantDealsHistory") const MonethaGateway = artifacts.require("MonethaGateway") const MerchantWallet = artifacts.require("MerchantWallet") const Token = artifacts.require("ERC20Mintable") +const MonethaSupportedTokens = artifacts.require("MonethaSupportedTokens"); let merchantId; contract('PaymentProcessor', function (accounts) { @@ -39,14 +40,18 @@ contract('PaymentProcessor', function (accounts) { const MONETHA_VOUCHER_CONTRACT = "0x0000000000000000000000000000000000000000" // TODO: replace with mock or actual contract const VOUCHERS_APPLY = 0 - let processor, gateway, wallet, history, token + let processor, gateway, wallet, history, token, supportedToken before(async () => { gateway = await MonethaGateway.new(VAULT, ADMIN, MONETHA_VOUCHER_CONTRACT) wallet = await MerchantWallet.new(MERCHANT, "merchantId", FUND_ADDRESS) history = await MerchantDealsHistory.new("merchantId") + supportedToken = await MonethaSupportedTokens.new() + await supportedToken.setMonethaAddress(ADMIN, true) + processor = await PaymentProcessor.new( + supportedToken.address, "merchantId", history.address, gateway.address, @@ -59,6 +64,8 @@ contract('PaymentProcessor', function (accounts) { token = await Token.new() await token.mint(ACCEPTOR, PRICE) await token.approve(processor.address, PRICE, { from: ACCEPTOR }) + + await supportedToken.addToken("abc", token.address,{from: ADMIN}) }) it('should set Monetha address correctly', async () => { @@ -139,6 +146,10 @@ contract('PaymentProcessor', function (accounts) { order[4].should.equal(ORIGIN) }) + it("should not add order if the token is not supported", async function () { + await processor.addOrder(ORDER_ID2, PRICE, ACCEPTOR, ORIGIN, FEE, CLIENT, VOUCHERS_APPLY, { from: PROCESSOR }).should.be.rejectedWith(Revert); + }); + it('should not accept secure payment if token address is present', async () => { await processor.addOrder(ORDER_ID3, PRICE, ACCEPTOR, ORIGIN, FEE, token.address, VOUCHERS_APPLY, { from: PROCESSOR }) @@ -307,8 +318,11 @@ contract('PaymentProcessor', function (accounts) { let gateway = await MonethaGateway.new(VAULT, ADMIN, MONETHA_VOUCHER_CONTRACT) let wallet = await MerchantWallet.new(MERCHANT, merchantId, FUND_ADDRESS) let history = await MerchantDealsHistory.new(merchantId) + let supportedToken = await MonethaSupportedTokens.new() + await supportedToken.setMonethaAddress(ADMIN, true) let processor = await PaymentProcessor.new( + supportedToken.address, merchantId, history.address, gateway.address, @@ -319,7 +333,7 @@ contract('PaymentProcessor', function (accounts) { await gateway.setMonethaAddress(processor.address, true, { from: ADMIN }) await wallet.setMonethaAddress(processor.address, true) await history.setMonethaAddress(processor.address, true) - + await supportedToken.addToken("abc", token.address,{from: ADMIN}) await processor.addOrder(ORDER_ID, PRICE, ACCEPTOR, ORIGIN, FEE, TOKEN_ADDRESS, VOUCHERS_APPLY, { from: PROCESSOR }) return { processor, wallet } diff --git a/test/PrivatePaymentProcessorTests.js b/test/PrivatePaymentProcessorTests.js index 0214d46..f38c8bb 100644 --- a/test/PrivatePaymentProcessorTests.js +++ b/test/PrivatePaymentProcessorTests.js @@ -3,6 +3,7 @@ const {BigNumber} = require('./helpers/setup'); const PrivatePaymentProcessor = artifacts.require("PrivatePaymentProcessor") const MonethaGateway = artifacts.require("MonethaGateway") const MerchantWallet = artifacts.require("MerchantWallet") +const MonethaSupportedTokens = artifacts.require("MonethaSupportedTokens"); const Token = artifacts.require("ERC20Mintable") contract('PrivatePaymentProcessor', function (accounts) { @@ -26,7 +27,7 @@ contract('PrivatePaymentProcessor', function (accounts) { const MONETHA_VOUCHER_CONTRACT = "0x0000000000000000000000000000000000000000" // TODO: replace with mock or actual contract const VOUCHERS_APPLY = 0 - let processor, gateway, wallet, token + let processor, gateway, wallet, token, supportedToken before(async () => { gateway = await MonethaGateway.new(VAULT, PROCESSOR, MONETHA_VOUCHER_CONTRACT) @@ -36,7 +37,11 @@ contract('PrivatePaymentProcessor', function (accounts) { wallet = await MerchantWallet.new(MERCHANT, merchantId, FUND_ADDRESS) + supportedToken = await MonethaSupportedTokens.new() + await supportedToken.setMonethaAddress(PROCESSOR, true) + processor = await PrivatePaymentProcessor.new( + supportedToken.address, merchantId, gateway.address, wallet.address @@ -51,6 +56,8 @@ contract('PrivatePaymentProcessor', function (accounts) { await token.mint(PROCESSOR, PRICE) await token.mint(processor.address, PRICE) await token.approve(processor.address, PRICE, { from: PROCESSOR }) + + await supportedToken.addToken("abc", token.address,{from: PROCESSOR}) }) it('should indentify processor address as Monetha address', async () => { @@ -109,6 +116,19 @@ contract('PrivatePaymentProcessor', function (accounts) { await processor.setMonethaGateway(oldGateWayAddress, { from: OWNER }) }) + it('should not pay for order in tokens if the token is not supported', async () => { + await token.mint(ACCEPTOR, PRICE) + await token.approve(processor.address, PRICE, { from: ACCEPTOR }) + FUND_ADDRESS = accounts[3] + var FUND_ADDRESS_BALANCE = await token.balanceOf(FUND_ADDRESS) + FUND_ADDRESS_BALANCE.toNumber() + const monethaFee = FEE + var vaultBalance1 = await token.balanceOf(VAULT) + vaultBalance1.toNumber() + + await processor.payForOrderInTokens(ORDER_ID, ORIGIN, monethaFee, CLIENT, PRICE, { from: ACCEPTOR }).should.be.rejectedWith(Revert); + + }) it('should pay for order correctly in tokens', async () => { await token.mint(ACCEPTOR, PRICE) @@ -215,6 +235,7 @@ contract('PrivatePaymentProcessor', function (accounts) { wallet = await MerchantWallet.new(MERCHANT, merchantId, FUND_ADDRESS) processor = await PrivatePaymentProcessor.new( + supportedToken.address, merchantId, gateway.address, wallet.address