Browse Source

DERO AstroBWT webwallet daemon

webwallet
Captain 1 year ago
parent
commit
e41038b609
10 changed files with 1946 additions and 53 deletions
  1. +17
    -1
      blockchain/rpcserver/getoutputs.bin.go
  2. +69
    -6
      blockchain/rpcserver/iskeyimagespent.go
  3. +120
    -5
      blockchain/rpcserver/rpcserver.go
  4. +3
    -0
      blockchain/rpcserver/sendrawtransaction.go
  5. +772
    -0
      cmd/webwallet/main.go
  6. +91
    -26
      walletapi/daemon_communication.go
  7. +16
    -0
      walletapi/db.go
  8. +720
    -0
      walletapi/db_mem.go
  9. +1
    -9
      walletapi/rpc_get_transfer_by_txid.go
  10. +137
    -6
      walletapi/wallet.go

+ 17
- 1
blockchain/rpcserver/getoutputs.bin.go View File

@@ -31,6 +31,8 @@ func getoutputs(rw http.ResponseWriter, req *http.Request) {
start := int64(0)
stop := int64(0)
start_height := int64(0)
stop_height := int64(0)

{ // parse start query parameter
keys, ok := req.URL.Query()["startheight"]
@@ -56,6 +58,11 @@ func getoutputs(rw http.ResponseWriter, req *http.Request) {
}

}
stop_height = start_height + 10000
if stop_height > int64(chain.Load_TOPO_HEIGHT(nil)){
stop_height = int64(chain.Load_TOPO_HEIGHT(nil))
}
}
}

