2021-12-04 16:42:11 +00:00

529 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 "sort"
import "sync"
import "strings"
import "math/big"
import "crypto/rand"
import "encoding/binary"
import "github.com/go-logr/logr"
import "github.com/deroproject/derohe/rpc"
import "github.com/deroproject/derohe/cryptography/crypto"
import "github.com/deroproject/derohe/cryptography/bn256"
import "github.com/deroproject/derohe/walletapi/mnemonics"
import "github.com/deroproject/derohe/transaction"
//import "github.com/deroproject/derohe/blockchain/inputmaturity"
var logger logr.Logger = logr.Discard() // default discard all logs
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
Registered bool `json:"registered"`
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 map[crypto.Hash]uint64 `json:"balance"` // balance of account and other private scs
Balance_Locked uint64 `json:"balance_locked"` // balance locked
Balance_Result []rpc.GetEncryptedBalance_Result // used to cache last successful result
//Entries []rpc.Entry // all tx entries, basically transaction statement
EntriesNative map[crypto.Hash][]rpc.Entry // all subtokens are stored here
RingMembers map[string]int64 `json:"ring_members"` // ring members
sync.Mutex // syncronise modifications to this structure
}
func (w *Wallet_Memory) getEncryptedBalanceresult(scid crypto.Hash) rpc.GetEncryptedBalance_Result {
for _, e := range w.account.Balance_Result {
if scid == e.SCID {
return e
}
}
return rpc.GetEncryptedBalance_Result{}
}
func (w *Wallet_Memory) setEncryptedBalanceresult(scid crypto.Hash, entry rpc.GetEncryptedBalance_Result) {
for i, e := range w.account.Balance_Result {
if scid == e.SCID {
w.account.Balance_Result[i] = entry
return
}
}
w.account.Balance_Result = append(w.account.Balance_Result, entry)
}
// add a entry in the suitable place
// this is always single threaded
func (w *Wallet_Memory) InsertReplace(scid crypto.Hash, e rpc.Entry) {
var entries []rpc.Entry
if _, ok := w.account.EntriesNative[scid]; ok {
entries = w.account.EntriesNative[scid]
} else {
}
i := sort.Search(len(entries), func(j int) bool {
return entries[j].TopoHeight >= e.TopoHeight && entries[j].TransactionPos >= e.TransactionPos && entries[j].Pos >= e.Pos
})
// entry already exists, we are probably rescanning/overwiting, delete anything afterwards
if i < len(entries) && entries[i].TopoHeight == e.TopoHeight && entries[i].TransactionPos == e.TransactionPos && entries[i].Pos == e.Pos {
entries = 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.
}
entries = append(entries, e)
if w.account.EntriesNative == nil {
w.account.EntriesNative = map[crypto.Hash][]rpc.Entry{}
}
w.account.EntriesNative[scid] = entries
}
// 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_Memory) 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_Memory) GetSeedinLanguage(lang string) (str string) {
return mnemonics.Key_To_Words(w.account.Keys.Secret.BigInt(), lang)
}
func (account *Account) GetAddress() (addr rpc.Address) {
addr.PublicKey = new(crypto.Point).Set(account.Keys.Public)
return
}
// convert a user account to address
func (w *Wallet_Memory) GetAddress() (addr rpc.Address) {
addr = w.account.GetAddress()
addr.Mainnet = w.account.mainnet
return addr
}
// get a random integrated address
func (w *Wallet_Memory) GetRandomIAddress8() (addr rpc.Address) {
addr = w.GetAddress()
// setup random 8 bytes of payment ID, it must be from non-deterministic RNG namely crypto random
var dstport [8]byte
rand.Read(dstport[:])
addr.Arguments = rpc.Arguments{{Name: rpc.RPC_DESTINATION_PORT, DataType: rpc.DataUint64, Value: binary.BigEndian.Uint64(dstport[:])}}
return
}
func (w *Wallet_Memory) 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_Memory) Get_Balance_scid(scid crypto.Hash) (mature_balance uint64, locked_balance uint64) {
return w.account.Balance[scid], 0
}
// get main balance directly
func (w *Wallet_Memory) Get_Balance() (mature_balance uint64, locked_balance uint64) {
var scid crypto.Hash
return w.account.Balance[scid], 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_Memory) Show_Transfers(scid crypto.Hash, coinbase bool, in bool, out bool, min_height, max_height uint64, sender, receiver string, dstport, srcport uint64) []rpc.Entry {
w.Lock()
defer w.Unlock()
var entries []rpc.Entry
if max_height == 0 {
max_height = 5000000000000
}
all_entries := w.account.EntriesNative[scid]
if all_entries == nil || len(all_entries) < 1 {
return entries
}
for _, e := range all_entries {
if e.Height >= min_height && e.Height <= max_height {
if coinbase && e.Coinbase {
entries = append(entries, e)
continue
}
if in && e.Incoming && !e.Coinbase {
entries = append(entries, e)
continue
}
if out && !(e.Incoming || e.Coinbase) {
entries = append(entries, e)
continue
}
}
}
//we have filtered by coinbase,in,out,min_height,max_height
// now we must filter by sernder receiver
return entries
}
// gets all the payments done to specific payment ID and filtered by specific block height
// we do need better rpc
func (w *Wallet_Memory) Get_Payments_Payment_ID(scid crypto.Hash, dst_port uint64, min_height uint64) (entries []rpc.Entry) {
return w.Get_Payments_DestinationPort(scid, dst_port, min_height)
}
// gets all the payments done to specific payment ID and filtered by specific block height
// we do need better rpc
func (w *Wallet_Memory) Get_Payments_DestinationPort(scid crypto.Hash, port uint64, min_height uint64) (entries []rpc.Entry) {
all_entries := w.account.EntriesNative[scid]
if all_entries == nil || len(all_entries) < 1 {
return
}
for _, e := range all_entries {
if e.Height >= min_height && e.DestinationPort == port {
entries = append(entries, e)
}
}
return
}
// return all payments within a tx there can be only 1 entry
// NOTE: what about multiple payments
func (w *Wallet_Memory) Get_Payments_TXID(txid string) (entry rpc.Entry) {
var zerohash crypto.Hash
all_entries := w.account.EntriesNative[zerohash]
if all_entries == nil || len(all_entries) < 1 {
return
}
for _, e := range all_entries {
if txid == e.TXID {
return e
}
}
return
}
// delete most of the data and prepare for rescan
func (w *Wallet_Memory) Clean() {
//w.account.Entries = w.account.Entries[:0]
for k := range w.account.EntriesNative {
delete(w.account.EntriesNative, k)
}
w.account.RingMembers = map[string]int64{}
w.account.Balance_Result = w.account.Balance_Result[:0]
w.account.Registered = false
}
// return height of wallet
func (w *Wallet_Memory) Get_Height() uint64 {
var scid crypto.Hash
return uint64(w.getEncryptedBalanceresult(scid).Height)
}
// return topoheight of wallet
func (w *Wallet_Memory) Get_TopoHeight() int64 {
var scid crypto.Hash
return w.getEncryptedBalanceresult(scid).Topoheight
}
func (w *Wallet_Memory) Get_Daemon_Height() uint64 {
return uint64(daemon_height)
}
// return topoheight of darmon
func (w *Wallet_Memory) Get_Daemon_TopoHeight() int64 {
return daemon_topoheight
}
func (w *Wallet_Memory) IsRegistered() bool {
return w.account.Registered
}
func (w *Wallet_Memory) Get_Registration_TopoHeight() int64 {
var scid crypto.Hash
return w.getEncryptedBalanceresult(scid).Registration
}
func (w *Wallet_Memory) 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_Memory) SetOfflineMode() bool {
current_mode := w.wallet_online_mode
w.wallet_online_mode = false
return current_mode
}
func (w *Wallet_Memory) SetNetwork(mainnet bool) bool {
w.account.mainnet = mainnet
return w.account.mainnet
}
func (w *Wallet_Memory) GetNetwork() bool {
return w.account.mainnet
}
// return current mode
func (w *Wallet_Memory) GetMode() bool {
return w.wallet_online_mode
}
// use the endpoint set by the program
func (w *Wallet_Memory) SetDaemonAddress(endpoint string) string {
Daemon_Endpoint = endpoint
return Daemon_Endpoint
}
func SetDaemonAddress(endpoint string) string {
Daemon_Endpoint = endpoint
return Daemon_Endpoint
}
// by default a wallet opens in Offline Mode
// however, It can be made online by calling this
func (w *Wallet_Memory) SetOnlineMode() bool {
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_Memory) SetRingSize(ringsize int) int {
defer w.save_if_disk() // save wallet
if ringsize >= 2 && ringsize <= 128 { //reasonable limits for mixin, atleastt for now, network should bump it to 256 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_Memory) GetRingSize() int {
if w.account.Ringsize < 2 {
return 2
}
return w.account.Ringsize
}
// sets a fee multiplier
func (w *Wallet_Memory) SetFeeMultiplier(x float32) float32 {
defer w.save_if_disk() // save wallet
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_Memory) GetFeeMultiplier() float32 {
if w.account.FeesMultiplier < 1.0 {
return 1.0
}
return w.account.FeesMultiplier
}
// get fees multiplied by multiplier
func (w *Wallet_Memory) 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_Memory) SetSeedLanguage(language string) string {
defer w.save_if_disk() // save wallet
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_Memory) GetSeedLanguage() string {
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_Memory) 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_Memory) 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_Memory) GetTXKey(txhash string) string {
/*for _, e := range w.account.Entries {
if !e.Coinbase && !e.Incoming && e.TXID == txhash {
return e.Proof
}
}*/
return ""
}
// never do any division operation on money due to floating point issues
// newbies, see type the next in python interpretor "3.33-3.13"
//
func FormatMoney(amount uint64) string {
return FormatMoneyPrecision(amount, 5) // default is 5 precision after floating point
}
// format money with specific precision
func FormatMoneyPrecision(amount uint64, precision int) string {
hard_coded_decimals := new(big.Float).SetInt64(100000)
float_amount, _, _ := big.ParseFloat(fmt.Sprintf("%d", amount), 10, 0, big.ToZero)
result := new(big.Float)
result.Quo(float_amount, hard_coded_decimals)
return result.Text('f', precision) // 5 is display precision after floating point
}