@ -0,0 +1,2 @@ |
|||
data-dir* |
|||
config.yaml |
@ -0,0 +1,11 @@ |
|||
# tor-eth-online-shop |
|||
|
|||
Online shop through Tor hidden service, that allows payments in Ethereum. |
|||
|
|||
|
|||
## Run |
|||
First edit configEXAMPLE.yaml to config.yaml, and edit the content. |
|||
Then, run: |
|||
``` |
|||
go run *.go |
|||
``` |
@ -0,0 +1,23 @@ |
|||
package main |
|||
|
|||
import "github.com/spf13/viper" |
|||
|
|||
type Config struct { |
|||
GethURL string |
|||
PrivK string |
|||
} |
|||
|
|||
var config Config |
|||
|
|||
func ReadConfig(path, filename string) { |
|||
viper.SetConfigName(filename) |
|||
viper.AddConfigPath(path) |
|||
viper.SetConfigType("yaml") |
|||
if err := viper.ReadInConfig(); err != nil { |
|||
panic(err) |
|||
} |
|||
err := viper.Unmarshal(&config) |
|||
if err != nil { |
|||
panic(err) |
|||
} |
|||
} |
@ -0,0 +1,2 @@ |
|||
privK: "da7079f082a1ced80c5dee3bf00752fd67f75321a637e5d5073ce1489af062d8" |
|||
gethurl: "https://ropsten.infura.io/TFnR8BWJlqZOKxHHZNcs" |
@ -0,0 +1,2 @@ |
|||
privK: "here the private key" |
|||
gethurl: "here the Geth URL" |
@ -0,0 +1,32 @@ |
|||
package main |
|||
|
|||
import ( |
|||
"crypto/ecdsa" |
|||
|
|||
"github.com/ethereum/go-ethereum/common" |
|||
"github.com/ethereum/go-ethereum/crypto" |
|||
"github.com/ethereum/go-ethereum/ethclient" |
|||
) |
|||
|
|||
var ( |
|||
client *ethclient.Client |
|||
key *ecdsa.PrivateKey |
|||
address common.Address |
|||
) |
|||
|
|||
func Web3Open() error { |
|||
// geth set up
|
|||
var err error |
|||
|
|||
client, err = ethclient.Dial(config.GethURL) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
key, err = crypto.HexToECDSA(config.PrivK) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
address = crypto.PubkeyToAddress(key.PublicKey) |
|||
|
|||
return nil |
|||
} |
@ -0,0 +1,51 @@ |
|||
package main |
|||
|
|||
import ( |
|||
"context" |
|||
"fmt" |
|||
"log" |
|||
"net/http" |
|||
"os" |
|||
"time" |
|||
|
|||
"github.com/cretz/bine/tor" |
|||
"github.com/ipsn/go-libtor" |
|||
) |
|||
|
|||
func main() { |
|||
ReadConfig(".", "config") |
|||
|
|||
// Start tor with some defaults + elevated verbosity
|
|||
fmt.Println("Starting and registering onion service, please wait a bit...") |
|||
t, err := tor.Start(nil, &tor.StartConf{ProcessCreator: libtor.Creator, DebugWriter: os.Stderr}) |
|||
if err != nil { |
|||
log.Panicf("Failed to start tor: %v", err) |
|||
} |
|||
defer t.Close() |
|||
|
|||
// Wait at most a few minutes to publish the service
|
|||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) |
|||
defer cancel() |
|||
|
|||
// Create an onion service to listen on any port but show as 80
|
|||
onion, err := t.Listen(ctx, &tor.ListenConf{RemotePorts: []int{80}}) |
|||
if err != nil { |
|||
log.Panicf("Failed to create onion service: %v", err) |
|||
} |
|||
defer onion.Close() |
|||
|
|||
fmt.Printf("Please open a Tor capable browser and navigate to http://%v.onion\n", onion.ID) |
|||
|
|||
// connect to eth
|
|||
// Ethereum
|
|||
err = Web3Open() |
|||
if err != nil { |
|||
log.Fatal(err) |
|||
} |
|||
|
|||
// Run a Hello-World HTTP service until terminated
|
|||
http.Handle("/", http.FileServer(http.Dir("./www"))) |
|||
http.HandleFunc("/api/purchase", handlePurchase) |
|||
http.HandleFunc("/api/txid", handleTxId) |
|||
http.Serve(onion, nil) |
|||
} |
@ -0,0 +1,19 @@ |
|||
package main |
|||
|
|||
import ( |
|||
"fmt" |
|||
"net/http" |
|||
) |
|||
|
|||
func handlePurchase(w http.ResponseWriter, r *http.Request) { |
|||
challenge, err := generateChallenge(20) |
|||
if err != nil { |
|||
panic(err) |
|||
return |
|||
} |
|||
fmt.Fprintf(w, challenge) |
|||
} |
|||
func handleTxId(w http.ResponseWriter, r *http.Request) { |
|||
|
|||
fmt.Fprintf(w, "ack") |
|||
} |
@ -0,0 +1,23 @@ |
|||
package main |
|||
|
|||
import ( |
|||
"crypto/rand" |
|||
"math/big" |
|||
) |
|||
|
|||
func generateChallenge(length int) (string, error) { |
|||
result := "" |
|||
for { |
|||
if len(result) >= length { |
|||
return result, nil |
|||
} |
|||
num, err := rand.Int(rand.Reader, big.NewInt(int64(127))) |
|||
if err != nil { |
|||
return "", err |
|||
} |
|||
n := num.Int64() |
|||
if n > 32 && n < 127 { |
|||
result += string(n) |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,71 @@ |
|||
<!doctype html> |
|||
<html lang="en"> |
|||
<head> |
|||
<!-- Required meta tags --> |
|||
<meta charset="utf-8"> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> |
|||
<!-- Bootstrap CSS --> |
|||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> |
|||
<title>tor-eth-online-shop</title> |
|||
</head> |
|||
<body> |
|||
<div class="container"> |
|||
<h1>tor-eth-online-shop</h1> |
|||
<div class="row" id="mainView"> |
|||
<div class="col-md-9"> |
|||
<div class="row"> |
|||
<div class="col-md-4"> |
|||
<div class="card"> |
|||
<img class="card-img-top" src="https://vignette.wikia.nocookie.net/joke-battles/images/f/f7/711MkPTJ4OL._SL1500_.jpg" alt="Card image cap"> |
|||
<div class="card-body"> |
|||
<h5 class="card-title">Product 1</h5> |
|||
<p class="card-text">Description.</p> |
|||
<a onclick="addProduct('id1')" href="#" class="btn btn-outline-success float-right">Add to card</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-4"> |
|||
<div class="card"> |
|||
<img class="card-img-top" src="https://5.imimg.com/data5/YX/SD/MY-2492706/slotted-carton-box-500x500.jpg" alt="Card image cap"> |
|||
<div class="card-body"> |
|||
<h5 class="card-title">Product 2</h5> |
|||
<p class="card-text">Description.</p> |
|||
<a onclick="addProduct('id2')" href="#" class="btn btn-outline-success float-right">Add to card</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-4"> |
|||
<div class="card"> |
|||
<img class="card-img-top" src="https://image.shutterstock.com/image-photo/closed-cardboard-box-taped-isolated-260nw-168691364.jpg" alt="Card image cap"> |
|||
<div class="card-body"> |
|||
<h5 class="card-title">Product 3</h5> |
|||
<p class="card-text">Description.</p> |
|||
<a onclick="addProduct('id3')" href="#" class="btn btn-outline-success float-right">Add to card</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3"> |
|||
<div class="card"> |
|||
<div class="card-body"> |
|||
<h3>Selected products</h3> |
|||
<p id="list" class="card-text"></p> |
|||
<a onclick="purchase()" href="#" class="btn btn-outline-success float-right">Purchase</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Optional JavaScript --> |
|||
<!-- jQuery first, then Popper.js, then Bootstrap JS --> |
|||
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> |
|||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script> |
|||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script> |
|||
|
|||
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> |
|||
|
|||
<script src="index.js"></script> |
|||
</body> |
|||
</html> |
@ -0,0 +1,77 @@ |
|||
var products = [ |
|||
{ |
|||
id: 'id1', |
|||
title: 'beer1', |
|||
price: '0.1' |
|||
}, |
|||
{ |
|||
id: 'id2', |
|||
title: 'beer2', |
|||
price: '0.1' |
|||
}, |
|||
{ |
|||
id: 'id3', |
|||
title: 'beer3', |
|||
price: '0.1' |
|||
} |
|||
]; |
|||
var list = {}; |
|||
function getProduct(id) { |
|||
for(var i=0; i<products.length; i++) { |
|||
if(products[i].id == id) { |
|||
return(products[i]); |
|||
} |
|||
} |
|||
return("not found"); |
|||
} |
|||
function addProduct(id) { |
|||
if (!list[id]) { |
|||
list[id] = 1; |
|||
} else { |
|||
list[id]++; |
|||
} |
|||
// update html
|
|||
var total = 0; |
|||
var html = ""; |
|||
html += "<ul class='list-group'>"; |
|||
for (var property in list) { |
|||
if (list.hasOwnProperty(property)) { |
|||
html += "<li class='list-group-item'>"; |
|||
html += property + " x" + list[property]; |
|||
html += "<div class='float-right'>"; |
|||
html += (getProduct(property).price * list[property]).toFixed(4) + " eth"; |
|||
html += "</div>"; |
|||
html += "</li>"; |
|||
total = (+(total) + +(getProduct(property).price * list[property])).toFixed(4); |
|||
} |
|||
} |
|||
html += "</ul>"; |
|||
html += "<br>"; |
|||
html += "<div class='float-right'>"; |
|||
html += "<b>Total: " + total + "</b>"; |
|||
html += "</div>"; |
|||
html += "<br>"; |
|||
document.getElementById("list").innerHTML = html; |
|||
} |
|||
|
|||
function purchase() { |
|||
var total = 0; |
|||
for (var property in list) { |
|||
if (list.hasOwnProperty(property)) { |
|||
total = (+(total) + +(getProduct(property).price * list[property])).toFixed(4); |
|||
} |
|||
} |
|||
var answ = confirm("total to pay: " + total + " eth"); |
|||
if (!answ) { |
|||
return; |
|||
} |
|||
axios.post('/api/purchase', { |
|||
list: list |
|||
}) |
|||
.then(function (response) { |
|||
console.log(response); |
|||
}) |
|||
.catch(function (error) { |
|||
console.log(error); |
|||
}); |
|||
} |