|
// Copyright 2017-2018 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 "sync"
|
|
|
|
import "github.com/deroproject/derosuite/config"
|
|
import "github.com/deroproject/derosuite/crypto"
|
|
import "github.com/deroproject/derosuite/crypto/ringct"
|
|
import "github.com/deroproject/derosuite/globals"
|
|
import "github.com/deroproject/derosuite/walletapi/mnemonics"
|
|
import "github.com/deroproject/derosuite/address"
|
|
import "github.com/deroproject/derosuite/blockchain/inputmaturity"
|
|
|
|
type _Keys struct {
|
|
Spendkey_Secret crypto.Key
|
|
Spendkey_Public crypto.Key
|
|
Viewkey_Secret crypto.Key
|
|
Viewkey_Public crypto.Key
|
|
}
|
|
|
|
type Account struct {
|
|
Keys _Keys
|
|
SeedLanguage string
|
|
|
|
ViewOnly bool // is this viewonly wallet
|
|
|
|
Index_Global uint64 // till where the indexes have been processed
|
|
Height uint64
|
|
|
|
Balance uint64 // total balance of account
|
|
Balance_Locked uint64 // balance locked
|
|
|
|
Outputs_Array []TX_Wallet_Data // all outputs found in the chain belonging to us, as found in chain
|
|
|
|
// uint64 si the Index_Global which is the unique number
|
|
Outputs_Index map[uint64]bool // all outputs which are ours for deduplication
|
|
Outputs_Ready map[uint64]TX_Wallet_Data // these outputs are ready for consumption ( maturity needs to be checked)
|
|
Keyimages_Ready map[crypto.Key]bool // keyimages which are ready to get consumed, // we monitor them to find which
|
|
Outputs_Consumed map[crypto.Key]TX_Wallet_Data // the key is the keyimage
|
|
|
|
sync.Mutex // syncronise modifications to this structure
|
|
}
|
|
|
|
// this structure is kept by wallet
|
|
type TX_Wallet_Data struct {
|
|
TXdata globals.TX_Output_Data // all the fields of output data
|
|
|
|
WAmount uint64 // actual amount, in case of miner it is verbatim, for other cases it decrypted
|
|
WKey ringct.CtKey // key which is used to later send this specific output
|
|
WKimage crypto.Key // key image which gets consumed when this output is spent
|
|
WSpent bool // whether this output has been spent
|
|
}
|
|
|
|
// generate keys from using random numbers
|
|
func Generate_Keys_From_Random() (user *Account, err error) {
|
|
user = &Account{}
|
|
seed := crypto.RandomScalar()
|
|
user.Keys = Generate_Keys_From_Seed(*seed)
|
|
|
|
// initialize maps now
|
|
user.Outputs_Index = map[uint64]bool{}
|
|
user.Outputs_Ready = map[uint64]TX_Wallet_Data{}
|
|
user.Outputs_Consumed = map[crypto.Key]TX_Wallet_Data{}
|
|
user.Keyimages_Ready = map[crypto.Key]bool{}
|
|
return
|
|
}
|
|
|
|
// generate keys from seed which is from the recovery words
|
|
// or we feed in direct
|
|
func Generate_Keys_From_Seed(Seed crypto.Key) (keys _Keys) {
|
|
|
|
// setup main keys
|
|
keys.Spendkey_Secret = Seed
|
|
keys.Spendkey_Public = *(Seed.PublicKey())
|
|
|
|
// view keys are generated from secret ( so as single recovery seed is enough )
|
|
hash := crypto.Key(crypto.Keccak256(Seed[:]))
|
|
crypto.ScReduce32(&hash)
|
|
keys.Viewkey_Secret = hash
|
|
keys.Viewkey_Public = *(keys.Viewkey_Secret.PublicKey())
|
|
|
|
return
|
|
}
|
|
|
|
// generate user account using recovery seeds
|
|
func Generate_Account_From_Recovery_Words(words string) (user *Account, err error) {
|
|
user = &Account{}
|
|
language, seed, err := mnemonics.Words_To_Key(words)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
user.SeedLanguage = language
|
|
user.Keys = Generate_Keys_From_Seed(seed)
|
|
|
|
// initialize maps now
|
|
|
|
user.Outputs_Index = map[uint64]bool{}
|
|
user.Outputs_Ready = map[uint64]TX_Wallet_Data{}
|
|
user.Outputs_Consumed = map[crypto.Key]TX_Wallet_Data{}
|
|
user.Keyimages_Ready = map[crypto.Key]bool{}
|
|
|
|
return
|
|
}
|
|
|
|
func Generate_Account_From_Seed(Seed crypto.Key) (user *Account, err error) {
|
|
user = &Account{}
|
|
|
|
// TODO check whether the seed is invalid
|
|
user.Keys = Generate_Keys_From_Seed(Seed)
|
|
|
|
// initialize maps now
|
|
user.Outputs_Index = map[uint64]bool{}
|
|
user.Outputs_Ready = map[uint64]TX_Wallet_Data{}
|
|
user.Outputs_Consumed = map[crypto.Key]TX_Wallet_Data{}
|
|
user.Keyimages_Ready = map[crypto.Key]bool{}
|
|
|
|
return
|
|
}
|
|
|
|
// generate keys for view only wallet
|
|
func Generate_Account_View_Only(Publicspend crypto.Key, ViewSecret crypto.Key) (user *Account, err error) {
|
|
|
|
user = &Account{}
|
|
|
|
// TODO check whether seed is valid secret
|
|
user.Keys.Spendkey_Public = Publicspend
|
|
user.Keys.Viewkey_Secret = ViewSecret
|
|
user.Keys.Viewkey_Public = *(ViewSecret.PublicKey())
|
|
user.ViewOnly = true
|
|
|
|
// initialize maps
|
|
user.Outputs_Index = map[uint64]bool{}
|
|
user.Outputs_Ready = map[uint64]TX_Wallet_Data{}
|
|
user.Outputs_Consumed = map[crypto.Key]TX_Wallet_Data{}
|
|
user.Keyimages_Ready = map[crypto.Key]bool{}
|
|
|
|
return
|
|
}
|
|
|
|
// convert key to seed using language
|
|
func (user *Account) GetSeed() (str string) {
|
|
return mnemonics.Key_To_Words(user.Keys.Spendkey_Secret, user.SeedLanguage)
|
|
}
|
|
|
|
// view wallet key consists of public spendkey and private view key
|
|
func (user *Account) GetViewWalletKey() (str string) {
|
|
return fmt.Sprintf("%s%s", user.Keys.Spendkey_Public, user.Keys.Viewkey_Secret)
|
|
}
|
|
|
|
// convert a user account to address
|
|
func (user *Account) GetAddress() (addr address.Address) {
|
|
switch globals.Config.Name {
|
|
case "testnet":
|
|
addr.Network = config.Testnet.Public_Address_Prefix //choose dETo
|
|
|
|
default:
|
|
fallthrough // assume mainnet
|
|
case "mainnet":
|
|
addr.Network = config.Mainnet.Public_Address_Prefix //choose dERo
|
|
|
|
//panic(fmt.Sprintf("Unknown Network \"%s\"", globals.Config.Name))
|
|
}
|
|
|
|
addr.SpendKey = user.Keys.Spendkey_Public
|
|
addr.ViewKey = user.Keys.Viewkey_Public
|
|
|
|
return
|
|
}
|
|
|
|
// one simple function which does all the crypto to find out whether output belongs to this account
|
|
// NOTE: this function only uses view key secret and Spendkey_Public
|
|
// output index is the position of vout within the tx list itself
|
|
func (user *Account) Is_Output_Ours(tx_public crypto.Key, output_index uint64, vout_key crypto.Key) bool {
|
|
derivation := crypto.KeyDerivation(&tx_public, &user.Keys.Viewkey_Secret)
|
|
derivation_public_key := derivation.KeyDerivation_To_PublicKey(output_index, user.Keys.Spendkey_Public)
|
|
|
|
return derivation_public_key == vout_key
|
|
}
|
|
|
|
// this function does all the keyderivation required for decrypting ringct outputs, generate keyimage etc
|
|
// also used when we build up a transaction for mining or sending amount
|
|
func (user *Account) Generate_Helper_Key_Image(tx_public crypto.Key, output_index uint64) (ephermal_secret, ephermal_public, keyimage crypto.Key) {
|
|
derivation := crypto.KeyDerivation(&tx_public, &user.Keys.Viewkey_Secret)
|
|
ephermal_public = derivation.KeyDerivation_To_PublicKey(output_index, user.Keys.Spendkey_Public)
|
|
ephermal_secret = derivation.KeyDerivation_To_PrivateKey(output_index, user.Keys.Spendkey_Secret)
|
|
|
|
keyimage = crypto.GenerateKeyImage(ephermal_public, ephermal_secret)
|
|
|
|
return
|
|
}
|
|
|
|
// this function decodes ringCT encoded output amounts
|
|
// this is only possible if signature is full or simple
|
|
func (user *Account) Decode_RingCT_Output(tx_public crypto.Key, output_index uint64, pkkey crypto.Key, tuple ringct.ECdhTuple, sigtype uint64) (amount uint64, mask ringct.Key, result bool) {
|
|
|
|
derivation := crypto.KeyDerivation(&tx_public, &user.Keys.Viewkey_Secret)
|
|
scalar_key := derivation.KeyDerivationToScalar(output_index)
|
|
|
|
switch sigtype {
|
|
case 0: // NOT possible , miner tx outputs are not hidden
|
|
return
|
|
case 1: // ringct MG // Both ringct outputs can be decoded using the same methods
|
|
// however, original implementation has different methods, maybe need to evaluate more
|
|
fallthrough
|
|
case 2: // ringct sample
|
|
|
|
amount, mask, result = ringct.Decode_Amount(tuple, ringct.Key(*scalar_key), ringct.Key(pkkey))
|
|
|
|
default:
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// add the transaction to our wallet record, so as funds can be later on tracked
|
|
// due to enhanced features, we have to wait and watch for all funds
|
|
// this will extract secret keys from newly arrived funds to consume them later on
|
|
func (user *Account) Add_Transaction_Record_Funds(txdata *globals.TX_Output_Data) (result bool) {
|
|
user.Lock()
|
|
defer user.Unlock()
|
|
|
|
var tx_wallet TX_Wallet_Data
|
|
// confirm once again that data belongs to this user
|
|
if !user.Is_Output_Ours(txdata.Tx_Public_Key, txdata.Index_within_tx, crypto.Key(txdata.InKey.Destination)) {
|
|
return false // output is not ours
|
|
}
|
|
|
|
// setup Amount
|
|
switch txdata.SigType {
|
|
case 0: // miner tx
|
|
tx_wallet.WAmount = txdata.Amount
|
|
tx_wallet.WKey.Mask = ringct.ZeroCommitment_From_Amount(txdata.Amount)
|
|
|
|
case 1, 2: // ringct full/simple
|
|
tx_wallet.WAmount, tx_wallet.WKey.Mask, result = user.Decode_RingCT_Output(txdata.Tx_Public_Key, txdata.Index_within_tx, crypto.Key(txdata.InKey.Mask), txdata.ECDHTuple,
|
|
txdata.SigType)
|
|
|
|
if result == false { // It's an internal error most probably
|
|
return false
|
|
}
|
|
}
|
|
|
|
tx_wallet.TXdata = *txdata
|
|
|
|
// check whether we are deduplicating, is the transaction already in our records, skip it
|
|
if _, ok := user.Outputs_Index[txdata.Index_Global]; ok { // transaction is already in our wallet, skip it for being duplicate
|
|
return false
|
|
}
|
|
|
|
// if wallet is viewonly, we cannot track when the funds were spent
|
|
// so lets skip the part, since we do not have th keys
|
|
if !user.ViewOnly { // it's a full wallet, track spendable and get ready to spend
|
|
secret_key, _, kimage := user.Generate_Helper_Key_Image(txdata.Tx_Public_Key, txdata.Index_within_tx)
|
|
user.Keyimages_Ready[kimage] = true // monitor this key image for consumption
|
|
|
|
tx_wallet.WKimage = kimage
|
|
tx_wallet.WKey.Destination = ringct.Key(secret_key)
|
|
}
|
|
|
|
// add tx info to wallet
|
|
user.Outputs_Index[txdata.Index_Global] = true // deduplication it if it ever comes again
|
|
user.Outputs_Ready[txdata.Index_Global] = tx_wallet
|
|
user.Outputs_Array = append(user.Outputs_Array, tx_wallet)
|
|
|
|
return true
|
|
}
|
|
|
|
// check whether our fund is consumed
|
|
// this is done by finding the keyimages floating in blockchain, to what keyimages belong to this account
|
|
// if match is found, we have consumed our funds
|
|
func (user *Account) Is_Our_Fund_Consumed(key_image crypto.Key) (amount uint64, result bool) {
|
|
if _, ok := user.Keyimages_Ready[key_image]; ok {
|
|
user.Lock()
|
|
defer user.Unlock()
|
|
|
|
for k, _ := range user.Outputs_Ready {
|
|
if user.Outputs_Ready[k].WKimage == key_image {
|
|
return user.Outputs_Ready[k].WAmount, true // return ammount and success
|
|
}
|
|
}
|
|
|
|
fmt.Printf("This case should NOT be possible theoritically\n")
|
|
return 0, true
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// add the transaction to record,
|
|
// this will mark the funds as consumed on the basis of keyimages
|
|
// locate the transaction and get the amount , this is O(n), so we can tell how much funds were spent
|
|
// cryptnote only allows to spend complete funds, change comes back
|
|
func (user *Account) Consume_Transaction_Record_Funds(txdata *globals.TX_Output_Data, key_image crypto.Key) bool {
|
|
var tx_wallet TX_Wallet_Data
|
|
if _, ok := user.Keyimages_Ready[key_image]; ok {
|
|
user.Lock()
|
|
defer user.Unlock()
|
|
|
|
for k, _ := range user.Outputs_Ready {
|
|
if user.Outputs_Ready[k].WKimage == key_image { // find the input corressponding to this image
|
|
|
|
// mark output as consumed, move it to consumed map, delete it from ready map
|
|
tx_wallet.TXdata = *txdata
|
|
tx_wallet.WAmount = user.Outputs_Ready[k].WAmount // take amount from original TX
|
|
tx_wallet.WSpent = true // mark this fund as spent
|
|
|
|
delete(user.Outputs_Ready, k)
|
|
|
|
user.Outputs_Consumed[key_image] = tx_wallet
|
|
user.Outputs_Array = append(user.Outputs_Array, user.Outputs_Consumed[key_image])
|
|
|
|
return true // return success
|
|
}
|
|
}
|
|
|
|
fmt.Printf("This case should NOT be possible theoritically\n")
|
|
// locate the transaction and get the amount , this is O(n)
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// 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 (user *Account) Get_Balance() (mature_balance uint64, locked_balance uint64) {
|
|
user.Lock()
|
|
defer user.Unlock()
|
|
|
|
for k := range user.Outputs_Ready {
|
|
|
|
if inputmaturity.Is_Input_Mature(user.Height,
|
|
user.Outputs_Ready[k].TXdata.Height,
|
|
user.Outputs_Ready[k].TXdata.Unlock_Height,
|
|
user.Outputs_Ready[k].TXdata.SigType) {
|
|
mature_balance += user.Outputs_Ready[k].WAmount
|
|
} else {
|
|
locked_balance += user.Outputs_Ready[k].WAmount
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|