diff --git a/hydrachain/examples/native/exchange/__init__.py b/hydrachain/examples/native/exchange/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hydrachain/examples/native/exchange/exchange_contract.py b/hydrachain/examples/native/exchange/exchange_contract.py new file mode 100755 index 0000000..6b34758 --- /dev/null +++ b/hydrachain/examples/native/exchange/exchange_contract.py @@ -0,0 +1,210 @@ +import ethereum.utils as utils +import ethereum.slogging as slogging +import hydrachain.native_contracts as nc +from hydrachain.nc_utils import isaddress, STATUS, FORBIDDEN, OK, INSUFFICIENTFUNDS +from hydrachain.examples.native.fungible.fungible_contract import Fungible +log = slogging.get_logger('contracts.exchange') + + +class Partial(nc.ABIEvent): + + "Triggerd when tokens are partially traded on the Exchange" + args = [dict(name='currencyPair', type='bytes32', indexed=True), + dict(name='seller', type='address', indexed=True), + dict(name='offerAmount', type='uint256', indexed=False), + dict(name='buyer', type='address', indexed=True), + dict(name='wantAmount', type='uint256', indexed=False)] + + +class Traded(nc.ABIEvent): + + "Triggerd when tokens are traded on the Exchange" + args = [dict(name='currencyPair', type='bytes32', indexed=True), + dict(name='seller', type='address', indexed=True), + dict(name='offerAmount', type='uint256', indexed=False), + dict(name='buyer', type='address', indexed=True), + dict(name='wantAmount', type='uint256', indexed=False)] + + +class Exchange(nc.NativeContract): + + """ + Exchange to trade token based on the Ethereum Token standard + https://github.com/ethereum/wiki/wiki/Standardized_Contract_APIs + """ + + address = utils.int_to_addr(4000) + events = [Partial, Traded] + + # Struct + order_creator = nc.List('address') + order_offerCurrency = nc.List('address') + order_offerAmount = nc.List('uint256') + order_wantCurrency = nc.List('address') + order_wantAmount = nc.List('uint256') + order_id = nc.Scalar('uint256') + + owner = nc.Scalar('address') + nextOrderId = nc.Scalar('uint256') + + def init(ctx, returns=STATUS): + log.DEV('In Exchange.init') + if isaddress(ctx.owner): + return FORBIDDEN + ctx.owner = ctx.tx_origin + # Initialize + ctx.nextOrderId = 1 + return OK + + @nc.constant + def get_nextOrderId(ctx, returns='uint256'): + return ctx.nextOrderId + + @nc.constant + def get_orderCreator(ctx, _offerId='uint256', returns='address'): + return ctx.order_creator[_offerId - 1] + + def place_order(ctx, _offerCurrency='address', _offerAmount='uint256', _wantCurrency='address', _wantAmount='uint256', returns='uint256'): + offer_id = nc.Scalar('uint256') + offer_id = 0 + # Deposit, coins at address: _offerCurrency + # from: Msg Sender + # to: Exchange + # Value: _offerAmount + r = ctx.call_abi( + _offerCurrency, + Fungible.transferFrom, + ctx.msg_sender, + ctx.address, + _offerAmount) + if r is OK: + # Store Offer + offer_id = ctx.nextOrderId + ctx.nextOrderId += 1 + ctx.order_creator.append(ctx.msg_sender) + ctx.order_offerCurrency.append(_offerCurrency) + ctx.order_offerAmount.append(_offerAmount) + ctx.order_wantCurrency.append(_wantCurrency) + ctx.order_wantAmount.append(_wantAmount) + return offer_id + elif r is INSUFFICIENTFUNDS: + return offer_id + else: + raise Exception() + + def claim_order_partial(ctx, _offerId='uint256', _offerAmount='uint256', _wantAmount='uint256', returns=STATUS): + # Check for the right rate + if _offerAmount >= ctx.order_offerAmount[_offerId - 1] or _wantAmount >= ctx.order_wantAmount[_offerId - 1]: + return FORBIDDEN + # Assert that: offerAmount/order_offerAmount == wantAmount/order_wantAmount + if _offerAmount * ctx.order_wantAmount[_offerId - 1] != _wantAmount * ctx.order_offerAmount[_offerId - 1]: + return FORBIDDEN + + # Send, coins at address: _wantCurrency + # from: Msg Sender + # to: Order Creator + # Value: _wantAmount + r = ctx.call_abi( + ctx.order_wantCurrency[_offerId - 1], + Fungible.transferFrom, + ctx.msg_sender, + ctx.order_creator[_offerId - 1], + _wantAmount) + + if r is OK: + # Send, coins at address: _offerAmount + # from: Exchange + # to: Msg Sender + # Value: _offerAmount + # i.e. execute 2nd side of the trade + r1 = ctx.call_abi( + ctx.order_offerCurrency[_offerId - 1], + Fungible.transfer, + ctx.msg_sender, + _offerAmount) + # TODO CurrencyPair + # currencyPair = (self.orders[_offerId].offerCurrency / 2**32) * 2**128 + (self.orders[_offerId].wantCurrency / 2**32) + # currencyPair = ctx.order_offerCurrency[_offerId - 1] + ctx.order_wantCurrency[_offerId - 1] + currencyPair = "" + ctx.Partial( + currencyPair, + ctx.order_creator[_offerId - 1], + _offerAmount, + ctx.msg_sender, + _wantAmount) + # Update order as partially matched + ctx.order_offerAmount[_offerId - 1] -= _offerAmount + ctx.order_wantAmount[_offerId - 1] -= _wantAmount + return OK + elif r is INSUFFICIENTFUNDS: + return INSUFFICIENTFUNDS + else: + raise Exception() + + + def claim_order(ctx, _offerId='uint256', returns=STATUS): + # Send, coins at address: _wantCurrency + # from: Msg Sender + # to: Order Creator + # Value: _wantAmount + r = ctx.call_abi( + ctx.order_wantCurrency[_offerId - 1], + Fungible.transferFrom, + ctx.msg_sender, + ctx.order_creator[_offerId - 1], + ctx.order_wantAmount[_offerId - 1]) + + if r is OK: + # Send, coins at address: _offerAmount + # from: Exchange + # to: Msg Sender + # Value: _offerAmount + # i.e. execute 2nd side of the trade + r1 = ctx.call_abi( + ctx.order_offerCurrency[_offerId - 1], + Fungible.transfer, + ctx.msg_sender, + ctx.order_offerAmount[_offerId - 1]) + # TODO CurrencyPair + # currencyPair = (self.orders[_offerId].offerCurrency / 2**32) * 2**128 + (self.orders[_offerId].wantCurrency / 2**32) + # currencyPair = ctx.order_offerCurrency[_offerId - 1] + ctx.order_wantCurrency[_offerId - 1] + currencyPair = "" + ctx.Traded( + currencyPair, + ctx.order_creator[_offerId - 1], + ctx.order_offerAmount[_offerId - 1], + ctx.msg_sender, + ctx.order_wantAmount[_offerId - 1]) + # Update order as executed + ctx.order_creator[_offerId - 1] = '\0' * 20 + ctx.order_offerCurrency[_offerId - 1] = '\0' * 20 + ctx.order_offerAmount[_offerId - 1] = 0 + ctx.order_wantCurrency[_offerId - 1] = '\0' * 20 + ctx.order_wantAmount[_offerId - 1] = 0 + return OK + elif r is INSUFFICIENTFUNDS: + return INSUFFICIENTFUNDS + else: + raise Exception() + + + def delete_order(ctx, _offerId='uint256', returns=STATUS): + # Only creator can delete its order + if ctx.msg_sender != ctx.order_creator[_offerId - 1]: + return FORBIDDEN + # Return, coins at address: _offerCurrency + # from: Exchange + # to: Order Creator + # Value: _offerAmount + r = ctx.call_abi( + ctx.order_offerCurrency[_offerId - 1], + Fungible.transfer, + ctx.order_creator[_offerId - 1], + ctx.order_offerAmount[_offerId - 1]) + # Update order as deleted + ctx.order_creator[_offerId - 1] = '\0' * 20 + ctx.order_offerCurrency[_offerId - 1] = '\0' * 20 + ctx.order_offerAmount[_offerId - 1] = 0 + ctx.order_wantCurrency[_offerId - 1] = '\0' * 20 + ctx.order_wantAmount[_offerId - 1] = 0 + return OK diff --git a/hydrachain/examples/native/exchange/test_exchange_contract.py b/hydrachain/examples/native/exchange/test_exchange_contract.py new file mode 100755 index 0000000..76631a7 --- /dev/null +++ b/hydrachain/examples/native/exchange/test_exchange_contract.py @@ -0,0 +1,187 @@ +from hydrachain import native_contracts as nc +from hydrachain.nc_utils import FORBIDDEN, OK, INSUFFICIENTFUNDS +from hydrachain.examples.native.fungible.fungible_contract import Fungible +from hydrachain.examples.native.exchange.exchange_contract import Exchange +from ethereum import tester +import ethereum.slogging as slogging +log = slogging.get_logger('test.exchange') + + +class User(object): + + def __init__(self, state, address, key): + self.state = state + self.address = address + self.key = key + + def add_proxy(self, name, address): + setattr(self, name, nc.tester_nac(self.state, self.key, address)) + + +def _prepare_env(state, logs, admin, alice, bob, eur_address, usd_address, exc_address): + # Create proxy EUR + admin.add_proxy('eur', eur_address) + assert OK == admin.eur.init(2**32) + assert OK == admin.eur.transfer(alice.address, 2**31) + alice.add_proxy('eur', eur_address) + bob.add_proxy('eur', eur_address) + assert admin.eur.balanceOf(admin.address) == 2**31 + + # Create proxy USD + admin.add_proxy('usd', usd_address) + assert OK == admin.usd.init(2**32) + assert OK == admin.usd.transfer(bob.address, 2**31) + alice.add_proxy('usd', usd_address) + bob.add_proxy('usd', usd_address) + assert admin.usd.balanceOf(admin.address) == 2**31 + + # Checking Total Sum + accounts = admin.eur.get_accounts() + assert accounts == [admin.address, alice.address] + total_sum = 0 + for account in accounts: + total_sum += admin.eur.balanceOf(account) + assert total_sum == 2**32 + accounts = admin.usd.get_accounts() + assert accounts == [admin.address, bob.address] + total_sum = 0 + for account in accounts: + total_sum += admin.usd.balanceOf(account) + assert total_sum == 2**32 + + # Create proxy Exchange + admin.add_proxy('exc', exc_address) + assert OK == admin.exc.init() + alice.add_proxy('exc', exc_address) + bob.add_proxy('exc', exc_address) + + return True + + +def test_exchange_template(): + """ + Tests; + Initialization, + Create orders, + Delete orders, + Partially claim orders, + Fully Claim orders. + Events; + Checking logs + """ + + # Register Contract Fungible + nc.registry.register(Fungible) + nc.registry.register(Exchange) + + state = tester.state() + logs = [] + admin = User(state, tester.a0, tester.k0) + alice = User(state, tester.a2, tester.k2) + bob = User(state, tester.a3, tester.k3) + + # create listeners + for evt_class in Fungible.events + Exchange.events: + nc.listen_logs(state, evt_class, callback=lambda e: logs.append(e)) + + # Initialization + eur_address = nc.tester_create_native_contract_instance(state, admin.key, Fungible) + usd_address = nc.tester_create_native_contract_instance(state, admin.key, Fungible) + exc_address = nc.tester_create_native_contract_instance(state, admin.key, Exchange) + _prepare_env(state, logs, admin, alice, bob, eur_address, usd_address, exc_address) + assert admin.eur.balanceOf(alice.address) == 2**31 + alice_eur_balance = admin.eur.balanceOf(alice.address) + assert admin.usd.balanceOf(bob.address) == 2**31 + bob_usd_balance = admin.usd.balanceOf(bob.address) + assert admin.exc.get_nextOrderId() == 1 + + # Alice does an IPO of her coin at the exchange + # Remark, + # Price is set by the division of the two quantities. + alice_coin_quantity = 100 + bob_coin_quantity = 100 + # Alice does a direct Debit for Exchange address + alice.eur.approve(exc_address, alice_coin_quantity) + # Place Order on Exchange + alice_offer_id = alice.exc.place_order( + eur_address, + alice_coin_quantity, + usd_address, + bob_coin_quantity) + + # Check log data of Transfer Event + assert len(logs) == 4 + l = logs[3] + assert l['event_type'] == 'Transfer' + assert l['from'] == alice.address + assert l['to'] == exc_address + assert l['value'] == alice_coin_quantity + assert alice_offer_id == 1 + assert alice.exc.get_orderCreator(alice_offer_id) == alice.address + assert alice.exc.get_nextOrderId() == 2 + # Check balances + assert admin.eur.balanceOf(alice.address) == alice_eur_balance - alice_coin_quantity + assert admin.eur.balanceOf(exc_address) == alice_coin_quantity + + # Alice deletes her offer at the exchange + assert OK == alice.exc.delete_order(alice_offer_id) + # Check log data of Transfer Event + assert len(logs) == 5 + l = logs[4] + assert l['event_type'] == 'Transfer' + assert l['from'] == exc_address + assert l['to'] == alice.address + assert l['value'] == alice_coin_quantity + + assert alice_offer_id == 1 + assert alice.exc.get_orderCreator(alice_offer_id) == '\0' * 20 + assert alice.exc.get_nextOrderId() == 2 + # Check balances + assert admin.eur.balanceOf(alice.address) == alice_eur_balance + assert admin.eur.balanceOf(exc_address) == 0 + assert alice.eur.allowance(exc_address) == 0 + + # Alice tries again, willing to lower her price. + alice.eur.approve(exc_address, 100) + alice_coin_quantity = 100 + bob_coin_quantity = 50 + alice_offer_id = alice.exc.place_order( + eur_address, + alice_coin_quantity, + usd_address, + bob_coin_quantity) + # check logs data of Transfer Event + assert alice_offer_id == 2 + assert alice.exc.get_orderCreator(alice_offer_id) == alice.address + assert alice.exc.get_nextOrderId() == 3 + + # Now Bob wants to share coins + # Bob does a direct Debit for Exchange address + bob.usd.approve(exc_address, bob_coin_quantity) + # First with partial amounts + alice_coin_partial = 10 + bob_coin_partial = 5 + assert OK == bob.exc.claim_order_partial(alice_offer_id, alice_coin_partial, bob_coin_partial) + assert admin.eur.balanceOf(alice.address) == alice_eur_balance - alice_coin_quantity + assert admin.usd.balanceOf(alice.address) == bob_coin_partial + assert admin.eur.balanceOf(bob.address) == alice_coin_partial + assert admin.usd.balanceOf(bob.address) == bob_usd_balance - bob_coin_partial + # Then the remaining amounts + assert OK == bob.exc.claim_order(alice_offer_id) + # Check balances of Exchange + assert admin.eur.balanceOf(exc_address) == 0 + assert admin.usd.balanceOf(exc_address) == 0 + assert alice.eur.allowance(exc_address) == 0 + assert bob.usd.allowance(exc_address) == 0 + # Check balances - Cross Currency + assert admin.eur.balanceOf(alice.address) == alice_eur_balance - alice_coin_quantity + assert admin.usd.balanceOf(alice.address) == bob_coin_quantity + assert admin.eur.balanceOf(bob.address) == alice_coin_quantity + assert admin.usd.balanceOf(bob.address) == bob_usd_balance - bob_coin_quantity + + # Print Logs + for l in logs: + print l['event_type'] + + nc.registry.unregister(Fungible) + nc.registry.unregister(Exchange)