Skip to content
This repository has been archived by the owner on Oct 31, 2023. It is now read-only.

Commit

Permalink
Merge pull request #1 from linki/version2
Browse files Browse the repository at this point in the history
Use keystore file instead of flags
  • Loading branch information
linki authored Nov 25, 2017
2 parents fc60ee1 + b54dc6e commit e30d1eb
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 35 deletions.
53 changes: 40 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# etherdelta-go

This is an example of how to interact with Ethereum contracts from Go through RPC.
This is an example of how to interact with an Ethereum contract from Go through RPC.

The example shows three things:
* how to generate Go language bindings given a contract's ABI definition,
* how to read data from the blockchain without spending gas, and
* how to change state on the blockchain by providing the correct private key and spending gas

# The contract library

It uses [abigen](https://github.com/ethereum/go-ethereum/tree/v1.7.2/cmd/abigen)
We use [abigen](https://github.com/ethereum/go-ethereum/tree/v1.7.2/cmd/abigen)
to generate Go language bindings given a contract's ABI definition.

To regenerate the code [install abigen](https://github.com/ethereum/go-ethereum/tree/v1.7.2#building-the-source), put the ABI definition into a file (e.g., [for the EtherDelta contract](https://etherscan.io/address/0x8d12a197cb00d4747a1fe03395095ce2a5cc6819#code)) and run:
Expand All @@ -22,28 +27,50 @@ The [example code](main.go) contains a working interaction with the Ethereum blo

When using [EtherDelta](https://etherdelta.com/) (a decentralized exchange based on smart contracts) a user deposits funds from her own address into a pool that can be accessed by EtherDelta for executing trades. Later the user withdraws any funds from the pool back to her own address. Often there's a tiny amount left in the pool which is tedious to withdraw manually.

`etherdelta-go` to the rescue; the following command will withdraw any remaining deposited funds back to the owner for a given token.

Build and install the binary:

```console
$ cd $GOPATH/src/github.com/linki/etherdelta-go
$ glide install --strip-vendor
$ go run main.go --endpoint "https://mainnet.infura.io" \
--token "0x27d...488" --owner "0x585...828C" --private-key "d4c...7b3"
$ go install
```

`etherdelta-go` to the rescue; the above command will withdraw any remaining deposited funds back to the owner for a given token. It takes the following arguments:
* an RPC endpoint to interact with the Ethereum blockchain (you can run your own Ethereum node or connect to one provided by [Infura](https://infura.io/) free of charge)
* the address of the token you want to withdraw, e.g., [AirToken](https://etherscan.io/token/0x27dce1ec4d3f72c3e457cc50354f1f975ddef488)
* the address of the owner of the tokens and where the funds will be withdrawn to (this should be one of yours)
* the private key corresponding to the owner's address in order to sign the withdraw transaction (this proves that you are the owner of the above address)
Make sure `$GOPATH/bin` is in your `$PATH`, then run it like that:

```console
$ etherdelta-go --token "0x27d...488" --keystore-file "UTC-...98c" --passphrase "my...pass"
Deposited tokens: 500000000
Withdrawing tokens: 500000000
Transaction hash: 0x1a6328...7ef38a

The example code shows two things:
$ # Wait for transaction to be mined, then run it again.

* how to read data from the blockchain without spending gas (e.g., printing the token balance for a particular address)
* how to change state in the blockchain by providing the correct private key and spending gas
$ etherdelta-go --token "0x27d...488" --keystore-file "UTC-...98c" --passphrase "my...pass"
Deposited tokens: 0
```

The example code uses one Gwei for the gas cost and 100,000 for the gas limit but you can configure that via the `gas-price` (in Gwei) and `gas-limit` flags.
It takes the following arguments:
* `token`: the address of the [ERC20](https://theethereum.wiki/w/index.php/ERC20_Token_Standard) [Token](https://etherscan.io/tokens) you want to withdraw, e.g., [AirToken](https://etherscan.io/token/0x27dce1ec4d3f72c3e457cc50354f1f975ddef488)
* `keystore-file`: the location of an Ethereum [Keystore file](https://theethereum.wiki/w/index.php/Accounts,_Addresses,_Public_And_Private_Keys,_And_Tokens#UTC_JSON_Keystore_File) which describes where the funds will be withdrawn to as well as contains the encrypted private key for that address, e.g., `~/Library/Ethereum/keystore/UTC--2017-11-...61d3f9`
* `passphrase`: the passphrase unlocking the `keystore-file`. (This proves that you are the owner of the Keystore file)

If the deposited token balance is zero then no transaction will be issued.

There are some optional arguments as well:
* `endpoint`: an RPC endpoint to interact with the Ethereum blockchain. You can run your own Ethereum node or connect to one provided by [Infura](https://infura.io/) free of charge. Defaults to `https://mainnet.infura.io`.
* `contract`: The address of the EtherDelta contract to use. Since contracts are immutable each change to the contract requires a new contract to be deployed and results in a new contract address. Defaults to the most recent contract as of November 2017: [0x8d12A197cB00D4747a1fe03395095ce2A5CC6819](https://etherscan.io/address/0x8d12A197cB00D4747a1fe03395095ce2A5CC6819).
* `skip-withdraw`: doesn't send any withdraw transaction but merely shows the token balance. Note that `passphrase` must still be provided and correct in order to successfully process the Keystore file. Defaults to `false` (enable withdrawal).
* `gas-price`: The gas price in wei for the withdrawal transaction. Defaults to `1000000000` which is 1 Gwei.
* `gas-limit`: The maximum gas to use for the withdrawal transaction. Defaults to `100000`. During my testing for AirToken the final gas for successful transactions hovered around `50000`.

Keeping the defaults for `gas-price` and `gas-limit` will result in a maximum transaction fee of `0.0001` ETH.

# Next steps

It would be nice to extend the tool to iterate over all tokens that have some deposited funds left in EtherDelta's pool and send it back to the owner.

# Disclaimer

No warranty for any funds lost. See [LICENSE](LICENSE) for details.
75 changes: 53 additions & 22 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,91 +2,122 @@ package main

import (
"fmt"
"io/ioutil"
"log"
"math/big"
"os"

"github.com/alecthomas/kingpin"

"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/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"

"github.com/linki/etherdelta-go/contract"
)

const (
version = "v0.1.0"
version = "v0.2.0"
defaultEndpoint = "https://mainnet.infura.io"
defaultContractAddress = "0x8d12A197cB00D4747a1fe03395095ce2A5CC6819"
defaultGasPrice = "1"
defaultGasPrice = "1000000000"
defaultGasLimit = "100000"
)

var (
endpoint string
contractAddress string
tokenAddress string
ownerAddress string
ownerPrivateKey string
keyStorePath string
passphrase string
skipWithdraw bool
gasPrice int64
gasLimit int64
)

func init() {
kingpin.Flag("endpoint", "Ethereum RPC endpoint").Required().StringVar(&endpoint)
kingpin.Flag("contract", "EtherDelta contract address").Default(defaultContractAddress).StringVar(&contractAddress)
kingpin.Flag("token", "The token's contract address").Required().StringVar(&tokenAddress)
kingpin.Flag("owner", "The owner's address").Required().StringVar(&ownerAddress)
kingpin.Flag("private-key", "The owner's address' private key").Required().StringVar(&ownerPrivateKey)
kingpin.Flag("gas-price", "The gas price in Gwei").Default(defaultGasPrice).Int64Var(&gasPrice)
kingpin.Flag("gas-limit", "The gas limit").Default(defaultGasLimit).Int64Var(&gasLimit)
kingpin.Flag("endpoint", "Ethereum RPC endpoint (optional, default: https://mainnet.infura.io)").Default(defaultEndpoint).StringVar(&endpoint)
kingpin.Flag("contract", "EtherDelta contract address (optional, default: 0x8d12A197cB00D4747a1fe03395095ce2A5CC6819)").Default(defaultContractAddress).StringVar(&contractAddress)
kingpin.Flag("token", "The token's contract address (required)").Required().StringVar(&tokenAddress)
kingpin.Flag("keystore-file", "The owner's Keystore file location (required)").Required().ExistingFileVar(&keyStorePath)
kingpin.Flag("passphrase", "The Keystore file's passphrase (required)").Required().StringVar(&passphrase)
kingpin.Flag("skip-withdraw", "Don't withdraw remaining tokens (optional, default: false)").BoolVar(&skipWithdraw)
kingpin.Flag("gas-price", "The gas price in wei (optional, default: 1000000000, i.e. 1 Gwei)").Default(defaultGasPrice).Int64Var(&gasPrice)
kingpin.Flag("gas-limit", "The gas limit; default: 100000").Default(defaultGasLimit).Int64Var(&gasLimit)
}

func main() {
// Parse command line flags.
kingpin.Version(version)
kingpin.Parse()

// Create an Ethereum client connecting to the provided RPC endpoint.
c, err := rpc.DialHTTP(endpoint)
if err != nil {
log.Fatalf("Failed to connect to the Ethereum network: %v", err)
}
conn := ethclient.NewClient(c)

// Create an instance of the EtherDelta contract at the specific address.
etherdelta, err := contract.NewEtherDelta(common.HexToAddress(contractAddress), conn)
if err != nil {
log.Fatalf("Failed to instantiate EtherDelta contract instance: %v", err)
}

// Open the provided Keystore file.
keyStoreFile, err := os.OpenFile(keyStorePath, os.O_RDONLY, 0400)
if err != nil {
log.Fatalf("Failed to open Keystore file: %v", err)
}
defer keyStoreFile.Close()

// Read the Keystore file's JSON content.
keyStoreJSON, err := ioutil.ReadAll(keyStoreFile)
if err != nil {
log.Fatalf("Failed to parse Keystore file: %v", err)
}

// Parse and decrypt the content given a passphrase.
ownerKey, err := keystore.DecryptKey(keyStoreJSON, passphrase)
if err != nil {
log.Fatalf("Failed to decrypt Keystore file: %v", err)
}

// Get the owner's as well as the token's addresses.
owner := ownerKey.Address
token := common.HexToAddress(tokenAddress)
owner := common.HexToAddress(ownerAddress)

// Retrieve the owner's deposited balance of the token.
balance, err := etherdelta.BalanceOf(nil, token, owner)
if err != nil {
log.Fatalf("Failed to retrieve balance: %v", err)
}
fmt.Println("Token balance:", balance)
fmt.Println("Deposited tokens:", balance)

if balance.Cmp(common.Big0) == 0 {
// If withdrawal is disabled or there's nothing to withdraw, end the program.
if skipWithdraw || balance.Cmp(common.Big0) == 0 {
os.Exit(0)
}

// Otherwise withdraw the deposited tokens.
fmt.Println("Withdrawing tokens:", balance)

privateKey, err := crypto.ToECDSA(common.Hex2Bytes(ownerPrivateKey))
if err != nil {
log.Fatalf("Failed to read private key: %v", err)
}

opts := bind.NewKeyedTransactor(privateKey)
// The transaction needs additional metadata, e.g. the private key for
// authorization as well as gas price and limit.
opts := bind.NewKeyedTransactor(ownerKey.PrivateKey)
opts.GasLimit = big.NewInt(gasLimit)
opts.GasPrice = big.NewInt(gasPrice * 1000000000)
opts.GasPrice = big.NewInt(gasPrice)

// Execute the WithdrawToken function of the EtherDelta contract given the
// required arguments: the token address and amount. Also provide the metadata
// created above in order to successfully create and sign the transaction.
tx, err := etherdelta.WithdrawToken(opts, token, balance)
if err != nil {
log.Fatalf("Failed to withdraw tokens: %v", err)
}

// Print the hash of the submitted transaction for tracking.
fmt.Println("Transaction hash:", tx.Hash().String())
}

0 comments on commit e30d1eb

Please sign in to comment.