From 71b4cff90c0bea98d52748e28ccec04f2d89e36b Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker Date: Sun, 17 Jun 2018 20:20:21 +0200 Subject: [PATCH 01/12] add smart contract for the Ethereum atomic swaps includes README with instructions how to test and deploy the (Solidity) atomic swap contract using truffle --- cmd/ethatomicswap/solidity/.gitignore | 1 + cmd/ethatomicswap/solidity/README.md | 28 + .../solidity/contracts/AtomicSwap.sol | 178 ++++ .../solidity/contracts/Migrations.sol | 23 + .../migrations/1_initial_migration.js | 5 + .../migrations/2_atomicswap_migration.js | 6 + .../solidity/test/TestAtomicSwap.js | 839 ++++++++++++++++++ cmd/ethatomicswap/solidity/test/exceptions.js | 22 + cmd/ethatomicswap/solidity/test/utils.js | 3 + cmd/ethatomicswap/solidity/truffle-config.js | 18 + cmd/ethatomicswap/solidity/truffle.js | 18 + 11 files changed, 1141 insertions(+) create mode 100644 cmd/ethatomicswap/solidity/.gitignore create mode 100644 cmd/ethatomicswap/solidity/README.md create mode 100644 cmd/ethatomicswap/solidity/contracts/AtomicSwap.sol create mode 100644 cmd/ethatomicswap/solidity/contracts/Migrations.sol create mode 100644 cmd/ethatomicswap/solidity/migrations/1_initial_migration.js create mode 100644 cmd/ethatomicswap/solidity/migrations/2_atomicswap_migration.js create mode 100644 cmd/ethatomicswap/solidity/test/TestAtomicSwap.js create mode 100644 cmd/ethatomicswap/solidity/test/exceptions.js create mode 100644 cmd/ethatomicswap/solidity/test/utils.js create mode 100644 cmd/ethatomicswap/solidity/truffle-config.js create mode 100644 cmd/ethatomicswap/solidity/truffle.js diff --git a/cmd/ethatomicswap/solidity/.gitignore b/cmd/ethatomicswap/solidity/.gitignore new file mode 100644 index 0000000..d163863 --- /dev/null +++ b/cmd/ethatomicswap/solidity/.gitignore @@ -0,0 +1 @@ +build/ \ No newline at end of file diff --git a/cmd/ethatomicswap/solidity/README.md b/cmd/ethatomicswap/solidity/README.md new file mode 100644 index 0000000..8da15a1 --- /dev/null +++ b/cmd/ethatomicswap/solidity/README.md @@ -0,0 +1,28 @@ +# AtomicSwap Smart Contract for the EVM + +In this directory you can find the smart contract, written in Solidity, +to be used together with the `ethatomicswap` tool. + +## WARNING + +This contract has only recently been developed, and has not received any external audits yet. Please use common sense when doing anything that deals with real money! We take no responsibility for any security problem you might experience while using this contract. + +## Test + +You can test the AtomicSwap smart contract, +found as [/cmd/ethatomicswap/solidity/contracts/AtomicSwap.sol](/cmd/ethatomicswap/solidity/contracts/AtomicSwap.sol) using a single command. It has however following prerequisites: + +* Install NodeJS (10.5.0), which bundles _npm_ as well; +* Install truffle: `npm install -g truffle`; +* Install and run Ganache: ; + +Once you have fulfilled all prerequisites listed above, +you can run the unit tests provided with the AtomicSwap contract, using: + +``` +truffle test +``` + +## Deploy + +// TODO diff --git a/cmd/ethatomicswap/solidity/contracts/AtomicSwap.sol b/cmd/ethatomicswap/solidity/contracts/AtomicSwap.sol new file mode 100644 index 0000000..bdeceff --- /dev/null +++ b/cmd/ethatomicswap/solidity/contracts/AtomicSwap.sol @@ -0,0 +1,178 @@ +// Copyright (c) 2017 Altcoin Exchange, Inc +// Copyright (c) 2018 The Decred developers and Contributors +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +pragma solidity ^0.4.23; + +// Notes on security warnings: +// + block.timestamp is safe to use, +// given that our timestamp can tolerate a 30-second drift in time; + +contract AtomicSwap { + enum Kind { Initiator, Participant } + enum State { Empty, Filled, Redeemed, Refunded } + + struct Swap { + uint initTimestamp; + uint refundTime; + bytes32 secretHash; + bytes32 secret; + address initiator; + address participant; + uint256 value; + Kind kind; + State state; + } + + mapping(bytes32 => Swap) public swaps; + + event Refunded( + uint refundTime, + bytes32 secretHash, + address refunder, + uint256 value + ); + + event Redeemed( + uint redeemTime, + bytes32 secretHash, + bytes32 secret, + address redeemer, + uint256 value + ); + + event Participated( + uint initTimestamp, + uint refundTime, + bytes32 secretHash, + address initiator, + address participant, + uint256 value + ); + + event Initiated( + uint initTimestamp, + uint refundTime, + bytes32 secretHash, + address initiator, + address participant, + uint256 value + ); + + constructor() public {} + + modifier isRefundable(bytes32 secretHash, address refunder) { + require(swaps[secretHash].state == State.Filled); + if (swaps[secretHash].kind == Kind.Participant) { + require(swaps[secretHash].participant == refunder); + } else { + require(swaps[secretHash].initiator == refunder); + } + uint preRefundTimestamp = swaps[secretHash].initTimestamp; + preRefundTimestamp += swaps[secretHash].refundTime; + require(block.timestamp > preRefundTimestamp); + _; + } + + modifier isRedeemable(bytes32 secretHash, bytes32 secret, address redeemer) { + require(swaps[secretHash].state == State.Filled); + if (swaps[secretHash].kind == Kind.Participant) { + require(swaps[secretHash].initiator == redeemer); + } else { + require(swaps[secretHash].participant == redeemer); + } + require(sha256(abi.encodePacked(secret)) == secretHash); + _; + } + + modifier isInitiator(bytes32 secretHash) { + require(msg.sender == swaps[secretHash].initiator); + _; + } + + modifier isNotInitiated(bytes32 secretHash) { + require(swaps[secretHash].state == State.Empty); + _; + } + + function initiate(uint refundTime, bytes32 secretHash, address participant) + public + payable + isNotInitiated(secretHash) + { + swaps[secretHash].initTimestamp = block.timestamp; + swaps[secretHash].refundTime = refundTime; + swaps[secretHash].secretHash = secretHash; + swaps[secretHash].initiator = msg.sender; + swaps[secretHash].participant = participant; + swaps[secretHash].value = msg.value; + swaps[secretHash].kind = Kind.Initiator; + swaps[secretHash].state = State.Filled; + emit Initiated( + block.timestamp, + refundTime, + secretHash, + msg.sender, + participant, + msg.value + ); + } + + function participate(uint refundTime, bytes32 secretHash, address initiator) + public + payable + isNotInitiated(secretHash) + { + swaps[secretHash].initTimestamp = block.timestamp; + swaps[secretHash].refundTime = refundTime; + swaps[secretHash].secretHash = secretHash; + swaps[secretHash].initiator = initiator; + swaps[secretHash].participant = msg.sender; + swaps[secretHash].value = msg.value; + swaps[secretHash].kind = Kind.Participant; + swaps[secretHash].state = State.Filled; + emit Participated( + block.timestamp, + refundTime, + secretHash, + initiator, + msg.sender, + msg.value + ); + } + + function redeem(bytes32 secret, bytes32 secretHash) + public + isRedeemable(secretHash, secret, msg.sender) + { + msg.sender.transfer(swaps[secretHash].value); + + swaps[secretHash].state = State.Redeemed; + swaps[secretHash].secret = secret; + + emit Redeemed( + block.timestamp, + swaps[secretHash].secretHash, + swaps[secretHash].secret, + msg.sender, + swaps[secretHash].value + ); + } + + function refund(bytes32 secretHash) + public + isRefundable(secretHash, msg.sender) + { + msg.sender.transfer(swaps[secretHash].value); + + swaps[secretHash].state = State.Refunded; + + emit Refunded( + block.timestamp, + swaps[secretHash].secretHash, + msg.sender, + swaps[secretHash].value + ); + } +} diff --git a/cmd/ethatomicswap/solidity/contracts/Migrations.sol b/cmd/ethatomicswap/solidity/contracts/Migrations.sol new file mode 100644 index 0000000..97c708c --- /dev/null +++ b/cmd/ethatomicswap/solidity/contracts/Migrations.sol @@ -0,0 +1,23 @@ +pragma solidity ^0.4.23; + +contract Migrations { + address public owner; + uint public last_completed_migration; + + constructor() public { + owner = msg.sender; + } + + modifier restricted() { + if (msg.sender == owner) _; + } + + function setCompleted(uint completed) public restricted { + last_completed_migration = completed; + } + + function upgrade(address new_address) public restricted { + Migrations upgraded = Migrations(new_address); + upgraded.setCompleted(last_completed_migration); + } +} diff --git a/cmd/ethatomicswap/solidity/migrations/1_initial_migration.js b/cmd/ethatomicswap/solidity/migrations/1_initial_migration.js new file mode 100644 index 0000000..4d5f3f9 --- /dev/null +++ b/cmd/ethatomicswap/solidity/migrations/1_initial_migration.js @@ -0,0 +1,5 @@ +var Migrations = artifacts.require("./Migrations.sol"); + +module.exports = function(deployer) { + deployer.deploy(Migrations); +}; diff --git a/cmd/ethatomicswap/solidity/migrations/2_atomicswap_migration.js b/cmd/ethatomicswap/solidity/migrations/2_atomicswap_migration.js new file mode 100644 index 0000000..d3a6b43 --- /dev/null +++ b/cmd/ethatomicswap/solidity/migrations/2_atomicswap_migration.js @@ -0,0 +1,6 @@ +var AtomicSwap = artifacts.require("AtomicSwap"); + +module.exports = function(deployer) { + // deployment steps + deployer.deploy(AtomicSwap); +}; \ No newline at end of file diff --git a/cmd/ethatomicswap/solidity/test/TestAtomicSwap.js b/cmd/ethatomicswap/solidity/test/TestAtomicSwap.js new file mode 100644 index 0000000..0559950 --- /dev/null +++ b/cmd/ethatomicswap/solidity/test/TestAtomicSwap.js @@ -0,0 +1,839 @@ +// Copyright (c) 2018 The Decred developers and Contributors +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +const AtomicSwap = artifacts.require("AtomicSwap"); + +contract("AtomicSwap tests", accounts => { + let tryCatch = require("./exceptions.js").tryCatch; + let errTypes = require("./exceptions.js").errTypes; + let utils = require("./utils.js"); + + // Solidity Enums aren't exported, + // a manual integer mapping is therefore required + const [kindInitiator, kindParticipant] = [0, 1]; + const [stateEmpty, stateFilled, stateRedeemed, stateRefunded] = [0, 1, 2, 3]; + + // contractAccount is used only to deploy the contracts, + // firstAccount and secondAccount are used for valid and invalid transfers, + // and thridAccount and fourthAccount are used only for invalid transfers + const [contractAccount, firstAccount, secondAccount, thirdAccount, fourthAccount] = accounts; + + // atomicSwap gets assigned, before each unit test, + // the instance of a newly deployed AtomicSwap smart contract + let atomicSwap; + + // define constant hash+secretHash + const secret = "0x64f1ddd4cc83a3aaf37a7f290ec922dc764de023acdd11bf76c24378b086a017"; + const secretHash = "0xd4ebb2bf3e7898c18f6fe07d8eb8e7084e0bae52ae44a42ca6cdba240f58549f"; + // wrong hash+secretHash + const wrongSecretHash = "0xe3b25a963d024e7788d97ae1030bdb279731edb190f25d4aa5d38c400e08634e"; + const wrongSecret = "0x686f661e0c2f7678d2751db8662cc56cb9b6a7bdfd0524f0a841006c244cfc37"; + // empty secret + const emptySecret = "0x0000000000000000000000000000000000000000000000000000000000000000"; + + beforeEach(async () => { + atomicSwap = await AtomicSwap.new(); + + console.log("balance of accounts before test:") + console.log(" * balance of account #1: " + web3.eth.getBalance(firstAccount).toString()); + console.log(" * balance of account #2: " + web3.eth.getBalance(secondAccount).toString()); + console.log(" * balance of account #3: " + web3.eth.getBalance(thirdAccount).toString()); + console.log(" * balance of account #4: " + web3.eth.getBalance(fourthAccount).toString()); + }); + + afterEach(async () => { + console.log("balance of accounts after test:") + console.log(" * balance of account #1: " + web3.eth.getBalance(firstAccount).toString()); + console.log(" * balance of account #2: " + web3.eth.getBalance(secondAccount).toString()); + console.log(" * balance of account #3: " + web3.eth.getBalance(thirdAccount).toString()); + console.log(" * balance of account #4: " + web3.eth.getBalance(fourthAccount).toString()); + }); + + it("should be able to redeem a participation contract", async () => { + const contractAmount = web3.toBigNumber(web3.toWei('0.01', 'ether')); + const refundTime = 60; + + let initTimestamp; + + // store initial balance of our accounts, + // so that we can check if the locked value + // transfers indeed between accounts + let balanceFirstAccount = web3.eth.getBalance(firstAccount); + let balanceSecondAccount = web3.eth.getBalance(secondAccount); + let expectedBalanceFirstAccount = balanceFirstAccount; + let expectedBalanceSecondAccount = balanceSecondAccount; + + // ensure our contract does not exist yet + var [,,,,,,,,contractState] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractState, stateEmpty, "state should equal Empty"); + + // sanity balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // create participation contract + await atomicSwap.participate(refundTime, secretHash, secondAccount, + {from: firstAccount, value: contractAmount, gasPrice: 0}). + then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Participated", "Expected Participated event"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "Value should equal contractAmount"); + assert.equal(firstLog.args.secretHash, secretHash, "SecretHash should be as expected"); + assert.equal(firstLog.args.refundTime, refundTime, "RefundTime should be as expected"); + assert.equal(firstLog.args.initiator, secondAccount, "Initiator should equal secondAccount"); + assert.equal(firstLog.args.participant, firstAccount, "Participant should equal firstAccount"); + initTimestamp = firstLog.args.initTimestamp.toNumber(); + }); + + // update balance and check it again + balanceFirstAccount = web3.eth.getBalance(firstAccount); + expectedBalanceFirstAccount = expectedBalanceFirstAccount.add(contractAmount.negated()); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // assert all contract details + var [ + contractTime, + contractRefundTime, + contractSecretHash, + contractSecret, + contractInitiator, + contractParticipant, + contractValue, + contractKind, + contractState, + ] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractSecretHash, secretHash, "secretHash should be as expected"); + assert.equal(contractSecret, emptySecret, "secret should still be nil"); + assert.equal(contractInitiator, secondAccount, "initiator should equal secondAccount"); + assert.equal(contractParticipant, firstAccount, "participant should equal firstAccount"); + assert.equal(contractValue.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(contractKind, kindParticipant, "kind should equal Participant"); + assert.equal(contractState, stateFilled, "state should equal Filled"); + + // creating another contract using the same secretHash should fail + await tryCatch(atomicSwap.participate(refundTime, secretHash, secondAccount, + {from: firstAccount, value: contractAmount, gasPrice: 0}), errTypes.revert); + // even when using different accounts + await tryCatch(atomicSwap.participate(refundTime, secretHash, fourthAccount, + {from: thirdAccount, value: contractAmount, gasPrice: 0}), errTypes.revert); + // and even when trying to create an initiation contract, instead of an participation contract + await tryCatch(atomicSwap.initiate(refundTime, secretHash, fourthAccount, + {from: thirdAccount, value: contractAmount, gasPrice: 0}), errTypes.revert); + + // only the initiator can refund a contract + await tryCatch(atomicSwap.refund(secretHash, + {from: secondAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.refund(secretHash, + {from: thirdAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.refund(secretHash, + {from: fourthAccount, gasPrice: 0}), errTypes.revert); + + // but even the initiator cannot refund, given the refundTime has not yet been reached + await tryCatch(atomicSwap.refund(secretHash, + {from: firstAccount, gasPrice: 0}), errTypes.revert); + + // only the the participant can redeem a contract + await tryCatch(atomicSwap.redeem(secret, secretHash, + {from: firstAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.redeem(secret, secretHash, + {from: thirdAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.redeem(secret, secretHash, + {from: fourthAccount, gasPrice: 0}), errTypes.revert); + + // the participant has to give however give the correct secret hash + await tryCatch(atomicSwap.redeem(secret, wrongSecretHash, + {from: secondAccount, gasPrice: 0}), errTypes.revert); + // and the correct secret + await tryCatch(atomicSwap.redeem(wrongSecret, secretHash, + {from: secondAccount, gasPrice: 0}), errTypes.revert); + // in fact, the secretHash has to be the correct one and the secretHash has to equal sha256(secret) + await tryCatch(atomicSwap.redeem(wrongSecret, wrongSecretHash, + {from: secondAccount, gasPrice: 0}), errTypes.revert); + + // redeem the participation contract as the the participant + await atomicSwap.redeem(secret, secretHash, {from: secondAccount, gasPrice: 0}). + then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Redeemed", "Expected Redeemed event"); + assert.isAtLeast(firstLog.args.redeemTime.toNumber(), initTimestamp, + "redeem time " + firstLog.args.redeemTime + + " should be atleast equal to the init timestamp " + + initTimestamp.toString()); + assert.equal(firstLog.args.secretHash, secretHash, "secretHash should be as expected"); + assert.equal(firstLog.args.secret, secret, "secret should be as expected"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(firstLog.args.redeemer, secondAccount, "redeemer should equal secondAccount"); + }); + + // ensure balance updates of second account + balanceSecondAccount = web3.eth.getBalance(secondAccount); + expectedBalanceSecondAccount = expectedBalanceSecondAccount.add(contractAmount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should have decreased by txn cost, " + + "and should have received contract amount"); + + // assert state has now been updated, + // and that our contract still exists + var [ + contractTime, + contractRefundTime, + contractSecretHash, + contractSecret, + contractInitiator, + contractParticipant, + contractValue, + contractKind, + contractState, + ] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractSecretHash, secretHash, "secretHash should be as expected"); + assert.equal(contractSecret, secret, "secret should no longer be nil and instead be as expected"); + assert.equal(contractInitiator, secondAccount, "initiator should equal secondAccount"); + assert.equal(contractParticipant, firstAccount, "participant should equal firstAccount"); + assert.equal(contractValue.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(contractKind, kindParticipant, "kind should equal Participant"); + assert.equal(contractState, stateRedeemed, "state should equal Redeemed"); + + // last balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + }); + + it("should be able to redeem a participation contract even when refunding is already possible", async () => { + const contractAmount = web3.toBigNumber(web3.toWei('0.01', 'ether')); + const refundTime = 1; + + let initTimestamp; + + // store initial balance of our accounts, + // so that we can check if the locked value + // transfers indeed between accounts + let balanceFirstAccount = web3.eth.getBalance(firstAccount); + let balanceSecondAccount = web3.eth.getBalance(secondAccount); + let expectedBalanceFirstAccount = balanceFirstAccount; + let expectedBalanceSecondAccount = balanceSecondAccount; + + // ensure our contract does not exist yet + var [,,,,,,,,contractState] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractState, stateEmpty, "state should equal Empty"); + + // sanity balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // create participation contract + await atomicSwap.participate(refundTime, secretHash, secondAccount, + {from: firstAccount, value: contractAmount, gasPrice: 0}). + then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Participated", "Expected Participated event"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "Value should equal contractAmount"); + assert.equal(firstLog.args.secretHash, secretHash, "SecretHash should be as expected"); + assert.equal(firstLog.args.refundTime, refundTime, "RefundTime should be as expected"); + assert.equal(firstLog.args.initiator, secondAccount, "Initiator should equal secondAccount"); + assert.equal(firstLog.args.participant, firstAccount, "Participant should equal firstAccount"); + initTimestamp = firstLog.args.initTimestamp.toNumber(); + }); + + // update balance and check it again + balanceFirstAccount = web3.eth.getBalance(firstAccount); + expectedBalanceFirstAccount = expectedBalanceFirstAccount.add(contractAmount.negated()); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // sleep so that the refund-period gets reached + await utils.sleep(refundTime * 2000); + + // redeem the participation contract as the the participant + await atomicSwap.redeem(secret, secretHash, {from: secondAccount, gasPrice: 0}). + then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Redeemed", "Expected Redeemed event"); + assert.isAtLeast(firstLog.args.redeemTime.toNumber(), initTimestamp, + "redeem time " + firstLog.args.redeemTime + + " should be atleast equal to the init timestamp " + + initTimestamp.toString()); + assert.equal(firstLog.args.secretHash, secretHash, "secretHash should be as expected"); + assert.equal(firstLog.args.secret, secret, "secret should be as expected"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(firstLog.args.redeemer, secondAccount, "redeemer should equal secondAccount"); + }); + + // ensure balance updates of second account + balanceSecondAccount = web3.eth.getBalance(secondAccount); + expectedBalanceSecondAccount = expectedBalanceSecondAccount.add(contractAmount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should have decreased by txn cost, " + + "and should have received contract amount"); + + // assert state and that our contract still exists + var [ + contractTime, + contractRefundTime, + contractSecretHash, + contractSecret, + contractInitiator, + contractParticipant, + contractValue, + contractKind, + contractState, + ] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractSecretHash, secretHash, "secretHash should be as expected"); + assert.equal(contractSecret, secret, "secret should no longer be nil and instead be as expected"); + assert.equal(contractInitiator, secondAccount, "initiator should equal secondAccount"); + assert.equal(contractParticipant, firstAccount, "participant should equal firstAccount"); + assert.equal(contractValue.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(contractKind, kindParticipant, "kind should equal Participant"); + assert.equal(contractState, stateRedeemed, "state should equal Redeemed"); + + // last balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + }); + + it("should be able to redeem an initiation contract", async () => { + const contractAmount = web3.toBigNumber(web3.toWei('0.01', 'ether')); + const refundTime = 60; + + let initTimestamp; + + // store initial balance of our accounts, + // so that we can check if the locked value + // transfers indeed between accounts + let balanceFirstAccount = web3.eth.getBalance(firstAccount); + let balanceSecondAccount = web3.eth.getBalance(secondAccount); + let expectedBalanceFirstAccount = balanceFirstAccount; + let expectedBalanceSecondAccount = balanceSecondAccount; + + // ensure our contract does not exist yet + var [,,,,,,,,contractState] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractState, stateEmpty, "state should equal Empty"); + + // sanity balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // create initiation contract + await atomicSwap.initiate(refundTime, secretHash, secondAccount, + {from: firstAccount, value: contractAmount, gasPrice: 0}). + then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Initiated", "Expected Initiated event"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "Value should equal contractAmount"); + assert.equal(firstLog.args.secretHash, secretHash, "SecretHash should be as expected"); + assert.equal(firstLog.args.refundTime, refundTime, "RefundTime should be as expected"); + assert.equal(firstLog.args.participant, secondAccount, "Participant should equal secondAccount"); + assert.equal(firstLog.args.initiator, firstAccount, "Initiator should equal firstAccount"); + initTimestamp = firstLog.args.initTimestamp.toNumber(); + }); + + // update balance and check it again + balanceFirstAccount = web3.eth.getBalance(firstAccount); + expectedBalanceFirstAccount = expectedBalanceFirstAccount.add(contractAmount.negated()); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // assert all contract details + var [ + contractTime, + contractRefundTime, + contractSecretHash, + contractSecret, + contractInitiator, + contractParticipant, + contractValue, + contractKind, + contractState, + ] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractSecretHash, secretHash, "secretHash should be as expected"); + assert.equal(contractSecret, emptySecret, "secret should still be nil"); + assert.equal(contractInitiator, firstAccount, "initiator should equal firstAccount"); + assert.equal(contractParticipant, secondAccount, "participant should equal secondAccount"); + assert.equal(contractValue.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(contractKind, kindInitiator, "kind should equal Initiator"); + assert.equal(contractState, stateFilled, "state should equal Filled"); + + // creating another contract using the same secretHash should fail + await tryCatch(atomicSwap.initiate(refundTime, secretHash, secondAccount, + {from: firstAccount, value: contractAmount, gasPrice: 0}), errTypes.revert); + // even when using different accounts + await tryCatch(atomicSwap.initiate(refundTime, secretHash, fourthAccount, + {from: thirdAccount, value: contractAmount, gasPrice: 0}), errTypes.revert); + // and even when trying to create a participation contract, instead of an initiation contract + await tryCatch(atomicSwap.participate(refundTime, secretHash, fourthAccount, + {from: thirdAccount, value: contractAmount, gasPrice: 0}), errTypes.revert); + + // only the participant can refund a contract + await tryCatch(atomicSwap.refund(secretHash, + {from: secondAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.refund(secretHash, + {from: thirdAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.refund(secretHash, + {from: fourthAccount, gasPrice: 0}), errTypes.revert); + + // but even the participant cannot refund, given the refundTime has not yet been reached + await tryCatch(atomicSwap.refund(secretHash, + {from: firstAccount, gasPrice: 0}), errTypes.revert); + + // only the the initiator can redeem a contract + await tryCatch(atomicSwap.redeem(secret, secretHash, + {from: firstAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.redeem(secret, secretHash, + {from: thirdAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.redeem(secret, secretHash, + {from: fourthAccount, gasPrice: 0}), errTypes.revert); + + // the initiator has to give however give the correct secret hash + await tryCatch(atomicSwap.redeem(secret, wrongSecretHash, + {from: secondAccount, gasPrice: 0}), errTypes.revert); + // and the correct secret + await tryCatch(atomicSwap.redeem(wrongSecret, secretHash, + {from: secondAccount, gasPrice: 0}), errTypes.revert); + // in fact, the secretHash has to be the correct one and the secretHash has to equal sha256(secret) + await tryCatch(atomicSwap.redeem(wrongSecret, wrongSecretHash, + {from: secondAccount, gasPrice: 0}), errTypes.revert); + + // redeem the initiation contract + await atomicSwap.redeem(secret, secretHash, {from: secondAccount, gasPrice: 0}). + then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Redeemed", "Expected Redeemed event"); + assert.isAtLeast(firstLog.args.redeemTime.toNumber(), initTimestamp, + "redeem time " + firstLog.args.redeemTime + + " should be atleast equal to the init timestamp " + + initTimestamp.toString()); + assert.equal(firstLog.args.secretHash, secretHash, "secretHash should be as expected"); + assert.equal(firstLog.args.secret, secret, "secret should be as expected"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(firstLog.args.redeemer, secondAccount, "redeemer should equal secondAccount"); + }); + + // ensure balance updates of second account + balanceSecondAccount = web3.eth.getBalance(secondAccount); + expectedBalanceSecondAccount = expectedBalanceSecondAccount.add(contractAmount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should have decreased by txn cost, " + + "and should have received contract amount"); + + // assert state has now been updated, + // and that our contract still exists + var [ + contractTime, + contractRefundTime, + contractSecretHash, + contractSecret, + contractInitiator, + contractParticipant, + contractValue, + contractKind, + contractState, + ] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractSecretHash, secretHash, "secretHash should be as expected"); + assert.equal(contractSecret, secret, "secret should no longer be nil and instead be as expected"); + assert.equal(contractInitiator, firstAccount, "initiator should equal firstAccount"); + assert.equal(contractParticipant, secondAccount, "participant should equal secondAccount"); + assert.equal(contractValue.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(contractKind, kindInitiator, "kind should equal Initiator"); + assert.equal(contractState, stateRedeemed, "state should equal Redeemed"); + + // last balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + }); + + it("should be able to redeem an initiation contract even when refunding is already possible", async () => { + const contractAmount = web3.toBigNumber(web3.toWei('0.01', 'ether')); + const refundTime = 1; + + let initTimestamp; + + // store initial balance of our accounts, + // so that we can check if the locked value + // transfers indeed between accounts + let balanceFirstAccount = web3.eth.getBalance(firstAccount); + let balanceSecondAccount = web3.eth.getBalance(secondAccount); + let expectedBalanceFirstAccount = balanceFirstAccount; + let expectedBalanceSecondAccount = balanceSecondAccount; + + // ensure our contract does not exist yet + var [,,,,,,,,contractState] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractState, stateEmpty, "state should equal Empty"); + + // sanity balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // create initiation contract + await atomicSwap.initiate(refundTime, secretHash, secondAccount, + {from: firstAccount, value: contractAmount, gasPrice: 0}). + then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Initiated", "Expected Initiated event"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "Value should equal contractAmount"); + assert.equal(firstLog.args.secretHash, secretHash, "SecretHash should be as expected"); + assert.equal(firstLog.args.refundTime, refundTime, "RefundTime should be as expected"); + assert.equal(firstLog.args.participant, secondAccount, "Participant should equal secondAccount"); + assert.equal(firstLog.args.initiator, firstAccount, "Initiator should equal firstAccount"); + initTimestamp = firstLog.args.initTimestamp.toNumber(); + }); + + // update balance and check it again + balanceFirstAccount = web3.eth.getBalance(firstAccount); + expectedBalanceFirstAccount = expectedBalanceFirstAccount.add(contractAmount.negated()); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // sleep so that the refund-period gets reached + await utils.sleep(refundTime * 2000); + + // redeem the initiation contract + await atomicSwap.redeem(secret, secretHash, {from: secondAccount, gasPrice: 0}). + then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Redeemed", "Expected Redeemed event"); + assert.isAtLeast(firstLog.args.redeemTime.toNumber(), initTimestamp, + "redeem time " + firstLog.args.redeemTime + + " should be atleast equal to the init timestamp " + + initTimestamp.toString()); + assert.equal(firstLog.args.secretHash, secretHash, "secretHash should be as expected"); + assert.equal(firstLog.args.secret, secret, "secret should be as expected"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(firstLog.args.redeemer, secondAccount, "redeemer should equal secondAccount"); + }); + + // ensure balance updates of second account + balanceSecondAccount = web3.eth.getBalance(secondAccount); + expectedBalanceSecondAccount = expectedBalanceSecondAccount.add(contractAmount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should have decreased by txn cost, " + + "and should have received contract amount"); + + // assert state and that our contract still exists + var [ + contractTime, + contractRefundTime, + contractSecretHash, + contractSecret, + contractInitiator, + contractParticipant, + contractValue, + contractKind, + contractState, + ] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractSecretHash, secretHash, "secretHash should be as expected"); + assert.equal(contractSecret, secret, "secret should no longer be nil and instead be as expected"); + assert.equal(contractInitiator, firstAccount, "initiator should equal firstAccount"); + assert.equal(contractParticipant, secondAccount, "participant should equal secondAccount"); + assert.equal(contractValue.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(contractKind, kindInitiator, "kind should equal Initiator"); + assert.equal(contractState, stateRedeemed, "state should equal Redeemed"); + + // last balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + }); + + it("should be able to refund a participation contract", async () => { + const contractAmount = web3.toBigNumber(web3.toWei('0.01', 'ether')); + const refundTime = 1; + + let initTimestamp; + + // store initial balance of our accounts, + // so that we can check if the locked value + // transfers indeed between accounts + let balanceFirstAccount = web3.eth.getBalance(firstAccount); + let balanceSecondAccount = web3.eth.getBalance(secondAccount); + let expectedBalanceFirstAccount = balanceFirstAccount; + let expectedBalanceSecondAccount = balanceSecondAccount; + + // ensure our contract does not exist yet + var [,,,,,,,,contractState] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractState, stateEmpty, "state should equal Empty"); + + // sanity balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // create participation contract + await atomicSwap.participate(refundTime, secretHash, secondAccount, + {from: firstAccount, value: contractAmount, gasPrice: 0}). + then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Participated", "Expected Participated event"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "Value should equal contractAmount"); + assert.equal(firstLog.args.secretHash, secretHash, "SecretHash should be as expected"); + assert.equal(firstLog.args.refundTime, refundTime, "RefundTime should be as expected"); + assert.equal(firstLog.args.initiator, secondAccount, "Initiator should equal secondAccount"); + assert.equal(firstLog.args.participant, firstAccount, "Participant should equal firstAccount"); + initTimestamp = firstLog.args.initTimestamp.toNumber(); + }); + + // update balance and check it again + balanceFirstAccount = web3.eth.getBalance(firstAccount); + expectedBalanceFirstAccount = expectedBalanceFirstAccount.add(contractAmount.negated()); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // assert all contract details + var [ + contractTime, + contractRefundTime, + contractSecretHash, + contractSecret, + contractInitiator, + contractParticipant, + contractValue, + contractKind, + contractState, + ] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractSecretHash, secretHash, "secretHash should be as expected"); + assert.equal(contractSecret, emptySecret, "secret should still be nil"); + assert.equal(contractInitiator, secondAccount, "initiator should equal secondAccount"); + assert.equal(contractParticipant, firstAccount, "participant should equal firstAccount"); + assert.equal(contractValue.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(contractKind, kindParticipant, "kind should equal Participant"); + assert.equal(contractState, stateFilled, "state should equal Filled"); + + await utils.sleep(refundTime * 2000); + + // only the participant can refund a contract + await tryCatch(atomicSwap.refund(secretHash, + {from: secondAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.refund(secretHash, + {from: thirdAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.refund(secretHash, + {from: fourthAccount, gasPrice: 0}), errTypes.revert); + + atomicSwap.refund(secretHash, {from: firstAccount, gasPrice: 0}).then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Refunded", "Expected Refunded event"); + assert.isAtLeast(firstLog.args.refundTime.toNumber(), initTimestamp, + "refund time " + firstLog.args.refundTime + + " should be atleast equal to the init timestamp " + + initTimestamp.toString()); + assert.equal(firstLog.args.secretHash, secretHash, "secretHash should be as expected"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(firstLog.args.refunder, firstAccount, "refunder should equal firstAccount"); + }); + + await utils.sleep(1000); // refund balance updates seem to take longer for some reason + + // ensure balance updates of first account + balanceFirstAccount = web3.eth.getBalance(firstAccount); + expectedBalanceFirstAccount = expectedBalanceFirstAccount.add(contractAmount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should have decreased by txn cost, " + + "and should have received contract amount back"); + + // assert state has now been updated, + // and that our contract still exists + var [ + contractTime, + contractRefundTime, + contractSecretHash, + contractSecret, + contractInitiator, + contractParticipant, + contractValue, + contractKind, + contractState, + ] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractSecretHash, secretHash, "secretHash should be as expected"); + assert.equal(contractSecret, emptySecret, "secret should still be nil"); + assert.equal(contractInitiator, secondAccount, "initiator should equal secondAccount"); + assert.equal(contractParticipant, firstAccount, "participant should equal firstAccount"); + assert.equal(contractValue.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(contractKind, kindParticipant, "kind should equal Participant"); + assert.equal(contractState, stateRefunded, "state should equal Refunded"); + + // last balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + }); + + it("should be able to refund an initiation contract", async () => { + const contractAmount = web3.toBigNumber(web3.toWei('0.01', 'ether')); + const refundTime = 1; + + let initTimestamp; + + // store initial balance of our accounts, + // so that we can check if the locked value + // transfers indeed between accounts + let balanceFirstAccount = web3.eth.getBalance(firstAccount); + let balanceSecondAccount = web3.eth.getBalance(secondAccount); + let expectedBalanceFirstAccount = balanceFirstAccount; + let expectedBalanceSecondAccount = balanceSecondAccount; + + // ensure our contract does not exist yet + var [,,,,,,,,contractState] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractState, stateEmpty, "state should equal Empty"); + + // sanity balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // create initiation contract + await atomicSwap.initiate(refundTime, secretHash, secondAccount, + {from: firstAccount, value: contractAmount, gasPrice: 0}). + then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Initiated", "Expected Initiated event"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "Value should equal contractAmount"); + assert.equal(firstLog.args.secretHash, secretHash, "SecretHash should be as expected"); + assert.equal(firstLog.args.refundTime, refundTime, "RefundTime should be as expected"); + assert.equal(firstLog.args.initiator, firstAccount, "Initiator should equal secondAccount"); + assert.equal(firstLog.args.participant, secondAccount, "Participant should equal firstAccount"); + initTimestamp = firstLog.args.initTimestamp.toNumber(); + }); + + // update balance and check it again + balanceFirstAccount = web3.eth.getBalance(firstAccount); + expectedBalanceFirstAccount = expectedBalanceFirstAccount.add(contractAmount.negated()); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // assert all contract details + var [ + contractTime, + contractRefundTime, + contractSecretHash, + contractSecret, + contractInitiator, + contractParticipant, + contractValue, + contractKind, + contractState, + ] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractSecretHash, secretHash, "secretHash should be as expected"); + assert.equal(contractSecret, emptySecret, "secret should still be nil"); + assert.equal(contractInitiator, firstAccount, "initiator should equal firstAccount"); + assert.equal(contractParticipant, secondAccount, "participant should equal secondAccount"); + assert.equal(contractValue.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(contractKind, kindInitiator, "kind should equal Initiator"); + assert.equal(contractState, stateFilled, "state should equal Filled"); + + await utils.sleep(refundTime * 2000); + + // only the initiator can refund a contract + await tryCatch(atomicSwap.refund(secretHash, + {from: secondAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.refund(secretHash, + {from: thirdAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.refund(secretHash, + {from: fourthAccount, gasPrice: 0}), errTypes.revert); + atomicSwap.refund(secretHash, {from: firstAccount, gasPrice: 0}).then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Refunded", "Expected Refunded event"); + assert.isAtLeast(firstLog.args.refundTime.toNumber(), initTimestamp, + "refund time " + firstLog.args.refundTime + + " should be atleast equal to the init timestamp " + + initTimestamp.toString()); + assert.equal(firstLog.args.secretHash, secretHash, "secretHash should be as expected"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(firstLog.args.refunder, firstAccount, "refunder should equal firstAccount"); + }); + + await utils.sleep(1000); // refund balance updates seem to take longer for some reason + + // ensure balance updates of first account + balanceFirstAccount = web3.eth.getBalance(firstAccount); + expectedBalanceFirstAccount = expectedBalanceFirstAccount.add(contractAmount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should have decreased by txn cost, " + + "and should have received contract amount back"); + + // assert state has now been updated, + // and that our contract still exists + var [ + contractTime, + contractRefundTime, + contractSecretHash, + contractSecret, + contractInitiator, + contractParticipant, + contractValue, + contractKind, + contractState, + ] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractSecretHash, secretHash, "secretHash should be as expected"); + assert.equal(contractSecret, emptySecret, "secret should still be nil"); + assert.equal(contractInitiator, firstAccount, "initiator should equal firstAccount"); + assert.equal(contractParticipant, secondAccount, "participant should equal secondAccount"); + assert.equal(contractValue.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(contractKind, kindInitiator, "kind should equal Initiator"); + assert.equal(contractState, stateRefunded, "state should equal Refunded"); + + // last balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + }); +}); diff --git a/cmd/ethatomicswap/solidity/test/exceptions.js b/cmd/ethatomicswap/solidity/test/exceptions.js new file mode 100644 index 0000000..d6eb8ba --- /dev/null +++ b/cmd/ethatomicswap/solidity/test/exceptions.js @@ -0,0 +1,22 @@ +module.exports.errTypes = { + revert : "revert", + outOfGas : "out of gas", + invalidJump : "invalid JUMP", + invalidOpcode : "invalid opcode", + stackOverflow : "stack overflow", + stackUnderflow : "stack underflow", + staticStateChange : "static state change" +} + +module.exports.tryCatch = async function(promise, errType) { + try { + await promise; + throw null; + } + catch (error) { + assert(error, "Expected an error but did not get one"); + assert(error.message.startsWith(PREFIX + errType), "Expected an error starting with '" + PREFIX + errType + "' but got '" + error.message + "' instead"); + } +}; + +const PREFIX = "VM Exception while processing transaction: "; \ No newline at end of file diff --git a/cmd/ethatomicswap/solidity/test/utils.js b/cmd/ethatomicswap/solidity/test/utils.js new file mode 100644 index 0000000..34f01fc --- /dev/null +++ b/cmd/ethatomicswap/solidity/test/utils.js @@ -0,0 +1,3 @@ +module.exports.sleep = async function(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +}; diff --git a/cmd/ethatomicswap/solidity/truffle-config.js b/cmd/ethatomicswap/solidity/truffle-config.js new file mode 100644 index 0000000..0855df1 --- /dev/null +++ b/cmd/ethatomicswap/solidity/truffle-config.js @@ -0,0 +1,18 @@ +/* + * NB: since truffle-hdwallet-provider 0.0.5 you must wrap HDWallet providers in a + * function when declaring them. Failure to do so will cause commands to hang. ex: + * ``` + * mainnet: { + * provider: function() { + * return new HDWalletProvider(mnemonic, 'https://mainnet.infura.io/') + * }, + * network_id: '1', + * gas: 4500000, + * gasPrice: 10000000000, + * }, + */ + +module.exports = { + // See + // to customize your Truffle configuration! +}; diff --git a/cmd/ethatomicswap/solidity/truffle.js b/cmd/ethatomicswap/solidity/truffle.js new file mode 100644 index 0000000..0855df1 --- /dev/null +++ b/cmd/ethatomicswap/solidity/truffle.js @@ -0,0 +1,18 @@ +/* + * NB: since truffle-hdwallet-provider 0.0.5 you must wrap HDWallet providers in a + * function when declaring them. Failure to do so will cause commands to hang. ex: + * ``` + * mainnet: { + * provider: function() { + * return new HDWalletProvider(mnemonic, 'https://mainnet.infura.io/') + * }, + * network_id: '1', + * gas: 4500000, + * gasPrice: 10000000000, + * }, + */ + +module.exports = { + // See + // to customize your Truffle configuration! +}; From 43e0960fdbb1a3d5ba913fa2405aa18d8d9aad37 Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker Date: Fri, 22 Jun 2018 14:35:50 +0200 Subject: [PATCH 02/12] update travis.yml to also unit test the EVM smart contract --- .travis.yml | 39 ++++++++++++++++++---------- cmd/ethatomicswap/solidity/README.md | 4 ++- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3b5c0a1..164f625 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,26 @@ -language: go -go: - - 1.9.x -sudo: false -install: - - go get -v github.com/golang/dep/cmd/dep - - dep ensure - - go install ./cmd/... - - go get -v github.com/alecthomas/gometalinter - - gometalinter --install -script: - - export PATH=$PATH:$HOME/gopath/bin - - ./goclean.sh +jobs: + include: + - stage: test + language: go + go: + - 1.9.x + - 1.10.x + sudo: false + install: + - go get -v github.com/golang/dep/cmd/dep + - dep ensure + - go install ./cmd/... + - go get -v github.com/alecthomas/gometalinter + - gometalinter --install + script: + - export PATH=$PATH:$HOME/gopath/bin + - ./goclean.sh + - stage: test + language: node_js + node_js: + - "node" + install: + - npm install -g truffle + script: + - cd cmd/ethatomicswap/solidity + - truffle test diff --git a/cmd/ethatomicswap/solidity/README.md b/cmd/ethatomicswap/solidity/README.md index 8da15a1..5dafea4 100644 --- a/cmd/ethatomicswap/solidity/README.md +++ b/cmd/ethatomicswap/solidity/README.md @@ -14,7 +14,9 @@ found as [/cmd/ethatomicswap/solidity/contracts/AtomicSwap.sol](/cmd/ethatomicsw * Install NodeJS (10.5.0), which bundles _npm_ as well; * Install truffle: `npm install -g truffle`; -* Install and run Ganache: ; + +Optionally you can also install and run +Ganache ( ). Once you have fulfilled all prerequisites listed above, you can run the unit tests provided with the AtomicSwap contract, using: From e3c27ae52706a0f6f5648e13451a41df3af6abed Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker Date: Sat, 23 Jun 2018 13:23:18 +0200 Subject: [PATCH 03/12] move ethatomicswap contract src and add generated Go contract includes the generate.go file which allows you to generate the atomicswap.go file yourself, using the Solidity AtomicSwap source file --- .travis.yml | 2 +- cmd/ethatomicswap/contract/.gitignore | 1 + cmd/ethatomicswap/contract/atomicswap.go | 814 ++++++++++++++++++ cmd/ethatomicswap/contract/generate.go | 12 + .../{solidity => contract/src}/.gitignore | 0 .../{solidity => contract/src}/README.md | 0 .../src}/contracts/AtomicSwap.sol | 0 .../src}/contracts/Migrations.sol | 0 .../src}/migrations/1_initial_migration.js | 0 .../src}/migrations/2_atomicswap_migration.js | 0 .../src}/test/TestAtomicSwap.js | 0 .../src}/test/exceptions.js | 0 .../{solidity => contract/src}/test/utils.js | 0 .../src}/truffle-config.js | 0 .../{solidity => contract/src}/truffle.js | 7 + 15 files changed, 835 insertions(+), 1 deletion(-) create mode 100644 cmd/ethatomicswap/contract/.gitignore create mode 100644 cmd/ethatomicswap/contract/atomicswap.go create mode 100644 cmd/ethatomicswap/contract/generate.go rename cmd/ethatomicswap/{solidity => contract/src}/.gitignore (100%) rename cmd/ethatomicswap/{solidity => contract/src}/README.md (100%) rename cmd/ethatomicswap/{solidity => contract/src}/contracts/AtomicSwap.sol (100%) rename cmd/ethatomicswap/{solidity => contract/src}/contracts/Migrations.sol (100%) rename cmd/ethatomicswap/{solidity => contract/src}/migrations/1_initial_migration.js (100%) rename cmd/ethatomicswap/{solidity => contract/src}/migrations/2_atomicswap_migration.js (100%) rename cmd/ethatomicswap/{solidity => contract/src}/test/TestAtomicSwap.js (100%) rename cmd/ethatomicswap/{solidity => contract/src}/test/exceptions.js (100%) rename cmd/ethatomicswap/{solidity => contract/src}/test/utils.js (100%) rename cmd/ethatomicswap/{solidity => contract/src}/truffle-config.js (100%) rename cmd/ethatomicswap/{solidity => contract/src}/truffle.js (80%) diff --git a/.travis.yml b/.travis.yml index 164f625..c8295f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,5 +22,5 @@ jobs: install: - npm install -g truffle script: - - cd cmd/ethatomicswap/solidity + - cd cmd/ethatomicswap/contract/src - truffle test diff --git a/cmd/ethatomicswap/contract/.gitignore b/cmd/ethatomicswap/contract/.gitignore new file mode 100644 index 0000000..c4d76c4 --- /dev/null +++ b/cmd/ethatomicswap/contract/.gitignore @@ -0,0 +1 @@ +*.abi \ No newline at end of file diff --git a/cmd/ethatomicswap/contract/atomicswap.go b/cmd/ethatomicswap/contract/atomicswap.go new file mode 100644 index 0000000..b34befa --- /dev/null +++ b/cmd/ethatomicswap/contract/atomicswap.go @@ -0,0 +1,814 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package contract + +import ( + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// ContractABI is the input ABI used to generate the binding from. +const ContractABI = "[{\"constant\":false,\"inputs\":[{\"name\":\"refundTime\",\"type\":\"uint256\"},{\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"name\":\"initiator\",\"type\":\"address\"}],\"name\":\"participate\",\"outputs\":[],\"payable\":true,\"stateMutability\":\"payable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"secretHash\",\"type\":\"bytes32\"}],\"name\":\"refund\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"refundTime\",\"type\":\"uint256\"},{\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"name\":\"participant\",\"type\":\"address\"}],\"name\":\"initiate\",\"outputs\":[],\"payable\":true,\"stateMutability\":\"payable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"secret\",\"type\":\"bytes32\"},{\"name\":\"secretHash\",\"type\":\"bytes32\"}],\"name\":\"redeem\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"swaps\",\"outputs\":[{\"name\":\"initTimestamp\",\"type\":\"uint256\"},{\"name\":\"refundTime\",\"type\":\"uint256\"},{\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"name\":\"secret\",\"type\":\"bytes32\"},{\"name\":\"initiator\",\"type\":\"address\"},{\"name\":\"participant\",\"type\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\"},{\"name\":\"kind\",\"type\":\"uint8\"},{\"name\":\"state\",\"type\":\"uint8\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"refundTime\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"refunder\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Refunded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"redeemTime\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"secret\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"redeemer\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Redeemed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"initTimestamp\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"refundTime\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"initiator\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"participant\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Participated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"initTimestamp\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"refundTime\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"initiator\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"participant\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Initiated\",\"type\":\"event\"}]" + +// Contract is an auto generated Go binding around an Ethereum contract. +type Contract struct { + ContractCaller // Read-only binding to the contract + ContractTransactor // Write-only binding to the contract + ContractFilterer // Log filterer for contract events +} + +// ContractCaller is an auto generated read-only Go binding around an Ethereum contract. +type ContractCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ContractTransactor is an auto generated write-only Go binding around an Ethereum contract. +type ContractTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ContractFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type ContractFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ContractSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type ContractSession struct { + Contract *Contract // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ContractCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type ContractCallerSession struct { + Contract *ContractCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// ContractTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type ContractTransactorSession struct { + Contract *ContractTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ContractRaw is an auto generated low-level Go binding around an Ethereum contract. +type ContractRaw struct { + Contract *Contract // Generic contract binding to access the raw methods on +} + +// ContractCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type ContractCallerRaw struct { + Contract *ContractCaller // Generic read-only contract binding to access the raw methods on +} + +// ContractTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type ContractTransactorRaw struct { + Contract *ContractTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewContract creates a new instance of Contract, bound to a specific deployed contract. +func NewContract(address common.Address, backend bind.ContractBackend) (*Contract, error) { + contract, err := bindContract(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Contract{ContractCaller: ContractCaller{contract: contract}, ContractTransactor: ContractTransactor{contract: contract}, ContractFilterer: ContractFilterer{contract: contract}}, nil +} + +// NewContractCaller creates a new read-only instance of Contract, bound to a specific deployed contract. +func NewContractCaller(address common.Address, caller bind.ContractCaller) (*ContractCaller, error) { + contract, err := bindContract(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &ContractCaller{contract: contract}, nil +} + +// NewContractTransactor creates a new write-only instance of Contract, bound to a specific deployed contract. +func NewContractTransactor(address common.Address, transactor bind.ContractTransactor) (*ContractTransactor, error) { + contract, err := bindContract(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &ContractTransactor{contract: contract}, nil +} + +// NewContractFilterer creates a new log filterer instance of Contract, bound to a specific deployed contract. +func NewContractFilterer(address common.Address, filterer bind.ContractFilterer) (*ContractFilterer, error) { + contract, err := bindContract(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &ContractFilterer{contract: contract}, nil +} + +// bindContract binds a generic wrapper to an already deployed contract. +func bindContract(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := abi.JSON(strings.NewReader(ContractABI)) + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Contract *ContractRaw) Call(opts *bind.CallOpts, result interface{}, method string, params ...interface{}) error { + return _Contract.Contract.ContractCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Contract *ContractRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Contract.Contract.ContractTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Contract *ContractRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Contract.Contract.ContractTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Contract *ContractCallerRaw) Call(opts *bind.CallOpts, result interface{}, method string, params ...interface{}) error { + return _Contract.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Contract *ContractTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Contract.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Contract *ContractTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Contract.Contract.contract.Transact(opts, method, params...) +} + +// Swaps is a free data retrieval call binding the contract method 0xeb84e7f2. +// +// Solidity: function swaps( bytes32) constant returns(initTimestamp uint256, refundTime uint256, secretHash bytes32, secret bytes32, initiator address, participant address, value uint256, kind uint8, state uint8) +func (_Contract *ContractCaller) Swaps(opts *bind.CallOpts, arg0 [32]byte) (struct { + InitTimestamp *big.Int + RefundTime *big.Int + SecretHash [32]byte + Secret [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + Kind uint8 + State uint8 +}, error) { + ret := new(struct { + InitTimestamp *big.Int + RefundTime *big.Int + SecretHash [32]byte + Secret [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + Kind uint8 + State uint8 + }) + out := ret + err := _Contract.contract.Call(opts, out, "swaps", arg0) + return *ret, err +} + +// Swaps is a free data retrieval call binding the contract method 0xeb84e7f2. +// +// Solidity: function swaps( bytes32) constant returns(initTimestamp uint256, refundTime uint256, secretHash bytes32, secret bytes32, initiator address, participant address, value uint256, kind uint8, state uint8) +func (_Contract *ContractSession) Swaps(arg0 [32]byte) (struct { + InitTimestamp *big.Int + RefundTime *big.Int + SecretHash [32]byte + Secret [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + Kind uint8 + State uint8 +}, error) { + return _Contract.Contract.Swaps(&_Contract.CallOpts, arg0) +} + +// Swaps is a free data retrieval call binding the contract method 0xeb84e7f2. +// +// Solidity: function swaps( bytes32) constant returns(initTimestamp uint256, refundTime uint256, secretHash bytes32, secret bytes32, initiator address, participant address, value uint256, kind uint8, state uint8) +func (_Contract *ContractCallerSession) Swaps(arg0 [32]byte) (struct { + InitTimestamp *big.Int + RefundTime *big.Int + SecretHash [32]byte + Secret [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + Kind uint8 + State uint8 +}, error) { + return _Contract.Contract.Swaps(&_Contract.CallOpts, arg0) +} + +// Initiate is a paid mutator transaction binding the contract method 0xae052147. +// +// Solidity: function initiate(refundTime uint256, secretHash bytes32, participant address) returns() +func (_Contract *ContractTransactor) Initiate(opts *bind.TransactOpts, refundTime *big.Int, secretHash [32]byte, participant common.Address) (*types.Transaction, error) { + return _Contract.contract.Transact(opts, "initiate", refundTime, secretHash, participant) +} + +// Initiate is a paid mutator transaction binding the contract method 0xae052147. +// +// Solidity: function initiate(refundTime uint256, secretHash bytes32, participant address) returns() +func (_Contract *ContractSession) Initiate(refundTime *big.Int, secretHash [32]byte, participant common.Address) (*types.Transaction, error) { + return _Contract.Contract.Initiate(&_Contract.TransactOpts, refundTime, secretHash, participant) +} + +// Initiate is a paid mutator transaction binding the contract method 0xae052147. +// +// Solidity: function initiate(refundTime uint256, secretHash bytes32, participant address) returns() +func (_Contract *ContractTransactorSession) Initiate(refundTime *big.Int, secretHash [32]byte, participant common.Address) (*types.Transaction, error) { + return _Contract.Contract.Initiate(&_Contract.TransactOpts, refundTime, secretHash, participant) +} + +// Participate is a paid mutator transaction binding the contract method 0x1aa02853. +// +// Solidity: function participate(refundTime uint256, secretHash bytes32, initiator address) returns() +func (_Contract *ContractTransactor) Participate(opts *bind.TransactOpts, refundTime *big.Int, secretHash [32]byte, initiator common.Address) (*types.Transaction, error) { + return _Contract.contract.Transact(opts, "participate", refundTime, secretHash, initiator) +} + +// Participate is a paid mutator transaction binding the contract method 0x1aa02853. +// +// Solidity: function participate(refundTime uint256, secretHash bytes32, initiator address) returns() +func (_Contract *ContractSession) Participate(refundTime *big.Int, secretHash [32]byte, initiator common.Address) (*types.Transaction, error) { + return _Contract.Contract.Participate(&_Contract.TransactOpts, refundTime, secretHash, initiator) +} + +// Participate is a paid mutator transaction binding the contract method 0x1aa02853. +// +// Solidity: function participate(refundTime uint256, secretHash bytes32, initiator address) returns() +func (_Contract *ContractTransactorSession) Participate(refundTime *big.Int, secretHash [32]byte, initiator common.Address) (*types.Transaction, error) { + return _Contract.Contract.Participate(&_Contract.TransactOpts, refundTime, secretHash, initiator) +} + +// Redeem is a paid mutator transaction binding the contract method 0xb31597ad. +// +// Solidity: function redeem(secret bytes32, secretHash bytes32) returns() +func (_Contract *ContractTransactor) Redeem(opts *bind.TransactOpts, secret [32]byte, secretHash [32]byte) (*types.Transaction, error) { + return _Contract.contract.Transact(opts, "redeem", secret, secretHash) +} + +// Redeem is a paid mutator transaction binding the contract method 0xb31597ad. +// +// Solidity: function redeem(secret bytes32, secretHash bytes32) returns() +func (_Contract *ContractSession) Redeem(secret [32]byte, secretHash [32]byte) (*types.Transaction, error) { + return _Contract.Contract.Redeem(&_Contract.TransactOpts, secret, secretHash) +} + +// Redeem is a paid mutator transaction binding the contract method 0xb31597ad. +// +// Solidity: function redeem(secret bytes32, secretHash bytes32) returns() +func (_Contract *ContractTransactorSession) Redeem(secret [32]byte, secretHash [32]byte) (*types.Transaction, error) { + return _Contract.Contract.Redeem(&_Contract.TransactOpts, secret, secretHash) +} + +// Refund is a paid mutator transaction binding the contract method 0x7249fbb6. +// +// Solidity: function refund(secretHash bytes32) returns() +func (_Contract *ContractTransactor) Refund(opts *bind.TransactOpts, secretHash [32]byte) (*types.Transaction, error) { + return _Contract.contract.Transact(opts, "refund", secretHash) +} + +// Refund is a paid mutator transaction binding the contract method 0x7249fbb6. +// +// Solidity: function refund(secretHash bytes32) returns() +func (_Contract *ContractSession) Refund(secretHash [32]byte) (*types.Transaction, error) { + return _Contract.Contract.Refund(&_Contract.TransactOpts, secretHash) +} + +// Refund is a paid mutator transaction binding the contract method 0x7249fbb6. +// +// Solidity: function refund(secretHash bytes32) returns() +func (_Contract *ContractTransactorSession) Refund(secretHash [32]byte) (*types.Transaction, error) { + return _Contract.Contract.Refund(&_Contract.TransactOpts, secretHash) +} + +// ContractInitiatedIterator is returned from FilterInitiated and is used to iterate over the raw logs and unpacked data for Initiated events raised by the Contract contract. +type ContractInitiatedIterator struct { + Event *ContractInitiated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *ContractInitiatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(ContractInitiated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(ContractInitiated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *ContractInitiatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ContractInitiatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ContractInitiated represents a Initiated event raised by the Contract contract. +type ContractInitiated struct { + InitTimestamp *big.Int + RefundTime *big.Int + SecretHash [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterInitiated is a free log retrieval operation binding the contract event 0x75501a491c11746724d18ea6e5ac6a53864d886d653da6b846fdecda837cf576. +// +// Solidity: e Initiated(initTimestamp uint256, refundTime uint256, secretHash bytes32, initiator address, participant address, value uint256) +func (_Contract *ContractFilterer) FilterInitiated(opts *bind.FilterOpts) (*ContractInitiatedIterator, error) { + + logs, sub, err := _Contract.contract.FilterLogs(opts, "Initiated") + if err != nil { + return nil, err + } + return &ContractInitiatedIterator{contract: _Contract.contract, event: "Initiated", logs: logs, sub: sub}, nil +} + +// WatchInitiated is a free log subscription operation binding the contract event 0x75501a491c11746724d18ea6e5ac6a53864d886d653da6b846fdecda837cf576. +// +// Solidity: e Initiated(initTimestamp uint256, refundTime uint256, secretHash bytes32, initiator address, participant address, value uint256) +func (_Contract *ContractFilterer) WatchInitiated(opts *bind.WatchOpts, sink chan<- *ContractInitiated) (event.Subscription, error) { + + logs, sub, err := _Contract.contract.WatchLogs(opts, "Initiated") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(ContractInitiated) + if err := _Contract.contract.UnpackLog(event, "Initiated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ContractParticipatedIterator is returned from FilterParticipated and is used to iterate over the raw logs and unpacked data for Participated events raised by the Contract contract. +type ContractParticipatedIterator struct { + Event *ContractParticipated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *ContractParticipatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(ContractParticipated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(ContractParticipated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *ContractParticipatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ContractParticipatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ContractParticipated represents a Participated event raised by the Contract contract. +type ContractParticipated struct { + InitTimestamp *big.Int + RefundTime *big.Int + SecretHash [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterParticipated is a free log retrieval operation binding the contract event 0xe5571d467a528d7481c0e3bdd55ad528d0df6b457b07bab736c3e245c3aa16f4. +// +// Solidity: e Participated(initTimestamp uint256, refundTime uint256, secretHash bytes32, initiator address, participant address, value uint256) +func (_Contract *ContractFilterer) FilterParticipated(opts *bind.FilterOpts) (*ContractParticipatedIterator, error) { + + logs, sub, err := _Contract.contract.FilterLogs(opts, "Participated") + if err != nil { + return nil, err + } + return &ContractParticipatedIterator{contract: _Contract.contract, event: "Participated", logs: logs, sub: sub}, nil +} + +// WatchParticipated is a free log subscription operation binding the contract event 0xe5571d467a528d7481c0e3bdd55ad528d0df6b457b07bab736c3e245c3aa16f4. +// +// Solidity: e Participated(initTimestamp uint256, refundTime uint256, secretHash bytes32, initiator address, participant address, value uint256) +func (_Contract *ContractFilterer) WatchParticipated(opts *bind.WatchOpts, sink chan<- *ContractParticipated) (event.Subscription, error) { + + logs, sub, err := _Contract.contract.WatchLogs(opts, "Participated") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(ContractParticipated) + if err := _Contract.contract.UnpackLog(event, "Participated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ContractRedeemedIterator is returned from FilterRedeemed and is used to iterate over the raw logs and unpacked data for Redeemed events raised by the Contract contract. +type ContractRedeemedIterator struct { + Event *ContractRedeemed // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *ContractRedeemedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(ContractRedeemed) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(ContractRedeemed) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *ContractRedeemedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ContractRedeemedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ContractRedeemed represents a Redeemed event raised by the Contract contract. +type ContractRedeemed struct { + RedeemTime *big.Int + SecretHash [32]byte + Secret [32]byte + Redeemer common.Address + Value *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterRedeemed is a free log retrieval operation binding the contract event 0xe4da013d8c42cdfa76ab1d5c08edcdc1503d2da88d7accc854f0e57ebe45c591. +// +// Solidity: e Redeemed(redeemTime uint256, secretHash bytes32, secret bytes32, redeemer address, value uint256) +func (_Contract *ContractFilterer) FilterRedeemed(opts *bind.FilterOpts) (*ContractRedeemedIterator, error) { + + logs, sub, err := _Contract.contract.FilterLogs(opts, "Redeemed") + if err != nil { + return nil, err + } + return &ContractRedeemedIterator{contract: _Contract.contract, event: "Redeemed", logs: logs, sub: sub}, nil +} + +// WatchRedeemed is a free log subscription operation binding the contract event 0xe4da013d8c42cdfa76ab1d5c08edcdc1503d2da88d7accc854f0e57ebe45c591. +// +// Solidity: e Redeemed(redeemTime uint256, secretHash bytes32, secret bytes32, redeemer address, value uint256) +func (_Contract *ContractFilterer) WatchRedeemed(opts *bind.WatchOpts, sink chan<- *ContractRedeemed) (event.Subscription, error) { + + logs, sub, err := _Contract.contract.WatchLogs(opts, "Redeemed") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(ContractRedeemed) + if err := _Contract.contract.UnpackLog(event, "Redeemed", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ContractRefundedIterator is returned from FilterRefunded and is used to iterate over the raw logs and unpacked data for Refunded events raised by the Contract contract. +type ContractRefundedIterator struct { + Event *ContractRefunded // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *ContractRefundedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(ContractRefunded) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(ContractRefunded) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *ContractRefundedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ContractRefundedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ContractRefunded represents a Refunded event raised by the Contract contract. +type ContractRefunded struct { + RefundTime *big.Int + SecretHash [32]byte + Refunder common.Address + Value *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterRefunded is a free log retrieval operation binding the contract event 0xadb1dca52dfad065e50a1e25c2ee47ae54013a1f2d6f8ea5abace52eb4b7a4c8. +// +// Solidity: e Refunded(refundTime uint256, secretHash bytes32, refunder address, value uint256) +func (_Contract *ContractFilterer) FilterRefunded(opts *bind.FilterOpts) (*ContractRefundedIterator, error) { + + logs, sub, err := _Contract.contract.FilterLogs(opts, "Refunded") + if err != nil { + return nil, err + } + return &ContractRefundedIterator{contract: _Contract.contract, event: "Refunded", logs: logs, sub: sub}, nil +} + +// WatchRefunded is a free log subscription operation binding the contract event 0xadb1dca52dfad065e50a1e25c2ee47ae54013a1f2d6f8ea5abace52eb4b7a4c8. +// +// Solidity: e Refunded(refundTime uint256, secretHash bytes32, refunder address, value uint256) +func (_Contract *ContractFilterer) WatchRefunded(opts *bind.WatchOpts, sink chan<- *ContractRefunded) (event.Subscription, error) { + + logs, sub, err := _Contract.contract.WatchLogs(opts, "Refunded") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(ContractRefunded) + if err := _Contract.contract.UnpackLog(event, "Refunded", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} diff --git a/cmd/ethatomicswap/contract/generate.go b/cmd/ethatomicswap/contract/generate.go new file mode 100644 index 0000000..1c60639 --- /dev/null +++ b/cmd/ethatomicswap/contract/generate.go @@ -0,0 +1,12 @@ +package contract + +// prerequisite: install ethereum devtools +// +// go get -u github.com/ethereum/go-ethereum +// cd $GOPATH/src/github.com/ethereum/go-ethereum/ +// make +// make devtools +// + +//go:generate sh -c "solc --abi src/contracts/AtomicSwap.sol | awk '/JSON ABI/{x=1;next}x' > AtomicSwap.abi" +//go:generate abigen --abi=AtomicSwap.abi --pkg=contract --out=atomicswap.go diff --git a/cmd/ethatomicswap/solidity/.gitignore b/cmd/ethatomicswap/contract/src/.gitignore similarity index 100% rename from cmd/ethatomicswap/solidity/.gitignore rename to cmd/ethatomicswap/contract/src/.gitignore diff --git a/cmd/ethatomicswap/solidity/README.md b/cmd/ethatomicswap/contract/src/README.md similarity index 100% rename from cmd/ethatomicswap/solidity/README.md rename to cmd/ethatomicswap/contract/src/README.md diff --git a/cmd/ethatomicswap/solidity/contracts/AtomicSwap.sol b/cmd/ethatomicswap/contract/src/contracts/AtomicSwap.sol similarity index 100% rename from cmd/ethatomicswap/solidity/contracts/AtomicSwap.sol rename to cmd/ethatomicswap/contract/src/contracts/AtomicSwap.sol diff --git a/cmd/ethatomicswap/solidity/contracts/Migrations.sol b/cmd/ethatomicswap/contract/src/contracts/Migrations.sol similarity index 100% rename from cmd/ethatomicswap/solidity/contracts/Migrations.sol rename to cmd/ethatomicswap/contract/src/contracts/Migrations.sol diff --git a/cmd/ethatomicswap/solidity/migrations/1_initial_migration.js b/cmd/ethatomicswap/contract/src/migrations/1_initial_migration.js similarity index 100% rename from cmd/ethatomicswap/solidity/migrations/1_initial_migration.js rename to cmd/ethatomicswap/contract/src/migrations/1_initial_migration.js diff --git a/cmd/ethatomicswap/solidity/migrations/2_atomicswap_migration.js b/cmd/ethatomicswap/contract/src/migrations/2_atomicswap_migration.js similarity index 100% rename from cmd/ethatomicswap/solidity/migrations/2_atomicswap_migration.js rename to cmd/ethatomicswap/contract/src/migrations/2_atomicswap_migration.js diff --git a/cmd/ethatomicswap/solidity/test/TestAtomicSwap.js b/cmd/ethatomicswap/contract/src/test/TestAtomicSwap.js similarity index 100% rename from cmd/ethatomicswap/solidity/test/TestAtomicSwap.js rename to cmd/ethatomicswap/contract/src/test/TestAtomicSwap.js diff --git a/cmd/ethatomicswap/solidity/test/exceptions.js b/cmd/ethatomicswap/contract/src/test/exceptions.js similarity index 100% rename from cmd/ethatomicswap/solidity/test/exceptions.js rename to cmd/ethatomicswap/contract/src/test/exceptions.js diff --git a/cmd/ethatomicswap/solidity/test/utils.js b/cmd/ethatomicswap/contract/src/test/utils.js similarity index 100% rename from cmd/ethatomicswap/solidity/test/utils.js rename to cmd/ethatomicswap/contract/src/test/utils.js diff --git a/cmd/ethatomicswap/solidity/truffle-config.js b/cmd/ethatomicswap/contract/src/truffle-config.js similarity index 100% rename from cmd/ethatomicswap/solidity/truffle-config.js rename to cmd/ethatomicswap/contract/src/truffle-config.js diff --git a/cmd/ethatomicswap/solidity/truffle.js b/cmd/ethatomicswap/contract/src/truffle.js similarity index 80% rename from cmd/ethatomicswap/solidity/truffle.js rename to cmd/ethatomicswap/contract/src/truffle.js index 0855df1..eb0acbc 100644 --- a/cmd/ethatomicswap/solidity/truffle.js +++ b/cmd/ethatomicswap/contract/src/truffle.js @@ -15,4 +15,11 @@ module.exports = { // See // to customize your Truffle configuration! + networks: { + development: { + host: "127.0.0.1", + port: 7545, + network_id: "*" // Match any network id + } + } }; From 6773b2fa5940844352795ff03e77660b7ef09f58 Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker Date: Mon, 25 Jun 2018 21:56:43 +0200 Subject: [PATCH 04/12] ensure that contracts are created with refundTime and value --- .../contract/src/contracts/AtomicSwap.sol | 8 ++++++++ .../contract/src/test/TestAtomicSwap.js | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/cmd/ethatomicswap/contract/src/contracts/AtomicSwap.sol b/cmd/ethatomicswap/contract/src/contracts/AtomicSwap.sol index bdeceff..490390e 100644 --- a/cmd/ethatomicswap/contract/src/contracts/AtomicSwap.sol +++ b/cmd/ethatomicswap/contract/src/contracts/AtomicSwap.sol @@ -96,9 +96,16 @@ contract AtomicSwap { _; } + modifier hasNoNilValues(uint refundTime) { + require(msg.value > 0); + require(refundTime > 0); + _; + } + function initiate(uint refundTime, bytes32 secretHash, address participant) public payable + hasNoNilValues(refundTime) isNotInitiated(secretHash) { swaps[secretHash].initTimestamp = block.timestamp; @@ -122,6 +129,7 @@ contract AtomicSwap { function participate(uint refundTime, bytes32 secretHash, address initiator) public payable + hasNoNilValues(refundTime) isNotInitiated(secretHash) { swaps[secretHash].initTimestamp = block.timestamp; diff --git a/cmd/ethatomicswap/contract/src/test/TestAtomicSwap.js b/cmd/ethatomicswap/contract/src/test/TestAtomicSwap.js index 0559950..5722d61 100644 --- a/cmd/ethatomicswap/contract/src/test/TestAtomicSwap.js +++ b/cmd/ethatomicswap/contract/src/test/TestAtomicSwap.js @@ -836,4 +836,22 @@ contract("AtomicSwap tests", accounts => { assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), "balance of second account should be as expected"); }); + + it("shouldn't be possible to create a contract with no value", async () => { + const refundTime = 60; + + await tryCatch(atomicSwap.participate(refundTime, secretHash, secondAccount, + {from: firstAccount, value: 0, gasPrice: 0}), errTypes.revert) + await tryCatch(atomicSwap.initiate(refundTime, secretHash, secondAccount, + {from: firstAccount, value: 0, gasPrice: 0}), errTypes.revert) + }); + + it("shouldn't be possible to create a contract with no refundTime", async () => { + const contractAmount = web3.toBigNumber(web3.toWei('0.01', 'ether')); + + await tryCatch(atomicSwap.participate(0, secretHash, secondAccount, + {from: firstAccount, value: contractAmount, gasPrice: 0}), errTypes.revert) + await tryCatch(atomicSwap.initiate(0, secretHash, secondAccount, + {from: firstAccount, value: contractAmount, gasPrice: 0}), errTypes.revert) + }); }); From 3043d6be3cb0f1ee6addd5085cee440833a0b449 Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker Date: Mon, 16 Jul 2018 15:31:37 +0200 Subject: [PATCH 05/12] include binary contract code in generated Go code this allows the deployment (through Go) of the smart contract --- cmd/ethatomicswap/contract/.gitignore | 3 ++- cmd/ethatomicswap/contract/atomicswap.go | 16 ++++++++++++++++ cmd/ethatomicswap/contract/generate.go | 4 ++-- cmd/ethatomicswap/contract/src/truffle.js | 5 +++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/cmd/ethatomicswap/contract/.gitignore b/cmd/ethatomicswap/contract/.gitignore index c4d76c4..a101c15 100644 --- a/cmd/ethatomicswap/contract/.gitignore +++ b/cmd/ethatomicswap/contract/.gitignore @@ -1 +1,2 @@ -*.abi \ No newline at end of file +*.abi +*.bin diff --git a/cmd/ethatomicswap/contract/atomicswap.go b/cmd/ethatomicswap/contract/atomicswap.go index b34befa..a786af6 100644 --- a/cmd/ethatomicswap/contract/atomicswap.go +++ b/cmd/ethatomicswap/contract/atomicswap.go @@ -18,6 +18,22 @@ import ( // ContractABI is the input ABI used to generate the binding from. const ContractABI = "[{\"constant\":false,\"inputs\":[{\"name\":\"refundTime\",\"type\":\"uint256\"},{\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"name\":\"initiator\",\"type\":\"address\"}],\"name\":\"participate\",\"outputs\":[],\"payable\":true,\"stateMutability\":\"payable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"secretHash\",\"type\":\"bytes32\"}],\"name\":\"refund\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"refundTime\",\"type\":\"uint256\"},{\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"name\":\"participant\",\"type\":\"address\"}],\"name\":\"initiate\",\"outputs\":[],\"payable\":true,\"stateMutability\":\"payable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"secret\",\"type\":\"bytes32\"},{\"name\":\"secretHash\",\"type\":\"bytes32\"}],\"name\":\"redeem\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"swaps\",\"outputs\":[{\"name\":\"initTimestamp\",\"type\":\"uint256\"},{\"name\":\"refundTime\",\"type\":\"uint256\"},{\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"name\":\"secret\",\"type\":\"bytes32\"},{\"name\":\"initiator\",\"type\":\"address\"},{\"name\":\"participant\",\"type\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\"},{\"name\":\"kind\",\"type\":\"uint8\"},{\"name\":\"state\",\"type\":\"uint8\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"refundTime\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"refunder\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Refunded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"redeemTime\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"secret\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"redeemer\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Redeemed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"initTimestamp\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"refundTime\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"initiator\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"participant\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Participated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"initTimestamp\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"refundTime\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"initiator\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"participant\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Initiated\",\"type\":\"event\"}]" +// ContractBin is the compiled bytecode used for deploying new contracts. +const ContractBin = `608060405234801561001057600080fd5b506110ac806100206000396000f30060806040526004361061006d576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680631aa02853146100725780637249fbb6146100c0578063ae052147146100f1578063b31597ad1461013f578063eb84e7f21461017e575b600080fd5b6100be600480360381019080803590602001909291908035600019169060200190929190803573ffffffffffffffffffffffffffffffffffffffff16906020019092919050505061027f565b005b3480156100cc57600080fd5b506100ef6004803603810190808035600019169060200190929190505050610576565b005b61013d600480360381019080803590602001909291908035600019169060200190929190803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506108bb565b005b34801561014b57600080fd5b5061017c60048036038101908080356000191690602001909291908035600019169060200190929190505050610bb2565b005b34801561018a57600080fd5b506101ad6004803603810190808035600019169060200190929190505050610fd8565b604051808a8152602001898152602001886000191660001916815260200187600019166000191681526020018673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200184815260200183600181111561024f57fe5b60ff16815260200182600381111561026357fe5b60ff168152602001995050505050505050505060405180910390f35b8260003411151561028f57600080fd5b60008111151561029e57600080fd5b82600060038111156102ac57fe5b600080836000191660001916815260200190815260200160002060070160019054906101000a900460ff1660038111156102e257fe5b1415156102ee57600080fd5b4260008086600019166000191681526020019081526020016000206000018190555084600080866000191660001916815260200190815260200160002060010181905550836000808660001916600019168152602001908152602001600020600201816000191690555082600080866000191660001916815260200190815260200160002060040160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555033600080866000191660001916815260200190815260200160002060050160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550346000808660001916600019168152602001908152602001600020600601819055506001600080866000191660001916815260200190815260200160002060070160006101000a81548160ff0219169083600181111561046c57fe5b02179055506001600080866000191660001916815260200190815260200160002060070160016101000a81548160ff021916908360038111156104ab57fe5b02179055507fe5571d467a528d7481c0e3bdd55ad528d0df6b457b07bab736c3e245c3aa16f44286868633346040518087815260200186815260200185600019166000191681526020018473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001828152602001965050505050505060405180910390a15050505050565b803360006001600381111561058757fe5b600080856000191660001916815260200190815260200160002060070160019054906101000a900460ff1660038111156105bd57fe5b1415156105c957600080fd5b6001808111156105d557fe5b600080856000191660001916815260200190815260200160002060070160009054906101000a900460ff16600181111561060b57fe5b141561068d578173ffffffffffffffffffffffffffffffffffffffff16600080856000191660001916815260200190815260200160002060050160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614151561068857600080fd5b610705565b8173ffffffffffffffffffffffffffffffffffffffff16600080856000191660001916815260200190815260200160002060040160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614151561070457600080fd5b5b600080846000191660001916815260200190815260200160002060000154905060008084600019166000191681526020019081526020016000206001015481019050804211151561075557600080fd5b3373ffffffffffffffffffffffffffffffffffffffff166108fc6000808760001916600019168152602001908152602001600020600601549081150290604051600060405180830381858888f193505050501580156107b8573d6000803e3d6000fd5b506003600080866000191660001916815260200190815260200160002060070160016101000a81548160ff021916908360038111156107f357fe5b02179055507fadb1dca52dfad065e50a1e25c2ee47ae54013a1f2d6f8ea5abace52eb4b7a4c842600080876000191660001916815260200190815260200160002060020154336000808960001916600019168152602001908152602001600020600601546040518085815260200184600019166000191681526020018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200182815260200194505050505060405180910390a150505050565b826000341115156108cb57600080fd5b6000811115156108da57600080fd5b82600060038111156108e857fe5b600080836000191660001916815260200190815260200160002060070160019054906101000a900460ff16600381111561091e57fe5b14151561092a57600080fd5b4260008086600019166000191681526020019081526020016000206000018190555084600080866000191660001916815260200190815260200160002060010181905550836000808660001916600019168152602001908152602001600020600201816000191690555033600080866000191660001916815260200190815260200160002060040160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555082600080866000191660001916815260200190815260200160002060050160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550346000808660001916600019168152602001908152602001600020600601819055506000806000866000191660001916815260200190815260200160002060070160006101000a81548160ff02191690836001811115610aa857fe5b02179055506001600080866000191660001916815260200190815260200160002060070160016101000a81548160ff02191690836003811115610ae757fe5b02179055507f75501a491c11746724d18ea6e5ac6a53864d886d653da6b846fdecda837cf5764286863387346040518087815260200186815260200185600019166000191681526020018473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001828152602001965050505050505060405180910390a15050505050565b80823360016003811115610bc257fe5b600080856000191660001916815260200190815260200160002060070160019054906101000a900460ff166003811115610bf857fe5b141515610c0457600080fd5b600180811115610c1057fe5b600080856000191660001916815260200190815260200160002060070160009054906101000a900460ff166001811115610c4657fe5b1415610cc8578073ffffffffffffffffffffffffffffffffffffffff16600080856000191660001916815260200190815260200160002060040160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16141515610cc357600080fd5b610d40565b8073ffffffffffffffffffffffffffffffffffffffff16600080856000191660001916815260200190815260200160002060050160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16141515610d3f57600080fd5b5b82600019166002836040516020018082600019166000191681526020019150506040516020818303038152906040526040518082805190602001908083835b602083101515610da45780518252602082019150602081019050602083039250610d7f565b6001836020036101000a0380198251168184511680821785525050505050509050019150506020604051808303816000865af1158015610de8573d6000803e3d6000fd5b5050506040513d6020811015610dfd57600080fd5b810190808051906020019092919050505060001916141515610e1e57600080fd5b3373ffffffffffffffffffffffffffffffffffffffff166108fc6000808760001916600019168152602001908152602001600020600601549081150290604051600060405180830381858888f19350505050158015610e81573d6000803e3d6000fd5b506002600080866000191660001916815260200190815260200160002060070160016101000a81548160ff02191690836003811115610ebc57fe5b021790555084600080866000191660001916815260200190815260200160002060030181600019169055507fe4da013d8c42cdfa76ab1d5c08edcdc1503d2da88d7accc854f0e57ebe45c59142600080876000191660001916815260200190815260200160002060020154600080886000191660001916815260200190815260200160002060030154336000808a600019166000191681526020019081526020016000206006015460405180868152602001856000191660001916815260200184600019166000191681526020018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018281526020019550505050505060405180910390a15050505050565b60006020528060005260406000206000915090508060000154908060010154908060020154908060030154908060040160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16908060050160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16908060060154908060070160009054906101000a900460ff16908060070160019054906101000a900460ff169050895600a165627a7a72305820081a82269020bc584dfffd0f5cad637d66d08ba4234a58c31e0dc6329fbe966b0029` + +// DeployContract deploys a new Ethereum contract, binding an instance of Contract to it. +func DeployContract(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *Contract, error) { + parsed, err := abi.JSON(strings.NewReader(ContractABI)) + if err != nil { + return common.Address{}, nil, nil, err + } + address, tx, contract, err := bind.DeployContract(auth, parsed, common.FromHex(ContractBin), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &Contract{ContractCaller: ContractCaller{contract: contract}, ContractTransactor: ContractTransactor{contract: contract}, ContractFilterer: ContractFilterer{contract: contract}}, nil +} + // Contract is an auto generated Go binding around an Ethereum contract. type Contract struct { ContractCaller // Read-only binding to the contract diff --git a/cmd/ethatomicswap/contract/generate.go b/cmd/ethatomicswap/contract/generate.go index 1c60639..5bfc6b6 100644 --- a/cmd/ethatomicswap/contract/generate.go +++ b/cmd/ethatomicswap/contract/generate.go @@ -6,7 +6,7 @@ package contract // cd $GOPATH/src/github.com/ethereum/go-ethereum/ // make // make devtools -// //go:generate sh -c "solc --abi src/contracts/AtomicSwap.sol | awk '/JSON ABI/{x=1;next}x' > AtomicSwap.abi" -//go:generate abigen --abi=AtomicSwap.abi --pkg=contract --out=atomicswap.go +//go:generate sh -c "solc --bin src/contracts/AtomicSwap.sol | awk '/Binary:/{x=1;next}x' > AtomicSwap.bin" +//go:generate abigen --bin=AtomicSwap.bin --abi=AtomicSwap.abi --pkg=contract --out=atomicswap.go diff --git a/cmd/ethatomicswap/contract/src/truffle.js b/cmd/ethatomicswap/contract/src/truffle.js index eb0acbc..53915a8 100644 --- a/cmd/ethatomicswap/contract/src/truffle.js +++ b/cmd/ethatomicswap/contract/src/truffle.js @@ -16,6 +16,11 @@ module.exports = { // See // to customize your Truffle configuration! networks: { + development: { + host: "127.0.0.1", + port: 7545, + network_id: "*" // Match any network id + }, development: { host: "127.0.0.1", port: 7545, From 0c31e0e0fcfbef41247a2b27c04533091b1cfa58 Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker Date: Wed, 18 Jul 2018 15:48:25 +0200 Subject: [PATCH 06/12] add initial version of ethatomicswap a tool to support atomic swaps for Ethereum --- .travis.yml | 1 + Gopkg.lock | 119 +++- cmd/ethatomicswap/main.go | 1075 ++++++++++++++++++++++++++++++++ cmd/ethatomicswap/main_test.go | 59 ++ 4 files changed, 1252 insertions(+), 2 deletions(-) create mode 100644 cmd/ethatomicswap/main.go create mode 100644 cmd/ethatomicswap/main_test.go diff --git a/.travis.yml b/.travis.yml index c8295f5..13ee162 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,7 @@ jobs: script: - export PATH=$PATH:$HOME/gopath/bin - ./goclean.sh + - go test -v -race ./cmd/ethatomicswap - stage: test language: node_js node_js: diff --git a/Gopkg.lock b/Gopkg.lock index cf60660..5a8695e 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -25,6 +25,12 @@ ] revision = "5312a61534124124185d41f09206b9fef1d88403" +[[projects]] + branch = "master" + name = "github.com/aristanetworks/goarista" + packages = ["monotime"] + revision = "625ff285aa35926943df199ab15deba045716206" + [[projects]] branch = "master" name = "github.com/bitgoin/lyra2rev2" @@ -136,6 +142,43 @@ ] revision = "1dc40f82683013a2111dd6bcfbd22d3dd219bc34" +[[projects]] + name = "github.com/ethereum/go-ethereum" + packages = [ + ".", + "accounts", + "accounts/abi", + "accounts/abi/bind", + "accounts/keystore", + "common", + "common/hexutil", + "common/math", + "common/mclock", + "core/types", + "crypto", + "crypto/randentropy", + "crypto/secp256k1", + "crypto/sha3", + "ethclient", + "ethdb", + "event", + "log", + "metrics", + "p2p/netutil", + "params", + "rlp", + "rpc", + "trie" + ] + revision = "37685930d953bcbe023f9bc65b135a8d8b8f1488" + version = "v1.8.12" + +[[projects]] + name = "github.com/go-stack/stack" + packages = ["."] + revision = "259ab82a6cad3992b4e21ff5cac294ccb06474bc" + version = "v1.7.0" + [[projects]] branch = "master" name = "github.com/golang/protobuf" @@ -148,6 +191,12 @@ ] revision = "bbd03ef6da3a115852eaf24c8a1c46aeb39aa175" +[[projects]] + branch = "master" + name = "github.com/golang/snappy" + packages = ["."] + revision = "2e65f85255dbc3072edf28d6b5b8efc472979f5a" + [[projects]] branch = "ltcatomicswap" name = "github.com/ltcsuite/ltcd" @@ -211,6 +260,12 @@ packages = ["wallet/txrules"] revision = "970e161f293ff12ebbadae51db1f9d47f7414967" +[[projects]] + name = "github.com/pborman/uuid" + packages = ["."] + revision = "e790cca94e6cc75c7064b1332e63811d4aae1a53" + version = "v1.1" + [[projects]] branch = "master" name = "github.com/polissuite/gopolis" @@ -247,6 +302,37 @@ packages = ["."] revision = "66b5c7eba786ac6ce9ab51a24e24bb2d319b488c" +[[projects]] + name = "github.com/rjeczalik/notify" + packages = ["."] + revision = "52ae50d8490436622a8941bd70c3dbe0acdd4bbf" + version = "v0.9.0" + +[[projects]] + name = "github.com/rs/cors" + packages = ["."] + revision = "ca016a06a5753f8ba03029c0aa5e54afb1bf713f" + version = "v1.4.0" + +[[projects]] + branch = "master" + name = "github.com/syndtr/goleveldb" + packages = [ + "leveldb", + "leveldb/cache", + "leveldb/comparer", + "leveldb/errors", + "leveldb/filter", + "leveldb/iterator", + "leveldb/journal", + "leveldb/memdb", + "leveldb/opt", + "leveldb/storage", + "leveldb/table", + "leveldb/util" + ] + revision = "c4c61651e9e37fa117f53c5a906d3b63090d8445" + [[projects]] branch = "master" name = "github.com/vertcoin/vtcd" @@ -394,7 +480,8 @@ "idna", "internal/timeseries", "lex/httplex", - "trace" + "trace", + "websocket" ] revision = "cbe0f9307d0156177f9dd5dc85da1a31abc5f2fb" @@ -428,6 +515,16 @@ revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" version = "v0.3.0" +[[projects]] + branch = "master" + name = "golang.org/x/tools" + packages = [ + "go/ast/astutil", + "imports", + "internal/fastwalk" + ] + revision = "32950ab3be12acf6d472893021373669979907ab" + [[projects]] branch = "master" name = "google.golang.org/genproto" @@ -464,9 +561,27 @@ revision = "8e4536a86ab602859c20df5ebfd0bd4228d08655" version = "v1.10.0" +[[projects]] + name = "gopkg.in/fatih/set.v0" + packages = ["."] + revision = "57907de300222151a123d29255ed17f5ed43fad3" + version = "v0.1.0" + +[[projects]] + branch = "v2" + name = "gopkg.in/karalabe/cookiejar.v2" + packages = ["collections/prque"] + revision = "8dcd6a7f4951f6ff3ee9cbb919a06d8925822e57" + +[[projects]] + branch = "v2" + name = "gopkg.in/natefinch/npipe.v2" + packages = ["."] + revision = "c1b8fa8bdccecb0b8db834ee0b92fdbcfa606dd6" + [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "b403e3758f7b9d3985451de9323dbf395cc4ab3e22b068e87d291fec5d05ae02" + inputs-digest = "c5a64aedde3cf8fcfe7ef9244e2f1be049ac77ce90901b34af3520616cb0ed83" solver-name = "gps-cdcl" solver-version = 1 diff --git a/cmd/ethatomicswap/main.go b/cmd/ethatomicswap/main.go new file mode 100644 index 0000000..6c84e13 --- /dev/null +++ b/cmd/ethatomicswap/main.go @@ -0,0 +1,1075 @@ +// Copyright (c) 2018 The Decred developers and Contributors +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package main + +import ( + "bufio" + "bytes" + "context" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "errors" + "flag" + "fmt" + "io/ioutil" + "math/big" + "os" + "strings" + "time" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/rpc" + + "github.com/decred/atomicswap/cmd/ethatomicswap/contract" +) + +var ( + chainConfig = params.MainnetChainConfig +) + +const ( + initiateLockPeriodInSeconds = 48 * 60 * 60 + participateLockPeriodInSeconds = 24 * 60 * 60 + + maxGasLimit = 210000 +) + +// TODO: find way to not require keyFile/passphrase +// (and certainly not in the way we currently do) + +var ( + flagset = flag.NewFlagSet("", flag.ExitOnError) + connectFlag = flagset.String("s", "http://localhost:8545", "endpoint of Ethereum RPC server") + contractFlag = flagset.String("c", "", "hex-enoded address of the deployed contract") + keyFileFlag = flagset.String("keyfile", "", "file containing the key used for signing") + passphraseFlag = flagset.String("passphrase", "", "passphrase used for decrypting the key") + timeoutFlag = flagset.Duration("t", 0, "optional timeout of any call made") + testnetFlag = flagset.Bool("testnet", false, "use testnet (Rinkeby) network") +) + +// TODO: better error reporting: +// now all contract-originated errors return +// "failed to estimate gas needed: gas required exceeds allowance or always failing transaction" + +// There are two directions that the atomic swap can be performed, as the +// initiator can be on either chain. This tool only deals with creating the +// Bitcoin transactions for these swaps. A second tool should be used for the +// transaction on the other chain. Any chain can be used so long as it supports +// OP_SHA256 and OP_CHECKLOCKTIMEVERIFY. +// +// Example scenerios using bitcoin as the second chain: +// +// Scenerio 1: +// cp1 initiates (dcr) +// cp2 participates with cp1 H(S) (eth) +// cp1 redeems eth revealing S +// - must verify H(S) in contract is hash of known secret +// cp2 redeems dcr with S +// +// Scenerio 2: +// cp1 initiates (eth) +// cp2 participates with cp1 H(S) (dcr) +// cp1 redeems dcr revealing S +// - must verify H(S) in contract is hash of known secret +// cp2 redeems eth with S + +func init() { + flagset.Usage = func() { + fmt.Println("Usage: ethatomicswap [flags] cmd [cmd args]") + fmt.Println() + fmt.Println("Commands:") + fmt.Println(" initiate ") + fmt.Println(" participate ") + fmt.Println(" redeem ") + fmt.Println(" refund ") + fmt.Println(" extractsecret ") + fmt.Println(" auditcontract ") + fmt.Println() + fmt.Println("Extra Commands:") + fmt.Println(" deploycontract") + fmt.Println(" validatedeployedcontract ") + fmt.Println() + fmt.Println("Flags:") + flagset.PrintDefaults() + } +} + +type command interface { + runCommand(swapContractTransactor) error +} + +// offline commands don't require wallet RPC. +type offlineCommand interface { + command + runOfflineCommand() error +} + +type initiateCmd struct { + cp2Addr common.Address + amount *big.Int // in wei +} + +type participateCmd struct { + cp1Addr common.Address + amount *big.Int // in wei + secretHash [32]byte +} + +type redeemCmd struct { + contractTx *types.Transaction + secret [32]byte +} + +type refundCmd struct { + contractTx *types.Transaction +} + +type extractSecretCmd struct { + redemptionTx *types.Transaction + secretHash [32]byte +} + +type auditContractCmd struct { + contractTx *types.Transaction +} + +type deployContractCmd struct{} + +type validateDeployedContractCmd struct { + deployTx *types.Transaction +} + +func main() { + err, showUsage := run() + if err != nil { + fmt.Fprintln(os.Stderr, err) + } + if showUsage { + flagset.Usage() + } + if err != nil || showUsage { + os.Exit(1) + } +} + +func checkCmdArgLength(args []string, required int) (nArgs int) { + if len(args) < required { + return 0 + } + for i, arg := range args[:required] { + if len(arg) != 1 && strings.HasPrefix(arg, "-") { + return i + } + } + return required +} + +const ( + weiPrecision = 18 +) + +func parseEthAsWei(str string) (*big.Int, error) { + initialParts := strings.SplitN(str, ".", 2) + if len(initialParts) == 1 { + // a round number, simply multiply and go + i, ok := big.NewInt(0).SetString(initialParts[0], 10) + if !ok { + return nil, errors.New("invalid round amount") + } + switch i.Cmp(big.NewInt(0)) { + case -1: + return nil, errors.New("invalid round amount: cannot be negative") + case 0: + return nil, errors.New("invalid round amount: cannot be nil") + } + return i.Mul(i, new(big.Int).Exp(big.NewInt(10), big.NewInt(weiPrecision), nil)), nil + } + + whole := initialParts[0] + dac := initialParts[1] + sn := uint(weiPrecision) + if l := uint(len(dac)); l < sn { + sn = l + } + whole += initialParts[1][:sn] + dac = dac[sn:] + for i := range dac { + if dac[i] != '0' { + return nil, errors.New("invalid or too precise amount") + } + } + i, ok := big.NewInt(0).SetString(whole, 10) + if !ok { + return nil, errors.New("invalid amount") + } + switch i.Cmp(big.NewInt(0)) { + case -1: + return nil, errors.New("invalid round amount: cannot be negative") + case 0: + return nil, errors.New("invalid round amount: cannot be nil") + } + i.Mul(i, big.NewInt(0).Exp( + big.NewInt(10), big.NewInt(int64(weiPrecision-sn)), nil)) + + switch i.Cmp(big.NewInt(0)) { + case -1: + return nil, errors.New("invalid round amount: cannot be negative") + case 0: + return nil, errors.New("invalid round amount: cannot be nil") + } + return i, nil +} + +func formatWeiAsEthString(w *big.Int) string { + if w.Cmp(big.NewInt(0)) == 0 { + return "0" + } + + str := w.String() + l := uint(len(str)) + if l > weiPrecision { + idx := l - weiPrecision + str = strings.TrimRight(str[:idx]+"."+str[idx:], "0") + str = strings.TrimRight(str, ".") + if len(str) == 0 { + return "0" + } + return str + } + str = "0." + strings.Repeat("0", int(weiPrecision-l)) + str + str = strings.TrimRight(str, "0") + str = strings.TrimRight(str, ".") + return str +} + +func run() (err error, showUsage bool) { + flagset.Parse(os.Args[1:]) + args := flagset.Args() + if len(args) == 0 { + return nil, true + } + cmdArgs := 0 + switch args[0] { + case "initiate": + cmdArgs = 2 + case "participate": + cmdArgs = 3 + case "redeem": + cmdArgs = 2 + case "refund": + cmdArgs = 1 + case "extractsecret": + cmdArgs = 2 + case "auditcontract": + cmdArgs = 1 + case "deploycontract": + cmdArgs = 0 + case "validatedeployedcontract": + cmdArgs = 1 + default: + return fmt.Errorf("unknown command %v", args[0]), true + } + nArgs := checkCmdArgLength(args[1:], cmdArgs) + flagset.Parse(args[1+nArgs:]) + if nArgs < cmdArgs { + return fmt.Errorf("%s: too few arguments", args[0]), true + } + if flagset.NArg() != 0 { + return fmt.Errorf("unexpected argument: %s", flagset.Arg(0)), true + } + + if *testnetFlag { + chainConfig = params.RinkebyChainConfig + } + + var cmd command + switch args[0] { + case "initiate": + cp2Addr := common.HexToAddress(args[1]) + amount, err := parseEthAsWei(args[2]) + if err != nil { + return fmt.Errorf("unexpected amount argument (%v): %v", args[2], err), true + } + cmd = &initiateCmd{ + cp2Addr: cp2Addr, + amount: amount, + } + + case "participate": + cp1Addr := common.HexToAddress(args[1]) + amount, err := parseEthAsWei(args[2]) + if err != nil { + return fmt.Errorf("unexpected amount argument (%v): %v", args[2], err), true + } + secretHash, err := hexDecodeSha256Hash("secret hash", args[3]) + if err != nil { + return err, true + } + cmd = &participateCmd{ + cp1Addr: cp1Addr, + amount: amount, + secretHash: secretHash, + } + + case "redeem": + contractTx, err := hexDecodeTransaction(args[1]) + if err != nil { + return err, true + } + secret, err := hexDecodeSha256Hash("secret", args[2]) + if err != nil { + return err, true + } + cmd = &redeemCmd{ + contractTx: contractTx, + secret: secret, + } + + case "refund": + contractTx, err := hexDecodeTransaction(args[1]) + if err != nil { + return err, true + } + cmd = &refundCmd{ + contractTx: contractTx, + } + + case "extractsecret": + redemptionTx, err := hexDecodeTransaction(args[1]) + if err != nil { + return err, true + } + secretHash, err := hexDecodeSha256Hash("secret hash", args[2]) + if err != nil { + return err, true + } + cmd = &extractSecretCmd{ + redemptionTx: redemptionTx, + secretHash: secretHash, + } + + case "auditcontract": + contractTx, err := hexDecodeTransaction(args[1]) + if err != nil { + return err, true + } + cmd = &auditContractCmd{ + contractTx: contractTx, + } + + case "deploycontract": + cmd = new(deployContractCmd) + + case "validatedeployedcontract": + deployTx, err := hexDecodeTransaction(args[1]) + if err != nil { + return err, true + } + cmd = &validateDeployedContractCmd{ + deployTx: deployTx, + } + + default: + panic(fmt.Sprintf("unknown command %v", args[0])) + } + + // Offline commands don't need to talk to the wallet. + if cmd, ok := cmd.(offlineCommand); ok { + return cmd.runOfflineCommand(), false + } + + client, err := dialClient() + if err != nil { + return fmt.Errorf("rpc connect: %v", err), false + } + defer client.Close() + + // create (swap) contract transactor + contractAddr, err := getDeployedContractAddress() + if err != nil { + return fmt.Errorf("failed to get contract address: %v", err), false + } + sct, err := newSwapContractTransactor(client, contractAddr) + if err != nil { + return err, false + } + + err = cmd.runCommand(sct) + return err, false +} + +func getDeployedContractAddress() (common.Address, error) { + contractAddress := *contractFlag + if contractAddress != "" { + return common.HexToAddress(contractAddress), nil + } + switch chainConfig { + case params.MainnetChainConfig: + return common.Address{}, errors.New("no default contract exist yet for the main net") + case params.RinkebyChainConfig: + return common.HexToAddress("2661CBAa149721f7c5FAB3FA88C1EA564A683631"), nil + } + + panic("unknown chain config for chain ID: " + chainConfig.ChainID.String()) +} + +func sha256Hash(x []byte) [sha256.Size]byte { + h := sha256.Sum256(x) + return h +} + +func hexDecodeSha256Hash(name, str string) (hash [sha256.Size]byte, err error) { + slice, err := hex.DecodeString(strings.TrimPrefix(str, "0x")) + if err != nil { + err = errors.New(name + " must be hex encoded") + return + } + if len(slice) != sha256.Size { + err = errors.New(name + " has wrong size") + return + } + copy(hash[:], slice) + return +} + +func hexDecodeTransaction(str string) (*types.Transaction, error) { + slice, err := hex.DecodeString(strings.TrimPrefix(str, "0x")) + if err != nil { + return nil, errors.New("transaction must be hex encoded") + } + var tx types.Transaction + err = rlp.DecodeBytes(slice, &tx) + if err != nil { + return nil, fmt.Errorf("failed to decode transaction: %v", err) + } + return &tx, nil +} + +func generateSecretHashPair() (secret, secretHash [sha256.Size]byte) { + rand.Read(secret[:]) + secretHash = sha256Hash(secret[:]) + return +} + +func newTransactOpts() (*bind.TransactOpts, error) { + f, err := os.Open(*keyFileFlag) + if err != nil { + return nil, fmt.Errorf("failed to open key file: %v", err) + } + return bind.NewTransactor(f, *passphraseFlag) +} + +func promptPublishTx(name string) (bool, error) { + reader := bufio.NewReader(os.Stdin) + for { + fmt.Printf("Publish %s transaction? [y/N] ", name) + answer, err := reader.ReadString('\n') + if err != nil { + return false, err + } + answer = strings.TrimSpace(strings.ToLower(answer)) + + switch answer { + case "y", "yes": + return true, nil + case "n", "no", "": + return false, nil + default: + fmt.Println("please answer y or n") + continue + } + } +} + +func calcGasCost(limit uint64, c *ethclient.Client) (*big.Int, error) { + price, err := c.SuggestGasPrice(context.Background()) + if err != nil { + return nil, err + } + return price.Mul(price, big.NewInt(int64(limit))), nil +} + +func unpackContractInputParams(abi abi.ABI, tx *types.Transaction) (params struct { + LockDuration *big.Int + SecretHash [sha256.Size]byte + ToAddress common.Address +}, err error) { + txData := tx.Data() + + // first 4 bytes contain the id, so let's get method using that ID + method, err := abi.MethodById(txData[:4]) + if err != nil { + err = fmt.Errorf("failed to get method using its parsed id: %v", err) + return + } + + // unpack and return the params + paramSlice := []interface{}{ // unpack as slice, so we don't enforce field names + ¶ms.LockDuration, + ¶ms.SecretHash, + ¶ms.ToAddress, + } + err = method.Inputs.Unpack(¶mSlice, txData[4:]) + if err != nil { + err = fmt.Errorf("failed to unpack method's input params: %v", err) + } + return +} + +func (cmd *initiateCmd) runCommand(sct swapContractTransactor) error { + secret, secretHash := generateSecretHashPair() + tx, err := sct.initiateTx(cmd.amount, secretHash, cmd.cp2Addr) + if err != nil { + return fmt.Errorf("failed to create initiate TX: %v", err) + } + + fmt.Printf("Amount: %s Wei (%s ETH)\n\n", + cmd.amount.String(), formatWeiAsEthString(cmd.amount)) + + fmt.Printf("Secret: %x\n", secret) + fmt.Printf("Secret hash: %x\n\n", secretHash) + + initiateTxCost := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) + fmt.Printf("Contract fee: %s ETH\n", formatWeiAsEthString(initiateTxCost)) + refundTxCost, err := sct.maxGasCost() + if err != nil { + return fmt.Errorf("failed to estimate max gas cost for refund tx: %v", err) + } + fmt.Printf("Refund fee: %s ETH (max)\n\n", formatWeiAsEthString(refundTxCost)) + + fmt.Printf("Chain ID: %s\n", chainConfig.ChainID.String()) + fmt.Printf("Contract Address: %x\n", sct.contractAddr) + + fmt.Printf("Contract transaction (%x):\n", tx.Hash()) + txBytes, err := rlp.EncodeToBytes(tx) + if err != nil { + return fmt.Errorf("failed to encode contract TX: %v", err) + } + fmt.Printf("%x\n\n", txBytes) + + publish, err := promptPublishTx("contract") + if err != nil || !publish { + return err + } + + err = tx.Send() + if err != nil { + return err + } + fmt.Printf("Published contract transaction (%x)\n", tx.Hash()) + return nil +} + +func (cmd *participateCmd) runCommand(sct swapContractTransactor) error { + tx, err := sct.participateTx(cmd.amount, cmd.secretHash, cmd.cp1Addr) + if err != nil { + return fmt.Errorf("failed to create participate TX: %v", err) + } + + fmt.Printf("Amount: %s Wei (%s ETH)\n\n", + cmd.amount.String(), formatWeiAsEthString(cmd.amount)) + + participateTxCost := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) + fmt.Printf("Contract fee: %s ETH\n", formatWeiAsEthString(participateTxCost)) + refundTxCost, err := sct.maxGasCost() + if err != nil { + return fmt.Errorf("failed to estimate max gas cost for refund tx: %v", err) + } + fmt.Printf("Refund fee: %s ETH (max)\n\n", formatWeiAsEthString(refundTxCost)) + + fmt.Printf("Chain ID: %s\n", chainConfig.ChainID.String()) + fmt.Printf("Contract Address: %x\n", sct.contractAddr) + + fmt.Printf("Contract transaction (%x):\n", tx.Hash()) + txBytes, err := rlp.EncodeToBytes(tx) + if err != nil { + return fmt.Errorf("failed to encode contract TX: %v", err) + } + fmt.Printf("%x\n\n", txBytes) + + publish, err := promptPublishTx("contract") + if err != nil || !publish { + return err + } + + err = tx.Send() + if err != nil { + return err + } + fmt.Printf("Published contract transaction (%x)\n", tx.Hash()) + return nil +} + +func (cmd *redeemCmd) runCommand(sct swapContractTransactor) error { + params, err := unpackContractInputParams(sct.abi, cmd.contractTx) + if err != nil { + return err + } + expectedSecretHash := sha256Hash(cmd.secret[:]) + if expectedSecretHash != params.SecretHash { + return fmt.Errorf( + "contract transaction contains unexpected secret hash (%x)", + params.SecretHash) + } + tx, err := sct.redeemTx(params.SecretHash, cmd.secret) + if err != nil { + return fmt.Errorf("failed to create redeem TX: %v", err) + } + + redeemTxCost := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) + fmt.Printf("Redeem fee: %s ETH\n\n", formatWeiAsEthString(redeemTxCost)) + + fmt.Printf("Chain ID: %s\n", chainConfig.ChainID.String()) + fmt.Printf("Contract Address: %x\n", sct.contractAddr) + + fmt.Printf("Redeem transaction (%x):\n", tx.Hash()) + txBytes, err := rlp.EncodeToBytes(tx) + if err != nil { + return fmt.Errorf("failed to encode redeem TX: %v", err) + } + fmt.Printf("%x\n\n", txBytes) + + publish, err := promptPublishTx("redeem") + if err != nil || !publish { + return err + } + + err = tx.Send() + if err != nil { + return err + } + fmt.Printf("Published redeem transaction (%x)\n", tx.Hash()) + return nil +} + +func (cmd *refundCmd) runCommand(sct swapContractTransactor) error { + params, err := unpackContractInputParams(sct.abi, cmd.contractTx) + if err != nil { + return err + } + tx, err := sct.refundTx(params.SecretHash) + if err != nil { + return fmt.Errorf("failed to create refund TX: %v", err) + } + + refundTxCost := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) + fmt.Printf("Refund fee: %s ETH\n\n", formatWeiAsEthString(refundTxCost)) + + fmt.Printf("Chain ID: %s\n", chainConfig.ChainID.String()) + fmt.Printf("Contract Address: %x\n", sct.contractAddr) + + fmt.Printf("Refund transaction (%x):\n", tx.Hash()) + txBytes, err := rlp.EncodeToBytes(tx) + if err != nil { + return fmt.Errorf("failed to encode refund TX: %v", err) + } + fmt.Printf("%x\n\n", txBytes) + + publish, err := promptPublishTx("refund") + if err != nil || !publish { + return err + } + + err = tx.Send() + if err != nil { + return err + } + fmt.Printf("Published refund transaction (%x)\n", tx.Hash()) + return nil +} + +func (cmd *extractSecretCmd) runCommand(swapContractTransactor) error { + return cmd.runOfflineCommand() +} + +func (cmd *extractSecretCmd) runOfflineCommand() error { + abi, err := abi.JSON(strings.NewReader(contract.ContractABI)) + if err != nil { + return fmt.Errorf("failed to read (smart) contract ABI: %v", err) + } + + txData := cmd.redemptionTx.Data() + + // first 4 bytes contain the id, so let's get method using that ID + method, err := abi.MethodById(txData[:4]) + if err != nil { + return fmt.Errorf("failed to get method using its parsed id: %v", err) + } + if method.Name != "redeem" { + return fmt.Errorf("unexpected name for unpacked method ID: %s", method.Name) + } + + // prepare the params + params := struct { + Secret [sha256.Size]byte + SecretHash [sha256.Size]byte + }{} + + // unpack the params + err = method.Inputs.Unpack(¶ms, txData[4:]) + if err != nil { + return fmt.Errorf("failed to unpack method's input params: %v", err) + } + + // ensure secret hash is the same as the given one + if cmd.secretHash != params.SecretHash { + return fmt.Errorf("unexpected secret hash found: %x", params.SecretHash) + } + secretHash := sha256Hash(params.Secret[:]) + if params.SecretHash != secretHash { + return fmt.Errorf("unexpected secret found: %x", params.Secret) + } + + // print secret + fmt.Printf("Secret: %x\n", params.Secret) + return nil +} + +func (cmd *auditContractCmd) runCommand(sct swapContractTransactor) error { + // unpack input params from contract tx + params, err := unpackContractInputParams(sct.abi, cmd.contractTx) + if err != nil { + return err + } + + rpcTransaction := struct { + tx *types.Transaction + BlockNumber *string + BlockHash *common.Hash + From *common.Address + }{} + + // get transaction by hash + contractHash := cmd.contractTx.Hash() + err = sct.client.rpcClient.CallContext(newContext(), + &rpcTransaction, "eth_getTransactionByHash", contractHash) + if err != nil { + return fmt.Errorf( + "failed to find transaction (%x): %v", contractHash, err) + } + if rpcTransaction.BlockNumber == nil || *rpcTransaction.BlockNumber == "" || *rpcTransaction.BlockNumber == "0" { + return fmt.Errorf("transaction (%x) is pending", contractHash) + } + + // get block in order to know the timestamp of the txn + block, err := sct.client.BlockByHash(newContext(), *rpcTransaction.BlockHash) + if err != nil { + return fmt.Errorf( + "failed to find block (%x): %v", rpcTransaction.BlockHash, err) + } + + // compute the locktime + lockTime := time.Unix(block.Time().Int64()+params.LockDuration.Int64(), 0) + + // print contract info + + fmt.Printf("Contract address: %x\n", cmd.contractTx.To()) + fmt.Printf("Contract value: %s ETH\n", formatWeiAsEthString(cmd.contractTx.Value())) + fmt.Printf("Recipient address: %x\n", params.ToAddress) + fmt.Printf("Author's refund address: %x\n\n", rpcTransaction.From) + + fmt.Printf("Secret hash: %x\n\n", params.SecretHash) + + // NOTE: + // the reason we require th node for this method, + // is because we need to be able to know the transaction's timestamp + + fmt.Printf("Locktime: %v\n", lockTime.UTC()) + reachedAt := lockTime.Sub(time.Now().UTC()).Truncate(time.Second) + if reachedAt > 0 { + fmt.Printf("Locktime reached in %v\n", reachedAt) + } else { + fmt.Printf("Contract refund time lock has expired\n") + } + return nil +} + +func (cmd *deployContractCmd) runCommand(sct swapContractTransactor) error { + auth, err := sct.calcBaseOpts(nil) + if err != nil { + return fmt.Errorf("failed to create transact opts: %v", err) + } + addr, tx, _, err := contract.DeployContract(auth, sct.client.Client) + if err != nil { + return fmt.Errorf("failed to deploy contract: %v", err) + } + + // print info + fmt.Printf("Contract Address: %x\n", addr) + fmt.Printf("Deployment transaction (%x):\n", tx.Hash()) + txBytes, err := rlp.EncodeToBytes(tx) + if err != nil { + return fmt.Errorf("failed to encode deployment TX: %v", err) + } + fmt.Printf("%x\n\n", txBytes) + return nil +} + +func (cmd *validateDeployedContractCmd) runCommand(swapContractTransactor) error { + return cmd.runOfflineCommand() +} + +func (cmd *validateDeployedContractCmd) runOfflineCommand() error { + if bytes.Compare(cmd.deployTx.Data(), contractBin) != 0 { + return errors.New("deployed contract is invalid (make sure to use the same Solidity contract source code and Compiler version (0.4.24))") + } + fmt.Println("Contract is valid") + return nil +} + +// newSwapContractTransactor creates a new swapContract instance, +// see swapContractTransactor for more information +func newSwapContractTransactor(c *ethClient, contractAddr common.Address) (swapContractTransactor, error) { + parsed, err := abi.JSON(strings.NewReader(contract.ContractABI)) + if err != nil { + return swapContractTransactor{}, fmt.Errorf("failed to read (smart) contract ABI: %v", err) + } + signer, fromAddr, err := newSigner() + if err != nil { + return swapContractTransactor{}, fmt.Errorf("failed to create tx signer: %v", err) + } + return swapContractTransactor{ + abi: parsed, + signer: signer, + client: c, + fromAddr: fromAddr, + contractAddr: contractAddr, + }, nil +} + +// newSigner creates a signer func using the flag-passed +// private credentials of the sender +func newSigner() (bind.SignerFn, common.Address, error) { + json, err := ioutil.ReadFile(*keyFileFlag) + if err != nil { + return nil, common.Address{}, fmt.Errorf("failed to read key file (%s): %v", *keyFileFlag, err) + } + key, err := keystore.DecryptKey(json, *passphraseFlag) + if err != nil { + return nil, common.Address{}, fmt.Errorf("failed to decrypt (JSON) file (%s): %v", *keyFileFlag, err) + } + privKey := key.PrivateKey + keyAddr := crypto.PubkeyToAddress(privKey.PublicKey) + return func(signer types.Signer, address common.Address, tx *types.Transaction) (*types.Transaction, error) { + if address != keyAddr { + return nil, errors.New("not authorized to sign this account") + } + signature, err := crypto.Sign(signer.Hash(tx).Bytes(), privKey) + if err != nil { + return nil, err + } + return tx.WithSignature(signer, signature) + }, keyAddr, nil +} + +type ( + // swapContractTransactor allows the creation of transactions for the different + // atomic swap actions + swapContractTransactor struct { + abi abi.ABI + signer bind.SignerFn + client *ethClient + fromAddr common.Address + contractAddr common.Address + } + + // swapTransaction adds send functionality to the transaction, + // such that it can be send in an easy way + swapTransaction struct { + *types.Transaction + client *ethClient + ctx context.Context + } +) + +func (sct *swapContractTransactor) initiateTx(amount *big.Int, secretHash [sha256.Size]byte, participant common.Address) (*swapTransaction, error) { + return sct.newTransaction( + amount, "initiate", + // lock duration + big.NewInt(initiateLockPeriodInSeconds), + // secret hash + secretHash, + // participant + participant, + ) +} + +func (sct *swapContractTransactor) participateTx(amount *big.Int, secretHash [sha256.Size]byte, initiator common.Address) (*swapTransaction, error) { + return sct.newTransaction( + amount, "participate", + // lock duration + big.NewInt(participateLockPeriodInSeconds), + // secret hash + secretHash, + // participant + initiator, + ) +} + +func (sct *swapContractTransactor) redeemTx(secretHash, secret [sha256.Size]byte) (*swapTransaction, error) { + return sct.newTransaction( + nil, "redeem", + // secret, + secret, + // secret hash + secretHash, + ) +} + +func (sct *swapContractTransactor) refundTx(secretHash [sha256.Size]byte) (*swapTransaction, error) { + return sct.newTransaction( + nil, "refund", + // secret hash + secretHash, + ) +} + +func (sct *swapContractTransactor) maxGasCost() (*big.Int, error) { + ctx := newContext() + gasPrice, err := sct.client.SuggestGasPrice(ctx) + if err != nil { + return nil, fmt.Errorf("failed to suggest gas price: %v", err) + } + return gasPrice.Mul(gasPrice, big.NewInt(maxGasLimit)), nil +} + +func (sct *swapContractTransactor) newTransaction(amount *big.Int, name string, params ...interface{}) (*swapTransaction, error) { + // pack up the parameters and contractn ame + input, err := sct.abi.Pack(name, params...) + if err != nil { + return nil, fmt.Errorf("failed to pack input") + } + + // define the TransactOpts for binding + opts, err := sct.calcBaseOpts(amount) + if err != nil { + return nil, err + } + opts.GasLimit, err = sct.calcGasLimit(opts.Context, opts.Value, opts.GasPrice, input) + if err != nil { + return nil, err + } + + // create the raw transaction + rawTx := types.NewTransaction( + opts.Nonce.Uint64(), + sct.contractAddr, + opts.Value, + opts.GasLimit, + opts.GasPrice, + input, + ) + + // sign the transaction and return it + signedTx, err := opts.Signer(types.HomesteadSigner{}, opts.From, rawTx) + if err != nil { + return nil, fmt.Errorf("failed to sign transaction: %v", err) + } + return &swapTransaction{ + Transaction: signedTx, + client: sct.client, + ctx: opts.Context, + }, nil +} + +func (sct *swapContractTransactor) calcBaseOpts(amount *big.Int) (*bind.TransactOpts, error) { + ctx := newContext() + nonce, err := sct.client.PendingNonceAt(ctx, sct.fromAddr) + if err != nil { + return nil, fmt.Errorf( + "failed to retrieve account (%x) nonce: %v", + sct.fromAddr, err) + } + gasPrice, err := sct.client.SuggestGasPrice(ctx) + if err != nil { + return nil, fmt.Errorf("failed to suggest gas price: %v", err) + } + if amount == nil { + amount = new(big.Int) + } + return &bind.TransactOpts{ + From: sct.fromAddr, + Nonce: new(big.Int).SetUint64(nonce), + Signer: sct.signer, + Value: amount, + GasPrice: gasPrice, + Context: ctx, + }, nil +} + +func (sct *swapContractTransactor) calcGasLimit(ctx context.Context, amount, gasPrice *big.Int, input []byte) (uint64, error) { + if code, err := sct.client.PendingCodeAt(ctx, sct.contractAddr); err != nil { + return 0, fmt.Errorf("failed to estimate gas needed: %v", err) + } else if len(code) == 0 { + return 0, fmt.Errorf("failed to estimate gas needed: %v", bind.ErrNoCode) + } + // If the contract surely has code (or code is not needed), estimate the transaction + msg := ethereum.CallMsg{ + From: sct.fromAddr, + To: &sct.contractAddr, + Value: amount, + Data: input, + } + gasLimit, err := sct.client.EstimateGas(ctx, msg) + if err != nil { + return 0, fmt.Errorf("failed to estimate gas needed: %v", err) + } + if gasLimit > maxGasLimit { + return 0, fmt.Errorf("%d exceeds the hardcoded gas limit of %d", gasLimit, maxGasLimit) + } + return gasLimit, nil +} + +func (st *swapTransaction) Send() error { + err := st.client.SendTransaction(st.ctx, st.Transaction) + if err != nil { + return fmt.Errorf("failed to send transaction: %v", err) + } + return nil +} + +func dialClient() (*ethClient, error) { + c, err := rpc.DialContext(context.Background(), *connectFlag) + if err != nil { + return nil, err + } + return ðClient{ + Client: ethclient.NewClient(c), + rpcClient: c, + }, nil +} + +type ethClient struct { + *ethclient.Client + rpcClient *rpc.Client +} + +func newContext() context.Context { + if *timeoutFlag == 0 { + return context.Background() + } + ctx, _ := context.WithTimeout(context.Background(), *timeoutFlag) + return ctx +} + +var ( + contractBin = func() []byte { + b, err := hex.DecodeString(contract.ContractBin) + if err != nil { + panic("invalid binary contract: " + err.Error()) + } + return b + }() +) diff --git a/cmd/ethatomicswap/main_test.go b/cmd/ethatomicswap/main_test.go new file mode 100644 index 0000000..8dd92c5 --- /dev/null +++ b/cmd/ethatomicswap/main_test.go @@ -0,0 +1,59 @@ +package main + +import ( + "math/big" + "strings" + "testing" +) + +func TestParseEthAsWei(t *testing.T) { + bi := func(str string) *big.Int { + i, ok := new(big.Int).SetString(str, 10) + if !ok { + t.Fatal("failed to turn " + str + " into a big.Int") + } + return i + } + testCases := []struct { + Input string + ExpectedOutput *big.Int + }{ + {"-0", nil}, // nil isn't allowed + {"0", nil}, // nil isn't allowed + {"-123", nil}, // negative numbers aren't allowed + {"0.0000000000000000001", nil}, // too precise + {"1", big.NewInt(1000000000000000000)}, + {"1.1", big.NewInt(1100000000000000000)}, + {"1.123", big.NewInt(1123000000000000000)}, + {"0.001", big.NewInt(1000000000000000)}, + {"0.000000000000000001", big.NewInt(1)}, + {"123456789.987654321", bi("123456789987654321000000000")}, + {"0.00100", big.NewInt(1000000000000000)}, + {"0001", big.NewInt(1000000000000000000)}, + {"0001.100", big.NewInt(1100000000000000000)}, + } + for idx, testCase := range testCases { + x, err := parseEthAsWei(testCase.Input) + if testCase.ExpectedOutput == nil { + if err == nil { + t.Error(idx, "expected fail parsing, but it didn't") + } + continue + } + if err != nil { + t.Error(idx, "expected to parse, but it didn't", err) + continue + } + if testCase.ExpectedOutput.Cmp(x) != 0 { + t.Error(idx, testCase.ExpectedOutput.String(), "!=", x.String()) + } + str := formatWeiAsEthString(x) + strippedTestCase := strings.Trim(testCase.Input, "0") + if strippedTestCase[0] == '.' { + strippedTestCase = "0" + strippedTestCase + } + if str != strippedTestCase { + t.Error(idx, str, "!=", strippedTestCase) + } + } +} From ab14a4e851d0cc7d09e2756fea88ecb6b29d6316 Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker Date: Thu, 19 Jul 2018 15:03:24 +0200 Subject: [PATCH 07/12] ask permission before deploying eth contract --- cmd/ethatomicswap/main.go | 62 ++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/cmd/ethatomicswap/main.go b/cmd/ethatomicswap/main.go index 6c84e13..287fd44 100644 --- a/cmd/ethatomicswap/main.go +++ b/cmd/ethatomicswap/main.go @@ -797,23 +797,34 @@ func (cmd *auditContractCmd) runCommand(sct swapContractTransactor) error { } func (cmd *deployContractCmd) runCommand(sct swapContractTransactor) error { - auth, err := sct.calcBaseOpts(nil) + tx, err := sct.deployTx() if err != nil { - return fmt.Errorf("failed to create transact opts: %v", err) - } - addr, tx, _, err := contract.DeployContract(auth, sct.client.Client) - if err != nil { - return fmt.Errorf("failed to deploy contract: %v", err) + return fmt.Errorf("failed to create deploy TX: %v", err) } - // print info - fmt.Printf("Contract Address: %x\n", addr) - fmt.Printf("Deployment transaction (%x):\n", tx.Hash()) + deployTxCost := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) + fmt.Printf("Deploy fee: %s ETH\n\n", formatWeiAsEthString(deployTxCost)) + + fmt.Printf("Chain ID: %s\n", chainConfig.ChainID.String()) + fmt.Printf("Contract Address: %x\n", sct.contractAddr) + + fmt.Printf("Deploy transaction (%x):\n", tx.Hash()) txBytes, err := rlp.EncodeToBytes(tx) if err != nil { - return fmt.Errorf("failed to encode deployment TX: %v", err) + return fmt.Errorf("failed to encode deploy TX: %v", err) } fmt.Printf("%x\n\n", txBytes) + + publish, err := promptPublishTx("deploy") + if err != nil || !publish { + return err + } + + err = tx.Send() + if err != nil { + return err + } + fmt.Printf("Published deploy transaction (%x)\n", tx.Hash()) return nil } @@ -936,6 +947,10 @@ func (sct *swapContractTransactor) refundTx(secretHash [sha256.Size]byte) (*swap ) } +func (sct *swapContractTransactor) deployTx() (*swapTransaction, error) { + return sct.newTransactionWithInput(nil, false, common.FromHex(contract.ContractBin)) +} + func (sct *swapContractTransactor) maxGasCost() (*big.Int, error) { ctx := newContext() gasPrice, err := sct.client.SuggestGasPrice(ctx) @@ -946,18 +961,21 @@ func (sct *swapContractTransactor) maxGasCost() (*big.Int, error) { } func (sct *swapContractTransactor) newTransaction(amount *big.Int, name string, params ...interface{}) (*swapTransaction, error) { - // pack up the parameters and contractn ame + // pack up the parameters and contract name input, err := sct.abi.Pack(name, params...) if err != nil { return nil, fmt.Errorf("failed to pack input") } + return sct.newTransactionWithInput(amount, true, input) +} +func (sct *swapContractTransactor) newTransactionWithInput(amount *big.Int, contractCall bool, input []byte) (*swapTransaction, error) { // define the TransactOpts for binding opts, err := sct.calcBaseOpts(amount) if err != nil { return nil, err } - opts.GasLimit, err = sct.calcGasLimit(opts.Context, opts.Value, opts.GasPrice, input) + opts.GasLimit, err = sct.calcGasLimit(opts.Context, opts.Value, opts.GasPrice, contractCall, input) if err != nil { return nil, err } @@ -1009,25 +1027,29 @@ func (sct *swapContractTransactor) calcBaseOpts(amount *big.Int) (*bind.Transact }, nil } -func (sct *swapContractTransactor) calcGasLimit(ctx context.Context, amount, gasPrice *big.Int, input []byte) (uint64, error) { - if code, err := sct.client.PendingCodeAt(ctx, sct.contractAddr); err != nil { - return 0, fmt.Errorf("failed to estimate gas needed: %v", err) - } else if len(code) == 0 { - return 0, fmt.Errorf("failed to estimate gas needed: %v", bind.ErrNoCode) +func (sct *swapContractTransactor) calcGasLimit(ctx context.Context, amount, gasPrice *big.Int, contractCall bool, input []byte) (uint64, error) { + if contractCall { + if code, err := sct.client.PendingCodeAt(ctx, sct.contractAddr); err != nil { + return 0, fmt.Errorf("failed to estimate gas needed: %v", err) + } else if len(code) == 0 { + return 0, fmt.Errorf("failed to estimate gas needed: %v", bind.ErrNoCode) + } } // If the contract surely has code (or code is not needed), estimate the transaction msg := ethereum.CallMsg{ From: sct.fromAddr, - To: &sct.contractAddr, Value: amount, Data: input, } + if contractCall { + msg.To = &sct.contractAddr + } gasLimit, err := sct.client.EstimateGas(ctx, msg) if err != nil { return 0, fmt.Errorf("failed to estimate gas needed: %v", err) } - if gasLimit > maxGasLimit { - return 0, fmt.Errorf("%d exceeds the hardcoded gas limit of %d", gasLimit, maxGasLimit) + if contractCall && gasLimit > maxGasLimit { + return 0, fmt.Errorf("%d exceeds the hardcoded code-call gas limit of %d", gasLimit, maxGasLimit) } return gasLimit, nil } From e5b2d8a8ed4df10c3de45bddbce92143300623d1 Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker Date: Thu, 19 Jul 2018 18:49:39 +0200 Subject: [PATCH 08/12] improve ethatomiswap tool + clearer error messages (by pre-validating method requirements), it makes the commands slightly slower, but gives clearer errors + improved authentication flow: + (a) authenticate by signing from the CLI itself: give path to fileKey as a flag and enter securily the passphrase using the STDIN to decrypt that file + (b) or give a (by the daemon known) account address, as to use it to sign with that account from the daemon + (c) or give no account info at all, and use the first found account address instead (works best if only one account is known by the daemon) Note: (b) and (c) is not as secure, as it relies that your daemon has the accounts unlocked, making it possible for anyone that can access its RPC endpoints to use your unlocked accounts --- Gopkg.lock | 8 +- cmd/ethatomicswap/main.go | 326 +++++++++++++++++++++++++++++++------- 2 files changed, 274 insertions(+), 60 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 5a8695e..35b9b73 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -31,6 +31,12 @@ packages = ["monotime"] revision = "625ff285aa35926943df199ab15deba045716206" +[[projects]] + name = "github.com/bgentry/speakeasy" + packages = ["."] + revision = "4aabc24848ce5fd31929f7d1e4ea74d3709c14cd" + version = "v0.1.0" + [[projects]] branch = "master" name = "github.com/bitgoin/lyra2rev2" @@ -582,6 +588,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "c5a64aedde3cf8fcfe7ef9244e2f1be049ac77ce90901b34af3520616cb0ed83" + inputs-digest = "69b21e03f99e857dd0d1f4978f569bbf1c3d515d359bf9e48144d3dd14e41619" solver-name = "gps-cdcl" solver-version = 1 diff --git a/cmd/ethatomicswap/main.go b/cmd/ethatomicswap/main.go index 287fd44..4e98c2d 100644 --- a/cmd/ethatomicswap/main.go +++ b/cmd/ethatomicswap/main.go @@ -20,11 +20,13 @@ import ( "strings" "time" + "github.com/bgentry/speakeasy" ethereum "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" @@ -46,23 +48,15 @@ const ( maxGasLimit = 210000 ) -// TODO: find way to not require keyFile/passphrase -// (and certainly not in the way we currently do) - var ( - flagset = flag.NewFlagSet("", flag.ExitOnError) - connectFlag = flagset.String("s", "http://localhost:8545", "endpoint of Ethereum RPC server") - contractFlag = flagset.String("c", "", "hex-enoded address of the deployed contract") - keyFileFlag = flagset.String("keyfile", "", "file containing the key used for signing") - passphraseFlag = flagset.String("passphrase", "", "passphrase used for decrypting the key") - timeoutFlag = flagset.Duration("t", 0, "optional timeout of any call made") - testnetFlag = flagset.Bool("testnet", false, "use testnet (Rinkeby) network") + flagset = flag.NewFlagSet("", flag.ExitOnError) + connectFlag = flagset.String("s", "http://localhost:8545", "endpoint of Ethereum RPC server") + contractFlag = flagset.String("c", "", "hex-enoded address of the deployed contract") + accountFlag = flagset.String("account", "", "account file, account address or nothing for the daemon's first account") + timeoutFlag = flagset.Duration("t", 0, "optional timeout of any call made") + testnetFlag = flagset.Bool("testnet", false, "use testnet (Rinkeby) network") ) -// TODO: better error reporting: -// now all contract-originated errors return -// "failed to estimate gas needed: gas required exceeds allowance or always failing transaction" - // There are two directions that the atomic swap can be performed, as the // initiator can be on either chain. This tool only deals with creating the // Bitcoin transactions for these swaps. A second tool should be used for the @@ -463,14 +457,6 @@ func generateSecretHashPair() (secret, secretHash [sha256.Size]byte) { return } -func newTransactOpts() (*bind.TransactOpts, error) { - f, err := os.Open(*keyFileFlag) - if err != nil { - return nil, fmt.Errorf("failed to open key file: %v", err) - } - return bind.NewTransactor(f, *passphraseFlag) -} - func promptPublishTx(name string) (bool, error) { reader := bufio.NewReader(os.Stdin) for { @@ -541,6 +527,10 @@ func (cmd *initiateCmd) runCommand(sct swapContractTransactor) error { fmt.Printf("Secret: %x\n", secret) fmt.Printf("Secret hash: %x\n\n", secretHash) + if sct.autoAccount { + fmt.Printf("Author's refund address: %x\n\n", sct.fromAddr) + } + initiateTxCost := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) fmt.Printf("Contract fee: %s ETH\n", formatWeiAsEthString(initiateTxCost)) refundTxCost, err := sct.maxGasCost() @@ -581,6 +571,10 @@ func (cmd *participateCmd) runCommand(sct swapContractTransactor) error { fmt.Printf("Amount: %s Wei (%s ETH)\n\n", cmd.amount.String(), formatWeiAsEthString(cmd.amount)) + if sct.autoAccount { + fmt.Printf("Author's refund address: %x\n\n", sct.fromAddr) + } + participateTxCost := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) fmt.Printf("Contract fee: %s ETH\n", formatWeiAsEthString(participateTxCost)) refundTxCost, err := sct.maxGasCost() @@ -617,12 +611,6 @@ func (cmd *redeemCmd) runCommand(sct swapContractTransactor) error { if err != nil { return err } - expectedSecretHash := sha256Hash(cmd.secret[:]) - if expectedSecretHash != params.SecretHash { - return fmt.Errorf( - "contract transaction contains unexpected secret hash (%x)", - params.SecretHash) - } tx, err := sct.redeemTx(params.SecretHash, cmd.secret) if err != nil { return fmt.Errorf("failed to create redeem TX: %v", err) @@ -847,29 +835,64 @@ func newSwapContractTransactor(c *ethClient, contractAddr common.Address) (swapC if err != nil { return swapContractTransactor{}, fmt.Errorf("failed to read (smart) contract ABI: %v", err) } - signer, fromAddr, err := newSigner() - if err != nil { - return swapContractTransactor{}, fmt.Errorf("failed to create tx signer: %v", err) - } - return swapContractTransactor{ - abi: parsed, - signer: signer, - client: c, - fromAddr: fromAddr, - contractAddr: contractAddr, - }, nil + switch account := *accountFlag; { + case account == "": + var accounts []common.Address + err := c.rpcClient.CallContext(newContext(), &accounts, "eth_accounts") + if err != nil { + return swapContractTransactor{}, fmt.Errorf("failed to list unlocked accounts: %v", err) + } + if len(accounts) == 0 { + return swapContractTransactor{}, errors.New("no unlocked accounts were found") + } + // sign using daemon with a random account + return swapContractTransactor{ + abi: parsed, + client: c, + contractAddr: contractAddr, + fromAddr: accounts[0], + autoAccount: true, + }, nil + + case common.IsHexAddress(account): + // sign using daemon + return swapContractTransactor{ + abi: parsed, + client: c, + contractAddr: contractAddr, + fromAddr: common.HexToAddress(account), + }, nil + + default: + // sign using given key + signer, fromAddr, err := newSigner(account) + if err != nil { + return swapContractTransactor{}, fmt.Errorf("failed to create tx signer: %v", err) + } + return swapContractTransactor{ + abi: parsed, + signer: signer, + client: c, + fromAddr: fromAddr, + contractAddr: contractAddr, + }, nil + } } // newSigner creates a signer func using the flag-passed // private credentials of the sender -func newSigner() (bind.SignerFn, common.Address, error) { - json, err := ioutil.ReadFile(*keyFileFlag) +func newSigner(path string) (bind.SignerFn, common.Address, error) { + json, err := ioutil.ReadFile(path) + if err != nil { + return nil, common.Address{}, fmt.Errorf("failed to read encrypted account/key file (%s) content: %v", path, err) + } + passphrase, err := speakeasy.Ask("Account passphrase: ") if err != nil { - return nil, common.Address{}, fmt.Errorf("failed to read key file (%s): %v", *keyFileFlag, err) + return nil, common.Address{}, fmt.Errorf("failed to get passphrase from STDIN: %v", err) } - key, err := keystore.DecryptKey(json, *passphraseFlag) + key, err := keystore.DecryptKey(json, passphrase) if err != nil { - return nil, common.Address{}, fmt.Errorf("failed to decrypt (JSON) file (%s): %v", *keyFileFlag, err) + return nil, common.Address{}, fmt.Errorf("failed to decrypt (JSON) account/key file (%s): %v", path, err) } privKey := key.PrivateKey keyAddr := crypto.PubkeyToAddress(privKey.PublicKey) @@ -894,6 +917,9 @@ type ( client *ethClient fromAddr common.Address contractAddr common.Address + autoAccount bool // defines if an account is automatically selected + + _contract *contract.Contract // created only once } // swapTransaction adds send functionality to the transaction, @@ -906,6 +932,17 @@ type ( ) func (sct *swapContractTransactor) initiateTx(amount *big.Int, secretHash [sha256.Size]byte, participant common.Address) (*swapTransaction, error) { + // validate tx does not exist yet, + // as to provide more meaningful error messages + switch _, err := sct.getSwapContract(secretHash); err { + case errNotExists: + // this is what we want + case nil: + return nil, errors.New("secret hash is already used for another atomic swap contract") + default: + return nil, fmt.Errorf("unexpected error while checking for an existing contract: %v", err) + } + // create initiate tx return sct.newTransaction( amount, "initiate", // lock duration @@ -918,6 +955,16 @@ func (sct *swapContractTransactor) initiateTx(amount *big.Int, secretHash [sha25 } func (sct *swapContractTransactor) participateTx(amount *big.Int, secretHash [sha256.Size]byte, initiator common.Address) (*swapTransaction, error) { + // validate tx does not exist yet, + // as to provide more meaningful error messages + switch _, err := sct.getSwapContract(secretHash); err { + case errNotExists: + // this is what we want + case nil: + return nil, errors.New("secret hash is already used for another atomic swap contract") + default: + return nil, fmt.Errorf("unexpected error while checking for an existing contract: %v", err) + } return sct.newTransaction( amount, "participate", // lock duration @@ -930,6 +977,34 @@ func (sct *swapContractTransactor) participateTx(amount *big.Int, secretHash [sh } func (sct *swapContractTransactor) redeemTx(secretHash, secret [sha256.Size]byte) (*swapTransaction, error) { + // validate swap contract, + // as to provide more meaningful errors + sc, err := sct.getSwapContract(secretHash) + if err != nil { + return nil, err + } + if sc.SecretHash != secretHash { + return nil, errors.New("invalid secret hash registered") + } + if userSecretHash := sha256Hash(secret[:]); sc.SecretHash != userSecretHash { + return nil, errors.New("secret does not match secret hash") + } + switch sc.Kind { + case swapKindInitiator: + if sc.Participant != sct.fromAddr { + return nil, fmt.Errorf("only the participant can redeem: unexpected address: %x", sct.fromAddr) + } + case swapKindParticipant: + if sc.Initiator != sct.fromAddr { + return nil, fmt.Errorf("only the initiator can redeem: unexpected address: %x", sct.fromAddr) + } + default: + return nil, fmt.Errorf("invalid atomic swap contract kind: %d", sc.Kind) + } + if sc.State != swapStateFilled { + return nil, errors.New("inactive atomic swap contract") + } + // create redeem tx return sct.newTransaction( nil, "redeem", // secret, @@ -940,6 +1015,35 @@ func (sct *swapContractTransactor) redeemTx(secretHash, secret [sha256.Size]byte } func (sct *swapContractTransactor) refundTx(secretHash [sha256.Size]byte) (*swapTransaction, error) { + // validate swap contract, + // as to provide more meaningful errors + sc, err := sct.getSwapContract(secretHash) + if err != nil { + return nil, err + } + if sc.SecretHash != secretHash { + return nil, errors.New("invalid secret hash registered") + } + switch sc.Kind { + case swapKindInitiator: + if sc.Initiator != sct.fromAddr { + return nil, fmt.Errorf("only the participant can refund: unexpected address: %x", sct.fromAddr) + } + case swapKindParticipant: + if sc.Participant != sct.fromAddr { + return nil, fmt.Errorf("only the initiator can refund: unexpected address: %x", sct.fromAddr) + } + default: + return nil, fmt.Errorf("invalid atomic swap contract kind: %d", sc.Kind) + } + if sc.State != swapStateFilled { + return nil, errors.New("inactive atomic swap contract") + } + lockTime := time.Unix(bigIntPtrToUint64(sc.InitTimestamp)+bigIntPtrToUint64(sc.RefundTime), 0) + if dur := lockTime.Sub(time.Now()); dur >= 0 { + return nil, fmt.Errorf("contract is still locked for %v", dur+time.Second) // add 1 as to deal with the `0` second case + } + // create refund tx return sct.newTransaction( nil, "refund", // secret hash @@ -947,6 +1051,13 @@ func (sct *swapContractTransactor) refundTx(secretHash [sha256.Size]byte) (*swap ) } +func bigIntPtrToUint64(i *big.Int) int64 { + if i == nil { + return 0 + } + return i.Int64() +} + func (sct *swapContractTransactor) deployTx() (*swapTransaction, error) { return sct.newTransactionWithInput(nil, false, common.FromHex(contract.ContractBin)) } @@ -960,6 +1071,54 @@ func (sct *swapContractTransactor) maxGasCost() (*big.Int, error) { return gasPrice.Mul(gasPrice, big.NewInt(maxGasLimit)), nil } +const ( + swapStateEmpty uint8 = iota + swapStateFilled + swapStateRedeemed + swapStateRefunded +) + +const ( + swapKindInitiator uint8 = iota + swapKindParticipant +) + +var ( + errNotExists = errors.New("atomic swap contract does not exist") +) + +func (sct *swapContractTransactor) getSwapContract(secretHash [32]byte) (*struct { + InitTimestamp *big.Int + RefundTime *big.Int + SecretHash [32]byte + Secret [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + Kind uint8 + State uint8 +}, error) { + if sct._contract == nil { + var err error + sct._contract, err = contract.NewContract(sct.contractAddr, sct.client.Client) + if err != nil { + return nil, fmt.Errorf("failed to bind smart contract (at %x): %v", sct.contractAddr, err) + } + } + sc, err := sct._contract.Swaps(&bind.CallOpts{ + Pending: false, + From: sct.fromAddr, + Context: newContext(), + }, secretHash) + if err != nil { + return nil, fmt.Errorf("failed to get swap contract from smart contract (at %x): %v", err) + } + if sc.State == swapStateEmpty { + return nil, errNotExists + } + return &sc, nil +} + func (sct *swapContractTransactor) newTransaction(amount *big.Int, name string, params ...interface{}) (*swapTransaction, error) { // pack up the parameters and contract name input, err := sct.abi.Pack(name, params...) @@ -975,25 +1134,74 @@ func (sct *swapContractTransactor) newTransactionWithInput(amount *big.Int, cont if err != nil { return nil, err } - opts.GasLimit, err = sct.calcGasLimit(opts.Context, opts.Value, opts.GasPrice, contractCall, input) + opts.GasLimit, err = sct.calcGasLimit(newContext(), opts.Value, opts.GasPrice, contractCall, input) if err != nil { return nil, err } - // create the raw transaction - rawTx := types.NewTransaction( - opts.Nonce.Uint64(), - sct.contractAddr, - opts.Value, - opts.GasLimit, - opts.GasPrice, - input, - ) - - // sign the transaction and return it - signedTx, err := opts.Signer(types.HomesteadSigner{}, opts.From, rawTx) - if err != nil { - return nil, fmt.Errorf("failed to sign transaction: %v", err) + // sign using daemon or do it client-side if desired + var signedTx *types.Transaction + if opts.Signer == nil { + var toAddr *common.Address + if contractCall { + toAddr = &sct.contractAddr + } + // sign transaction using the daemon + var result struct { + Raw string `json:"raw"` + Tx types.Transaction `json:"tx"` + } + err = sct.client.rpcClient.CallContext(newContext(), &result, "eth_signTransaction", struct { + From common.Address `json:"from"` + To *common.Address `json:"to"` + Gas hexutil.Uint64 `json:"gas"` + GasPrice hexutil.Big `json:"gasPrice"` + Value hexutil.Big `json:"value"` + Nonce hexutil.Uint64 `json:"nonce"` + Data hexutil.Bytes `json:"data"` + }{ + From: opts.From, + To: toAddr, + Gas: hexutil.Uint64(opts.GasLimit), + GasPrice: hexutil.Big(*opts.GasPrice), + Value: func() hexutil.Big { + if amount == nil { + return hexutil.Big{} + } + return hexutil.Big(*amount) + }(), + Nonce: hexutil.Uint64(opts.Nonce.Uint64()), + Data: hexutil.Bytes(input), + }) + if err != nil { + return nil, fmt.Errorf("failed to sign transaction from daemon: %v", err) + } + signedTx = &result.Tx + } else { + var rawTx *types.Transaction + if contractCall { + rawTx = types.NewTransaction( + opts.Nonce.Uint64(), + sct.contractAddr, + opts.Value, + opts.GasLimit, + opts.GasPrice, + input, + ) + } else { + rawTx = types.NewContractCreation( + opts.Nonce.Uint64(), + opts.Value, + opts.GasLimit, + opts.GasPrice, + input, + ) + } + // sign ourselves + signedTx, err = opts.Signer(types.HomesteadSigner{}, opts.From, rawTx) + if err != nil { + return nil, fmt.Errorf("failed to sign transaction from client: %v", err) + } } return &swapTransaction{ Transaction: signedTx, From d2be32402e4d7cbbb82b0ac4f303b1fdf48b6641 Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker Date: Fri, 20 Jul 2018 10:40:50 +0200 Subject: [PATCH 09/12] add ethereum to the list of supported chains --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b598d16..106fcb9 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ exists for the following coins and wallets: * Vertcoin ([Vertcoin Core](https://github.com/vertcoin/vertcoin)) * Viacoin ([Viacoin Core](https://github.com/viacoin/viacoin)) * Zcoin ([Zcoin Core](https://github.com/zcoinofficial/zcoin)) +* Ethereum ([Go Ethereum](https://github.com/ethereum/go-ethereum)) Pull requests implementing support for additional cryptocurrencies and wallets are encouraged. See [GitHub project From aa4bd41cd99ad2fc0d6cf0fd382d39470e098961 Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker Date: Fri, 20 Jul 2018 10:55:28 +0200 Subject: [PATCH 10/12] fix go linter reported errors and added some more code comments as well --- cmd/ethatomicswap/main.go | 99 +++++++++++++++++++++++++++++++-------- 1 file changed, 79 insertions(+), 20 deletions(-) diff --git a/cmd/ethatomicswap/main.go b/cmd/ethatomicswap/main.go index 4e98c2d..a94d1cd 100644 --- a/cmd/ethatomicswap/main.go +++ b/cmd/ethatomicswap/main.go @@ -21,6 +21,7 @@ import ( "time" "github.com/bgentry/speakeasy" + ethereum "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -741,8 +742,10 @@ func (cmd *auditContractCmd) runCommand(sct swapContractTransactor) error { // get transaction by hash contractHash := cmd.contractTx.Hash() - err = sct.client.rpcClient.CallContext(newContext(), + ctx := newContext() + err = sct.client.rpcClient.CallContext(ctx, &rpcTransaction, "eth_getTransactionByHash", contractHash) + ctx.Cancel() if err != nil { return fmt.Errorf( "failed to find transaction (%x): %v", contractHash, err) @@ -752,7 +755,9 @@ func (cmd *auditContractCmd) runCommand(sct swapContractTransactor) error { } // get block in order to know the timestamp of the txn - block, err := sct.client.BlockByHash(newContext(), *rpcTransaction.BlockHash) + ctx = newContext() + block, err := sct.client.BlockByHash(ctx, *rpcTransaction.BlockHash) + ctx.Cancel() if err != nil { return fmt.Errorf( "failed to find block (%x): %v", rpcTransaction.BlockHash, err) @@ -821,7 +826,7 @@ func (cmd *validateDeployedContractCmd) runCommand(swapContractTransactor) error } func (cmd *validateDeployedContractCmd) runOfflineCommand() error { - if bytes.Compare(cmd.deployTx.Data(), contractBin) != 0 { + if !bytes.Equal(cmd.deployTx.Data(), contractBin) { return errors.New("deployed contract is invalid (make sure to use the same Solidity contract source code and Compiler version (0.4.24))") } fmt.Println("Contract is valid") @@ -838,7 +843,9 @@ func newSwapContractTransactor(c *ethClient, contractAddr common.Address) (swapC switch account := *accountFlag; { case account == "": var accounts []common.Address - err := c.rpcClient.CallContext(newContext(), &accounts, "eth_accounts") + ctx := newContext() + err := c.rpcClient.CallContext(ctx, &accounts, "eth_accounts") + ctx.Cancel() if err != nil { return swapContractTransactor{}, fmt.Errorf("failed to list unlocked accounts: %v", err) } @@ -927,7 +934,6 @@ type ( swapTransaction struct { *types.Transaction client *ethClient - ctx context.Context } ) @@ -1040,8 +1046,8 @@ func (sct *swapContractTransactor) refundTx(secretHash [sha256.Size]byte) (*swap return nil, errors.New("inactive atomic swap contract") } lockTime := time.Unix(bigIntPtrToUint64(sc.InitTimestamp)+bigIntPtrToUint64(sc.RefundTime), 0) - if dur := lockTime.Sub(time.Now()); dur >= 0 { - return nil, fmt.Errorf("contract is still locked for %v", dur+time.Second) // add 1 as to deal with the `0` second case + if dur := time.Until(lockTime).Truncate(time.Second); dur >= 0 { + return nil, fmt.Errorf("contract is still locked for %v", dur+time.Second) } // create refund tx return sct.newTransaction( @@ -1065,12 +1071,18 @@ func (sct *swapContractTransactor) deployTx() (*swapTransaction, error) { func (sct *swapContractTransactor) maxGasCost() (*big.Int, error) { ctx := newContext() gasPrice, err := sct.client.SuggestGasPrice(ctx) + ctx.Cancel() if err != nil { return nil, fmt.Errorf("failed to suggest gas price: %v", err) } return gasPrice.Mul(gasPrice, big.NewInt(maxGasLimit)), nil } +// states have to be mapped 1-to-1 with Enum AtomicSwap.State, +// as found in ./contract/src/contracts/AtomicSwap.sol +// +// This isn't part of the Ethereum-generated Go code found in the child "contract" pkg, +// given that the ABI does not export Enums. const ( swapStateEmpty uint8 = iota swapStateFilled @@ -1078,15 +1090,25 @@ const ( swapStateRefunded ) +// kinds have to be mapped 1-to-1 with Enum AtomicSwap.Kind, +// as found in ./contract/src/contracts/AtomicSwap.sol +// +// This isn't part of the Ethereum-generated Go code found in the child "contract" pkg, +// given that the ABI does not export Enums. const ( swapKindInitiator uint8 = iota swapKindParticipant ) var ( + // error reported when an atomic swap contract (identified by a secret hash), + // has the state Empty, indicating it doesn't exist yet. errNotExists = errors.New("atomic swap contract does not exist") ) +// getSwapContract is a free contract call, +// which allows us to retrieve an atomic swap contract from a deployed AtomicSwap smart contract, +// using the secret hash used in that atomic swap contract as this contract's identifier. func (sct *swapContractTransactor) getSwapContract(secretHash [32]byte) (*struct { InitTimestamp *big.Int RefundTime *big.Int @@ -1105,13 +1127,15 @@ func (sct *swapContractTransactor) getSwapContract(secretHash [32]byte) (*struct return nil, fmt.Errorf("failed to bind smart contract (at %x): %v", sct.contractAddr, err) } } + ctx := newContext() sc, err := sct._contract.Swaps(&bind.CallOpts{ Pending: false, From: sct.fromAddr, - Context: newContext(), + Context: ctx, }, secretHash) + ctx.Cancel() if err != nil { - return nil, fmt.Errorf("failed to get swap contract from smart contract (at %x): %v", err) + return nil, fmt.Errorf("failed to get swap contract from smart contract (at %x): %v", sct.contractAddr, err) } if sc.State == swapStateEmpty { return nil, errNotExists @@ -1134,7 +1158,7 @@ func (sct *swapContractTransactor) newTransactionWithInput(amount *big.Int, cont if err != nil { return nil, err } - opts.GasLimit, err = sct.calcGasLimit(newContext(), opts.Value, opts.GasPrice, contractCall, input) + opts.GasLimit, err = sct.calcGasLimit(opts.Value, opts.GasPrice, contractCall, input) if err != nil { return nil, err } @@ -1151,7 +1175,8 @@ func (sct *swapContractTransactor) newTransactionWithInput(amount *big.Int, cont Raw string `json:"raw"` Tx types.Transaction `json:"tx"` } - err = sct.client.rpcClient.CallContext(newContext(), &result, "eth_signTransaction", struct { + ctx := newContext() + err = sct.client.rpcClient.CallContext(ctx, &result, "eth_signTransaction", struct { From common.Address `json:"from"` To *common.Address `json:"to"` Gas hexutil.Uint64 `json:"gas"` @@ -1173,6 +1198,7 @@ func (sct *swapContractTransactor) newTransactionWithInput(amount *big.Int, cont Nonce: hexutil.Uint64(opts.Nonce.Uint64()), Data: hexutil.Bytes(input), }) + ctx.Cancel() if err != nil { return nil, fmt.Errorf("failed to sign transaction from daemon: %v", err) } @@ -1206,19 +1232,21 @@ func (sct *swapContractTransactor) newTransactionWithInput(amount *big.Int, cont return &swapTransaction{ Transaction: signedTx, client: sct.client, - ctx: opts.Context, }, nil } func (sct *swapContractTransactor) calcBaseOpts(amount *big.Int) (*bind.TransactOpts, error) { ctx := newContext() nonce, err := sct.client.PendingNonceAt(ctx, sct.fromAddr) + ctx.Cancel() if err != nil { return nil, fmt.Errorf( "failed to retrieve account (%x) nonce: %v", sct.fromAddr, err) } + ctx = newContext() gasPrice, err := sct.client.SuggestGasPrice(ctx) + ctx.Cancel() if err != nil { return nil, fmt.Errorf("failed to suggest gas price: %v", err) } @@ -1231,13 +1259,15 @@ func (sct *swapContractTransactor) calcBaseOpts(amount *big.Int) (*bind.Transact Signer: sct.signer, Value: amount, GasPrice: gasPrice, - Context: ctx, }, nil } -func (sct *swapContractTransactor) calcGasLimit(ctx context.Context, amount, gasPrice *big.Int, contractCall bool, input []byte) (uint64, error) { +func (sct *swapContractTransactor) calcGasLimit(amount, gasPrice *big.Int, contractCall bool, input []byte) (uint64, error) { if contractCall { - if code, err := sct.client.PendingCodeAt(ctx, sct.contractAddr); err != nil { + ctx := newContext() + code, err := sct.client.PendingCodeAt(ctx, sct.contractAddr) + ctx.Cancel() + if err != nil { return 0, fmt.Errorf("failed to estimate gas needed: %v", err) } else if len(code) == 0 { return 0, fmt.Errorf("failed to estimate gas needed: %v", bind.ErrNoCode) @@ -1252,7 +1282,9 @@ func (sct *swapContractTransactor) calcGasLimit(ctx context.Context, amount, gas if contractCall { msg.To = &sct.contractAddr } + ctx := newContext() gasLimit, err := sct.client.EstimateGas(ctx, msg) + ctx.Cancel() if err != nil { return 0, fmt.Errorf("failed to estimate gas needed: %v", err) } @@ -1263,7 +1295,9 @@ func (sct *swapContractTransactor) calcGasLimit(ctx context.Context, amount, gas } func (st *swapTransaction) Send() error { - err := st.client.SendTransaction(st.ctx, st.Transaction) + ctx := newContext() + err := st.client.SendTransaction(ctx, st.Transaction) + ctx.Cancel() if err != nil { return fmt.Errorf("failed to send transaction: %v", err) } @@ -1286,15 +1320,40 @@ type ethClient struct { rpcClient *rpc.Client } -func newContext() context.Context { +// newContext creates a context which HAS +// to be manually cancelled, as to not leak any resources +func newContext() *cancelableContext { if *timeoutFlag == 0 { - return context.Background() + ctx, cancelFn := context.WithCancel(context.Background()) + return &cancelableContext{ + Context: ctx, + cancelFn: cancelFn, + } + } + ctx, cancelFn := context.WithTimeout(context.Background(), *timeoutFlag) + return &cancelableContext{ + Context: ctx, + cancelFn: cancelFn, } - ctx, _ := context.WithTimeout(context.Background(), *timeoutFlag) - return ctx +} + +type cancelableContext struct { + context.Context + cancelFn context.CancelFunc +} + +func (cc *cancelableContext) Cancel() { + cc.cancelFn() } var ( + // decode the byte code of the smart contract used + // during the initialisation phase of this CLI tool, + // as to ensure the hex-encoded string is valid at all times. + // + // This prevents of having a hidden error, + // due to the fact that it is only ever used in + // our extra smart-contract-related commands. contractBin = func() []byte { b, err := hex.DecodeString(contract.ContractBin) if err != nil { From fd6cfb25b0eb534e9ed3a45b35390b5a15a498e8 Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker Date: Fri, 20 Jul 2018 11:37:46 +0200 Subject: [PATCH 11/12] fix truffle.js as to ensure truffle test works again --- cmd/ethatomicswap/contract/src/truffle.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/cmd/ethatomicswap/contract/src/truffle.js b/cmd/ethatomicswap/contract/src/truffle.js index 53915a8..0855df1 100644 --- a/cmd/ethatomicswap/contract/src/truffle.js +++ b/cmd/ethatomicswap/contract/src/truffle.js @@ -15,16 +15,4 @@ module.exports = { // See // to customize your Truffle configuration! - networks: { - development: { - host: "127.0.0.1", - port: 7545, - network_id: "*" // Match any network id - }, - development: { - host: "127.0.0.1", - port: 7545, - network_id: "*" // Match any network id - } - } }; From 2f6679b9150b2ef364cd7bc75a49c21a7d124a91 Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker Date: Thu, 6 Sep 2018 09:57:59 +0200 Subject: [PATCH 12/12] fix order of utility list and copyright headers --- README.md | 2 +- cmd/ethatomicswap/contract/generate.go | 4 ++++ cmd/ethatomicswap/contract/src/contracts/AtomicSwap.sol | 7 ++++--- .../contract/src/migrations/2_atomicswap_migration.js | 4 ++++ cmd/ethatomicswap/contract/src/test/TestAtomicSwap.js | 6 +++--- cmd/ethatomicswap/contract/src/test/exceptions.js | 4 ++++ cmd/ethatomicswap/contract/src/test/utils.js | 4 ++++ cmd/ethatomicswap/main.go | 6 +++++- cmd/ethatomicswap/main_test.go | 3 +++ 9 files changed, 32 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 106fcb9..ba9a98c 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ exists for the following coins and wallets: * Bitcoin ([Bitcoin Core](https://github.com/bitcoin/bitcoin)) * Bitcoin Cash ([Bitcoin ABC](https://github.com/Bitcoin-ABC/bitcoin-abc), [Bitcoin Unlimited](https://github.com/BitcoinUnlimited/BitcoinUnlimited), [Bitcoin XT](https://github.com/bitcoinxt/bitcoinxt)) * Decred ([dcrwallet](https://github.com/decred/dcrwallet)) +* Ethereum ([Go Ethereum](https://github.com/ethereum/go-ethereum)) * Litecoin ([Litecoin Core](https://github.com/litecoin-project/litecoin)) * Monacoin ([Monacoin Core](https://github.com/monacoinproject/monacoin)) * Particl ([Particl Core](https://github.com/particl/particl-core)) @@ -19,7 +20,6 @@ exists for the following coins and wallets: * Vertcoin ([Vertcoin Core](https://github.com/vertcoin/vertcoin)) * Viacoin ([Viacoin Core](https://github.com/viacoin/viacoin)) * Zcoin ([Zcoin Core](https://github.com/zcoinofficial/zcoin)) -* Ethereum ([Go Ethereum](https://github.com/ethereum/go-ethereum)) Pull requests implementing support for additional cryptocurrencies and wallets are encouraged. See [GitHub project diff --git a/cmd/ethatomicswap/contract/generate.go b/cmd/ethatomicswap/contract/generate.go index 5bfc6b6..06d3af4 100644 --- a/cmd/ethatomicswap/contract/generate.go +++ b/cmd/ethatomicswap/contract/generate.go @@ -1,3 +1,7 @@ +// Copyright (c) 2018 BetterToken BVBA +// Use of this source code is governed by an MIT +// license that can be found at https://github.com/rivine/rivine/blob/master/LICENSE. + package contract // prerequisite: install ethereum devtools diff --git a/cmd/ethatomicswap/contract/src/contracts/AtomicSwap.sol b/cmd/ethatomicswap/contract/src/contracts/AtomicSwap.sol index 490390e..a90a181 100644 --- a/cmd/ethatomicswap/contract/src/contracts/AtomicSwap.sol +++ b/cmd/ethatomicswap/contract/src/contracts/AtomicSwap.sol @@ -1,7 +1,8 @@ // Copyright (c) 2017 Altcoin Exchange, Inc -// Copyright (c) 2018 The Decred developers and Contributors -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. + +// Copyright (c) 2018 BetterToken BVBA +// Use of this source code is governed by an MIT +// license that can be found at https://github.com/rivine/rivine/blob/master/LICENSE. pragma solidity ^0.4.23; diff --git a/cmd/ethatomicswap/contract/src/migrations/2_atomicswap_migration.js b/cmd/ethatomicswap/contract/src/migrations/2_atomicswap_migration.js index d3a6b43..9854c67 100644 --- a/cmd/ethatomicswap/contract/src/migrations/2_atomicswap_migration.js +++ b/cmd/ethatomicswap/contract/src/migrations/2_atomicswap_migration.js @@ -1,3 +1,7 @@ +// Copyright (c) 2018 BetterToken BVBA +// Use of this source code is governed by an MIT +// license that can be found at https://github.com/rivine/rivine/blob/master/LICENSE. + var AtomicSwap = artifacts.require("AtomicSwap"); module.exports = function(deployer) { diff --git a/cmd/ethatomicswap/contract/src/test/TestAtomicSwap.js b/cmd/ethatomicswap/contract/src/test/TestAtomicSwap.js index 5722d61..d7fcea2 100644 --- a/cmd/ethatomicswap/contract/src/test/TestAtomicSwap.js +++ b/cmd/ethatomicswap/contract/src/test/TestAtomicSwap.js @@ -1,6 +1,6 @@ -// Copyright (c) 2018 The Decred developers and Contributors -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. +// Copyright (c) 2018 BetterToken BVBA +// Use of this source code is governed by an MIT +// license that can be found at https://github.com/rivine/rivine/blob/master/LICENSE. const AtomicSwap = artifacts.require("AtomicSwap"); diff --git a/cmd/ethatomicswap/contract/src/test/exceptions.js b/cmd/ethatomicswap/contract/src/test/exceptions.js index d6eb8ba..3e1c881 100644 --- a/cmd/ethatomicswap/contract/src/test/exceptions.js +++ b/cmd/ethatomicswap/contract/src/test/exceptions.js @@ -1,3 +1,7 @@ +// Copyright (c) 2018 BetterToken BVBA +// Use of this source code is governed by an MIT +// license that can be found at https://github.com/rivine/rivine/blob/master/LICENSE. + module.exports.errTypes = { revert : "revert", outOfGas : "out of gas", diff --git a/cmd/ethatomicswap/contract/src/test/utils.js b/cmd/ethatomicswap/contract/src/test/utils.js index 34f01fc..d89a39e 100644 --- a/cmd/ethatomicswap/contract/src/test/utils.js +++ b/cmd/ethatomicswap/contract/src/test/utils.js @@ -1,3 +1,7 @@ +// Copyright (c) 2018 BetterToken BVBA +// Use of this source code is governed by an MIT +// license that can be found at https://github.com/rivine/rivine/blob/master/LICENSE. + module.exports.sleep = async function(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }; diff --git a/cmd/ethatomicswap/main.go b/cmd/ethatomicswap/main.go index a94d1cd..b90cb3f 100644 --- a/cmd/ethatomicswap/main.go +++ b/cmd/ethatomicswap/main.go @@ -1,7 +1,11 @@ -// Copyright (c) 2018 The Decred developers and Contributors +// Copyright (c) 2017 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. +// Copyright (c) 2018 BetterToken BVBA +// Use of this source code is governed by an MIT +// license that can be found at https://github.com/rivine/rivine/blob/master/LICENSE. + package main import ( diff --git a/cmd/ethatomicswap/main_test.go b/cmd/ethatomicswap/main_test.go index 8dd92c5..2036e9d 100644 --- a/cmd/ethatomicswap/main_test.go +++ b/cmd/ethatomicswap/main_test.go @@ -1,3 +1,6 @@ +// Copyright (c) 2018 BetterToken BVBA +// Use of this source code is governed by an MIT +// license that can be found at https://github.com/rivine/rivine/blob/master/LICENSE. package main import (