529 lines
16 KiB
Go
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{{rpc.RPC_DESTINATION_PORT, rpc.DataUint64, 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 w.Daemon_Height
|
||
|
}
|
||
|
|
||
|
// return topoheight of darmon
|
||
|
func (w *Wallet_Memory) Get_Daemon_TopoHeight() int64 {
|
||
|
return w.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
|
||
|
}
|