@@ -92,6 +99,7 @@ func getoutputs(rw http.ResponseWriter, req *http.Request) {
//biggest_output_index := chain.Block_Count_Vout(nil,top_id) + chain.Get_Block_Output_Index(nil,top_id)

biggest_output_index := int64(0)

// convert height to block
top_block, err := chain.Load_Block_Topological_order_at_index(nil, chain.Load_TOPO_HEIGHT(nil))
@@ -100,6 +108,14 @@ func getoutputs(rw http.ResponseWriter, req *http.Request) {
_, biggest_output_index = chain.Get_Block_Output_Index(nil, top_block)

}
if stop_height != 0 {
top_block, err := chain.Load_Block_Topological_order_at_index(nil,stop_height)
if err == nil {
_, biggest_output_index = chain.Get_Block_Output_Index(nil, top_block)
}
}

if stop == 0 || stop > biggest_output_index {
stop = biggest_output_index
@@ -109,7 +125,7 @@ func getoutputs(rw http.ResponseWriter, req *http.Request) {
if start >= stop {
start = stop - 1
}
// lz4writer := lz4.NewWriter(rw)
//lz4writer.HighCompression = true // enable extreme but slow compression
//lz4writer.BlockMaxSize = 256*1024 // small block size to decrease memory consumption


+ 69
- 6
blockchain/rpcserver/iskeyimagespent.go View File

@@ -16,11 +16,16 @@

package rpcserver

import "io"
import "fmt"
import "net/http"
import "strings"
import "context"

//import "encoding/hex"
import "encoding/json"
import "github.com/intel-go/fastjson"
import "github.com/osamingo/jsonrpc"

import "github.com/deroproject/derosuite/crypto"
import "github.com/deroproject/derosuite/structures"
@@ -35,15 +40,16 @@ import "github.com/deroproject/derosuite/structures"

type IsKeyImageSpent_Handler struct{}

func iskeyimagespent(rw http.ResponseWriter, req *http.Request) {
decoder := json.NewDecoder(req.Body)
func (ki IsKeyImageSpent_Handler) ServeJSONRPC(c context.Context, params *fastjson.RawMessage) (interface{}, *jsonrpc.Error) {
var p structures.Is_Key_Image_Spent_Params
var result structures.Is_Key_Image_Spent_Result
err := decoder.Decode(&p)
if err != nil {
panic(err)

if err := jsonrpc.Unmarshal(params, &p); err != nil {
return result, nil
fmt.Printf("Key_images handler json unmarshal err %s \n", err)
return nil, err
}
defer req.Body.Close()

for i := range p.Key_images {
hash := crypto.HashHexToHash(strings.TrimSpace(p.Key_images[i]))
@@ -63,6 +69,63 @@ func iskeyimagespent(rw http.ResponseWriter, req *http.Request) {
result.Spent_Status = append(result.Spent_Status, 0) // 0 mark means unspent
}

result.Status = "OK"

return result, nil
}

func iskeyimagespent(rw http.ResponseWriter, req *http.Request) {

rw.Header().Set("content-type", "application/json")
decoder := json.NewDecoder(req.Body)
var p structures.Is_Key_Image_Spent_Params
var result structures.Is_Key_Image_Spent_Result

// if it's a request with keyimage in url, process and return here
q := req.URL.Query()
if q["ki"] != nil && q["ki"][0] != "" {
hash := crypto.HashHexToHash(strings.TrimSpace(q["ki"][0]))

// check in blockchain
if _, ok := chain.Read_KeyImage_Status(nil, hash); ok {
result.Spent_Status = append(result.Spent_Status, 1) // 1 mark means spent in blockchain

} else if chain.Mempool.Mempool_Keyimage_Spent(hash) {
result.Spent_Status = append(result.Spent_Status, 2) // 2 mark means spent in pool

} else {

result.Spent_Status = append(result.Spent_Status, 0) // 0 mark means unspent
}

} else {

err := decoder.Decode(&p)
if err != nil {
if err != io.EOF {
panic(err)
}
}
defer req.Body.Close()

for i := range p.Key_images {
hash := crypto.HashHexToHash(strings.TrimSpace(p.Key_images[i]))

// check in blockchain
if _, ok := chain.Read_KeyImage_Status(nil, hash); ok {
result.Spent_Status = append(result.Spent_Status, 1) // 1 mark means spent in blockchain
continue
}

// check in pool
if chain.Mempool.Mempool_Keyimage_Spent(hash) {
result.Spent_Status = append(result.Spent_Status, 2) // 2 mark means spent in pool
continue
}

result.Spent_Status = append(result.Spent_Status, 0) // 0 mark means unspent
}
}
result.Status = "OK"
//logger.Debugf("Request %+v", p)



+ 120
- 5
blockchain/rpcserver/rpcserver.go View File

@@ -17,12 +17,15 @@
package rpcserver

import "io"
import "os"
import "fmt"
import "net"
import "time"
import "context"
import "strings"
import "sync"
import "sync/atomic"
import "crypto/tls"

//import "context"
import "net/http"
@@ -155,6 +158,10 @@ func (r *RPCServer) Run() {
if err := mr.RegisterMethod("gettxpool", GetTxPool_Handler{}, structures.GetTxPool_Params{}, structures.GetTxPool_Result{}); err != nil {
log.Fatalln(err)
}
if err := mr.RegisterMethod("is_key_image_spent", IsKeyImageSpent_Handler{}, structures.Is_Key_Image_Spent_Params{}, structures.Is_Key_Image_Spent_Result{}); err != nil {
log.Fatalln(err)
}

// create a new mux
r.mux = http.NewServeMux()
@@ -178,12 +185,41 @@ func (r *RPCServer) Run() {
}
}

// if we pin the certificate, we can have the perfect score
// but for certain reasons, we are not pinning them
//w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
cfg := &tls.Config{
/* MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
PreferServerCipherSuites: true,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
},*/
}
logger.Infof("RPC will listen on %s", default_address)
r.Lock()
r.srv = &http.Server{Addr: default_address, Handler: r.mux}
r.srv = &http.Server{
Addr: default_address,
Handler: r.mux,
ReadTimeout: 5 * time.Second,
ReadHeaderTimeout: 5 * time.Second,
WriteTimeout: 40 * time.Second,
TLSConfig: cfg,
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0),
}
r.Unlock()

r.mux.HandleFunc("/", hello)
// serve any static files from this directory
r.mux.Handle("/static/", http.FileServer(http.Dir("./webroot")))
//r.mux.HandleFunc("/", hello)
r.mux.Handle("/json_rpc", mr)

// handle nasty http requests
@@ -216,15 +252,94 @@ func (r *RPCServer) Run() {
r.mux.HandleFunc("/metrics", prometheus.InstrumentHandler("dero", promhttp.HandlerFor(metrics.Registry, promhttp.HandlerOpts{})))

}

//r.mux.HandleFunc("/json_rpc/debug", mr.ServeDebug)

if err := r.srv.ListenAndServe(); err != http.ErrServerClosed {
//r.srv.SetKeepAlivesEnabled(true)
wrappedMux := NewInternalOrFileSystem(r.mux)
r.srv.Handler = wrappedMux;

// the tls keys self certified can be generated using
// openssl ecparam -genkey -name secp384r1 -out server.key
// openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650

if _, err := os.Stat("./keys/server.crt"); !os.IsNotExist(err){
if _, err := os.Stat("./keys/server.key"); !os.IsNotExist(err){
if err := r.srv.ListenAndServeTLS("keys/server.crt","keys/server.key"); err != http.ErrServerClosed {
logger.Warnf("ERR TLS listening to address err %s", err)
}
}
}else{
if err := r.srv.ListenAndServe(); err != http.ErrServerClosed {
logger.Warnf("ERR listening to address err %s", err)
}
}
}

}


//this middleware see whether a request can be served from the filesystem
type InternalOrFileSystem struct {
handler http.Handler
}

//ServeHTTP handles the request by passing it to the real
//handler
func (l *InternalOrFileSystem) ServeHTTP(w http.ResponseWriter, r *http.Request) {
start := time.Now()
_ = start
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Max-Age", "3600")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With")
// more protections
w.Header().Set("X-Frame-Options", "DENY") // never load in an iframe
w.Header().Set("X-XSS-Protection", "1; mode=blockFilter") // do we need XSS protection, since everything is on client
w.Header().Set("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval' img-src 'self' data: ") // CSP disable access to all other domains

// enable caching for static assets
if strings.Contains(r.URL.Path,"/static") || r.URL.Path == "/" {
w.Header().Set("Cache-Control", "public, max-age=7776000, must-revalidate")
}
// NOTE: redirect / to wallet
// NOTE: use hard-coded paths, other directory traversal attacks are possible
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
//http.ServeFile(w, r, "./webroot/static/wallet.html") // this does not sent content-type
file, err := os.Open("./webroot/static/wallet.html")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
http.Error(w, fmt.Sprintf("Unable to open and read file : %v", err),404)
return
}
defer file.Close()
http.ServeContent(w, r, "index.html", time.Now(), file) // this will properly set the headers
return
}
l.handler.ServeHTTP(w, r) // pass request to RPC handler
// log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
}

//NewLogger constructs a new middleware handler to hook some static file requests
func NewInternalOrFileSystem(handlerToWrap http.Handler) *InternalOrFileSystem {
return &InternalOrFileSystem{handlerToWrap}
}


func hello(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "Hello world!")
}

+ 3
- 0
blockchain/rpcserver/sendrawtransaction.go View File

@@ -16,6 +16,7 @@

package rpcserver

import "io"
import "fmt"
import "net/http"
import "encoding/hex"
@@ -48,8 +49,10 @@ func SendRawTransaction_Handler(rw http.ResponseWriter, req *http.Request) {
}()
err := decoder.Decode(&p)
if err != nil {
if err != io.EOF {
logger.Warnf("err while decoding incoming sendrawtransaaction json err: %s", err)
return
}
}
defer req.Body.Close()



+ 772
- 0
cmd/webwallet/main.go View File

@@ -0,0 +1,772 @@
// +build js,wasm

package main

import (
//"encoding/base64"
"encoding/hex"
"encoding/json"
//"io/ioutil"
//"log"
//"net/http"
"fmt"
"net/url"
"strconv"
"syscall/js"
"time"
// "bytes"
"runtime/debug"
"strings"
)
import "github.com/romana/rlog"
import "github.com/deroproject/derosuite/walletapi"
import "github.com/deroproject/derosuite/globals"
import "github.com/deroproject/derosuite/config"
import "github.com/deroproject/derosuite/address"
import "github.com/deroproject/derosuite/transaction"
import "github.com/deroproject/derosuite/crypto"

var miner_tx bool = false

var Local_wallet_instance *walletapi.Wallet

func register_wallet_callbacks() {

js_ping := func(params []js.Value) {}
js.Global().Set("go_pinger", js.NewCallback(js_ping))

js_Create_New_Wallet := func(params []js.Value) {
error_message := "error"
filename := params[0].String()
password := params[1].String()

w, err := walletapi.Create_Encrypted_Wallet_Random(filename, password)

if err == nil {
error_message = "success"
Local_wallet_instance = w
Local_wallet_instance.SetDaemonAddress(daemon_address)
} else {
error_message = err.Error()
}

js.Global().Set("error_message", error_message)
}
js.Global().Set("DERO_JS_Create_New_Wallet", js.NewCallback(js_Create_New_Wallet))

js_Create_Encrypted_Wallet_From_Recovery_Words := func(params []js.Value) {
error_message := "error"

w, err := walletapi.Create_Encrypted_Wallet_From_Recovery_Words(params[0].String(), params[1].String(), params[2].String())

if err == nil {
error_message = "success"
Local_wallet_instance = w
Local_wallet_instance.SetDaemonAddress(daemon_address)
} else {
error_message = err.Error()
}

js.Global().Set("error_message", error_message)

}
js.Global().Set("DERO_JS_Create_Encrypted_Wallet_From_Recovery_Words", js.NewCallback(js_Create_Encrypted_Wallet_From_Recovery_Words))

js_Open_Encrypted_Wallet := func(params []js.Value) {

error_message := "error"

// convert typed array to go array
// this may be slow and needs to be optimized
// as optimization we are converting the data in javascript to hex
// and here we are hex decoding as it is faster than converting each value of typed array
// TODO: later when this gets fixed by go devs, we can incorporate it
/*
db_array := make([]byte,params[2].Length(),params[2].Length())
for i := 0; i < len(db_array); i++ {
db_array[i]= byte(params[2].Index(i).Int())
}
*/

src := []byte(params[2].String())
db_array := make([]byte, hex.DecodedLen(len(src)))
n, err := hex.Decode(db_array, src)
db_array = db_array[:n]

if err != nil {

rlog.Warnf("error decoding hex string \n", err)
}

rlog.Infof("i passed DB of size %d\n", len(db_array))
w, err := walletapi.Open_Encrypted_Wallet(params[0].String(), params[1].String(), db_array)
if err == nil {
error_message = "success"
Local_wallet_instance = w
Local_wallet_instance.SetDaemonAddress(daemon_address)

rlog.Infof("Successfully opened wallet\n")
} else {
error_message = err.Error()

rlog.Warnf("Error opened wallet %s\n", err)
}

js.Global().Set("error_message", error_message)
}
js.Global().Set("DERO_JS_Open_Encrypted_Wallet", js.NewCallback(js_Open_Encrypted_Wallet))

js_Create_Wallet := func(params []js.Value) {

filename := params[0].String()
password := params[1].String()
seed_hex := params[2].String()
error_message := "error"

var seed crypto.Key
seed_raw, err := hex.DecodeString(strings.TrimSpace(seed_hex))
if len(seed_raw) != 32 || err != nil {
err = fmt.Errorf("Recovery Only key must be 64 chars hexadecimal chars")
rlog.Errorf("err %s", err)
error_message = err.Error()
} else {

copy(seed[:], seed_raw[:32])
wallet, err := walletapi.Create_Encrypted_Wallet(filename, password, seed)

if err != nil {
error_message = err.Error()
} else {
error_message = "success"
Local_wallet_instance = wallet
Local_wallet_instance.SetDaemonAddress(daemon_address)
}
}

js.Global().Set("error_message", error_message)

}
js.Global().Set("DERO_JS_Create_Wallet", js.NewCallback(js_Create_Wallet))

js_Create_Encrypted_Wallet_ViewOnly := func(params []js.Value) {
filename := params[0].String()
password := params[1].String()
viewkey := params[2].String()
error_message := "error"

wallet, err := walletapi.Create_Encrypted_Wallet_ViewOnly(filename, password, viewkey)

if err != nil {
error_message = err.Error()
} else {
error_message = "success"
Local_wallet_instance = wallet
Local_wallet_instance.SetDaemonAddress(daemon_address)
}

js.Global().Set("error_message", error_message)
}
js.Global().Set("DERO_JS_Create_Encrypted_Wallet_ViewOnly", js.NewCallback(js_Create_Encrypted_Wallet_ViewOnly))

js_GenerateIAddress := func(params []js.Value) {
generate_integrated_address()
}
js.Global().Set("DERO_JS_GenerateIAddress", js.NewCallback(js_GenerateIAddress))

js_GetSeedinLanguage := func(params []js.Value) {
seed := "Some error occurred"
if Local_wallet_instance != nil && len(params) == 1 {
seed = Local_wallet_instance.GetSeedinLanguage(params[0].String())
}
js.Global().Set("wallet_seed", seed)
}
js.Global().Set("DERO_JS_GetSeedinLanguage", js.NewCallback(js_GetSeedinLanguage))

js_TX_history := func(params []js.Value) {
go func() {
error_message := "Wallet is Closed"
var buffer []byte
var err error

defer func() {
js.Global().Set("tx_history", string(buffer))
js.Global().Set("error_message", error_message)
}()

if Local_wallet_instance != nil {

min_height, _ := strconv.ParseUint(params[6].String(), 0, 64)
max_height, _ := strconv.ParseUint(params[7].String(), 0, 64)

entries := Local_wallet_instance.Show_Transfers(params[0].Bool(), params[1].Bool(), params[2].Bool(), params[3].Bool(), params[4].Bool(), params[5].Bool(), min_height, max_height)

if len(entries) == 0 {
return
}
buffer, err = json.Marshal(entries)
if err != nil {
error_message = err.Error()
return
}
}

}()
}
js.Global().Set("DERO_JS_TX_History", js.NewCallback(js_TX_history))

js_Transfer2 := func(params []js.Value) {
transfer_error := "error"
var transfer_txid, transfer_txhex, transfer_fee, transfer_amount, transfer_inputs_sum, transfer_change string

defer func() {
rlog.Warnf("setting values of tranfer variables")
js.Global().Set("transfer_txid", transfer_txid)
js.Global().Set("transfer_txhex", transfer_txhex)
js.Global().Set("transfer_amount", transfer_amount)
js.Global().Set("transfer_fee", transfer_fee)
js.Global().Set("transfer_inputs_sum", transfer_inputs_sum)
js.Global().Set("transfer_change", transfer_change)
js.Global().Set("transfer_error", transfer_error)
rlog.Warnf("setting values of tranfesr variables %s ", transfer_error)
}()

var address_list []address.Address
var amount_list []uint64

if params[0].Length() != params[1].Length() {

return
}

for i := 0; i < params[0].Length(); i++ { // convert string address to our native form
a, err := globals.ParseValidateAddress(params[0].Index(i).String())
if err != nil {
rlog.Warnf("Parsing address failed %s err %s\n", params[0].Index(i).String(), err)
transfer_error = err.Error()
return
}
address_list = append(address_list, *a)
}

for i := 0; i < params[1].Length(); i++ { // convert string address to our native form
amount, err := globals.ParseAmount(params[1].Index(i).String())
if err != nil {
rlog.Warnf("Parsing address failed %s err %s\n", params[0].Index(i).String(), err)
transfer_error = err.Error()
return
//return nil, jsonrpc.ErrInvalidParams()
}

amount_list = append(amount_list, amount)
}

payment_id := params[2].String()

if len(payment_id) > 0 && !(len(payment_id) == 64 || len(payment_id) == 16) {
transfer_error = "Invalid payment ID"
return // we should give invalid payment ID
}
if _, err := hex.DecodeString(payment_id); err != nil {
transfer_error = "Invalid payment ID"
return // we should give invalid payment ID
}

unlock_time := uint64(0)
fees_per_kb := uint64(0)
mixin := uint64(0)

tx, inputs, input_sum, change, err := Local_wallet_instance.Transfer(address_list, amount_list, unlock_time, payment_id, fees_per_kb, mixin)
_ = inputs
if err != nil {
rlog.Warnf("Error while building Transaction err %s\n", err)
transfer_error = err.Error()
return
//return nil, &jsonrpc.Error{Code: -2, Message: fmt.Sprintf("Error while building Transaction err %s", err)}

}

rlog.Infof("Inputs Selected for %s \n", globals.FormatMoney(input_sum))
amount := uint64(0)
for i := range amount_list {
amount += amount_list[i]
}
rlog.Infof("Transfering total amount %s \n", globals.FormatMoney(amount))
rlog.Infof("change amount ( will come back ) %s \n", globals.FormatMoney(change))
rlog.Infof("fees %s \n", globals.FormatMoney(tx.RctSignature.Get_TX_Fee()))

rlog.Infof(" size of tx %d", len(hex.EncodeToString(tx.Serialize())))

transfer_fee = globals.FormatMoney12(tx.RctSignature.Get_TX_Fee())
transfer_amount = globals.FormatMoney12(amount)
transfer_change = globals.FormatMoney12(change)
transfer_inputs_sum = globals.FormatMoney12(input_sum)
transfer_txid = tx.GetHash().String()
transfer_txhex = hex.EncodeToString(tx.Serialize())
transfer_error = "success"
}

js_Transfer := func(params []js.Value) {
go js_Transfer2(params)
}
js.Global().Set("DERO_JS_Transfer", js.NewCallback(js_Transfer))

js_Transfer_Everything2 := func(params []js.Value) {
transfer_error := "error"
var transfer_txid, transfer_txhex, transfer_fee, transfer_amount, transfer_inputs_sum, transfer_change string

defer func() {
rlog.Warnf("setting values of tranfer variables")
js.Global().Set("transfer_txid", transfer_txid)
js.Global().Set("transfer_txhex", transfer_txhex)
js.Global().Set("transfer_amount", transfer_amount)
js.Global().Set("transfer_fee", transfer_fee)
js.Global().Set("transfer_inputs_sum", transfer_inputs_sum)
js.Global().Set("transfer_change", transfer_change)
js.Global().Set("transfer_error", transfer_error)
rlog.Warnf("setting values of tranfesr variables %s ", transfer_error)
}()

var address_list []address.Address
var amount_list []uint64

if params[0].Length() != 1 {
return
}

for i := 0; i < params[0].Length(); i++ { // convert string address to our native form
a, err := globals.ParseValidateAddress(params[0].Index(i).String())
if err != nil {
rlog.Warnf("Parsing address failed %s err %s\n", params[0].Index(i).String(), err)
transfer_error = err.Error()
return
//return nil, jsonrpc.ErrInvalidParams()
}
address_list = append(address_list, *a)
}

payment_id := params[1].String()

if len(payment_id) > 0 && !(len(payment_id) == 64 || len(payment_id) == 16) {
transfer_error = "Invalid payment ID"
return // we should give invalid payment ID
}
if _, err := hex.DecodeString(payment_id); err != nil {
transfer_error = "Invalid payment ID"
return // we should give invalid payment ID
}

//unlock_time := uint64(0)
fees_per_kb := uint64(0)
mixin := uint64(0)

tx, inputs, input_sum, err := Local_wallet_instance.Transfer_Everything(address_list[0], payment_id, 0, fees_per_kb, mixin)
_ = inputs
if err != nil {
rlog.Warnf("Error while building Everything Transaction err %s\n", err)
transfer_error = err.Error()
return
//return nil, &jsonrpc.Error{Code: -2, Message: fmt.Sprintf("Error while building Transaction err %s", err)}

}

rlog.Infof("Inputs Selected for %s \n", globals.FormatMoney(input_sum))
amount := uint64(0)
for i := range amount_list {
amount += amount_list[i]
}
amount = uint64(input_sum - tx.RctSignature.Get_TX_Fee())
change := uint64(0)
rlog.Infof("Transfering everything total amount %s \n", globals.FormatMoney(amount))
rlog.Infof("change amount ( will come back ) %s \n", globals.FormatMoney(change))
rlog.Infof("fees %s \n", globals.FormatMoney(tx.RctSignature.Get_TX_Fee()))

rlog.Infof(" size of tx %d", len(hex.EncodeToString(tx.Serialize())))

transfer_fee = globals.FormatMoney12(tx.RctSignature.Get_TX_Fee())
transfer_amount = globals.FormatMoney12(amount)
transfer_change = globals.FormatMoney12(change)
transfer_inputs_sum = globals.FormatMoney12(input_sum)
transfer_txid = tx.GetHash().String()
transfer_txhex = hex.EncodeToString(tx.Serialize())
transfer_error = "success"
}

js_Transfer_Everything := func(params []js.Value) {
go js_Transfer_Everything2(params)
}
js.Global().Set("DERO_JS_Transfer_Everything", js.NewCallback(js_Transfer_Everything))

js_Relay_TX2 := func(params []js.Value) {
hex_tx := strings.TrimSpace(params[0].String())
rlog.Warnf("tx decoding hex")
tx_bytes, err := hex.DecodeString(hex_tx)
rlog.Warnf("tx decoding hex err %s", err)

if err != nil {
js.Global().Set("relay_error", fmt.Sprintf("Transaction Could NOT be hex decoded err %s", err))
return
}

var tx transaction.Transaction

err = tx.DeserializeHeader(tx_bytes)
rlog.Warnf("tx decoding tx err %s", err)

if err != nil {
js.Global().Set("relay_error", fmt.Sprintf("Transaction Could NOT be deserialized err %s", err))
return
}

err = Local_wallet_instance.SendTransaction(&tx) // relay tx to daemon/network
rlog.Infof("tx relaying tx err %s", err)

if err != nil {
js.Global().Set("relay_error", fmt.Sprintf("Transaction sending failed txid = %s, err %s", tx.GetHash(), err))
return
}
js.Global().Set("relay_error", "success")
}

js_Relay_TX := func(params []js.Value) {
go js_Relay_TX2(params)
}
js.Global().Set("DERO_JS_Relay_TX", js.NewCallback(js_Relay_TX))

js_Close_Encrypted_Wallet := func(params []js.Value) {
if Local_wallet_instance != nil {
Local_wallet_instance.Close_Encrypted_Wallet()
Local_wallet_instance = nil

fmt.Printf("Wallet has been closed\n")
}

}
js.Global().Set("DERO_JS_Close_Encrypted_Wallet", js.NewCallback(js_Close_Encrypted_Wallet))

// these function does NOT report back anything
js_Rescan_Blockchain := func(params []js.Value) {
if Local_wallet_instance != nil {
Local_wallet_instance.Clean() // clean existing data from wallet
Local_wallet_instance.Rescan_From_Height(0) // we are setting it to zero, it will be automatically convert to start height if it is set
}
}
js.Global().Set("DERO_JS_Rescan_Blockchain", js.NewCallback(js_Rescan_Blockchain))

// this function does NOT report back anything
js_SetOnline := func(params []js.Value) {
if Local_wallet_instance != nil {
Local_wallet_instance.SetOnlineMode()
}
}
js.Global().Set("DERO_JS_SetOnline", js.NewCallback(js_SetOnline))

// this function does NOT report back anything
js_SetOffline := func(params []js.Value) {
if Local_wallet_instance != nil {
Local_wallet_instance.SetOfflineMode()
}
}
js.Global().Set("DERO_JS_SetOffline", js.NewCallback(js_SetOffline))

// this function does NOT report back anything
js_ChangePassword := func(params []js.Value) {
if Local_wallet_instance != nil {
Local_wallet_instance.Set_Encrypted_Wallet_Password(params[0].String())
}
}
js.Global().Set("DERO_JS_ChangePassword", js.NewCallback(js_ChangePassword))

// this function does NOT report back anything
js_SetInitialHeight := func(params []js.Value) {
if Local_wallet_instance != nil {
Local_wallet_instance.SetInitialHeight(int64(params[0].Int()))
}
}
js.Global().Set("DERO_JS_SetInitialHeight", js.NewCallback(js_SetInitialHeight))

// this function does NOT report back anything
js_SetMixin := func(params []js.Value) {
if Local_wallet_instance != nil {
Local_wallet_instance.SetMixin((params[0].Int()))
}
}
js.Global().Set("DERO_JS_SetMixin", js.NewCallback(js_SetMixin))

// this function does NOT report back anything
js_SetFeeMultiplier := func(params []js.Value) {
if Local_wallet_instance != nil {
Local_wallet_instance.SetFeeMultiplier(float32(params[0].Float()))
}
}
js.Global().Set("DERO_JS_SetFeeMultiplier", js.NewCallback(js_SetFeeMultiplier))
// this function does NOT report back anything
js_SetSyncTime := func(params []js.Value) {
if Local_wallet_instance != nil {
Local_wallet_instance.SetDelaySync(int64(params[0].Int()))
}
}
js.Global().Set("DERO_JS_SetSyncTime", js.NewCallback(js_SetSyncTime))

// this function does NOT report back anything
js_SetDaemonAddress := func(params []js.Value) {
if Local_wallet_instance != nil {
Local_wallet_instance.SetDaemonAddress(params[0].String())
}
}
js.Global().Set("DERO_JS_SetDaemonAddress", js.NewCallback(js_SetDaemonAddress))
// some apis to detect parse validate address
// this will setup some fields
js_VerifyAddress := func(params []js.Value) {

var address_main, address_paymentid, address_error string
var address_valid, address_integrated bool

address_error = "error"
addr, err := globals.ParseValidateAddress(params[0].String())
if err == nil {
address_valid = true
if addr.IsIntegratedAddress() {
address_integrated = true
address_paymentid = fmt.Sprintf("%x", addr.PaymentID)
} else {
address_integrated = false
}
address_error = "success"
} else {
address_error = err.Error()
address_valid = false
address_integrated = false
}

js.Global().Set("address_error", address_error)
js.Global().Set("address_main", address_main)
js.Global().Set("address_paymentid", address_paymentid)
js.Global().Set("address_valid", address_valid)
js.Global().Set("address_integrated", address_integrated)

}

js.Global().Set("DERO_JS_VerifyAddress", js.NewCallback(js_VerifyAddress))

js_VerifyAmount := func(params []js.Value) {
var amount_valid bool
lamountstr := strings.TrimSpace(params[0].String())
_, err := globals.ParseAmount(lamountstr)

if err != nil {
js.Global().Set("amount_valid", amount_valid)
js.Global().Set("amount_error", err.Error())
return
}
amount_valid = true
js.Global().Set("amount_valid", amount_valid)
js.Global().Set("amount_error", "success")
}
js.Global().Set("DERO_JS_VerifyAmount", js.NewCallback(js_VerifyAmount))

js_VerifyPassword := func(params []js.Value) {
password_error := "error"
if Local_wallet_instance != nil {
valid := Local_wallet_instance.Check_Password(params[0].String())
if valid {
password_error = "success"
}
}
js.Global().Set("password_error", password_error)
}
js.Global().Set("DERO_JS_VerifyPassword", js.NewCallback(js_VerifyPassword))

js_GetEncryptedCopy := func(params []js.Value) {
wallet_encrypted_error := "error"
var err error
var encrypted_bytes []byte
if Local_wallet_instance != nil {
encrypted_bytes, err = Local_wallet_instance.GetEncryptedCopy()
if err == nil {
wallet_encrypted_error = "success"
} else {
wallet_encrypted_error = err.Error()
}
}

typeu8array := js.TypedArrayOf(encrypted_bytes)
js.Global().Set("wallet_encrypted_dump", typeu8array)
typeu8array.Release()
js.Global().Set("wallet_encrypted_error", wallet_encrypted_error)
}

js.Global().Set("DERO_JS_GetEncryptedCopy", js.NewCallback(js_GetEncryptedCopy))

}

// if this remain empty, default 127.0.0.1:20206 is used
var daemon_address = "" // this is setup below at runtime

// this wasm module exports necessary wallet apis to javascript
func main() {

fmt.Printf("running go")
globals.Arguments = map[string]interface{}{}

globals.Arguments["--testnet"] = false

globals.Config = config.Mainnet
//globals.Initialize()

debug.SetGCPercent(40) // start GC at 40%

href := js.Global().Get("location").Get("href")
u, err := url.Parse(href.String())
if err == nil {
r := strings.NewReplacer("0", "",
"1", "",
"2", "",
"3", "",
"4", "",
"5", "",
"6", "",
"7", "",
"8", "",
"9", "",
".", "",
":", "",
)
rlog.Infof("u %+v", u)
rlog.Infof("scheme %+v", u.Scheme)
rlog.Infof("Host %+v", u.Host)
if u.Scheme == "http" || u.Scheme == "" { // we do not support DNS names for http, for security reasons
if len(r.Replace(u.Host)) == 0 { // number is an ipadress
if strings.Contains(u.Host, ":") {
daemon_address = u.Host // set the daemon address
} else {
daemon_address = u.Host + ":80" // set the daemon address
}
}
} else if u.Scheme == "https" {
if strings.Contains(u.Host, ":") {
daemon_address = u.Scheme + "://" + u.Host // set the daemon address
} else {
daemon_address = u.Scheme + "://" + u.Host + ":443" // set the daemon address
}
}

if len(daemon_address) == 0 {
if globals.IsMainnet() {
daemon_address = "127.0.0.1:20206"
} else {
daemon_address = "127.0.0.1:30306"
}
}

}

register_wallet_callbacks()
go update_balance()

select {} // if this return, program will exit
}

func update_balance() {

wallet_version_string := config.Version.String()
for {
unlocked_balance := uint64(0)
locked_balance := uint64(0)
total_balance := uint64(0)

wallet_height := uint64(0)
daemon_height := uint64(0)
wallet_topo_height := uint64(0)
daemon_topo_height := uint64(0)

wallet_initial_height := int64(0)

wallet_address := ""

wallet_available := false
wallet_complete := true
wallet_online := false
wallet_mixin := 5
wallet_fees_multiplier := float64(1.5)
wallet_daemon_address := ""
wallet_sync_time := int64(0)
wallet_minimum_topo_height := int64(-1)

if Local_wallet_instance != nil {
unlocked_balance, locked_balance = Local_wallet_instance.Get_Balance()

total_balance = unlocked_balance + locked_balance

wallet_height = Local_wallet_instance.Get_Height()
daemon_height = Local_wallet_instance.Get_Daemon_Height()
wallet_topo_height = uint64(Local_wallet_instance.Get_TopoHeight())
daemon_topo_height = uint64(Local_wallet_instance.Get_Daemon_TopoHeight())

wallet_address = Local_wallet_instance.GetAddress().String()
wallet_available = true

wallet_complete = !Local_wallet_instance.Is_View_Only()

wallet_initial_height = Local_wallet_instance.GetInitialHeight()

wallet_online = Local_wallet_instance.GetMode()

wallet_mixin = Local_wallet_instance.GetMixin()

wallet_fees_multiplier = float64(Local_wallet_instance.GetFeeMultiplier())
wallet_daemon_address = Local_wallet_instance.Daemon_Endpoint
wallet_sync_time = Local_wallet_instance.SetDelaySync(0)
wallet_minimum_topo_height = Local_wallet_instance.GetMinimumTopoHeight()

}
js.Global().Set("wallet_address", wallet_address)
js.Global().Set("total_balance", globals.FormatMoney12(total_balance))
js.Global().Set("locked_balance", globals.FormatMoney12(locked_balance))
js.Global().Set("unlocked_balance", globals.FormatMoney12(unlocked_balance))
js.Global().Set("wallet_height", wallet_height)
js.Global().Set("daemon_height", daemon_height)

js.Global().Set("wallet_topo_height", wallet_topo_height)
js.Global().Set("daemon_topo_height", daemon_topo_height)
js.Global().Set("wallet_available", wallet_available)
js.Global().Set("wallet_complete", wallet_complete)
js.Global().Set("wallet_initial_height", wallet_initial_height)

js.Global().Set("wallet_online", wallet_online)
js.Global().Set("wallet_mixin", wallet_mixin)
js.Global().Set("wallet_fees_multiplier", wallet_fees_multiplier)
js.Global().Set("wallet_daemon_address", wallet_daemon_address)
js.Global().Set("wallet_version_string", wallet_version_string)
js.Global().Set("wallet_sync_time", wallet_sync_time)
js.Global().Set("wallet_minimum_topo_height", wallet_minimum_topo_height)

time.Sleep(100 * time.Millisecond) // update max 10 times per second

}
}

var i32_address, i32_address_paymentid string
var i8_address, i8_address_paymentid string

// generate integrated address at user demand
func generate_integrated_address() {
if Local_wallet_instance != nil {

i8 := Local_wallet_instance.GetRandomIAddress8()
i32 := Local_wallet_instance.GetRandomIAddress32()

js.Global().Set("random_i32_address", i32.String())
js.Global().Set("random_i32_address_paymentid", fmt.Sprintf("%x", i32.PaymentID))

js.Global().Set("random_i8_address", i8.String())
js.Global().Set("random_i8_address_paymentid", fmt.Sprintf("%x", i8.PaymentID))

}

}

+ 91
- 26
walletapi/daemon_communication.go View File

@@ -79,10 +79,6 @@ func buildurl(endpoint string) string {
// this will tell whether the wallet can connection successfully to daemon or not
func (w *Wallet) IsDaemonOnline() (err error) {

if globals.Arguments["--remote"] == true && globals.IsMainnet() {
w.Daemon_Endpoint = config.REMOTE_DAEMON
}

// if user provided endpoint has error, use default
if w.Daemon_Endpoint == "" {
w.Daemon_Endpoint = "127.0.0.1:" + fmt.Sprintf("%d", config.Mainnet.RPC_Default_Port)
@@ -120,7 +116,7 @@ func (w *Wallet) IsDaemonOnline() (err error) {
// if daemon connection breaks or comes live again
if err == nil {
if !Connected {
rlog.Infof("Connection to RPC server successful %s", buildurl(w.Daemon_Endpoint))
rlog.Debugf("Connection to RPC server successful %s", buildurl(w.Daemon_Endpoint))
Connected = true
}
} else {
@@ -132,6 +128,8 @@ func (w *Wallet) IsDaemonOnline() (err error) {

}
Connected = false
w.Daemon_Height = 0
w.Daemon_TopoHeight = 0

return
}
@@ -140,14 +138,21 @@ func (w *Wallet) IsDaemonOnline() (err error) {
if err != nil {
rlog.Errorf("Daemon getinfo RPC parsing error err: %s\n", err)
Connected = false
w.Daemon_Height = 0
w.Daemon_TopoHeight = 0
return
}
rlog.Infof("info from rpcinfo %+v\n", info)
// detect whether both are in different modes
// daemon is in testnet and wallet in mainnet or
// daemon
if info.Testnet != !globals.IsMainnet() {
err = fmt.Errorf("Mainnet/TestNet is different between wallet/daemon.Please run daemon/wallet without --testnet")
rlog.Criticalf("%s", err)
w.Daemon_Height = 0
w.Daemon_TopoHeight = 0
return
}

@@ -157,6 +162,15 @@ func (w *Wallet) IsDaemonOnline() (err error) {
if info.Height >= 0 {
w.Daemon_Height = uint64(info.Height)
w.Daemon_TopoHeight = info.TopoHeight
if w.account.StartHeight == -1 && info.TopoHeight > 1000000{
w.account.StartHeight = info.TopoHeight - 1000
}
// activate booster if we are lagging behind
if ((w.Daemon_TopoHeight - w.account.TopoHeight) > 1000 ){
w.account.booster = time.Now() // this will activate booster
}
}
w.dynamic_fees_per_kb = info.Dynamic_fee_per_kb // set fee rate, it can work for quite some time,

@@ -282,32 +296,47 @@ func (w *Wallet) Sync_Wallet_With_Daemon() {
return
}

rlog.Infof("wallet topo height %d daemon online topo height %d\n", w.account.TopoHeight, w.Daemon_TopoHeight)
// the daemon must first setup the height properly, then only we will proceed
if w.account.StartHeight <= -1 {
return
}

start_height, err := w.DetectSyncPoint()
if err != nil {
rlog.Infof("wallet topo height %d daemon online topo height %d\n", w.account.TopoHeight, w.Daemon_TopoHeight)
start_height := w.account.TopoHeight
if start_height < w.account.StartHeight {
start_height = w.account.StartHeight
}

if start_height > 25 {
start_height = start_height - 25
}
if runtime.GOOS != "js" {
start_height_tmp, err := w.DetectSyncPoint()
if err != nil {
rlog.Errorf("Error while detecting sync point err %s", err)
return
}
return
}
start_height = int64(start_height_tmp)
}

// the safety cannot be tuned off in openbsd, see boltdb documentation
// if we are doing major rescanning, turn of db safety features
// they will be activated again on resync


if (w.Daemon_TopoHeight - int64(start_height)) > 50 { // get db into async mode
w.Lock()
w.db.NoSync = true
w.Unlock()
defer func() {
w.Lock()
w.db.NoSync = false
w.db.Sync()
w.Unlock()
}()
w.disable_sync()
defer w.enable_sync()
}

rlog.Infof("requesting outputs from height %d\n", start_height)
response, err := http.Get(fmt.Sprintf("%s/getoutputs.bin?startheight=%d",buildurl(w.Daemon_Endpoint), start_height))
response, err := http.Get(fmt.Sprintf("%s/getoutputs.bin?startheight=%d",buildurl(w.Daemon_Endpoint), start_height))
if err != nil {
rlog.Errorf("Error while requesting outputs from daemon err %s", err)
} else {
@@ -327,9 +356,13 @@ func (w *Wallet) Sync_Wallet_With_Daemon() {
for i := 0; i < runtime.GOMAXPROCS(0); i++ {
workers <- i
}

for {
var output globals.TX_Output_Data

err = decoder.Decode(&output)
if err == io.EOF { // reached eof
break
@@ -344,6 +377,13 @@ func (w *Wallet) Sync_Wallet_With_Daemon() {
return
default:
}
if runtime.GOOS == "js" {
w.Add_Transaction_Record_Funds(&output) // add the funds to wallet if they are ours
}else{


<-workers
// try to consume all data sent by the daemon
@@ -351,6 +391,7 @@ func (w *Wallet) Sync_Wallet_With_Daemon() {
w.Add_Transaction_Record_Funds(&output) // add the funds to wallet if they are ours
workers <- 0
}()
}

}

@@ -361,14 +402,38 @@ func (w *Wallet) Sync_Wallet_With_Daemon() {
return
}

// triggers syncing with wallet every 5 seconds
// triggers syncing with wallet every Y seconds ( configurable)
// less time means, higher resources consumption(network/compute) basically inversely proportional
func (w *Wallet) sync_loop() {
counter := int64(86400) // initial loop must go through ASAP
for {
if w.account.delay_time < 7 { // make sure time is sane
w.account.delay_time = 7
}
select { // quit midway if required
case <-w.quit:
return
case <-time.After(5 * time.Second):
case <-time.After( time.Second):
}
// if we are lagging behind do fast sync
if (w.Daemon_TopoHeight - w.account.TopoHeight) > 100 {
counter = counter + 86400
}
if counter < w.account.delay_time {
counter++
// check whether booster is active, it activates after TX is sent, for 4 minutes
// booster triggers every 7 seconds
if counter >= 7 && (time.Now().Sub(w.account.booster) < 4 * 60 * time.Second) { // apply booster
// rlog.Warnf("booster is active")
}else{
continue
}
}
counter = 0

if !w.wallet_online_mode { // wallet requested to be in offline mode
return
@@ -463,6 +528,7 @@ func (w *Wallet) SendTransaction(tx *transaction.Transaction) (err error) {
}

if result.Status == "OK" {
w.account.booster = time.Now() // this will activate booster
return nil
} else {
err = fmt.Errorf("Err %s", result.Status)
@@ -480,7 +546,6 @@ func (w *Wallet) SendTransaction(tx *transaction.Transaction) (err error) {
// maybe the server can broadcast a bloomfilter or something else from the mempool keyimages
//
func (w *Wallet) IsKeyImageSpent(keyimage crypto.Key) (spent bool) {

defer func() {
if r := recover(); r != nil {
rlog.Warnf("Recovered while adding new block, Stack trace below keyimage %s", keyimage)
@@ -495,7 +560,7 @@ func (w *Wallet) IsKeyImageSpent(keyimage crypto.Key) (spent bool) {

spent = true // default assume the funds are spent

rlog.Warnf("checking whether key image are spent in pool %s", keyimage)
//rlog.Warnf("checking whether key image are spent in pool %s", keyimage)

var params structures.Is_Key_Image_Spent_Params
var result structures.Is_Key_Image_Spent_Result


+ 16
- 0
walletapi/db.go View File

@@ -1,3 +1,5 @@
// +build !wasm

package walletapi

import "os"
@@ -651,3 +653,17 @@ func itob(v uint64) []byte {
binary.BigEndian.PutUint64(b, uint64(v))
return b
}


func (w *Wallet) disable_sync(){
w.Lock()
w.db.NoSync = true
w.Unlock()
}

func (w *Wallet) enable_sync(){
w.Lock()
w.db.NoSync = false
w.db.Sync()
w.Unlock()
}

+ 720
- 0
walletapi/db_mem.go View File

@@ -0,0 +1,720 @@
// +build js,wasm

package walletapi

//import "os"
import "fmt"

//import "time"
//import "bytes"
import "runtime"
import "crypto/rand"
import "crypto/sha1"
import "sync"
import "strings"

//import "encoding/gob"
import "encoding/hex"
import "encoding/json"
import "encoding/binary"

import "github.com/romana/rlog"
import "github.com/vmihailenco/msgpack"

import "github.com/blang/semver"
import "golang.org/x/crypto/pbkdf2" // // used to encrypt master password ( so user can change his password anytime)

import "github.com/deroproject/derosuite/crypto"
import "github.com/deroproject/derosuite/walletapi/mnemonics"

const FUNDS_BUCKET = "FUNDS" // stores all incoming funds, key is global output index encrypted form
const FUNDS_AVAILABLE = "FUNDS_AVAILABLE" // indices of all funds ready to spend
const FUNDS_SPENT = "FUNDS_SPENT" // indices of all funds already spent
const FUNDS_SPENT_WHERE = "FUNDS_SPENT_WHERE" // mapping which output -> spent where
const FUNDS_BUCKET_OUTGOING = "FUNDS_OUTGOING" // stores all tx where our funds were spent

const RING_BUCKET = "RING_BUCKET" // to randomly choose ring members when transactions are created
const KEYIMAGE_BUCKET = "KEYIMAGE_BUCKET" // used to track which funds have been spent (only on chain ) and which output was consumed
const SECRET_KEY_BUCKET = "TXSECRETKEY" // used to keep secret keys for any tx this wallet has created
const TX_OUT_DETAILS_BUCKET = "TX_OUT_DETAILS" // used to keep secret keys for any tx this wallet has created

const HEIGHT_TO_BLOCK_BUCKET = "H2BLOCK_BUCKET" // used to track height to block hash mapping
const OUR_TX_BUCKET = "OUR_TX_BUCKET" // this contains all TXs in which we have spent OUR FUNDS

// the strings are prepended so as there can NEVER be collision between TXID and payment ID
// as paymentID are chosen by users
const TXID = "TXID" // all TX to output index will have this string prepended
const PAYID = "PAYID" // all payment ID to output index will have this string prepended,

// PAYMENT ID itself is a bucket, TODO COLLISIONS ???
//const FUNDS_BY_PAYMENT_ID_BUCKET ="PAYMENTID_BUCKET" // each payment id is a bucket itself, which stores OUTPUT_INDEX

//const FUNDS_BY_PAYMENT_ID_BUCKET = 10 // each payment id is a bucket itself
const FUNDS_BY_TX_ID_BUCKET = 11 // each tx id is a bucket
const FUNDS_BY_BLOCK_HEIGHT_BUCKET = 12 // funds sorted by block height
const FUNDS_SPENT_BY_BLOCK_HEIGHT_BUCKET = 13 // funds spent by block height
const NOTES_BY_TXID = 14 // any notes attached

// the db has 3 buckets, one
// funds ( funds which have arrived ), they can have attached , notes addresses, funds which payment id are linked to
// funds_available, contains only output indices
// funds_spent, contains only output indices
// searchable by payment id and searhable by txid and searchable by block height

// fundstate : available, spent, spentinpool, fund state works as state machine
// search_by_paymentid ( funds from a specific payment id can be accessed here )
// search_by_txid ( funds from a specific payment id can be accessed here )
// search_by_block_height ( funds from a specific payment id can be accessed here )

// address book will have random number based entries

// see this https://godoc.org/golang.org/x/crypto/pbkdf2
type KDF struct {
Hashfunction string `json:"hash"` //"SHA1" currently only sha1 is supported
Keylen int `json:"keylen"`
Iterations int `json:"iterations"`
Salt []byte `json:"salt"`
}

// this is stored in disk in encrypted form
type Wallet struct {
Version semver.Version `json:"version"` // database version
Secret []byte `json:"secret"` // actual unlocker to the DB, depends on password from user, stored encrypted
// secret key used to encrypt all DB data ( both keys and values )
// this is always in encrypted form

KDF KDF `json:"kdf"`

account *Account //`json:"-"` // not serialized, we store an encrypted version // keys, seed language etc settings
Account_Encrypted []byte `json:"account_encrypted"`

pbkdf2_password []byte // used to encrypt metadata on updates
master_password []byte // single password which never changes

Daemon_Endpoint string `json:"-"` // endpoint used to communicate with daemon
Daemon_Height uint64 `json:"-"` // used to track daemon height ony if wallet in online
Daemon_TopoHeight int64 `json:"-"` // used to track daemon topo height ony if wallet in online

wallet_online_mode bool // set whether the mode is online or offline
// an offline wallet can be converted to online mode, calling.
// SetOffline() and vice versa using SetOnline
// used to create transaction with this fee rate,
//if this is lower than network, then created transaction will be rejected by network
dynamic_fees_per_kb uint64
quit chan bool // channel to quit any processing go routines

// db *bolt.DB // access to DB

DB_mem map[string]map[string]map[string][]byte `json:"DB_mem"`

rpcserver *RPCServer // reference to RPCserver

id string // first 8 bytes of wallet address , to put into logs to identify different wallets in case many are active

db_mem_mutex sync.RWMutex
transfer_mutex sync.Mutex // to avoid races within the transfer
//sync.Mutex // used to syncronise access
sync.RWMutex
}

const META_BUCKET = "METADATA" // all metadata is stored in this bucket
const BLOCKS_BUCKET = "BLOCKS" // stores height to block hash mapping for later on syncing

var BLOCKCHAIN_UNIVERSE = []byte("BLOCKCHAIN_UNIVERSE") // all main chain txs are stored in this bucket

// when smart contracts are implemented, each will have it's own universe to track and maintain transactions

// this file implements the encrypted data store at rest
func Create_Encrypted_Wallet(filename string, password string, seed crypto.Key) (w *Wallet, err error) {
rlog.Infof("Creating Wallet from recovery seed")
w = &Wallet{}
w.Version, err = semver.Parse("0.0.1-alpha.preview.github")

if err != nil {
return
}

/*
if _, err = os.Stat(filename); err == nil {
err = fmt.Errorf("File '%s' already exists", filename)
rlog.Errorf("err creating wallet %s", err)
return
}
*/

w.DB_mem = map[string]map[string]map[string][]byte{}

/*
w.db, err = bolt.Open(filename, 0600, &bolt.Options{Timeout: 1 * time.Second})

if err != nil {
rlog.Errorf("err opening boltdb file %s", err)
return
}
*/
// generate account keys
w.account, err = Generate_Account_From_Seed(seed)
if err != nil {
return
}

// generate a 64 byte key to be used as master Key
w.master_password = make([]byte, 32, 32)
_, err = rand.Read(w.master_password)
if err != nil {
return
}

err = w.Set_Encrypted_Wallet_Password(password) // lock the db with the password

w.quit = make(chan bool)

w.id = string((w.account.GetAddress().String())[:8]) // set unique id for logs

rlog.Infof("Successfully created wallet %s", w.id)
return
}

// create an encrypted wallet using electrum recovery words
func Create_Encrypted_Wallet_From_Recovery_Words(filename string, password string, electrum_seed string) (w *Wallet, err error) {
rlog.Infof("Creating Wallet from recovery words")

language, seed, err := mnemonics.Words_To_Key(electrum_seed)
if err != nil {
rlog.Errorf("err parsing recovery words %s", err)
return
}
w, err = Create_Encrypted_Wallet(filename, password, seed)

if err != nil {
rlog.Errorf("err creating wallet %s", err)
return
}

w.account.SeedLanguage = language
rlog.Infof("Successfully created wallet %s", w.id)
return
}

// create an encrypted wallet using using random data
func Create_Encrypted_Wallet_Random(filename string, password string) (w *Wallet, err error) {
rlog.Infof("Creating Wallet Randomly")
w, err = Create_Encrypted_Wallet(filename, password, *crypto.RandomScalar())

if err != nil {
rlog.Errorf("err %s", err)
return
}
w.account.StartHeight = -1 // new accounts are automatically set to ignore previous chain

// TODO setup seed language, default is already english
rlog.Infof("Successfully created wallet %s", w.id)
return
}

// create an encrypted wallet using using random data
func Create_Encrypted_Wallet_ViewOnly(filename string, password string, viewkey string) (w *Wallet, err error) {

var public_spend, private_view crypto.Key
rlog.Infof("Creating View Only Wallet")
view_raw, err := hex.DecodeString(strings.TrimSpace(viewkey))
if len(view_raw) != 64 || err != nil {
err = fmt.Errorf("View Only key must be 128 chars hexadecimal chars")
rlog.Errorf("err %s", err)
return
}

copy(public_spend[:], view_raw[:32])
copy(private_view[:], view_raw[32:64])

// create encrypted wallet randomly and then swap the keys
w, err = Create_Encrypted_Wallet(filename, password, *crypto.RandomScalar())

if err != nil {
rlog.Errorf("err %s", err)
return
}

// swap the keys
w.account.Keys.Spendkey_Public = public_spend
w.account.Keys.Viewkey_Secret = private_view
w.account.Keys.Viewkey_Public = *(private_view.PublicKey())
w.account.ViewOnly = true

w.Save_Wallet() // save wallet data
rlog.Infof("Successfully created view only wallet %s", w.id)
return
}

// create an encrypted wallet using using random data
func Create_Encrypted_Wallet_NonDeterministic(filename string, password string, secretkey, viewkey string) (w *Wallet, err error) {

var secret_spend, secret_view crypto.Key
rlog.Infof("Creating View Only Wallet")
spend_raw, err := hex.DecodeString(strings.TrimSpace(secretkey))
if len(spend_raw) != 32 || err != nil {
err = fmt.Errorf("View Only key must be 64 chars hexadecimal chars")
rlog.Errorf("err %s", err)
return
}

copy(secret_spend[:], spend_raw[:32])

view_raw, err := hex.DecodeString(strings.TrimSpace(viewkey))
if len(view_raw) != 32 || err != nil {
err = fmt.Errorf("Spend Only key must be 64 chars hexadecimal chars")
rlog.Errorf("err %s", err)
return
}

copy(secret_view[:], view_raw[:32])

// create encrypted wallet randomly and then swap the keys
w, err = Create_Encrypted_Wallet(filename, password, *crypto.RandomScalar())

if err != nil {
rlog.Errorf("err %s", err)
return
}

// swap the keys
w.account.Keys.Spendkey_Secret = secret_spend
w.account.Keys.Spendkey_Public = *(secret_spend.PublicKey())
w.account.Keys.Viewkey_Secret = secret_view
w.account.Keys.Viewkey_Public = *(secret_view.PublicKey())

w.Save_Wallet() // save wallet data
rlog.Infof("Successfully created view only wallet %s", w.id)
return
}

// wallet must already be open
func (w *Wallet) Set_Encrypted_Wallet_Password(password string) (err error) {

if w == nil {
return
}
w.Lock()

// set up KDF structure
w.KDF.Salt = make([]byte, 32, 32)
_, err = rand.Read(w.KDF.Salt)
if err != nil {
w.Unlock()
return
}
w.KDF.Keylen = 32
w.KDF.Iterations = 262144
if runtime.GOOS == "js" {
w.KDF.Iterations = 64 * 1024 // mobiles need to save battery
}
w.KDF.Hashfunction = "SHA1"

// lets generate the bcrypted password

w.pbkdf2_password = Generate_Key(w.KDF, password)

w.Unlock()
w.Save_Wallet() // save wallet data

return
}

func Open_Encrypted_Wallet(filename string, password string, filedata []byte) (w *Wallet, err error) {
w = &Wallet{}

if len(filedata) < 512 {
return nil, fmt.Errorf("Invalid/Corrupt waalet file")
}

w.DB_mem = map[string]map[string]map[string][]byte{}

err = msgpack.Unmarshal(filedata, &w)
if err != nil {
rlog.Warnf("Loading Wallet err %s\n", err)
return nil, err
}

/*
w.db, err = bolt.Open(filename, 0600, &bolt.Options{Timeout: 1 * time.Second})

if err != nil {
rlog.Errorf("err opening boltdb %s", err)
return
}

// read the metadata from metadat bucket
w.db.View(func(tx *bolt.Tx) error {
// Assume bucket exists and has keys
b := tx.Bucket([]byte(META_BUCKET))

v := b.Get([]byte(META_BUCKET))

if v == nil || len(v) == 0 {
err = fmt.Errorf("Invalid Database, Could not find meta data")
rlog.Errorf("err opening wallet %s", err)
return err
}

//fmt.Printf("v %+v\n",string(v)) // DO NOT dump account keys

// deserialize json data
err = json.Unmarshal(v, &w)
if err != nil {
rlog.Errorf("err parsing metabucket %s", err)
return err
}


// todo make any routines necessary, such as sync etc

return nil
})*/

// try to deseal password and store it
w.pbkdf2_password = Generate_Key(w.KDF, password)

// try to decrypt the master password with the pbkdf2
w.master_password, err = DecryptWithKey(w.pbkdf2_password, w.Secret) // decrypt the master key
if err != nil {
rlog.Errorf("err opening secret err: %s ", err)
err = fmt.Errorf("Invalid Password")
//w.db.Close()
w = nil
return
}

// password has been found, open the account

account_bytes, err := w.Decrypt(w.Account_Encrypted)
if err != nil {
rlog.Errorf("err opening account err: %s ", err)
err = fmt.Errorf("Invalid Password")
//w.db.Close()
w = nil
return
}

w.account = &Account{} // allocate a new instance
err = json.Unmarshal(account_bytes, w.account)
if err != nil {
return
}

w.id = string((w.account.GetAddress().String())[:8]) // set unique id for logs

w.quit = make(chan bool)

return

}

// check whether the already opened wallet can use this password
func (w *Wallet) Check_Password(password string) bool {
w.Lock()
defer w.Unlock()
if w == nil {
return false
}

pbkdf2_password := Generate_Key(w.KDF, password)

// TODO we can compare pbkdf2_password & w.pbkdf2_password, if they are equal password is vaid

// try to decrypt the master password with the pbkdf2
_, err := DecryptWithKey(pbkdf2_password, w.Secret) // decrypt the master key

if err == nil {
return true
}
rlog.Warnf("%s Invalid Password", w.id)
return false

}

// save updated copy of wallet
func (w *Wallet) Save_Wallet() (err error) {
w.Lock()
defer w.Unlock()
if w == nil {
return
}

// encrypted the master password with the pbkdf2
w.Secret, err = EncryptWithKey(w.pbkdf2_password[:], w.master_password) // encrypt the master key
if err != nil {
return
}

// encrypt the account

account_serialized, err := json.Marshal(w.account)
if err != nil {
return
}
w.Account_Encrypted, err = w.Encrypt(account_serialized)
if err != nil {
return
}

/*
// json marshal wallet data struct, serialize it, encrypt it and store it
serialized, err := json.Marshal(&w)
if err != nil {
return
}
//fmt.Printf("Serialized %+v\n",serialized)

// let save the secret to DISK in encrypted form
err = w.db.Update(func(tx *bolt.Tx) (err error) {

bucket, err := tx.CreateBucketIfNotExists([]byte(META_BUCKET))
if err != nil {
return
}
err = bucket.Put([]byte(META_BUCKET), serialized)
return

})

*/
rlog.Infof("Saving wallet %s", w.id)
return
}

// close the wallet
func (w *Wallet) Close_Encrypted_Wallet() {
close(w.quit)

//time.Sleep(time.Second) // give goroutines some time to quit
rlog.Infof("Saving and Closing Wallet %s\n", w.id)
w.Save_Wallet()
w = nil
//w.DB_mem = nil // go GC will collect the items
// w.db.Sync()

// w.db.Close()
}

// this will give out an encrypted copy of the wallet
func (w *Wallet) GetEncryptedCopy() ([]byte, error) {

var dump_bytes []byte

if w == nil {
return dump_bytes, fmt.Errorf("Wallet is nil")
}

w.Save_Wallet()

w.Lock()
defer w.Unlock()

// Create an encoder and send a value.

/*enc := gob.NewEncoder(&dump)
err := enc.Encode(w)
*/
dump_bytes, err := msgpack.Marshal(&w)
if err != nil {
rlog.Warnf("Dumping Wallet err %s\n", err)
return dump_bytes, err
}

rlog.Infof("Dumping Wallet completed Successfully %d bytes", len(dump_bytes))

return dump_bytes, nil
}

// generate key from password
func Generate_Key(k KDF, password string) (key []byte) {
switch k.Hashfunction {
case "SHA1":
return pbkdf2.Key([]byte(password), k.Salt, k.Iterations, k.Keylen, sha1.New)

default:
return pbkdf2.Key([]byte(password), k.Salt, k.Iterations, k.Keylen, sha1.New)
}
}

// check whether a key exists
func (w *Wallet) check_key_exists(universe []byte, subbucket []byte, key []byte) (result bool) {

w.db_mem_mutex.RLock()
defer w.db_mem_mutex.RUnlock()
//fmt.Printf("Checking %s %s %x \n", string(universe), string(subbucket), key)

universe_map := w.DB_mem[string(w.Key2Key(universe))] //open universe bucket
if universe_map == nil {
return false
}
bucket := universe_map[string(w.Key2Key(subbucket))] // open subbucket
if bucket == nil {
return false
}

v := bucket[string(w.Key2Key(key))]

if v != nil {
// fmt.Printf("Found\n")
result = true
}

return // default is false
}

// delete specified key
func (w *Wallet) delete_key(universe []byte, subbucket []byte, key []byte) {
rlog.Tracef(1, "Deleting %s %s %x\n", string(universe), string(subbucket), key)

w.db_mem_mutex.Lock()
defer w.db_mem_mutex.Unlock()
//fmt.Printf("Checking %s %s %x \n", string(universe), string(subbucket), key)

universe_map := w.DB_mem[string(w.Key2Key(universe))] //open universe bucket
if universe_map == nil {
return
}
bucket := universe_map[string(w.Key2Key(subbucket))] // open subbucket
if bucket == nil {
return
}

delete(bucket, string(w.Key2Key(key)))

}

// delete specified key
func (w *Wallet) delete_bucket(universe []byte, subbucket []byte) {
rlog.Tracef(1, "Deleting bucket %s %s \n", string(universe), string(subbucket))

w.db_mem_mutex.Lock()
defer w.db_mem_mutex.Unlock()

universe_map := w.DB_mem[string(w.Key2Key(universe))] //open universe bucket
if universe_map == nil {
return
}
delete(universe_map, string(w.Key2Key(subbucket)))

}

// store a key-value, everything is encrypted
func (w *Wallet) store_key_value(universe []byte, subbucket []byte, key []byte, value []byte) error {

rlog.Tracef(1, "Storing %s %s %x\n", string(universe), string(subbucket), key)

w.db_mem_mutex.Lock()
defer w.db_mem_mutex.Unlock()

universe_map := w.DB_mem[string(w.Key2Key(universe))] //open universe bucket
if universe_map == nil {
universe_map = map[string]map[string][]byte{}
w.DB_mem[string(w.Key2Key(universe))] = universe_map

}
bucket := universe_map[string(w.Key2Key(subbucket))] // open subbucket
if bucket == nil {
bucket = map[string][]byte{}
w.DB_mem[string(w.Key2Key(universe))][string(w.Key2Key(subbucket))] = bucket
}

encrypted_value, err := w.Encrypt(value) // encrypt and seal the value
if err != nil {
return err
}
w.DB_mem[string(w.Key2Key(universe))][string(w.Key2Key(subbucket))][string(w.Key2Key(key))] = encrypted_value

return nil
}

func (w *Wallet) load_key_value(universe []byte, subbucket []byte, key []byte) (value []byte, err error) {

rlog.Tracef(1, "loading %s %s %x\n", string(universe), string(subbucket), key)

w.db_mem_mutex.RLock()
defer w.db_mem_mutex.RUnlock()

universe_map := w.DB_mem[string(w.Key2Key(universe))] //open universe bucket
if universe_map == nil {
return

}
bucket := universe_map[string(w.Key2Key(subbucket))] // open subbucket
if bucket == nil {
return
}

v := bucket[string(w.Key2Key(key))]
if v == nil {
return value, fmt.Errorf("%s %s %x NOT Found", string(universe), string(subbucket), key)
}

// fmt.Printf("length of encrypted value %d\n",len(v))

value, err = w.Decrypt(v)

return
}

// enumerate all keys from the bucket
// due to design enumeration is impossible,
// however, all the keys of a specific bucket, where necessary are added as values,
// for example, we never enumerate our funds, if we donot store them in FUNDS_AVAILABLE or FUNDS_SPENT bucket
// so we find all values in FUNDS_AVAILABLE and FUNDS_SPENT bucket, and then decode value to recover the funds
// this function should only be called for FUNDS_AVAILABLE or FUNDS_SPENT bucket
func (w *Wallet) load_all_values_from_bucket(universe []byte, subbucket []byte) (values [][]byte) {

w.db_mem_mutex.RLock()
defer w.db_mem_mutex.RUnlock()

universe_map := w.DB_mem[string(w.Key2Key(universe))] //open universe bucket
if universe_map == nil {
return

}
bucket := universe_map[string(w.Key2Key(subbucket))] // open subbucket
if bucket == nil {
return
}

for _, v := range bucket {
value, err := w.Decrypt(v)
if err == nil {
values = append(values, value)
}
}

return
}

func (w *Wallet) load_ring_member(index_global uint64) (r Ring_Member, err error) {

// store all data
data_bytes, err := w.load_key_value(BLOCKCHAIN_UNIVERSE, []byte(RING_BUCKET), itob(index_global))

if err != nil {
return
}

err = msgpack.Unmarshal(data_bytes, &r)

return
}

// itob returns an 8-byte big endian representation of v.
func itob(v uint64) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, uint64(v))
return b
}

// place holders
func (w *Wallet) disable_sync(){
}

func (w *Wallet) enable_sync(){

}

+ 1
- 9
walletapi/rpc_get_transfer_by_txid.go View File

@@ -63,9 +63,6 @@ func (h Get_Transfer_By_TXID_Handler) ServeJSONRPC(c context.Context, params *fa
Unlock_time: entry.Unlock_Time,

}
if entry.Height == 0 {
return nil, &jsonrpc.Error{Code: -8, Message: fmt.Sprintf("Transaction not found. TXID %s", p.TXID)}
}

for i := range entry.Details.Daddress {
result.Transfer.Destinations = append(result.Transfer.Destinations,
@@ -79,12 +76,7 @@ func (h Get_Transfer_By_TXID_Handler) ServeJSONRPC(c context.Context, params *fa
result.Transfer.Payment_ID = entry.Details.PaymentID
}

if entry.Status == 0 { // if we have an amount
result.Transfer.Type = "in"
// send the result
return result, nil