539 lines
16 KiB
Go
539 lines
16 KiB
Go
// Copyright 2017-2021 DERO Project. All rights reserved.
|
|
// Use of this source code in any form is governed by RESEARCH license.
|
|
// license can be found in the LICENSE file.
|
|
// GPG: 0F39 E425 8C65 3947 702A 8234 08B2 0360 A03A 9DE8
|
|
//
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
|
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
|
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
|
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
|
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
|
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
package walletapi
|
|
|
|
import "fmt"
|
|
import "net"
|
|
import "sort"
|
|
import "sync"
|
|
import "time"
|
|
import "bytes"
|
|
import "strings"
|
|
import "math/big"
|
|
import "crypto/rand"
|
|
|
|
//import "encoding/json"
|
|
//import "encoding/binary"
|
|
|
|
//import "github.com/romana/rlog"
|
|
//import "github.com/vmihailenco/msgpack"
|
|
|
|
//import "github.com/deroproject/derohe/config"
|
|
import "github.com/deroproject/derohe/structures"
|
|
import "github.com/deroproject/derohe/crypto"
|
|
import "github.com/deroproject/derohe/crypto/bn256"
|
|
|
|
//import "github.com/deroproject/derosuite/crypto/ringct"
|
|
//import "github.com/deroproject/derohe/globals"
|
|
import "github.com/deroproject/derohe/walletapi/mnemonics"
|
|
import "github.com/deroproject/derohe/address"
|
|
import "github.com/deroproject/derohe/transaction"
|
|
|
|
//import "github.com/deroproject/derohe/blockchain/inputmaturity"
|
|
|
|
type _Keys struct {
|
|
Secret *crypto.BNRed `json:"secret"`
|
|
Public *crypto.Point `json:"public"`
|
|
}
|
|
|
|
var Balance_lookup_table *LookupTable
|
|
|
|
type Account struct {
|
|
Keys _Keys `json:"keys"`
|
|
SeedLanguage string `json:"seedlanguage"`
|
|
FeesMultiplier float32 `json:"feesmultiplier"` // fees multiplier accurate to 2 decimals
|
|
Ringsize int `json:"ringsize"` // default mixn to use for txs
|
|
mainnet bool
|
|
|
|
Height uint64 `json:"height"` // block height till where blockchain has been scanned
|
|
TopoHeight int64 `json:"topoheight"` // block height till where blockchain has been scanned
|
|
|
|
Balance_Mature uint64 `json:"balance_mature"` // total balance of account
|
|
Balance_Locked uint64 `json:"balance_locked"` // balance locked
|
|
|
|
Balance_Result structures.GetEncryptedBalance_Result // used to cache last successful result
|
|
|
|
Entries []Entry // all tx entries, basically transaction statement
|
|
|
|
RingMembers map[string]int64 `json:"ring_members"` // ring members
|
|
|
|
sync.Mutex // syncronise modifications to this structure
|
|
}
|
|
|
|
// these structures are completely decoupled from blockchain and live only within the wallet
|
|
// all inputs and outputs which modify balance are presented by this structure
|
|
type Entry struct {
|
|
Height uint64 `json:"height"`
|
|
TopoHeight int64 `json:"topoheight"`
|
|
BlockHash string `json:"blockhash"`
|
|
MinerReward uint64 `json:"minerreward"`
|
|
TransactionPos int `json:"poswithinblock"` // pos within block is negative for coinbase
|
|
Coinbase bool `json:"coinbase"`
|
|
Incoming bool `json:"incoming"`
|
|
TXID crypto.Hash `json:"txid"`
|
|
Amount uint64 `json:"amount"`
|
|
Fees uint64 `json:"fees"`
|
|
PaymentID []byte `json:"payment_id"`
|
|
Proof string `json:"proof"`
|
|
Status byte `json:"status"`
|
|
Unlock_Time uint64 `json:"unlock_time"`
|
|
Time time.Time `json:"time"`
|
|
EWData string `json:"ewdata"` // encrypted wallet balance at that point in time
|
|
Secret_TX_Key string `json:"secret_tx_key"` // can be used to prove if available
|
|
Details structures.Outgoing_Transfer_Details `json:"details"` // actual details if available
|
|
}
|
|
|
|
// add a entry in the suitable place
|
|
// this is always single threaded
|
|
func (w *Wallet) InsertReplace(e Entry) {
|
|
|
|
i := sort.Search(len(w.account.Entries), func(j int) bool {
|
|
return w.account.Entries[j].TopoHeight >= e.TopoHeight && w.account.Entries[j].TransactionPos >= e.TransactionPos
|
|
})
|
|
|
|
// entry already exists, we are probably rescanning/overwiting, delete anything afterwards
|
|
if i < len(w.account.Entries) && w.account.Entries[i].TopoHeight == e.TopoHeight && w.account.Entries[i].TransactionPos == e.TransactionPos {
|
|
w.account.Entries = w.account.Entries[:i]
|
|
// x is present at data[i]
|
|
} else {
|
|
// x is not present in data,
|
|
// but i is the index where it would be inserted.
|
|
}
|
|
w.account.Entries = append(w.account.Entries, e)
|
|
|
|
}
|
|
|
|
// generate keys from using random numbers
|
|
func Generate_Keys_From_Random() (user *Account, err error) {
|
|
user = &Account{Ringsize: 4, FeesMultiplier: 1.5}
|
|
seed := crypto.RandomScalarBNRed()
|
|
user.Keys = Generate_Keys_From_Seed(seed)
|
|
|
|
return
|
|
}
|
|
|
|
// generate keys from seed which is from the recovery words
|
|
// or we feed in direct
|
|
func Generate_Keys_From_Seed(Seed *crypto.BNRed) (keys _Keys) {
|
|
|
|
// setup main keys
|
|
keys.Secret = Seed
|
|
keys.Public = crypto.GPoint.ScalarMult(Seed)
|
|
|
|
return
|
|
}
|
|
|
|
// generate user account using recovery seeds
|
|
func Generate_Account_From_Recovery_Words(words string) (user *Account, err error) {
|
|
user = &Account{Ringsize: 4, FeesMultiplier: 1.5}
|
|
language, seed, err := mnemonics.Words_To_Key(words)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
user.SeedLanguage = language
|
|
user.Keys = Generate_Keys_From_Seed(crypto.GetBNRed(seed))
|
|
|
|
return
|
|
}
|
|
|
|
func Generate_Account_From_Seed(Seed *crypto.BNRed) (user *Account, err error) {
|
|
user = &Account{Ringsize: 4, FeesMultiplier: 1.5}
|
|
|
|
// TODO check whether the seed is invalid
|
|
user.Keys = Generate_Keys_From_Seed(Seed)
|
|
|
|
return
|
|
}
|
|
|
|
// convert key to seed using language
|
|
func (w *Wallet) GetSeed() (str string) {
|
|
return mnemonics.Key_To_Words(w.account.Keys.Secret.BigInt(), w.account.SeedLanguage)
|
|
}
|
|
|
|
// convert key to seed using language
|
|
func (w *Wallet) GetSeedinLanguage(lang string) (str string) {
|
|
return mnemonics.Key_To_Words(w.account.Keys.Secret.BigInt(), lang)
|
|
}
|
|
|
|
func (account *Account) GetAddress() (addr address.Address) {
|
|
addr.PublicKey = account.Keys.Public
|
|
return
|
|
}
|
|
|
|
// convert a user account to address
|
|
func (w *Wallet) GetAddress() (addr address.Address) {
|
|
addr = w.account.GetAddress()
|
|
addr.Mainnet = w.account.mainnet
|
|
return addr
|
|
}
|
|
|
|
// get a random integrated address
|
|
func (w *Wallet) GetRandomIAddress8() (addr address.Address) {
|
|
addr = w.GetAddress()
|
|
|
|
// setup random 8 bytes of payment ID, it must be from non-deterministic RNG namely crypto random
|
|
addr.PaymentID = make([]byte, 8, 8)
|
|
rand.Read(addr.PaymentID[:])
|
|
|
|
return
|
|
}
|
|
|
|
func (w *Wallet) Get_Balance_Rescan() (mature_balance uint64, locked_balance uint64) {
|
|
return w.Get_Balance()
|
|
}
|
|
|
|
// get the unlocked balance ( amounts which are mature and can be spent at this time )
|
|
// offline wallets may get this wrong, since they may not have latest data
|
|
|
|
//
|
|
func (w *Wallet) Get_Balance() (mature_balance uint64, locked_balance uint64) {
|
|
return w.account.Balance_Mature, 0
|
|
|
|
}
|
|
|
|
// finds all inputs which have been received/spent etc
|
|
// TODO this code can be easily parallelised and need to be parallelised
|
|
// if only the availble is requested, then the wallet is very fast
|
|
// the spent tracking may make it slow ( in case of large probably million txs )
|
|
//TODO currently we do not track POOL at all any where ( except while building tx)
|
|
// if payment_id is true, only entries with payment ids are returned
|
|
// min_height/max height represent topoheight
|
|
func (w *Wallet) Show_Transfers(available bool, in bool, out bool, pool bool, failed bool, payment_id bool, min_height, max_height uint64) (entries []Entry) {
|
|
|
|
// dero_first_block_time := time.Unix(1512432000, 0) //Tuesday, December 5, 2017 12:00:00 AM
|
|
|
|
if max_height == 0 {
|
|
max_height = 50000000000
|
|
}
|
|
|
|
for _, e := range w.account.Entries {
|
|
if e.Height >= min_height && e.Height <= max_height {
|
|
if in && (e.Incoming || e.Coinbase) {
|
|
|
|
if payment_id && len(e.PaymentID) >= 8 {
|
|
entries = append(entries, e)
|
|
} else {
|
|
entries = append(entries, e)
|
|
}
|
|
continue
|
|
}
|
|
if out && !e.Incoming {
|
|
if payment_id && len(e.PaymentID) >= 8 {
|
|
entries = append(entries, e)
|
|
} else {
|
|
entries = append(entries, e)
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// gets all the payments done to specific payment ID and filtered by specific block height
|
|
// we do need better structures
|
|
func (w *Wallet) Get_Payments_Payment_ID(payid []byte, min_height uint64) (entries []Entry) {
|
|
for _, e := range w.account.Entries {
|
|
if e.Height >= min_height {
|
|
if bytes.Compare(payid, e.PaymentID[:]) == 0 {
|
|
entries = append(entries, e)
|
|
}
|
|
}
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// return all payments within a tx there can be only 1 entry
|
|
// NOTE:
|
|
func (w *Wallet) Get_Payments_TXID(txid []byte) (entry Entry) {
|
|
for _, e := range w.account.Entries {
|
|
if bytes.Compare(txid, e.TXID[:]) == 0 {
|
|
return e
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// get the unlocked balance ( amounts which are mature and can be spent at this time )
|
|
// offline wallets may get this wrong, since they may not have latest data
|
|
// TODO: for offline wallets, we must make all balance as mature
|
|
//
|
|
func (w *Wallet) Start_RPC_Server(address string) (err error) {
|
|
w.Lock()
|
|
defer w.Unlock()
|
|
|
|
tcpAddr, err := net.ResolveTCPAddr("tcp", address)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
w.rpcserver, err = RPCServer_Start(w, tcpAddr.String())
|
|
if err != nil {
|
|
w.rpcserver = nil
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (w *Wallet) Stop_RPC_Server() {
|
|
w.Lock()
|
|
defer w.Unlock()
|
|
|
|
if w.rpcserver != nil {
|
|
w.rpcserver.RPCServer_Stop()
|
|
w.rpcserver = nil // remover reference
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// delete most of the data and prepare for rescan
|
|
func (w *Wallet) Clean() {
|
|
w.account.Entries = w.account.Entries[:0]
|
|
w.account.Balance_Result.Data = ""
|
|
}
|
|
|
|
// return height of wallet
|
|
func (w *Wallet) Get_Height() uint64 {
|
|
return uint64(w.account.Balance_Result.Height)
|
|
}
|
|
|
|
// return topoheight of wallet
|
|
func (w *Wallet) Get_TopoHeight() int64 {
|
|
return w.account.Balance_Result.Topoheight
|
|
}
|
|
|
|
func (w *Wallet) Get_Daemon_Height() uint64 {
|
|
w.Lock()
|
|
defer w.Unlock()
|
|
|
|
return w.Daemon_Height
|
|
}
|
|
|
|
func (w *Wallet) Get_Registration_TopoHeight() int64 {
|
|
return w.account.Balance_Result.Registration
|
|
}
|
|
|
|
func (w *Wallet) Get_Keys() _Keys {
|
|
return w.account.Keys
|
|
}
|
|
|
|
// by default a wallet opens in Offline Mode
|
|
// however, if the wallet is in online mode, it can be made offline instantly using this
|
|
func (w *Wallet) SetOfflineMode() bool {
|
|
w.Lock()
|
|
defer w.Unlock()
|
|
|
|
current_mode := w.wallet_online_mode
|
|
w.wallet_online_mode = false
|
|
return current_mode
|
|
}
|
|
|
|
func (w *Wallet) SetNetwork(mainnet bool) bool {
|
|
w.account.mainnet = mainnet
|
|
return w.account.mainnet
|
|
}
|
|
|
|
func (w *Wallet) GetNetwork() bool {
|
|
return w.account.mainnet
|
|
}
|
|
|
|
// return current mode
|
|
func (w *Wallet) GetMode() bool {
|
|
w.RLock()
|
|
defer w.RUnlock()
|
|
|
|
return w.wallet_online_mode
|
|
}
|
|
|
|
// use the endpoint set by the program
|
|
func (w *Wallet) SetDaemonAddress(endpoint string) string {
|
|
w.Lock()
|
|
defer w.Unlock()
|
|
|
|
w.Daemon_Endpoint = endpoint
|
|
return w.Daemon_Endpoint
|
|
}
|
|
|
|
// by default a wallet opens in Offline Mode
|
|
// however, It can be made online by calling this
|
|
func (w *Wallet) SetOnlineMode() bool {
|
|
w.Lock()
|
|
defer w.Unlock()
|
|
|
|
current_mode := w.wallet_online_mode
|
|
w.wallet_online_mode = true
|
|
|
|
if current_mode != true { // trigger subroutine if previous mode was offline
|
|
go w.sync_loop() // start sync subroutine
|
|
}
|
|
return current_mode
|
|
}
|
|
|
|
// by default a wallet opens in Offline Mode
|
|
// however, It can be made online by calling this
|
|
func (w *Wallet) SetRingSize(ringsize int) int {
|
|
defer w.Save_Wallet() // save wallet
|
|
w.Lock()
|
|
defer w.Unlock()
|
|
|
|
if ringsize >= 2 && ringsize <= 128 { //reasonable limits for mixin, atleastt for now, network should bump it to 13 on next HF
|
|
|
|
if crypto.IsPowerOf2(ringsize) {
|
|
w.account.Ringsize = ringsize
|
|
}
|
|
}
|
|
return w.account.Ringsize
|
|
}
|
|
|
|
// by default a wallet opens in Offline Mode
|
|
// however, It can be made online by calling this
|
|
func (w *Wallet) GetRingSize() int {
|
|
w.Lock()
|
|
defer w.Unlock()
|
|
if w.account.Ringsize < 2 {
|
|
return 2
|
|
}
|
|
return w.account.Ringsize
|
|
}
|
|
|
|
// sets a fee multiplier
|
|
func (w *Wallet) SetFeeMultiplier(x float32) float32 {
|
|
defer w.Save_Wallet() // save wallet
|
|
w.Lock()
|
|
defer w.Unlock()
|
|
if x < 1.0 { // fee cannot be less than 1.0, base fees
|
|
w.account.FeesMultiplier = 2.0
|
|
} else {
|
|
w.account.FeesMultiplier = x
|
|
}
|
|
return w.account.FeesMultiplier
|
|
}
|
|
|
|
// gets current fee multiplier
|
|
func (w *Wallet) GetFeeMultiplier() float32 {
|
|
w.Lock()
|
|
defer w.Unlock()
|
|
if w.account.FeesMultiplier < 1.0 {
|
|
return 1.0
|
|
}
|
|
return w.account.FeesMultiplier
|
|
}
|
|
|
|
// get fees multiplied by multiplier
|
|
func (w *Wallet) getfees(txfee uint64) uint64 {
|
|
multiplier := w.account.FeesMultiplier
|
|
if multiplier < 1.0 {
|
|
multiplier = 2.0
|
|
}
|
|
return txfee * uint64(multiplier*100.0) / 100
|
|
}
|
|
|
|
// Ability to change seed lanaguage
|
|
func (w *Wallet) SetSeedLanguage(language string) string {
|
|
defer w.Save_Wallet() // save wallet
|
|
w.Lock()
|
|
defer w.Unlock()
|
|
|
|
language_list := mnemonics.Language_List()
|
|
for i := range language_list {
|
|
if strings.ToLower(language) == strings.ToLower(language_list[i]) {
|
|
w.account.SeedLanguage = language_list[i]
|
|
}
|
|
}
|
|
return w.account.SeedLanguage
|
|
}
|
|
|
|
// retrieve current seed language
|
|
func (w *Wallet) GetSeedLanguage() string {
|
|
w.Lock()
|
|
defer w.Unlock()
|
|
if w.account.SeedLanguage == "" { // default is English
|
|
return "English"
|
|
}
|
|
return w.account.SeedLanguage
|
|
}
|
|
|
|
// retrieve secret key for any tx we may have created
|
|
func (w *Wallet) GetRegistrationTX() *transaction.Transaction {
|
|
var tx transaction.Transaction
|
|
tx.Version = 1
|
|
tx.TransactionType = transaction.REGISTRATION
|
|
add := w.account.Keys.Public.EncodeCompressed()
|
|
copy(tx.MinerAddress[:], add[:])
|
|
c, s := w.sign()
|
|
crypto.FillBytes(c, tx.C[:])
|
|
crypto.FillBytes(s, tx.S[:])
|
|
|
|
if !tx.IsRegistrationValid() {
|
|
panic("registration tx could not be generated. something failed.")
|
|
}
|
|
|
|
return &tx
|
|
}
|
|
|
|
// this basically does a Schnorr Signature on random information for registration
|
|
func (w *Wallet) sign() (c, s *big.Int) {
|
|
var tmppoint bn256.G1
|
|
|
|
tmpsecret := crypto.RandomScalar()
|
|
tmppoint.ScalarMult(crypto.G, tmpsecret)
|
|
|
|
serialize := []byte(fmt.Sprintf("%s%s", w.account.Keys.Public.G1().String(), tmppoint.String()))
|
|
c = crypto.ReducedHash(serialize)
|
|
s = new(big.Int).Mul(c, w.account.Keys.Secret.BigInt()) // basicaly scalar mul add
|
|
s = s.Mod(s, bn256.Order)
|
|
s = s.Add(s, tmpsecret)
|
|
s = s.Mod(s, bn256.Order)
|
|
|
|
return
|
|
}
|
|
|
|
// retrieve secret key for any tx we may have created
|
|
func (w *Wallet) GetTXKey(txhash crypto.Hash) string {
|
|
for _, e := range w.account.Entries {
|
|
if !e.Coinbase && !e.Incoming && e.TXID == txhash {
|
|
return e.Proof
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// we need better names for functions
|
|
func (w *Wallet) GetTXOutDetails(txhash crypto.Hash) (details structures.Outgoing_Transfer_Details) {
|
|
|
|
panic("not implemented")
|
|
/*
|
|
data_bytes, err := w.load_key_value(BLOCKCHAIN_UNIVERSE, []byte(TX_OUT_DETAILS_BUCKET), txhash[:])
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if len(data_bytes) > 10 {
|
|
json.Unmarshal(data_bytes, &details)
|
|
}
|
|
*/
|
|
return
|
|
}
|