2013-08-21 16:37:30 +02:00
|
|
|
/*
|
|
|
|
* Copyright (c) 2013 Conformal Systems LLC <info@conformal.com>
|
|
|
|
*
|
|
|
|
* Permission to use, copy, modify, and distribute this software for any
|
|
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
|
|
* copyright notice and this permission notice appear in all copies.
|
|
|
|
*
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
|
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
|
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
|
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
|
*/
|
|
|
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2013-10-04 21:02:17 +02:00
|
|
|
"encoding/hex"
|
2013-08-21 16:37:30 +02:00
|
|
|
"encoding/json"
|
Implement address rescanning.
When a wallet is opened, a rescan request will be sent to btcd with
all active addresses from the wallet, to rescan from the last synced
block (now saved to the wallet file) and the current best block.
As multi-account support is further explored, rescan requests should
be batched together to send a single request for all addresses from
all wallets.
This change introduces several changes to the wallet, tx, and utxo
files. Wallet files are still compatible, however, a rescan will try
to start at the genesis block since no correct "last synced to" or
"created at block X" was saved. The tx and utxo files, however, are
not compatible and should be deleted (or an error will occur on read).
If any errors occur opening the utxo file, a rescan will start
beginning at the creation block saved in the wallet.
2013-10-30 02:22:14 +01:00
|
|
|
"errors"
|
2013-08-21 16:37:30 +02:00
|
|
|
"fmt"
|
|
|
|
"github.com/conformal/btcjson"
|
2013-12-10 22:15:25 +01:00
|
|
|
"github.com/conformal/btcutil"
|
2013-11-22 19:42:25 +01:00
|
|
|
"github.com/conformal/btcwallet/tx"
|
2013-09-03 15:49:16 +02:00
|
|
|
"github.com/conformal/btcwallet/wallet"
|
2013-10-07 18:35:32 +02:00
|
|
|
"github.com/conformal/btcwire"
|
2013-11-12 18:01:32 +01:00
|
|
|
"github.com/conformal/btcws"
|
2013-08-21 19:25:22 +02:00
|
|
|
"time"
|
2013-08-21 16:37:30 +02:00
|
|
|
)
|
|
|
|
|
Implement address rescanning.
When a wallet is opened, a rescan request will be sent to btcd with
all active addresses from the wallet, to rescan from the last synced
block (now saved to the wallet file) and the current best block.
As multi-account support is further explored, rescan requests should
be batched together to send a single request for all addresses from
all wallets.
This change introduces several changes to the wallet, tx, and utxo
files. Wallet files are still compatible, however, a rescan will try
to start at the genesis block since no correct "last synced to" or
"created at block X" was saved. The tx and utxo files, however, are
not compatible and should be deleted (or an error will occur on read).
If any errors occur opening the utxo file, a rescan will start
beginning at the creation block saved in the wallet.
2013-10-30 02:22:14 +01:00
|
|
|
var (
|
|
|
|
// ErrBtcdDisconnected describes an error where an operation cannot
|
|
|
|
// successfully complete due to btcd not being connected to
|
|
|
|
// btcwallet.
|
|
|
|
ErrBtcdDisconnected = errors.New("btcd disconnected")
|
|
|
|
)
|
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
type cmdHandler func(chan []byte, btcjson.Cmd)
|
|
|
|
|
2013-11-18 19:31:58 +01:00
|
|
|
var rpcHandlers = map[string]cmdHandler{
|
2013-11-12 18:01:32 +01:00
|
|
|
// Standard bitcoind methods
|
2013-11-20 02:18:11 +01:00
|
|
|
"dumpprivkey": DumpPrivKey,
|
2013-11-12 18:01:32 +01:00
|
|
|
"getaddressesbyaccount": GetAddressesByAccount,
|
|
|
|
"getbalance": GetBalance,
|
|
|
|
"getnewaddress": GetNewAddress,
|
2013-11-20 02:18:11 +01:00
|
|
|
"importprivkey": ImportPrivKey,
|
2013-11-12 18:01:32 +01:00
|
|
|
"listaccounts": ListAccounts,
|
2013-11-22 19:42:25 +01:00
|
|
|
"listtransactions": ListTransactions,
|
2013-11-12 18:01:32 +01:00
|
|
|
"sendfrom": SendFrom,
|
|
|
|
"sendmany": SendMany,
|
|
|
|
"settxfee": SetTxFee,
|
|
|
|
"walletlock": WalletLock,
|
|
|
|
"walletpassphrase": WalletPassphrase,
|
|
|
|
|
2013-11-18 19:31:58 +01:00
|
|
|
// Extensions not exclusive to websocket connections.
|
2013-11-12 18:01:32 +01:00
|
|
|
"createencryptedwallet": CreateEncryptedWallet,
|
2013-11-18 19:31:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Extensions exclusive to websocket connections.
|
|
|
|
var wsHandlers = map[string]cmdHandler{
|
2013-12-10 22:15:25 +01:00
|
|
|
"getaddressbalance": GetAddressBalance,
|
2013-12-02 23:34:36 +01:00
|
|
|
"getbalances": GetBalances,
|
|
|
|
"listalltransactions": ListAllTransactions,
|
|
|
|
"walletislocked": WalletIsLocked,
|
2013-11-12 18:01:32 +01:00
|
|
|
}
|
|
|
|
|
2013-11-20 02:18:11 +01:00
|
|
|
// ProcessRequest checks the requests sent from a frontend. If the
|
|
|
|
// request method is one that must be handled by btcwallet, the
|
|
|
|
// request is processed here. Otherwise, the request is sent to btcd
|
|
|
|
// and btcd's reply is routed back to the frontend.
|
|
|
|
func ProcessRequest(frontend chan []byte, msg []byte, ws bool) {
|
2013-11-12 18:01:32 +01:00
|
|
|
// Parse marshaled command and check
|
|
|
|
cmd, err := btcjson.ParseMarshaledCmd(msg)
|
|
|
|
if err != nil {
|
|
|
|
// Check that msg is valid JSON-RPC. Reply to frontend
|
|
|
|
// with error if invalid.
|
|
|
|
if cmd == nil {
|
|
|
|
ReplyError(frontend, nil, &btcjson.ErrInvalidRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// btcwallet cannot handle this command, so defer handling
|
|
|
|
// to btcd.
|
|
|
|
DeferToBTCD(frontend, msg)
|
2013-08-21 16:37:30 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// Check for a handler to reply to cmd. If none exist, defer to btcd.
|
2013-11-18 19:31:58 +01:00
|
|
|
if f, ok := rpcHandlers[cmd.Method()]; ok {
|
|
|
|
f(frontend, cmd)
|
|
|
|
} else if f, ok := wsHandlers[cmd.Method()]; ws && ok {
|
2013-11-12 18:01:32 +01:00
|
|
|
f(frontend, cmd)
|
|
|
|
} else {
|
|
|
|
// btcwallet does not have a handler for the command. Pass
|
|
|
|
// to btcd and route replies back to the appropiate frontend.
|
|
|
|
DeferToBTCD(frontend, msg)
|
|
|
|
}
|
|
|
|
}
|
2013-08-21 20:46:20 +02:00
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// DeferToBTCD sends an unmarshaled command to btcd, modifying the id
|
|
|
|
// and setting up a reply route to route the reply from btcd back to
|
|
|
|
// the frontend reply channel with the original id.
|
|
|
|
func DeferToBTCD(frontend chan []byte, msg []byte) {
|
|
|
|
// msg cannot be sent to btcd directly, but the ID must instead be
|
|
|
|
// changed to include additonal routing information so replies can
|
|
|
|
// be routed back to the correct frontend. Unmarshal msg into a
|
|
|
|
// generic btcjson.Message struct so the ID can be modified and the
|
|
|
|
// whole thing re-marshaled.
|
|
|
|
var m btcjson.Message
|
|
|
|
json.Unmarshal(msg, &m)
|
|
|
|
|
|
|
|
// Create a new ID so replies can be routed correctly.
|
|
|
|
n := <-NewJSONID
|
|
|
|
var id interface{} = RouteID(m.Id, n)
|
|
|
|
m.Id = &id
|
|
|
|
|
|
|
|
// Marshal the request with modified ID.
|
|
|
|
newMsg, err := json.Marshal(m)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("DeferToBTCD: Cannot marshal message: %v", err)
|
|
|
|
return
|
2013-08-21 16:37:30 +02:00
|
|
|
}
|
2013-11-12 18:01:32 +01:00
|
|
|
|
|
|
|
// If marshaling suceeded, save the id and frontend reply channel
|
|
|
|
// so the reply can be sent to the correct frontend.
|
|
|
|
replyRouter.Lock()
|
|
|
|
replyRouter.m[n] = frontend
|
|
|
|
replyRouter.Unlock()
|
|
|
|
|
|
|
|
// Send message with modified ID to btcd.
|
|
|
|
btcdMsgs <- newMsg
|
|
|
|
}
|
|
|
|
|
|
|
|
// RouteID creates a JSON-RPC id for a frontend request that was deferred
|
|
|
|
// to btcd.
|
|
|
|
func RouteID(origID, routeID interface{}) string {
|
|
|
|
return fmt.Sprintf("btcwallet(%v)-%v", routeID, origID)
|
2013-08-21 16:37:30 +02:00
|
|
|
}
|
|
|
|
|
2013-11-20 02:18:11 +01:00
|
|
|
// ReplyError creates and marshals a btcjson.Reply with the error e,
|
2013-11-12 18:01:32 +01:00
|
|
|
// sending the reply to a frontend reply channel.
|
|
|
|
func ReplyError(frontend chan []byte, id interface{}, e *btcjson.Error) {
|
|
|
|
// Create a Reply with a non-nil error to marshal.
|
2013-08-21 17:14:21 +02:00
|
|
|
r := btcjson.Reply{
|
|
|
|
Error: e,
|
2013-08-21 19:25:22 +02:00
|
|
|
Id: &id,
|
|
|
|
}
|
2013-11-12 18:01:32 +01:00
|
|
|
|
|
|
|
// Marshal reply and send to frontend if marshaling suceeded.
|
2013-08-21 21:28:15 +02:00
|
|
|
if mr, err := json.Marshal(r); err == nil {
|
2013-11-12 18:01:32 +01:00
|
|
|
frontend <- mr
|
2013-08-21 19:25:22 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-11-20 02:18:11 +01:00
|
|
|
// ReplySuccess creates and marshals a btcjson.Reply with the result r,
|
2013-11-12 18:01:32 +01:00
|
|
|
// sending the reply to a frontend reply channel.
|
|
|
|
func ReplySuccess(frontend chan []byte, id interface{}, result interface{}) {
|
|
|
|
// Create a Reply with a non-nil result to marshal.
|
2013-08-21 19:25:22 +02:00
|
|
|
r := btcjson.Reply{
|
|
|
|
Result: result,
|
2013-08-21 19:43:05 +02:00
|
|
|
Id: &id,
|
2013-08-21 17:14:21 +02:00
|
|
|
}
|
2013-11-12 18:01:32 +01:00
|
|
|
|
|
|
|
// Marshal reply and send to frontend if marshaling suceeded.
|
2013-08-21 21:26:00 +02:00
|
|
|
if mr, err := json.Marshal(r); err == nil {
|
2013-11-12 18:01:32 +01:00
|
|
|
frontend <- mr
|
2013-08-21 17:14:21 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-11-20 02:18:11 +01:00
|
|
|
// DumpPrivKey replies to a dumpprivkey request with the private
|
|
|
|
// key for a single address, or an appropiate error if the wallet
|
|
|
|
// is locked.
|
|
|
|
func DumpPrivKey(frontend chan []byte, icmd btcjson.Cmd) {
|
|
|
|
// Type assert icmd to access parameters.
|
|
|
|
cmd, ok := icmd.(*btcjson.DumpPrivKeyCmd)
|
|
|
|
if !ok {
|
|
|
|
ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
switch key, err := accountstore.DumpWIFPrivateKey(cmd.Address); err {
|
|
|
|
case nil:
|
|
|
|
// Key was found.
|
|
|
|
ReplySuccess(frontend, cmd.Id(), key)
|
|
|
|
|
|
|
|
case wallet.ErrWalletLocked:
|
|
|
|
// Address was found, but the private key isn't
|
|
|
|
// accessible.
|
|
|
|
ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletUnlockNeeded)
|
2013-11-20 02:18:11 +01:00
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
default: // all other non-nil errors
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrWallet.Code,
|
|
|
|
Message: err.Error(),
|
2013-11-20 02:18:11 +01:00
|
|
|
}
|
2013-12-02 20:56:06 +01:00
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
2013-11-20 02:18:11 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// DumpWallet replies to a dumpwallet request with all private keys
|
|
|
|
// in a wallet, or an appropiate error if the wallet is locked.
|
2013-11-20 02:44:37 +01:00
|
|
|
// TODO: finish this to match bitcoind by writing the dump to a file.
|
2013-11-20 02:18:11 +01:00
|
|
|
func DumpWallet(frontend chan []byte, icmd btcjson.Cmd) {
|
|
|
|
// Type assert icmd to access parameters.
|
|
|
|
cmd, ok := icmd.(*btcjson.DumpWalletCmd)
|
|
|
|
if !ok {
|
|
|
|
ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
switch keys, err := accountstore.DumpKeys(); err {
|
|
|
|
case nil:
|
|
|
|
// Reply with sorted WIF encoded private keys
|
|
|
|
ReplySuccess(frontend, cmd.Id(), keys)
|
2013-11-20 02:18:11 +01:00
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
case wallet.ErrWalletLocked:
|
|
|
|
ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletUnlockNeeded)
|
2013-11-20 02:18:11 +01:00
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
default: // any other non-nil error
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrWallet.Code,
|
|
|
|
Message: err.Error(),
|
2013-11-20 02:18:11 +01:00
|
|
|
}
|
2013-12-02 20:56:06 +01:00
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
|
|
|
return
|
2013-11-20 02:18:11 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// GetAddressesByAccount replies to a getaddressesbyaccount request with
|
|
|
|
// all addresses for an account, or an error if the requested account does
|
|
|
|
// not exist.
|
|
|
|
func GetAddressesByAccount(frontend chan []byte, icmd btcjson.Cmd) {
|
|
|
|
// Type assert icmd to access parameters.
|
|
|
|
cmd, ok := icmd.(*btcjson.GetAddressesByAccountCmd)
|
2013-09-09 20:14:57 +02:00
|
|
|
if !ok {
|
2013-11-12 18:01:32 +01:00
|
|
|
ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal)
|
2013-09-09 20:14:57 +02:00
|
|
|
return
|
|
|
|
}
|
2013-10-08 04:17:27 +02:00
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
switch a, err := accountstore.Account(cmd.Account); err {
|
|
|
|
case nil:
|
|
|
|
// Reply with sorted active payment addresses.
|
|
|
|
ReplySuccess(frontend, cmd.Id(), a.SortedActivePaymentAddresses())
|
|
|
|
|
|
|
|
case ErrAcctNotExist:
|
2013-11-12 18:01:32 +01:00
|
|
|
ReplyError(frontend, cmd.Id(),
|
|
|
|
&btcjson.ErrWalletInvalidAccountName)
|
2013-10-08 04:17:27 +02:00
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
default: // all other non-nil errors
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrWallet.Code,
|
|
|
|
Message: err.Error(),
|
|
|
|
}
|
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
|
|
|
}
|
2013-08-21 16:37:30 +02:00
|
|
|
}
|
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// GetBalance replies to a getbalance request with the balance for an
|
|
|
|
// account (wallet), or an error if the requested account does not
|
|
|
|
// exist.
|
|
|
|
func GetBalance(frontend chan []byte, icmd btcjson.Cmd) {
|
|
|
|
// Type assert icmd to access parameters.
|
|
|
|
cmd, ok := icmd.(*btcjson.GetBalanceCmd)
|
2013-09-09 20:14:57 +02:00
|
|
|
if !ok {
|
2013-11-12 18:01:32 +01:00
|
|
|
ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal)
|
2013-09-09 20:14:57 +02:00
|
|
|
return
|
|
|
|
}
|
2013-09-03 17:00:01 +02:00
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
balance, err := accountstore.CalculateBalance(cmd.Account, cmd.MinConf)
|
|
|
|
if err != nil {
|
2013-11-12 18:01:32 +01:00
|
|
|
ReplyError(frontend, cmd.Id(),
|
|
|
|
&btcjson.ErrWalletInvalidAccountName)
|
|
|
|
return
|
2013-09-03 17:00:01 +02:00
|
|
|
}
|
2013-11-12 18:01:32 +01:00
|
|
|
|
|
|
|
// Reply with calculated balance.
|
2013-12-02 20:56:06 +01:00
|
|
|
ReplySuccess(frontend, cmd.Id(), balance)
|
2013-09-03 17:00:01 +02:00
|
|
|
}
|
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// GetBalances replies to a getbalances extension request by notifying
|
|
|
|
// the frontend of all balances for each opened account.
|
|
|
|
func GetBalances(frontend chan []byte, cmd btcjson.Cmd) {
|
|
|
|
NotifyBalances(frontend)
|
2013-10-29 07:19:40 +01:00
|
|
|
}
|
|
|
|
|
2013-12-10 22:15:25 +01:00
|
|
|
// GetAddressBalance replies to a getaddressbalance extension request
|
|
|
|
// by replying with the current balance (sum of unspent transaction
|
|
|
|
// output amounts) for a single address.
|
|
|
|
func GetAddressBalance(frontend chan []byte, icmd btcjson.Cmd) {
|
|
|
|
// Type assert icmd to access parameters.
|
|
|
|
cmd, ok := icmd.(*btcws.GetAddressBalanceCmd)
|
|
|
|
if !ok {
|
|
|
|
ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Is address valid?
|
|
|
|
pkhash, net, err := btcutil.DecodeAddress(cmd.Address)
|
|
|
|
if err != nil || net != cfg.Net() {
|
|
|
|
ReplyError(frontend, cmd.Id(), &btcjson.ErrInvalidAddressOrKey)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Look up account which holds this address.
|
|
|
|
aname, err := LookupAccountByAddress(cmd.Address)
|
|
|
|
if err == ErrNotFound {
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrInvalidAddressOrKey.Code,
|
|
|
|
Message: "Address not found in wallet",
|
|
|
|
}
|
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the account which holds the address in the request.
|
|
|
|
// This should not fail, so if it does, return an internal
|
|
|
|
// error to the frontend.
|
|
|
|
a, err := accountstore.Account(aname)
|
|
|
|
if err != nil {
|
|
|
|
ReplyError(frontend, cmd.Id(), &btcjson.ErrInternal)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
bal := a.CalculateAddressBalance(pkhash, int(cmd.Minconf))
|
|
|
|
ReplySuccess(frontend, cmd.Id(), bal)
|
|
|
|
}
|
|
|
|
|
2013-11-20 02:18:11 +01:00
|
|
|
// ImportPrivKey replies to an importprivkey request by parsing
|
|
|
|
// a WIF-encoded private key and adding it to an account.
|
|
|
|
func ImportPrivKey(frontend chan []byte, icmd btcjson.Cmd) {
|
|
|
|
// Type assert icmd to access parameters.
|
|
|
|
cmd, ok := icmd.(*btcjson.ImportPrivKeyCmd)
|
|
|
|
if !ok {
|
|
|
|
ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
// Get the acount included in the request. Yes, Label is the
|
|
|
|
// account name...
|
|
|
|
a, err := accountstore.Account(cmd.Label)
|
|
|
|
switch err {
|
|
|
|
case nil:
|
|
|
|
break
|
|
|
|
|
|
|
|
case ErrAcctNotExist:
|
2013-11-20 02:18:11 +01:00
|
|
|
ReplyError(frontend, cmd.Id(),
|
|
|
|
&btcjson.ErrWalletInvalidAccountName)
|
|
|
|
return
|
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
default:
|
2013-11-20 02:18:11 +01:00
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrWallet.Code,
|
|
|
|
Message: err.Error(),
|
|
|
|
}
|
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
// Import the private key, handling any errors.
|
|
|
|
switch err := a.ImportPrivKey(cmd.PrivKey, cmd.Rescan); err {
|
|
|
|
case nil:
|
|
|
|
// If the import was successful, reply with nil.
|
|
|
|
ReplySuccess(frontend, cmd.Id(), nil)
|
|
|
|
|
|
|
|
case wallet.ErrWalletLocked:
|
|
|
|
ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletUnlockNeeded)
|
|
|
|
|
|
|
|
default:
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrWallet.Code,
|
|
|
|
Message: err.Error(),
|
2013-11-20 02:18:11 +01:00
|
|
|
}
|
2013-12-02 20:56:06 +01:00
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
2013-11-20 02:18:11 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-11-15 17:44:24 +01:00
|
|
|
// NotifyBalances notifies an attached frontend of the current confirmed
|
2013-10-14 22:39:15 +02:00
|
|
|
// and unconfirmed account balances.
|
|
|
|
//
|
2013-12-02 20:56:06 +01:00
|
|
|
// TODO(jrick): Switch this to return a single JSON object
|
|
|
|
// (map[string]interface{}) of all accounts and their balances, instead of
|
|
|
|
// separate notifications for each account.
|
2013-11-15 17:44:24 +01:00
|
|
|
func NotifyBalances(frontend chan []byte) {
|
2013-12-02 20:56:06 +01:00
|
|
|
accountstore.NotifyBalances(frontend)
|
2013-10-09 17:23:54 +02:00
|
|
|
}
|
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// GetNewAddress responds to a getnewaddress request by getting a new
|
|
|
|
// address for an account. If the account does not exist, an appropiate
|
|
|
|
// error is returned to the frontend.
|
|
|
|
func GetNewAddress(frontend chan []byte, icmd btcjson.Cmd) {
|
|
|
|
// Type assert icmd to access parameters.
|
2013-11-12 18:40:20 +01:00
|
|
|
cmd, ok := icmd.(*btcjson.GetNewAddressCmd)
|
2013-09-09 20:14:57 +02:00
|
|
|
if !ok {
|
2013-11-12 18:01:32 +01:00
|
|
|
ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal)
|
2013-09-09 20:14:57 +02:00
|
|
|
return
|
|
|
|
}
|
2013-11-12 18:01:32 +01:00
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
a, err := accountstore.Account(cmd.Account)
|
|
|
|
switch err {
|
|
|
|
case nil:
|
|
|
|
break
|
|
|
|
|
|
|
|
case ErrAcctNotExist:
|
2013-11-12 18:01:32 +01:00
|
|
|
ReplyError(frontend, cmd.Id(),
|
|
|
|
&btcjson.ErrWalletInvalidAccountName)
|
|
|
|
return
|
2013-09-03 15:49:16 +02:00
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
case ErrBtcdDisconnected:
|
2013-11-21 17:57:28 +01:00
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrInternal.Code,
|
|
|
|
Message: "btcd disconnected",
|
|
|
|
}
|
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
|
|
|
return
|
2013-12-02 20:56:06 +01:00
|
|
|
|
|
|
|
default: // all other non-nil errors
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrWallet.Code,
|
|
|
|
Message: err.Error(),
|
|
|
|
}
|
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
|
|
|
return
|
2013-11-21 17:57:28 +01:00
|
|
|
}
|
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
addr, err := a.NewAddress()
|
|
|
|
switch err {
|
|
|
|
case nil:
|
|
|
|
// Reply with the new payment address string.
|
|
|
|
ReplySuccess(frontend, cmd.Id(), addr)
|
|
|
|
|
|
|
|
case wallet.ErrWalletLocked:
|
2013-11-21 17:57:28 +01:00
|
|
|
// The wallet is locked error may be sent if the keypool needs
|
|
|
|
// to be refilled, but the wallet is currently in a locked
|
|
|
|
// state. Notify the frontend that an unlock is needed to
|
|
|
|
// refill the keypool.
|
|
|
|
ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletKeypoolRanOut)
|
2013-12-02 20:56:06 +01:00
|
|
|
|
|
|
|
default: // all other non-nil errors
|
2013-11-21 17:57:28 +01:00
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrWallet.Code,
|
|
|
|
Message: err.Error(),
|
|
|
|
}
|
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
2013-11-12 18:01:32 +01:00
|
|
|
}
|
2013-09-09 20:14:57 +02:00
|
|
|
}
|
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// ListAccounts replies to a listaccounts request by returning a JSON
|
|
|
|
// object mapping account names with their balances.
|
|
|
|
func ListAccounts(frontend chan []byte, icmd btcjson.Cmd) {
|
|
|
|
// Type assert icmd to access parameters.
|
|
|
|
cmd, ok := icmd.(*btcjson.ListAccountsCmd)
|
|
|
|
if !ok {
|
|
|
|
ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal)
|
|
|
|
return
|
2013-10-09 17:23:54 +02:00
|
|
|
}
|
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
pairs := accountstore.ListAccounts(cmd.MinConf)
|
2013-10-09 17:23:54 +02:00
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// Reply with the map. This will be marshaled into a JSON object.
|
|
|
|
ReplySuccess(frontend, cmd.Id(), pairs)
|
2013-10-09 17:23:54 +02:00
|
|
|
}
|
|
|
|
|
2013-12-02 23:34:36 +01:00
|
|
|
// ListTransactions replies to a listtransactions request by returning an
|
|
|
|
// array of JSON objects with details of sent and recevied wallet
|
|
|
|
// transactions.
|
2013-11-22 19:42:25 +01:00
|
|
|
func ListTransactions(frontend chan []byte, icmd btcjson.Cmd) {
|
|
|
|
// Type assert icmd to access parameters.
|
|
|
|
cmd, ok := icmd.(*btcjson.ListTransactionsCmd)
|
|
|
|
if !ok {
|
|
|
|
ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
a, err := accountstore.Account(cmd.Account)
|
|
|
|
switch err {
|
|
|
|
case nil:
|
|
|
|
break
|
|
|
|
|
|
|
|
case ErrAcctNotExist:
|
2013-11-22 19:42:25 +01:00
|
|
|
ReplyError(frontend, cmd.Id(),
|
|
|
|
&btcjson.ErrWalletInvalidAccountName)
|
|
|
|
return
|
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
default: // all other non-nil errors
|
2013-11-22 19:42:25 +01:00
|
|
|
e := &btcjson.Error{
|
2013-12-02 20:56:06 +01:00
|
|
|
Code: btcjson.ErrWallet.Code,
|
2013-11-22 19:42:25 +01:00
|
|
|
Message: err.Error(),
|
|
|
|
}
|
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
switch txList, err := a.ListTransactions(cmd.From, cmd.Count); err {
|
|
|
|
case nil:
|
|
|
|
// Reply with the list of tx information.
|
|
|
|
ReplySuccess(frontend, cmd.Id(), txList)
|
|
|
|
|
|
|
|
case ErrBtcdDisconnected:
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrInternal.Code,
|
|
|
|
Message: "btcd disconnected",
|
2013-11-22 19:42:25 +01:00
|
|
|
}
|
2013-12-02 20:56:06 +01:00
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
2013-11-22 19:42:25 +01:00
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
default:
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrWallet.Code,
|
|
|
|
Message: err.Error(),
|
|
|
|
}
|
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
|
|
|
}
|
2013-11-22 19:42:25 +01:00
|
|
|
}
|
|
|
|
|
2013-12-02 23:34:36 +01:00
|
|
|
// ListAllTransactions replies to a listtransactions request by returning
|
|
|
|
// an array of JSON objects with details of sent and recevied wallet
|
|
|
|
// transactions. This is similar to ListTransactions, except it takes
|
|
|
|
// only a single optional argument for the account name and replies with
|
|
|
|
// all transactions.
|
|
|
|
func ListAllTransactions(frontend chan []byte, icmd btcjson.Cmd) {
|
|
|
|
// Type assert icmd to access parameters.
|
|
|
|
cmd, ok := icmd.(*btcws.ListAllTransactionsCmd)
|
|
|
|
if !ok {
|
|
|
|
ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
a, err := accountstore.Account(cmd.Account)
|
|
|
|
switch err {
|
|
|
|
case nil:
|
|
|
|
break
|
|
|
|
|
|
|
|
case ErrAcctNotExist:
|
|
|
|
ReplyError(frontend, cmd.Id(),
|
|
|
|
&btcjson.ErrWalletInvalidAccountName)
|
|
|
|
return
|
|
|
|
|
|
|
|
default: // all other non-nil errors
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrWallet.Code,
|
|
|
|
Message: err.Error(),
|
|
|
|
}
|
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
switch txList, err := a.ListAllTransactions(); err {
|
|
|
|
case nil:
|
|
|
|
// Reply with the list of tx information.
|
|
|
|
ReplySuccess(frontend, cmd.Id(), txList)
|
|
|
|
|
|
|
|
case ErrBtcdDisconnected:
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrInternal.Code,
|
|
|
|
Message: "btcd disconnected",
|
|
|
|
}
|
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
|
|
|
|
|
|
|
default:
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrWallet.Code,
|
|
|
|
Message: err.Error(),
|
|
|
|
}
|
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-09-09 20:14:57 +02:00
|
|
|
// SendFrom creates a new transaction spending unspent transaction
|
|
|
|
// outputs for a wallet to another payment address. Leftover inputs
|
|
|
|
// not sent to the payment address or a fee for the miner are sent
|
2013-11-12 18:01:32 +01:00
|
|
|
// back to a new address in the wallet. Upon success, the TxID
|
|
|
|
// for the created transaction is sent to the frontend.
|
|
|
|
func SendFrom(frontend chan []byte, icmd btcjson.Cmd) {
|
|
|
|
// Type assert icmd to access parameters.
|
|
|
|
cmd, ok := icmd.(*btcjson.SendFromCmd)
|
2013-09-09 20:14:57 +02:00
|
|
|
if !ok {
|
2013-11-12 18:01:32 +01:00
|
|
|
ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal)
|
2013-09-09 20:14:57 +02:00
|
|
|
return
|
|
|
|
}
|
2013-11-12 18:01:32 +01:00
|
|
|
|
|
|
|
// Check that signed integer parameters are positive.
|
|
|
|
if cmd.Amount < 0 {
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrInvalidParameter.Code,
|
|
|
|
Message: "amount must be positive",
|
2013-09-09 20:14:57 +02:00
|
|
|
}
|
2013-11-12 18:01:32 +01:00
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
|
|
|
return
|
2013-09-09 20:14:57 +02:00
|
|
|
}
|
2013-11-12 18:01:32 +01:00
|
|
|
if cmd.MinConf < 0 {
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrInvalidParameter.Code,
|
|
|
|
Message: "minconf must be positive",
|
2013-09-09 20:14:57 +02:00
|
|
|
}
|
2013-11-12 18:01:32 +01:00
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
|
|
|
return
|
2013-09-09 20:14:57 +02:00
|
|
|
}
|
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// Check that the account specified in the request exists.
|
2013-12-02 20:56:06 +01:00
|
|
|
a, err := accountstore.Account(cmd.FromAccount)
|
|
|
|
if err != nil {
|
2013-11-12 18:01:32 +01:00
|
|
|
ReplyError(frontend, cmd.Id(),
|
|
|
|
&btcjson.ErrWalletInvalidAccountName)
|
2013-10-29 07:19:40 +01:00
|
|
|
return
|
|
|
|
}
|
2013-11-12 18:01:32 +01:00
|
|
|
|
|
|
|
// Create map of address and amount pairs.
|
|
|
|
pairs := map[string]int64{
|
|
|
|
cmd.ToAddress: cmd.Amount,
|
2013-09-09 20:14:57 +02:00
|
|
|
}
|
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// Create transaction, replying with an error if the creation
|
|
|
|
// was not successful.
|
2013-12-04 01:22:47 +01:00
|
|
|
createdTx, err := a.txToPairs(pairs, cmd.MinConf)
|
2013-11-12 18:01:32 +01:00
|
|
|
switch {
|
|
|
|
case err == ErrNonPositiveAmount:
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrInvalidParameter.Code,
|
|
|
|
Message: "amount must be positive",
|
|
|
|
}
|
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
|
|
|
return
|
|
|
|
|
|
|
|
case err == wallet.ErrWalletLocked:
|
|
|
|
ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletUnlockNeeded)
|
|
|
|
return
|
|
|
|
|
|
|
|
case err != nil:
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrInternal.Code,
|
|
|
|
Message: err.Error(),
|
|
|
|
}
|
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
2013-09-09 20:14:57 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2013-11-06 18:37:46 +01:00
|
|
|
// If a change address was added, mark wallet as dirty, sync to disk,
|
|
|
|
// and Request updates for change address.
|
|
|
|
if len(createdTx.changeAddr) != 0 {
|
2013-11-15 17:44:24 +01:00
|
|
|
a.dirty = true
|
|
|
|
if err := a.writeDirtyToDisk(); err != nil {
|
2013-11-06 18:37:46 +01:00
|
|
|
log.Errorf("cannot write dirty wallet: %v", err)
|
|
|
|
}
|
2013-11-15 17:44:24 +01:00
|
|
|
a.ReqNewTxsForAddress(createdTx.changeAddr)
|
2013-11-06 18:37:46 +01:00
|
|
|
}
|
2013-10-28 19:13:20 +01:00
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// Create sendrawtransaction request with hexstring of the raw tx.
|
2013-10-15 16:40:23 +02:00
|
|
|
n := <-NewJSONID
|
2013-10-15 15:42:10 +02:00
|
|
|
var id interface{} = fmt.Sprintf("btcwallet(%v)", n)
|
2013-10-04 21:02:17 +02:00
|
|
|
m, err := btcjson.CreateMessageWithId("sendrawtransaction", id,
|
2013-10-28 16:18:37 +01:00
|
|
|
hex.EncodeToString(createdTx.rawTx))
|
2013-10-04 21:02:17 +02:00
|
|
|
if err != nil {
|
2013-11-12 18:01:32 +01:00
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrInternal.Code,
|
|
|
|
Message: err.Error(),
|
|
|
|
}
|
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
2013-10-04 21:02:17 +02:00
|
|
|
return
|
|
|
|
}
|
2013-11-12 18:01:32 +01:00
|
|
|
|
|
|
|
// Set up a reply handler to respond to the btcd reply.
|
2013-10-15 15:42:10 +02:00
|
|
|
replyHandlers.Lock()
|
|
|
|
replyHandlers.m[n] = func(result interface{}, err *btcjson.Error) bool {
|
2013-11-15 17:44:24 +01:00
|
|
|
return handleSendRawTxReply(frontend, cmd, result, err, a,
|
2013-11-12 18:01:32 +01:00
|
|
|
createdTx)
|
2013-10-15 15:42:10 +02:00
|
|
|
}
|
|
|
|
replyHandlers.Unlock()
|
2013-11-12 18:01:32 +01:00
|
|
|
|
|
|
|
// Send sendrawtransaction request to btcd.
|
2013-10-15 15:42:10 +02:00
|
|
|
btcdMsgs <- m
|
2013-09-09 20:14:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// SendMany creates a new transaction spending unspent transaction
|
|
|
|
// outputs for a wallet to any number of payment addresses. Leftover
|
|
|
|
// inputs not sent to the payment address or a fee for the miner are
|
2013-11-12 18:01:32 +01:00
|
|
|
// sent back to a new address in the wallet. Upon success, the TxID
|
|
|
|
// for the created transaction is sent to the frontend.
|
|
|
|
func SendMany(frontend chan []byte, icmd btcjson.Cmd) {
|
|
|
|
// Type assert icmd to access parameters.
|
|
|
|
cmd, ok := icmd.(*btcjson.SendManyCmd)
|
2013-09-09 20:14:57 +02:00
|
|
|
if !ok {
|
2013-11-12 18:01:32 +01:00
|
|
|
ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal)
|
2013-09-09 20:14:57 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// Check that minconf is positive.
|
|
|
|
if cmd.MinConf < 0 {
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrInvalidParameter.Code,
|
|
|
|
Message: "minconf must be positive",
|
2013-09-09 20:14:57 +02:00
|
|
|
}
|
2013-11-12 18:01:32 +01:00
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
|
|
|
return
|
2013-09-09 20:14:57 +02:00
|
|
|
}
|
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// Check that the account specified in the request exists.
|
2013-12-02 20:56:06 +01:00
|
|
|
a, err := accountstore.Account(cmd.FromAccount)
|
|
|
|
if err != nil {
|
2013-11-12 18:01:32 +01:00
|
|
|
ReplyError(frontend, cmd.Id(),
|
|
|
|
&btcjson.ErrWalletInvalidAccountName)
|
2013-09-09 20:14:57 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// Create transaction, replying with an error if the creation
|
|
|
|
// was not successful.
|
2013-12-04 01:22:47 +01:00
|
|
|
createdTx, err := a.txToPairs(cmd.Amounts, cmd.MinConf)
|
2013-11-12 18:01:32 +01:00
|
|
|
switch {
|
|
|
|
case err == ErrNonPositiveAmount:
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrInvalidParameter.Code,
|
|
|
|
Message: "amount must be positive",
|
|
|
|
}
|
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
|
|
|
return
|
|
|
|
|
|
|
|
case err == wallet.ErrWalletLocked:
|
|
|
|
ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletUnlockNeeded)
|
|
|
|
return
|
|
|
|
|
|
|
|
case err != nil:
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrInternal.Code,
|
|
|
|
Message: err.Error(),
|
|
|
|
}
|
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
2013-09-09 20:14:57 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2013-11-06 18:37:46 +01:00
|
|
|
// If a change address was added, mark wallet as dirty, sync to disk,
|
2013-11-12 18:01:32 +01:00
|
|
|
// and request updates for change address.
|
2013-11-06 18:37:46 +01:00
|
|
|
if len(createdTx.changeAddr) != 0 {
|
2013-11-15 17:44:24 +01:00
|
|
|
a.dirty = true
|
|
|
|
if err := a.writeDirtyToDisk(); err != nil {
|
2013-11-06 18:37:46 +01:00
|
|
|
log.Errorf("cannot write dirty wallet: %v", err)
|
|
|
|
}
|
2013-11-15 17:44:24 +01:00
|
|
|
a.ReqNewTxsForAddress(createdTx.changeAddr)
|
2013-11-06 18:37:46 +01:00
|
|
|
}
|
2013-10-28 19:13:20 +01:00
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// Create sendrawtransaction request with hexstring of the raw tx.
|
2013-10-15 16:40:23 +02:00
|
|
|
n := <-NewJSONID
|
2013-10-15 15:42:10 +02:00
|
|
|
var id interface{} = fmt.Sprintf("btcwallet(%v)", n)
|
2013-10-04 21:02:17 +02:00
|
|
|
m, err := btcjson.CreateMessageWithId("sendrawtransaction", id,
|
2013-10-28 16:18:37 +01:00
|
|
|
hex.EncodeToString(createdTx.rawTx))
|
2013-10-04 21:02:17 +02:00
|
|
|
if err != nil {
|
2013-11-12 18:01:32 +01:00
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrInternal.Code,
|
|
|
|
Message: err.Error(),
|
|
|
|
}
|
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
2013-10-04 21:02:17 +02:00
|
|
|
return
|
|
|
|
}
|
2013-11-12 18:01:32 +01:00
|
|
|
|
|
|
|
// Set up a reply handler to respond to the btcd reply.
|
2013-10-15 15:42:10 +02:00
|
|
|
replyHandlers.Lock()
|
|
|
|
replyHandlers.m[n] = func(result interface{}, err *btcjson.Error) bool {
|
2013-11-15 17:44:24 +01:00
|
|
|
return handleSendRawTxReply(frontend, cmd, result, err, a,
|
2013-11-12 18:01:32 +01:00
|
|
|
createdTx)
|
|
|
|
}
|
|
|
|
replyHandlers.Unlock()
|
Perform smarter UTXO tracking.
This change fixes many issues with the tracking of unspent transaction
outputs. First, notifications for when UTXOs arse spent are now
requested from btcd, and when spent, will be removed from the
UtxoStore.
Second, when transactions are created, the unconfirmed (not yet in a
block) Utxo (with block height -1 and zeroed block hash) is added to
the wallet's UtxoStore. Notifications for when this UTXO is spent are
also requested from btcd. After the tx appears in a block, because
the UTXO has a pkScript to be spent by another owned wallet address, a
notification with the UTXO will be sent to btcwallet. We already
store the unconfirmed UTXO, so at this point the actual block height
and hash are filled in.
Finally, when calculating the balance, if confirmations is zero,
unconfirmed UTXOs (block height -1) will be included in the balance.
Otherwise, they are ignored.
2013-10-22 15:55:53 +02:00
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// Send sendrawtransaction request to btcd.
|
|
|
|
btcdMsgs <- m
|
|
|
|
}
|
2013-10-15 16:27:54 +02:00
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
func handleSendRawTxReply(frontend chan []byte, icmd btcjson.Cmd,
|
2013-11-22 19:42:25 +01:00
|
|
|
result interface{}, e *btcjson.Error, a *Account,
|
2013-11-12 18:01:32 +01:00
|
|
|
txInfo *CreatedTx) bool {
|
2013-10-24 00:23:20 +02:00
|
|
|
|
2013-11-22 19:42:25 +01:00
|
|
|
if e != nil {
|
2013-11-27 22:07:00 +01:00
|
|
|
log.Errorf("Could not send tx: %v", e.Message)
|
2013-11-22 19:42:25 +01:00
|
|
|
ReplyError(frontend, icmd.Id(), e)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
txIDStr, ok := result.(string)
|
|
|
|
if !ok {
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrInternal.Code,
|
|
|
|
Message: "Unexpected type from btcd reply",
|
|
|
|
}
|
|
|
|
ReplyError(frontend, icmd.Id(), e)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
txID, err := btcwire.NewShaHashFromStr(txIDStr)
|
2013-11-12 18:01:32 +01:00
|
|
|
if err != nil {
|
2013-11-22 19:42:25 +01:00
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrInternal.Code,
|
|
|
|
Message: "Invalid hash string from btcd reply",
|
|
|
|
}
|
|
|
|
ReplyError(frontend, icmd.Id(), e)
|
2013-11-12 18:01:32 +01:00
|
|
|
return true
|
|
|
|
}
|
2013-10-15 16:27:54 +02:00
|
|
|
|
2013-11-22 19:42:25 +01:00
|
|
|
// Add to transaction store.
|
|
|
|
sendtx := &tx.SendTx{
|
|
|
|
TxID: *txID,
|
|
|
|
Time: txInfo.time.Unix(),
|
|
|
|
BlockHeight: -1,
|
|
|
|
Fee: txInfo.fee,
|
|
|
|
Receivers: txInfo.outputs,
|
|
|
|
}
|
|
|
|
a.TxStore.Lock()
|
|
|
|
a.TxStore.s = append(a.TxStore.s, sendtx)
|
|
|
|
a.TxStore.dirty = true
|
|
|
|
a.TxStore.Unlock()
|
|
|
|
|
2013-12-02 23:34:36 +01:00
|
|
|
// Notify frontends of new SendTx.
|
|
|
|
bs, err := GetCurBlock()
|
|
|
|
if err == nil {
|
|
|
|
for _, details := range sendtx.TxInfo(a.Name(), bs.Height, a.Net()) {
|
|
|
|
NotifyNewTxDetails(frontendNotificationMaster, a.Name(),
|
|
|
|
details)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// Remove previous unspent outputs now spent by the tx.
|
2013-11-15 17:44:24 +01:00
|
|
|
a.UtxoStore.Lock()
|
|
|
|
modified := a.UtxoStore.s.Remove(txInfo.inputs)
|
2013-11-22 19:42:25 +01:00
|
|
|
a.UtxoStore.dirty = a.UtxoStore.dirty || modified
|
2013-09-09 20:14:57 +02:00
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// Add unconfirmed change utxo (if any) to UtxoStore.
|
|
|
|
if txInfo.changeUtxo != nil {
|
2013-11-15 17:44:24 +01:00
|
|
|
a.UtxoStore.s = append(a.UtxoStore.s, txInfo.changeUtxo)
|
|
|
|
a.ReqSpentUtxoNtfn(txInfo.changeUtxo)
|
|
|
|
a.UtxoStore.dirty = true
|
2013-11-22 19:42:25 +01:00
|
|
|
}
|
|
|
|
a.UtxoStore.Unlock()
|
2013-11-12 18:01:32 +01:00
|
|
|
|
2013-11-22 19:42:25 +01:00
|
|
|
// Disk sync tx and utxo stores.
|
|
|
|
if err := a.writeDirtyToDisk(); err != nil {
|
|
|
|
log.Errorf("cannot sync dirty wallet: %v", err)
|
2013-11-12 18:01:32 +01:00
|
|
|
}
|
|
|
|
|
2013-11-22 19:42:25 +01:00
|
|
|
// Notify all frontends of account's new unconfirmed and
|
|
|
|
// confirmed balance.
|
|
|
|
confirmed := a.CalculateBalance(1)
|
|
|
|
unconfirmed := a.CalculateBalance(0) - confirmed
|
|
|
|
NotifyWalletBalance(frontendNotificationMaster, a.name, confirmed)
|
|
|
|
NotifyWalletBalanceUnconfirmed(frontendNotificationMaster, a.name, unconfirmed)
|
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// btcd cannot be trusted to successfully relay the tx to the
|
|
|
|
// Bitcoin network. Even if this succeeds, the rawtx must be
|
|
|
|
// saved and checked for an appearence in a later block. btcd
|
|
|
|
// will make a best try effort, but ultimately it's btcwallet's
|
|
|
|
// responsibility.
|
|
|
|
//
|
|
|
|
// Add hex string of raw tx to sent tx pool. If btcd disconnects
|
|
|
|
// and is reconnected, these txs are resent.
|
|
|
|
UnminedTxs.Lock()
|
2013-11-22 19:42:25 +01:00
|
|
|
UnminedTxs.m[TXID(*txID)] = txInfo
|
2013-11-12 18:01:32 +01:00
|
|
|
UnminedTxs.Unlock()
|
|
|
|
|
2013-11-27 20:50:43 +01:00
|
|
|
log.Infof("Successfully sent transaction %v", result)
|
2013-11-12 18:01:32 +01:00
|
|
|
ReplySuccess(frontend, icmd.Id(), result)
|
|
|
|
|
|
|
|
// The comments to be saved differ based on the underlying type
|
|
|
|
// of the cmd, so switch on the type to check whether it is a
|
|
|
|
// SendFromCmd or SendManyCmd.
|
|
|
|
//
|
|
|
|
// TODO(jrick): If message succeeded in being sent, save the
|
|
|
|
// transaction details with comments.
|
|
|
|
switch cmd := icmd.(type) {
|
|
|
|
case *btcjson.SendFromCmd:
|
|
|
|
_ = cmd.Comment
|
|
|
|
_ = cmd.CommentTo
|
|
|
|
|
|
|
|
case *btcjson.SendManyCmd:
|
|
|
|
_ = cmd.Comment
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
2013-09-03 15:49:16 +02:00
|
|
|
}
|
|
|
|
|
2013-12-04 01:22:47 +01:00
|
|
|
// SetTxFee sets the transaction fee per kilobyte added to transactions.
|
2013-11-12 18:01:32 +01:00
|
|
|
func SetTxFee(frontend chan []byte, icmd btcjson.Cmd) {
|
|
|
|
// Type assert icmd to access parameters.
|
|
|
|
cmd, ok := icmd.(*btcjson.SetTxFeeCmd)
|
2013-10-07 21:14:39 +02:00
|
|
|
if !ok {
|
2013-11-12 18:01:32 +01:00
|
|
|
ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal)
|
2013-10-07 21:14:39 +02:00
|
|
|
return
|
|
|
|
}
|
2013-11-12 18:01:32 +01:00
|
|
|
|
|
|
|
// Check that amount is not negative.
|
|
|
|
if cmd.Amount < 0 {
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrInvalidParams.Code,
|
|
|
|
Message: "amount cannot be negative",
|
|
|
|
}
|
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
2013-10-07 21:14:39 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// Set global tx fee.
|
2013-12-04 01:22:47 +01:00
|
|
|
TxFeeIncrement.Lock()
|
|
|
|
TxFeeIncrement.i = cmd.Amount
|
|
|
|
TxFeeIncrement.Unlock()
|
2013-10-07 21:14:39 +02:00
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// A boolean true result is returned upon success.
|
|
|
|
ReplySuccess(frontend, cmd.Id(), true)
|
2013-10-07 21:14:39 +02:00
|
|
|
}
|
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// CreateEncryptedWallet creates a new account with an encrypted
|
|
|
|
// wallet. If an account with the same name as the requested account
|
|
|
|
// name already exists, an invalid account name error is returned to
|
|
|
|
// the client.
|
2013-10-07 18:35:32 +02:00
|
|
|
//
|
2013-10-09 01:33:22 +02:00
|
|
|
// Wallets will be created on TestNet3, or MainNet if btcwallet is run with
|
|
|
|
// the --mainnet option.
|
2013-11-12 18:01:32 +01:00
|
|
|
func CreateEncryptedWallet(frontend chan []byte, icmd btcjson.Cmd) {
|
|
|
|
// Type assert icmd to access parameters.
|
|
|
|
cmd, ok := icmd.(*btcws.CreateEncryptedWalletCmd)
|
2013-09-09 20:14:57 +02:00
|
|
|
if !ok {
|
2013-11-12 18:01:32 +01:00
|
|
|
ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal)
|
2013-09-03 15:49:16 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
err := accountstore.CreateEncryptedWallet(cmd.Account, cmd.Description,
|
|
|
|
[]byte(cmd.Passphrase))
|
|
|
|
switch err {
|
|
|
|
case nil:
|
|
|
|
// A nil reply is sent upon successful wallet creation.
|
|
|
|
ReplySuccess(frontend, cmd.Id(), nil)
|
2013-10-29 07:19:40 +01:00
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
case ErrAcctNotExist:
|
2013-11-12 18:01:32 +01:00
|
|
|
ReplyError(frontend, cmd.Id(),
|
|
|
|
&btcjson.ErrWalletInvalidAccountName)
|
2013-09-03 15:49:16 +02:00
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
case ErrBtcdDisconnected:
|
2013-11-12 18:01:32 +01:00
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrInternal.Code,
|
|
|
|
Message: "btcd disconnected",
|
|
|
|
}
|
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
default:
|
2013-11-12 18:01:32 +01:00
|
|
|
ReplyError(frontend, cmd.Id(), &btcjson.ErrInternal)
|
2013-10-15 22:55:28 +02:00
|
|
|
}
|
2013-08-21 16:37:30 +02:00
|
|
|
}
|
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// WalletIsLocked responds to the walletislocked extension request by
|
|
|
|
// replying with the current lock state (false for unlocked, true for
|
|
|
|
// locked) of an account. An error is returned if the requested account
|
|
|
|
// does not exist.
|
|
|
|
func WalletIsLocked(frontend chan []byte, icmd btcjson.Cmd) {
|
|
|
|
// Type assert icmd to access parameters.
|
|
|
|
cmd, ok := icmd.(*btcws.WalletIsLockedCmd)
|
2013-09-09 20:14:57 +02:00
|
|
|
if !ok {
|
2013-11-12 18:01:32 +01:00
|
|
|
ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal)
|
|
|
|
return
|
2013-08-21 20:46:20 +02:00
|
|
|
}
|
2013-10-29 07:19:40 +01:00
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
a, err := accountstore.Account(cmd.Account)
|
|
|
|
switch err {
|
|
|
|
case nil:
|
|
|
|
break
|
|
|
|
|
|
|
|
case ErrAcctNotExist:
|
2013-11-12 18:01:32 +01:00
|
|
|
ReplyError(frontend, cmd.Id(),
|
|
|
|
&btcjson.ErrWalletInvalidAccountName)
|
|
|
|
return
|
2013-12-02 20:56:06 +01:00
|
|
|
|
|
|
|
default: // all other non-nil errors
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrWallet.Code,
|
|
|
|
Message: err.Error(),
|
|
|
|
}
|
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
|
|
|
return
|
2013-08-21 20:46:20 +02:00
|
|
|
}
|
2013-11-12 18:01:32 +01:00
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
a.mtx.RLock()
|
|
|
|
locked := a.Wallet.IsLocked()
|
|
|
|
a.mtx.RUnlock()
|
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// Reply with true for a locked wallet, and false for unlocked.
|
2013-12-02 20:56:06 +01:00
|
|
|
ReplySuccess(frontend, cmd.Id(), locked)
|
2013-08-21 20:46:20 +02:00
|
|
|
}
|
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// WalletLock responds to walletlock request by locking the wallet,
|
|
|
|
// replying with an error if the wallet is already locked.
|
2013-08-21 16:37:30 +02:00
|
|
|
//
|
|
|
|
// TODO(jrick): figure out how multiple wallets/accounts will work
|
2013-09-09 20:14:57 +02:00
|
|
|
// with this. Lock all the wallets, like if all accounts are locked
|
|
|
|
// for one bitcoind wallet?
|
2013-11-12 18:01:32 +01:00
|
|
|
func WalletLock(frontend chan []byte, icmd btcjson.Cmd) {
|
2013-12-02 20:56:06 +01:00
|
|
|
a, err := accountstore.Account("")
|
|
|
|
switch err {
|
|
|
|
case nil:
|
|
|
|
break
|
|
|
|
|
|
|
|
case ErrAcctNotExist:
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrWallet.Code,
|
|
|
|
Message: "default account does not exist",
|
|
|
|
}
|
|
|
|
ReplyError(frontend, icmd.Id(), e)
|
|
|
|
return
|
|
|
|
|
|
|
|
default: // all other non-nil errors
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrWallet.Code,
|
|
|
|
Message: err.Error(),
|
2013-08-21 19:25:22 +02:00
|
|
|
}
|
2013-12-02 20:56:06 +01:00
|
|
|
ReplyError(frontend, icmd.Id(), e)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
switch err := a.Lock(); err {
|
|
|
|
case nil:
|
2013-11-12 18:01:32 +01:00
|
|
|
ReplySuccess(frontend, icmd.Id(), nil)
|
2013-12-02 20:56:06 +01:00
|
|
|
|
|
|
|
default:
|
2013-11-21 17:23:50 +01:00
|
|
|
ReplyError(frontend, icmd.Id(),
|
2013-12-02 20:56:06 +01:00
|
|
|
&btcjson.ErrWalletWrongEncState)
|
2013-08-21 19:25:22 +02:00
|
|
|
}
|
2013-08-21 16:37:30 +02:00
|
|
|
}
|
|
|
|
|
2013-11-12 18:01:32 +01:00
|
|
|
// WalletPassphrase responds to the walletpassphrase request by unlocking
|
|
|
|
// the wallet. The decryption key is saved in the wallet until timeout
|
|
|
|
// seconds expires, after which the wallet is locked.
|
2013-08-21 16:37:30 +02:00
|
|
|
//
|
2013-09-09 20:14:57 +02:00
|
|
|
// TODO(jrick): figure out how to do this for non-default accounts.
|
2013-11-12 18:01:32 +01:00
|
|
|
func WalletPassphrase(frontend chan []byte, icmd btcjson.Cmd) {
|
|
|
|
// Type assert icmd to access parameters.
|
|
|
|
cmd, ok := icmd.(*btcjson.WalletPassphraseCmd)
|
2013-09-09 20:14:57 +02:00
|
|
|
if !ok {
|
2013-11-12 18:01:32 +01:00
|
|
|
ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal)
|
2013-08-21 16:37:30 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2013-12-02 20:56:06 +01:00
|
|
|
a, err := accountstore.Account("")
|
|
|
|
switch err {
|
|
|
|
case nil:
|
|
|
|
break
|
|
|
|
|
|
|
|
case ErrAcctNotExist:
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrWallet.Code,
|
|
|
|
Message: "default account does not exist",
|
|
|
|
}
|
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
|
|
|
return
|
|
|
|
|
|
|
|
default: // all other non-nil errors
|
|
|
|
e := &btcjson.Error{
|
|
|
|
Code: btcjson.ErrWallet.Code,
|
|
|
|
Message: err.Error(),
|
2013-08-21 18:07:57 +02:00
|
|
|
}
|
2013-12-02 20:56:06 +01:00
|
|
|
ReplyError(frontend, cmd.Id(), e)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
switch err := a.Unlock([]byte(cmd.Passphrase), cmd.Timeout); err {
|
|
|
|
case nil:
|
2013-11-12 18:01:32 +01:00
|
|
|
ReplySuccess(frontend, cmd.Id(), nil)
|
2013-12-02 20:56:06 +01:00
|
|
|
|
2013-10-09 17:23:54 +02:00
|
|
|
NotifyWalletLockStateChange("", false)
|
2013-12-02 20:56:06 +01:00
|
|
|
go func(timeout int64) {
|
|
|
|
time.Sleep(time.Second * time.Duration(timeout))
|
|
|
|
_ = a.Lock()
|
|
|
|
}(cmd.Timeout)
|
|
|
|
|
|
|
|
case ErrAcctNotExist:
|
2013-11-21 17:06:46 +01:00
|
|
|
ReplyError(frontend, cmd.Id(),
|
|
|
|
&btcjson.ErrWalletInvalidAccountName)
|
2013-12-02 20:56:06 +01:00
|
|
|
|
|
|
|
default:
|
|
|
|
ReplyError(frontend, cmd.Id(),
|
|
|
|
&btcjson.ErrWalletPassphraseIncorrect)
|
2013-08-21 16:37:30 +02:00
|
|
|
}
|
|
|
|
}
|
2013-08-22 18:00:37 +02:00
|
|
|
|
2013-10-14 22:39:15 +02:00
|
|
|
// AccountNtfn is a struct for marshalling any generic notification
|
|
|
|
// about a account for a wallet frontend.
|
|
|
|
//
|
|
|
|
// TODO(jrick): move to btcjson so it can be shared with frontends?
|
2013-10-09 17:23:54 +02:00
|
|
|
type AccountNtfn struct {
|
|
|
|
Account string `json:"account"`
|
|
|
|
Notification interface{} `json:"notification"`
|
|
|
|
}
|
|
|
|
|
2013-08-22 18:00:37 +02:00
|
|
|
// NotifyWalletLockStateChange sends a notification to all frontends
|
|
|
|
// that the wallet has just been locked or unlocked.
|
2013-10-09 17:23:54 +02:00
|
|
|
func NotifyWalletLockStateChange(account string, locked bool) {
|
2013-08-22 18:00:37 +02:00
|
|
|
var id interface{} = "btcwallet:newwalletlockstate"
|
|
|
|
m := btcjson.Reply{
|
2013-10-09 17:23:54 +02:00
|
|
|
Result: &AccountNtfn{
|
|
|
|
Account: account,
|
|
|
|
Notification: locked,
|
|
|
|
},
|
|
|
|
Id: &id,
|
2013-08-22 18:00:37 +02:00
|
|
|
}
|
|
|
|
msg, _ := json.Marshal(&m)
|
|
|
|
frontendNotificationMaster <- msg
|
|
|
|
}
|
2013-10-09 17:23:54 +02:00
|
|
|
|
2013-10-14 22:39:15 +02:00
|
|
|
// NotifyWalletBalance sends a confirmed account balance notification
|
|
|
|
// to a frontend.
|
2013-10-09 17:23:54 +02:00
|
|
|
func NotifyWalletBalance(frontend chan []byte, account string, balance float64) {
|
|
|
|
var id interface{} = "btcwallet:accountbalance"
|
|
|
|
m := btcjson.Reply{
|
|
|
|
Result: &AccountNtfn{
|
|
|
|
Account: account,
|
|
|
|
Notification: balance,
|
|
|
|
},
|
|
|
|
Id: &id,
|
|
|
|
}
|
|
|
|
msg, _ := json.Marshal(&m)
|
|
|
|
frontend <- msg
|
|
|
|
}
|
|
|
|
|
2013-12-02 23:34:36 +01:00
|
|
|
// NotifyWalletBalanceUnconfirmed sends a confirmed account balance
|
2013-10-14 22:39:15 +02:00
|
|
|
// notification to a frontend.
|
2013-10-09 17:23:54 +02:00
|
|
|
func NotifyWalletBalanceUnconfirmed(frontend chan []byte, account string, balance float64) {
|
|
|
|
var id interface{} = "btcwallet:accountbalanceunconfirmed"
|
|
|
|
m := btcjson.Reply{
|
|
|
|
Result: &AccountNtfn{
|
|
|
|
Account: account,
|
|
|
|
Notification: balance,
|
|
|
|
},
|
|
|
|
Id: &id,
|
|
|
|
}
|
|
|
|
msg, _ := json.Marshal(&m)
|
|
|
|
frontend <- msg
|
|
|
|
}
|
2013-12-02 23:34:36 +01:00
|
|
|
|
|
|
|
// NotifyNewTxDetails sends details of a new transaction to a frontend.
|
|
|
|
func NotifyNewTxDetails(frontend chan []byte, account string,
|
|
|
|
details map[string]interface{}) {
|
|
|
|
|
|
|
|
ntfn := btcws.NewTxNtfn(account, details)
|
|
|
|
mntfn, _ := ntfn.MarshalJSON()
|
|
|
|
frontend <- mntfn
|
|
|
|
}
|