Create transactions using saved utxo data.
This is a big change that also many general fixes to problems found when creating transactions. In particular the Utxo and Tx formats and serialization functions were updated with additional information that would be necessary for rolling back old utxo and tx data data after btcd chain switches. This change also implements the json methods 'sendfrom' and 'sendmany' to create a new transaction based on a frontend request. Transactions are currently not sent to btcd since the tx relay code is not finished yet, so a temporary error is returned back to frontends who try to send new transactions.
This commit is contained in:
parent
fa85e586fc
commit
63686347c6
9 changed files with 1327 additions and 507 deletions
350
cmd.go
350
cmd.go
|
@ -37,58 +37,26 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrNoWallet = errors.New("Wallet file does not exist.")
|
// ErrNoWallet describes an error where a wallet does not exist and
|
||||||
)
|
// must be created first.
|
||||||
|
ErrNoWallet = errors.New("wallet file does not exist")
|
||||||
|
|
||||||
|
cfg *config
|
||||||
|
log = seelog.Default
|
||||||
|
|
||||||
var (
|
|
||||||
log seelog.LoggerInterface = seelog.Default
|
|
||||||
cfg *config
|
|
||||||
curHeight = struct {
|
curHeight = struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
h int64
|
h int64
|
||||||
}{
|
}{
|
||||||
h: btcutil.BlockHeightUnknown,
|
h: btcutil.BlockHeightUnknown,
|
||||||
}
|
}
|
||||||
wallets = struct {
|
wallets = NewBtcWalletStore()
|
||||||
sync.RWMutex
|
|
||||||
m map[string]*BtcWallet
|
|
||||||
}{
|
|
||||||
m: make(map[string]*BtcWallet),
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
// BtcWallet is a structure containing all the components for a
|
||||||
tcfg, _, err := loadConfig()
|
// complete wallet. It contains the Armory-style wallet (to store
|
||||||
if err != nil {
|
// addresses and keys), and tx and utxo data stores, along with locks
|
||||||
fmt.Println(err)
|
// to prevent against incorrect multiple access.
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
cfg = tcfg
|
|
||||||
|
|
||||||
// Open wallet
|
|
||||||
w, err := OpenWallet(cfg, "")
|
|
||||||
if err != nil {
|
|
||||||
log.Info(err.Error())
|
|
||||||
} else {
|
|
||||||
wallets.Lock()
|
|
||||||
wallets.m[""] = w
|
|
||||||
wallets.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start HTTP server to listen and send messages to frontend and btcd
|
|
||||||
// backend. Try reconnection if connection failed.
|
|
||||||
for {
|
|
||||||
if err := ListenAndServe(); err == ConnRefused {
|
|
||||||
// wait and try again.
|
|
||||||
log.Info("Unable to connect to btcd. Retrying in 5 seconds.")
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
} else if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type BtcWallet struct {
|
type BtcWallet struct {
|
||||||
*wallet.Wallet
|
*wallet.Wallet
|
||||||
mtx sync.RWMutex
|
mtx sync.RWMutex
|
||||||
|
@ -106,6 +74,41 @@ type BtcWallet struct {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BtcWalletStore struct {
|
||||||
|
sync.RWMutex
|
||||||
|
m map[string]*BtcWallet
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBtcWalletStore returns an initialized and empty BtcWalletStore.
|
||||||
|
func NewBtcWalletStore() *BtcWalletStore {
|
||||||
|
return &BtcWalletStore{
|
||||||
|
m: make(map[string]*BtcWallet),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rollback reverts each stored BtcWallet to a state before the block
|
||||||
|
// with the passed chainheight and block hash was connected to the main
|
||||||
|
// chain. This is used to remove transactions and utxos for each wallet
|
||||||
|
// that occured on a chain no longer considered to be the main chain.
|
||||||
|
func (s *BtcWalletStore) Rollback(height int64, hash *btcwire.ShaHash) {
|
||||||
|
s.Lock()
|
||||||
|
for _, w := range s.m {
|
||||||
|
w.Rollback(height, hash)
|
||||||
|
}
|
||||||
|
s.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *BtcWallet) Rollback(height int64, hash *btcwire.ShaHash) {
|
||||||
|
// TODO(jrick): set dirty=true if modified.
|
||||||
|
w.UtxoStore.Lock()
|
||||||
|
w.UtxoStore.dirty = w.UtxoStore.dirty || w.UtxoStore.s.Rollback(height, hash)
|
||||||
|
w.UtxoStore.Unlock()
|
||||||
|
|
||||||
|
w.TxStore.Lock()
|
||||||
|
w.TxStore.dirty = w.TxStore.dirty || w.TxStore.s.Rollback(height, hash)
|
||||||
|
w.TxStore.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// walletdir returns the directory path which holds the wallet, utxo,
|
// walletdir returns the directory path which holds the wallet, utxo,
|
||||||
// and tx files.
|
// and tx files.
|
||||||
func walletdir(cfg *config, account string) string {
|
func walletdir(cfg *config, account string) string {
|
||||||
|
@ -119,6 +122,9 @@ func walletdir(cfg *config, account string) string {
|
||||||
return filepath.Join(cfg.DataDir, wname)
|
return filepath.Join(cfg.DataDir, wname)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpenWallet opens a wallet described by account in the data
|
||||||
|
// directory specified by cfg. If the wallet does not exist, ErrNoWallet
|
||||||
|
// is returned as an error.
|
||||||
func OpenWallet(cfg *config, account string) (*BtcWallet, error) {
|
func OpenWallet(cfg *config, account string) (*BtcWallet, error) {
|
||||||
wdir := walletdir(cfg, account)
|
wdir := walletdir(cfg, account)
|
||||||
fi, err := os.Stat(wdir)
|
fi, err := os.Stat(wdir)
|
||||||
|
@ -126,14 +132,14 @@ func OpenWallet(cfg *config, account string) (*BtcWallet, error) {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
// Attempt data directory creation
|
// Attempt data directory creation
|
||||||
if err = os.MkdirAll(wdir, 0700); err != nil {
|
if err = os.MkdirAll(wdir, 0700); err != nil {
|
||||||
return nil, fmt.Errorf("Cannot create data directory:", err)
|
return nil, fmt.Errorf("cannot create data directory: %s", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("Error checking data directory:", err)
|
return nil, fmt.Errorf("error checking data directory: %s", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !fi.IsDir() {
|
if !fi.IsDir() {
|
||||||
return nil, fmt.Errorf("Data directory '%s' is not a directory.", cfg.DataDir)
|
return nil, fmt.Errorf("data directory '%s' is not a directory", cfg.DataDir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,45 +151,44 @@ func OpenWallet(cfg *config, account string) (*BtcWallet, error) {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
// Must create and save wallet first.
|
// Must create and save wallet first.
|
||||||
return nil, ErrNoWallet
|
return nil, ErrNoWallet
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("Cannot open wallet file:", err)
|
|
||||||
}
|
}
|
||||||
|
return nil, fmt.Errorf("cannot open wallet file: %s", err)
|
||||||
}
|
}
|
||||||
defer wfile.Close()
|
defer wfile.Close()
|
||||||
if txfile, err = os.Open(txfilepath); err != nil {
|
if txfile, err = os.Open(txfilepath); err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
if txfile, err = os.Create(txfilepath); err != nil {
|
if txfile, err = os.Create(txfilepath); err != nil {
|
||||||
return nil, fmt.Errorf("Cannot create tx file:", err)
|
return nil, fmt.Errorf("cannot create tx file: %s", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("Cannot open tx file:", err)
|
return nil, fmt.Errorf("cannot open tx file: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defer txfile.Close()
|
defer txfile.Close()
|
||||||
if utxofile, err = os.Open(utxofilepath); err != nil {
|
if utxofile, err = os.Open(utxofilepath); err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
if utxofile, err = os.Create(utxofilepath); err != nil {
|
if utxofile, err = os.Create(utxofilepath); err != nil {
|
||||||
return nil, fmt.Errorf("Cannot create utxo file:", err)
|
return nil, fmt.Errorf("cannot create utxo file: %s", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("Cannot open utxo file:", err)
|
return nil, fmt.Errorf("cannot open utxo file: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defer utxofile.Close()
|
defer utxofile.Close()
|
||||||
|
|
||||||
wlt := new(wallet.Wallet)
|
wlt := new(wallet.Wallet)
|
||||||
if _, err = wlt.ReadFrom(wfile); err != nil {
|
if _, err = wlt.ReadFrom(wfile); err != nil {
|
||||||
return nil, fmt.Errorf("Cannot read wallet:", err)
|
return nil, fmt.Errorf("cannot read wallet: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var txs tx.TxStore
|
var txs tx.TxStore
|
||||||
if _, err = txs.ReadFrom(txfile); err != nil {
|
if _, err = txs.ReadFrom(txfile); err != nil {
|
||||||
return nil, fmt.Errorf("Cannot read tx file:", err)
|
return nil, fmt.Errorf("cannot read tx file: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var utxos tx.UtxoStore
|
var utxos tx.UtxoStore
|
||||||
if _, err = utxos.ReadFrom(utxofile); err != nil {
|
if _, err = utxos.ReadFrom(utxofile); err != nil {
|
||||||
return nil, fmt.Errorf("Cannot read utxo file:", err)
|
return nil, fmt.Errorf("cannot read utxo file: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
w := &BtcWallet{
|
w := &BtcWallet{
|
||||||
|
@ -201,55 +206,58 @@ func getCurHeight() (height int64) {
|
||||||
curHeight.RUnlock()
|
curHeight.RUnlock()
|
||||||
if height != btcutil.BlockHeightUnknown {
|
if height != btcutil.BlockHeightUnknown {
|
||||||
return height
|
return height
|
||||||
} else {
|
}
|
||||||
seq.Lock()
|
|
||||||
n := seq.n
|
|
||||||
seq.n++
|
|
||||||
seq.Unlock()
|
|
||||||
|
|
||||||
m, err := btcjson.CreateMessageWithId("getblockcount",
|
seq.Lock()
|
||||||
fmt.Sprintf("btcwallet(%v)", n))
|
n := seq.n
|
||||||
if err != nil {
|
seq.n++
|
||||||
// Can't continue.
|
seq.Unlock()
|
||||||
return btcutil.BlockHeightUnknown
|
|
||||||
}
|
|
||||||
|
|
||||||
c := make(chan int64)
|
m, err := btcjson.CreateMessageWithId("getblockcount",
|
||||||
|
fmt.Sprintf("btcwallet(%v)", n))
|
||||||
|
if err != nil {
|
||||||
|
// Can't continue.
|
||||||
|
return btcutil.BlockHeightUnknown
|
||||||
|
}
|
||||||
|
|
||||||
replyHandlers.Lock()
|
c := make(chan int64)
|
||||||
replyHandlers.m[n] = func(result, e interface{}) bool {
|
|
||||||
if e != nil {
|
replyHandlers.Lock()
|
||||||
c <- btcutil.BlockHeightUnknown
|
replyHandlers.m[n] = func(result interface{}, e *btcjson.Error) bool {
|
||||||
return true
|
if e != nil {
|
||||||
}
|
c <- btcutil.BlockHeightUnknown
|
||||||
if balance, ok := result.(float64); ok {
|
|
||||||
c <- int64(balance)
|
|
||||||
} else {
|
|
||||||
c <- btcutil.BlockHeightUnknown
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
replyHandlers.Unlock()
|
if balance, ok := result.(float64); ok {
|
||||||
|
c <- int64(balance)
|
||||||
// send message
|
|
||||||
btcdMsgs <- m
|
|
||||||
|
|
||||||
// Block until reply is ready.
|
|
||||||
height = <-c
|
|
||||||
curHeight.Lock()
|
|
||||||
if height > curHeight.h {
|
|
||||||
curHeight.h = height
|
|
||||||
} else {
|
} else {
|
||||||
height = curHeight.h
|
c <- btcutil.BlockHeightUnknown
|
||||||
}
|
}
|
||||||
curHeight.Unlock()
|
return true
|
||||||
|
|
||||||
return height
|
|
||||||
}
|
}
|
||||||
|
replyHandlers.Unlock()
|
||||||
|
|
||||||
|
// send message
|
||||||
|
btcdMsgs <- m
|
||||||
|
|
||||||
|
// Block until reply is ready.
|
||||||
|
height = <-c
|
||||||
|
curHeight.Lock()
|
||||||
|
if height > curHeight.h {
|
||||||
|
curHeight.h = height
|
||||||
|
} else {
|
||||||
|
height = curHeight.h
|
||||||
|
}
|
||||||
|
curHeight.Unlock()
|
||||||
|
|
||||||
|
return height
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CalculateBalance sums the amounts of all unspent transaction
|
||||||
|
// outputs to addresses of a wallet and returns the balance as a
|
||||||
|
// float64.
|
||||||
func (w *BtcWallet) CalculateBalance(confirmations int) float64 {
|
func (w *BtcWallet) CalculateBalance(confirmations int) float64 {
|
||||||
var bal int64 // Measured in satoshi
|
var bal uint64 // Measured in satoshi
|
||||||
|
|
||||||
height := getCurHeight()
|
height := getCurHeight()
|
||||||
if height == btcutil.BlockHeightUnknown {
|
if height == btcutil.BlockHeightUnknown {
|
||||||
|
@ -257,12 +265,7 @@ func (w *BtcWallet) CalculateBalance(confirmations int) float64 {
|
||||||
}
|
}
|
||||||
|
|
||||||
w.UtxoStore.RLock()
|
w.UtxoStore.RLock()
|
||||||
for _, u := range w.UtxoStore.s.Confirmed {
|
for _, u := range w.UtxoStore.s {
|
||||||
if int(height-u.Height) >= confirmations {
|
|
||||||
bal += u.Amt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, u := range w.UtxoStore.s.Unconfirmed {
|
|
||||||
if int(height-u.Height) >= confirmations {
|
if int(height-u.Height) >= confirmations {
|
||||||
bal += u.Amt
|
bal += u.Amt
|
||||||
}
|
}
|
||||||
|
@ -271,6 +274,9 @@ func (w *BtcWallet) CalculateBalance(confirmations int) float64 {
|
||||||
return float64(bal) / satoshiPerBTC
|
return float64(bal) / satoshiPerBTC
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track requests btcd to send notifications of new transactions for
|
||||||
|
// each address stored in a wallet and sets up a new reply handler for
|
||||||
|
// these notifications.
|
||||||
func (w *BtcWallet) Track() {
|
func (w *BtcWallet) Track() {
|
||||||
seq.Lock()
|
seq.Lock()
|
||||||
n := seq.n
|
n := seq.n
|
||||||
|
@ -282,13 +288,20 @@ func (w *BtcWallet) Track() {
|
||||||
w.mtx.Unlock()
|
w.mtx.Unlock()
|
||||||
|
|
||||||
replyHandlers.Lock()
|
replyHandlers.Lock()
|
||||||
replyHandlers.m[n] = w.NewBlockTxHandler
|
replyHandlers.m[n] = w.newBlockTxHandler
|
||||||
replyHandlers.Unlock()
|
replyHandlers.Unlock()
|
||||||
for _, addr := range w.GetActiveAddresses() {
|
for _, addr := range w.GetActiveAddresses() {
|
||||||
go w.ReqNewTxsForAddress(addr)
|
go w.ReqNewTxsForAddress(addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RescanForAddress requests btcd to rescan the blockchain for new
|
||||||
|
// transactions to addr. This is useful for making btcwallet catch up to
|
||||||
|
// a long-running btcd process, or for importing addresses and rescanning
|
||||||
|
// for unspent tx outputs. If len(blocks) is 0, the entire blockchain is
|
||||||
|
// rescanned. If len(blocks) is 1, the rescan will begin at height
|
||||||
|
// blocks[0]. If len(blocks) is 2 or greater, the rescan will be
|
||||||
|
// performed for the block range blocks[0]...blocks[1] (inclusive).
|
||||||
func (w *BtcWallet) RescanForAddress(addr string, blocks ...int) {
|
func (w *BtcWallet) RescanForAddress(addr string, blocks ...int) {
|
||||||
seq.Lock()
|
seq.Lock()
|
||||||
n := seq.n
|
n := seq.n
|
||||||
|
@ -311,7 +324,7 @@ func (w *BtcWallet) RescanForAddress(addr string, blocks ...int) {
|
||||||
msg, _ := json.Marshal(m)
|
msg, _ := json.Marshal(m)
|
||||||
|
|
||||||
replyHandlers.Lock()
|
replyHandlers.Lock()
|
||||||
replyHandlers.m[n] = func(result, e interface{}) bool {
|
replyHandlers.m[n] = func(result interface{}, e *btcjson.Error) bool {
|
||||||
// TODO(jrick)
|
// TODO(jrick)
|
||||||
|
|
||||||
// btcd returns a nil result when the rescan is complete.
|
// btcd returns a nil result when the rescan is complete.
|
||||||
|
@ -324,6 +337,8 @@ func (w *BtcWallet) RescanForAddress(addr string, blocks ...int) {
|
||||||
btcdMsgs <- msg
|
btcdMsgs <- msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReqNewTxsForAddress sends a message to btcd to request tx updates
|
||||||
|
// for addr for each new block that is added to the blockchain.
|
||||||
func (w *BtcWallet) ReqNewTxsForAddress(addr string) {
|
func (w *BtcWallet) ReqNewTxsForAddress(addr string) {
|
||||||
w.mtx.RLock()
|
w.mtx.RLock()
|
||||||
n := w.NewBlockTxSeqN
|
n := w.NewBlockTxSeqN
|
||||||
|
@ -340,19 +355,15 @@ func (w *BtcWallet) ReqNewTxsForAddress(addr string) {
|
||||||
btcdMsgs <- msg
|
btcdMsgs <- msg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *BtcWallet) NewBlockTxHandler(result, e interface{}) bool {
|
// newBlockTxHandler is the handler function for btcd transaction
|
||||||
|
// notifications resulting from newly-attached blocks.
|
||||||
|
func (w *BtcWallet) newBlockTxHandler(result interface{}, e *btcjson.Error) bool {
|
||||||
if e != nil {
|
if e != nil {
|
||||||
if v, ok := e.(map[string]interface{}); ok {
|
log.Errorf("Tx Handler: Error %d received from btcd: %s",
|
||||||
if msg, ok := v["message"]; ok {
|
e.Code, e.Message)
|
||||||
log.Errorf("Tx Handler: Error received from btcd: %s", msg)
|
return false
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Errorf("Tx Handler: Error is non-nil but cannot be parsed.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(jrick): btcd also sends the block hash in the reply.
|
|
||||||
// Do we want it saved as well?
|
|
||||||
v, ok := result.(map[string]interface{})
|
v, ok := result.(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
// The first result sent from btcd is nil. This could be used to
|
// The first result sent from btcd is nil. This could be used to
|
||||||
|
@ -372,6 +383,11 @@ func (w *BtcWallet) NewBlockTxHandler(result, e interface{}) bool {
|
||||||
log.Error("Tx Handler: Unspecified receiver.")
|
log.Error("Tx Handler: Unspecified receiver.")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
blockhashBE, ok := v["blockhash"].(string)
|
||||||
|
if !ok {
|
||||||
|
log.Error("Tx Handler: Unspecified block hash.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
height, ok := v["height"].(float64)
|
height, ok := v["height"].(float64)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Error("Tx Handler: Unspecified height.")
|
log.Error("Tx Handler: Unspecified height.")
|
||||||
|
@ -398,8 +414,13 @@ func (w *BtcWallet) NewBlockTxHandler(result, e interface{}) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// btcd sends the tx hash as a BE string. Convert to a
|
// btcd sends the block and tx hashes as BE strings. Convert both
|
||||||
// LE ShaHash.
|
// to a LE ShaHash.
|
||||||
|
blockhash, err := btcwire.NewShaHashFromStr(blockhashBE)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Tx Handler: Block hash string cannot be parsed: " + err.Error())
|
||||||
|
return false
|
||||||
|
}
|
||||||
txhash, err := btcwire.NewShaHashFromStr(txhashBE)
|
txhash, err := btcwire.NewShaHashFromStr(txhashBE)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Tx Handler: Tx hash string cannot be parsed: " + err.Error())
|
log.Error("Tx Handler: Tx hash string cannot be parsed: " + err.Error())
|
||||||
|
@ -411,9 +432,10 @@ func (w *BtcWallet) NewBlockTxHandler(result, e interface{}) bool {
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
t := &tx.RecvTx{
|
t := &tx.RecvTx{
|
||||||
Amt: int64(amt),
|
Amt: uint64(amt),
|
||||||
}
|
}
|
||||||
copy(t.TxHash[:], txhash[:])
|
copy(t.TxHash[:], txhash[:])
|
||||||
|
copy(t.BlockHash[:], blockhash[:])
|
||||||
copy(t.SenderAddr[:], sender)
|
copy(t.SenderAddr[:], sender)
|
||||||
copy(t.ReceiverAddr[:], receiver)
|
copy(t.ReceiverAddr[:], receiver)
|
||||||
|
|
||||||
|
@ -424,28 +446,88 @@ func (w *BtcWallet) NewBlockTxHandler(result, e interface{}) bool {
|
||||||
w.TxStore.Unlock()
|
w.TxStore.Unlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
// Do not add output to utxo store if spent.
|
||||||
// Do not add output to utxo store if spent.
|
if !spent {
|
||||||
if spent {
|
go func() {
|
||||||
return
|
u := &tx.Utxo{
|
||||||
}
|
Amt: uint64(amt),
|
||||||
|
Height: int64(height),
|
||||||
|
}
|
||||||
|
copy(u.Out.Hash[:], txhash[:])
|
||||||
|
u.Out.Index = uint32(index)
|
||||||
|
copy(u.Addr[:], receiver)
|
||||||
|
copy(u.BlockHash[:], blockhash[:])
|
||||||
|
|
||||||
u := &tx.Utxo{
|
w.UtxoStore.Lock()
|
||||||
Amt: int64(amt),
|
w.UtxoStore.s = append(w.UtxoStore.s, u)
|
||||||
Height: int64(height),
|
w.UtxoStore.dirty = true
|
||||||
}
|
w.UtxoStore.Unlock()
|
||||||
copy(u.Out.Hash[:], txhash[:])
|
}()
|
||||||
u.Out.Index = uint32(index)
|
}
|
||||||
copy(u.Addr[:], receiver)
|
|
||||||
|
|
||||||
w.UtxoStore.Lock()
|
|
||||||
// All newly saved utxos are first classified as unconfirmed.
|
|
||||||
utxos := w.UtxoStore.s.Unconfirmed
|
|
||||||
w.UtxoStore.s.Unconfirmed = append(utxos, u)
|
|
||||||
w.UtxoStore.dirty = true
|
|
||||||
w.UtxoStore.Unlock()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Never remove this handler.
|
// Never remove this handler.
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
tcfg, _, err := loadConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
cfg = tcfg
|
||||||
|
|
||||||
|
// Open wallet
|
||||||
|
w, err := OpenWallet(cfg, "")
|
||||||
|
if err != nil {
|
||||||
|
log.Info(err.Error())
|
||||||
|
} else {
|
||||||
|
wallets.Lock()
|
||||||
|
wallets.m[""] = w
|
||||||
|
wallets.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Start HTTP server to listen and send messages to frontend and btcd
|
||||||
|
// backend. Try reconnection if connection failed.
|
||||||
|
for {
|
||||||
|
if err := FrontendListenAndServe(); err == ErrConnRefused {
|
||||||
|
// wait and try again.
|
||||||
|
log.Info("Unable to start frontend HTTP server. Retrying in 5 seconds.")
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
replies := make(chan error)
|
||||||
|
done := make(chan int)
|
||||||
|
go func() {
|
||||||
|
BtcdConnect(replies)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
selectLoop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
break selectLoop
|
||||||
|
case err := <-replies:
|
||||||
|
switch err {
|
||||||
|
case ErrConnRefused:
|
||||||
|
btcdConnected.c <- false
|
||||||
|
log.Info("btcd connection refused, retying in 5 seconds")
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
case ErrConnLost:
|
||||||
|
btcdConnected.c <- false
|
||||||
|
log.Info("btcd connection lost, retrying in 5 seconds")
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
case nil:
|
||||||
|
btcdConnected.c <- true
|
||||||
|
log.Info("Established connection to btcd.")
|
||||||
|
default:
|
||||||
|
log.Infof("Unhandled error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
392
cmdmgr.go
392
cmdmgr.go
|
@ -25,9 +25,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Errors
|
// Standard JSON-RPC 2.0 errors
|
||||||
var (
|
var (
|
||||||
// Standard JSON-RPC 2.0 errors
|
|
||||||
InvalidRequest = btcjson.Error{
|
InvalidRequest = btcjson.Error{
|
||||||
Code: -32600,
|
Code: -32600,
|
||||||
Message: "Invalid request",
|
Message: "Invalid request",
|
||||||
|
@ -48,8 +47,10 @@ var (
|
||||||
Code: -32700,
|
Code: -32700,
|
||||||
Message: "Parse error",
|
Message: "Parse error",
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// General application defined errors
|
// General application defined JSON errors
|
||||||
|
var (
|
||||||
MiscError = btcjson.Error{
|
MiscError = btcjson.Error{
|
||||||
Code: -1,
|
Code: -1,
|
||||||
Message: "Miscellaneous error",
|
Message: "Miscellaneous error",
|
||||||
|
@ -82,8 +83,10 @@ var (
|
||||||
Code: -22,
|
Code: -22,
|
||||||
Message: "Error parsing or validating structure in raw format",
|
Message: "Error parsing or validating structure in raw format",
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Wallet errors
|
// Wallet JSON errors
|
||||||
|
var (
|
||||||
WalletError = btcjson.Error{
|
WalletError = btcjson.Error{
|
||||||
Code: -4,
|
Code: -4,
|
||||||
Message: "Unspecified problem with wallet",
|
Message: "Unspecified problem with wallet",
|
||||||
|
@ -145,46 +148,53 @@ var (
|
||||||
// message method is one that must be handled by btcwallet, the request
|
// message method is one that must be handled by btcwallet, the request
|
||||||
// is processed here. Otherwise, the message is sent to btcd.
|
// is processed here. Otherwise, the message is sent to btcd.
|
||||||
func ProcessFrontendMsg(reply chan []byte, msg []byte) {
|
func ProcessFrontendMsg(reply chan []byte, msg []byte) {
|
||||||
cmd, err := btcjson.JSONGetMethod(msg)
|
var jsonMsg btcjson.Message
|
||||||
if err != nil {
|
if err := json.Unmarshal(msg, &jsonMsg); err != nil {
|
||||||
log.Error("Unable to parse JSON method from message.")
|
log.Errorf("ProcessFrontendMsg: Cannot unmarshal message: %v",
|
||||||
|
err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch cmd {
|
switch jsonMsg.Method {
|
||||||
// Standard bitcoind methods
|
// Standard bitcoind methods
|
||||||
case "getaddressesbyaccount":
|
case "getaddressesbyaccount":
|
||||||
GetAddressesByAccount(reply, msg)
|
GetAddressesByAccount(reply, &jsonMsg)
|
||||||
case "getbalance":
|
case "getbalance":
|
||||||
GetBalance(reply, msg)
|
GetBalance(reply, &jsonMsg)
|
||||||
case "getnewaddress":
|
case "getnewaddress":
|
||||||
GetNewAddress(reply, msg)
|
GetNewAddress(reply, &jsonMsg)
|
||||||
|
case "sendfrom":
|
||||||
|
SendFrom(reply, &jsonMsg)
|
||||||
|
case "sendmany":
|
||||||
|
SendMany(reply, &jsonMsg)
|
||||||
case "walletlock":
|
case "walletlock":
|
||||||
WalletLock(reply, msg)
|
WalletLock(reply, &jsonMsg)
|
||||||
case "walletpassphrase":
|
case "walletpassphrase":
|
||||||
WalletPassphrase(reply, msg)
|
WalletPassphrase(reply, &jsonMsg)
|
||||||
|
|
||||||
// btcwallet extensions
|
// btcwallet extensions
|
||||||
case "createencryptedwallet":
|
case "createencryptedwallet":
|
||||||
CreateEncryptedWallet(reply, msg)
|
CreateEncryptedWallet(reply, &jsonMsg)
|
||||||
case "walletislocked":
|
case "walletislocked":
|
||||||
WalletIsLocked(reply, msg)
|
WalletIsLocked(reply, &jsonMsg)
|
||||||
|
case "btcdconnected":
|
||||||
|
BtcdConnected(reply, &jsonMsg)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// btcwallet does not understand method. Pass to btcd.
|
// btcwallet does not understand method. Pass to btcd.
|
||||||
log.Info("Unknown btcwallet method ", cmd)
|
|
||||||
|
|
||||||
seq.Lock()
|
seq.Lock()
|
||||||
n := seq.n
|
n := seq.n
|
||||||
seq.n++
|
seq.n++
|
||||||
seq.Unlock()
|
seq.Unlock()
|
||||||
|
|
||||||
var m map[string]interface{}
|
var id interface{} = fmt.Sprintf("btcwallet(%v)-%v", n,
|
||||||
json.Unmarshal(msg, &m)
|
jsonMsg.Id)
|
||||||
m["id"] = fmt.Sprintf("btcwallet(%v)-%v", n, m["id"])
|
jsonMsg.Id = &id
|
||||||
newMsg, err := json.Marshal(m)
|
newMsg, err := json.Marshal(jsonMsg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Info("Error marshalling json: " + err.Error())
|
log.Errorf("ProcessFrontendMsg: Cannot marshal message: %v",
|
||||||
|
err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
replyRouter.Lock()
|
replyRouter.Lock()
|
||||||
replyRouter.m[n] = reply
|
replyRouter.m[n] = reply
|
||||||
|
@ -202,6 +212,8 @@ func ReplyError(reply chan []byte, id interface{}, e *btcjson.Error) {
|
||||||
}
|
}
|
||||||
if mr, err := json.Marshal(r); err == nil {
|
if mr, err := json.Marshal(r); err == nil {
|
||||||
reply <- mr
|
reply <- mr
|
||||||
|
} else {
|
||||||
|
log.Errorf("Cannot marshal json reply: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,11 +230,14 @@ func ReplySuccess(reply chan []byte, id interface{}, result interface{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAddressesByAccount replies with all addresses for an account.
|
// GetAddressesByAccount replies with all addresses for an account.
|
||||||
func GetAddressesByAccount(reply chan []byte, msg []byte) {
|
func GetAddressesByAccount(reply chan []byte, msg *btcjson.Message) {
|
||||||
var v map[string]interface{}
|
// TODO(jrick): check if we can make btcjson.Message.Params
|
||||||
json.Unmarshal(msg, &v)
|
// a []interface{} to avoid this.
|
||||||
params := v["params"].([]interface{})
|
params, ok := msg.Params.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
log.Error("GetAddressesByAccount: Cannot parse parameters.")
|
||||||
|
return
|
||||||
|
}
|
||||||
var result interface{}
|
var result interface{}
|
||||||
wallets.RLock()
|
wallets.RLock()
|
||||||
w := wallets.m[params[0].(string)]
|
w := wallets.m[params[0].(string)]
|
||||||
|
@ -232,32 +247,32 @@ func GetAddressesByAccount(reply chan []byte, msg []byte) {
|
||||||
} else {
|
} else {
|
||||||
result = []interface{}{}
|
result = []interface{}{}
|
||||||
}
|
}
|
||||||
ReplySuccess(reply, v["id"], result)
|
ReplySuccess(reply, msg.Id, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBalance replies with the balance for an account (wallet). If
|
// GetBalance replies with the balance for an account (wallet). If
|
||||||
// the requested wallet does not exist, a JSON error will be returned to
|
// the requested wallet does not exist, a JSON error will be returned to
|
||||||
// the client.
|
// the client.
|
||||||
//
|
func GetBalance(reply chan []byte, msg *btcjson.Message) {
|
||||||
// TODO(jrick): Actually calculate correct balance.
|
params, ok := msg.Params.([]interface{})
|
||||||
func GetBalance(reply chan []byte, msg []byte) {
|
if !ok {
|
||||||
var v map[string]interface{}
|
log.Error("GetBalance: Cannot parse parameters.")
|
||||||
json.Unmarshal(msg, &v)
|
return
|
||||||
params := v["params"].([]interface{})
|
}
|
||||||
var wname string
|
var wname string
|
||||||
conf := 1
|
conf := 1
|
||||||
if len(params) > 0 {
|
if len(params) > 0 {
|
||||||
if s, ok := params[0].(string); ok {
|
if s, ok := params[0].(string); ok {
|
||||||
wname = s
|
wname = s
|
||||||
} else {
|
} else {
|
||||||
ReplyError(reply, v["id"], &InvalidParams)
|
ReplyError(reply, msg.Id, &InvalidParams)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(params) > 1 {
|
if len(params) > 1 {
|
||||||
if f, ok := params[1].(float64); ok {
|
if f, ok := params[1].(float64); ok {
|
||||||
conf = int(f)
|
conf = int(f)
|
||||||
} else {
|
} else {
|
||||||
ReplyError(reply, v["id"], &InvalidParams)
|
ReplyError(reply, msg.Id, &InvalidParams)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,21 +282,23 @@ func GetBalance(reply chan []byte, msg []byte) {
|
||||||
var result interface{}
|
var result interface{}
|
||||||
if w != nil {
|
if w != nil {
|
||||||
result = w.CalculateBalance(conf)
|
result = w.CalculateBalance(conf)
|
||||||
ReplySuccess(reply, v["id"], result)
|
ReplySuccess(reply, msg.Id, result)
|
||||||
} else {
|
} else {
|
||||||
e := WalletInvalidAccountName
|
e := WalletInvalidAccountName
|
||||||
e.Message = fmt.Sprintf("Wallet for account '%s' does not exist.", wname)
|
e.Message = fmt.Sprintf("Wallet for account '%s' does not exist.", wname)
|
||||||
ReplyError(reply, v["id"], &e)
|
ReplyError(reply, msg.Id, &e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNewAddress gets or generates a new address for an account. If
|
// GetNewAddress gets or generates a new address for an account. If
|
||||||
// the requested wallet does not exist, a JSON error will be returned to
|
// the requested wallet does not exist, a JSON error will be returned to
|
||||||
// the client.
|
// the client.
|
||||||
func GetNewAddress(reply chan []byte, msg []byte) {
|
func GetNewAddress(reply chan []byte, msg *btcjson.Message) {
|
||||||
var v map[string]interface{}
|
params, ok := msg.Params.([]interface{})
|
||||||
json.Unmarshal(msg, &v)
|
if !ok {
|
||||||
params := v["params"].([]interface{})
|
log.Error("GetNewAddress: Cannot parse parameters.")
|
||||||
|
return
|
||||||
|
}
|
||||||
var wname string
|
var wname string
|
||||||
if len(params) == 0 || params[0].(string) == "" {
|
if len(params) == 0 || params[0].(string) == "" {
|
||||||
wname = ""
|
wname = ""
|
||||||
|
@ -294,15 +311,226 @@ func GetNewAddress(reply chan []byte, msg []byte) {
|
||||||
wallets.RUnlock()
|
wallets.RUnlock()
|
||||||
if w != nil {
|
if w != nil {
|
||||||
// TODO(jrick): generate new addresses if the address pool is empty.
|
// TODO(jrick): generate new addresses if the address pool is empty.
|
||||||
addr := w.NextUnusedAddress()
|
addr, err := w.NextUnusedAddress()
|
||||||
ReplySuccess(reply, v["id"], addr)
|
if err != nil {
|
||||||
|
e := InternalError
|
||||||
|
e.Message = fmt.Sprintf("New address generation not implemented yet")
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ReplySuccess(reply, msg.Id, addr)
|
||||||
} else {
|
} else {
|
||||||
e := WalletInvalidAccountName
|
e := WalletInvalidAccountName
|
||||||
e.Message = fmt.Sprintf("Wallet for account '%s' does not exist.", wname)
|
e.Message = fmt.Sprintf("Wallet for account '%s' does not exist.", wname)
|
||||||
ReplyError(reply, v["id"], &e)
|
ReplyError(reply, msg.Id, &e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// back to a new address in the wallet.
|
||||||
|
func SendFrom(reply chan []byte, msg *btcjson.Message) {
|
||||||
|
params, ok := msg.Params.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
log.Error("SendFrom: Cannot parse parameters.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var fromaccount, toaddr58, comment, commentto string
|
||||||
|
var famt, minconf float64
|
||||||
|
e := InvalidParams
|
||||||
|
if len(params) < 3 {
|
||||||
|
e.Message = "Too few parameters."
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if fromaccount, ok = params[0].(string); !ok {
|
||||||
|
e.Message = "fromaccount is not a string"
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if toaddr58, ok = params[1].(string); !ok {
|
||||||
|
e.Message = "tobitcoinaddress is not a string"
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if famt, ok = params[2].(float64); !ok {
|
||||||
|
e.Message = "amount is not a number"
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if famt < 0 {
|
||||||
|
e.Message = "amount cannot be negative"
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
amt, err := btcjson.JSONToAmount(famt)
|
||||||
|
if err != nil {
|
||||||
|
e.Message = "amount cannot be converted to integer"
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(params) > 3 {
|
||||||
|
if minconf, ok = params[3].(float64); !ok {
|
||||||
|
e.Message = "minconf is not a number"
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if minconf < 0 {
|
||||||
|
e.Message = "minconf cannot be negative"
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(params) > 4 {
|
||||||
|
if comment, ok = params[4].(string); !ok {
|
||||||
|
e.Message = "comment is not a string"
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(params) > 5 {
|
||||||
|
if commentto, ok = params[5].(string); !ok {
|
||||||
|
e.Message = "comment-to is not a string"
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is wallet for this account unlocked?
|
||||||
|
wallets.Lock()
|
||||||
|
w := wallets.m[fromaccount]
|
||||||
|
wallets.Unlock()
|
||||||
|
if w.IsLocked() {
|
||||||
|
ReplyError(reply, msg.Id, &WalletUnlockNeeded)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// fee needs to be a global, set from another json method.
|
||||||
|
var fee uint64
|
||||||
|
pairs := map[string]uint64{
|
||||||
|
toaddr58: uint64(amt),
|
||||||
|
}
|
||||||
|
rawtx, err := w.txToPairs(pairs, fee, int(minconf))
|
||||||
|
if err != nil {
|
||||||
|
e := InternalError
|
||||||
|
e.Message = err.Error()
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(jrick): Send rawtx off to btcd
|
||||||
|
_ = rawtx
|
||||||
|
|
||||||
|
// TODO(jrick): If message succeeded in being sent, save the
|
||||||
|
// transaction details with comments.
|
||||||
|
_, _ = comment, commentto
|
||||||
|
|
||||||
|
e = InternalError
|
||||||
|
e.Message = "Transaction validated but not sent to btcd."
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// sent back to a new address in the wallet.
|
||||||
|
func SendMany(reply chan []byte, msg *btcjson.Message) {
|
||||||
|
params, ok := msg.Params.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
log.Error("SendFrom: Cannot parse parameters.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var fromaccount, comment string
|
||||||
|
var minconf float64
|
||||||
|
var jsonPairs map[string]interface{}
|
||||||
|
e := InvalidParams
|
||||||
|
if len(params) < 3 {
|
||||||
|
e.Message = "Too few parameters."
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if fromaccount, ok = params[0].(string); !ok {
|
||||||
|
e.Message = "fromaccount is not a string"
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if jsonPairs, ok = params[1].(map[string]interface{}); !ok {
|
||||||
|
e.Message = "address and amount pairs is not a JSON object"
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pairs := make(map[string]uint64)
|
||||||
|
for toaddr58, iamt := range jsonPairs {
|
||||||
|
famt, ok := iamt.(float64)
|
||||||
|
if !ok {
|
||||||
|
e.Message = "amount is not a number"
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if famt < 0 {
|
||||||
|
e.Message = "amount cannot be negative"
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
amt, err := btcjson.JSONToAmount(famt)
|
||||||
|
if err != nil {
|
||||||
|
e.Message = "amount cannot be converted to integer"
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pairs[toaddr58] = uint64(amt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(params) > 1 {
|
||||||
|
if minconf, ok = params[2].(float64); !ok {
|
||||||
|
e.Message = "minconf is not a number"
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if minconf < 0 {
|
||||||
|
e.Message = "minconf cannot be negative"
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(params) > 2 {
|
||||||
|
if comment, ok = params[3].(string); !ok {
|
||||||
|
e.Message = "comment is not a string"
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is wallet for this account unlocked?
|
||||||
|
wallets.Lock()
|
||||||
|
w := wallets.m[fromaccount]
|
||||||
|
wallets.Unlock()
|
||||||
|
if w.IsLocked() {
|
||||||
|
ReplyError(reply, msg.Id, &WalletUnlockNeeded)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// fee needs to be a global, set from another json method.
|
||||||
|
var fee uint64
|
||||||
|
rawtx, err := w.txToPairs(pairs, fee, int(minconf))
|
||||||
|
if err != nil {
|
||||||
|
e := InternalError
|
||||||
|
e.Message = err.Error()
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(jrick): Send rawtx off to btcd
|
||||||
|
_ = rawtx
|
||||||
|
|
||||||
|
// TODO(jrick): If message succeeded in being sent, save the
|
||||||
|
// transaction details with comments.
|
||||||
|
_ = comment
|
||||||
|
|
||||||
|
e = InternalError
|
||||||
|
e.Message = "Transaction validated but not sent to btcd."
|
||||||
|
ReplyError(reply, msg.Id, &e)
|
||||||
|
}
|
||||||
|
|
||||||
// CreateEncryptedWallet creates a new encrypted wallet. The form of the command is:
|
// CreateEncryptedWallet creates a new encrypted wallet. The form of the command is:
|
||||||
//
|
//
|
||||||
// createencryptedwallet [account] [description] [passphrase]
|
// createencryptedwallet [account] [description] [passphrase]
|
||||||
|
@ -310,20 +538,22 @@ func GetNewAddress(reply chan []byte, msg []byte) {
|
||||||
// All three parameters are required, and must be of type string. If
|
// All three parameters are required, and must be of type string. If
|
||||||
// the wallet specified by account already exists, an invalid account
|
// the wallet specified by account already exists, an invalid account
|
||||||
// name error is returned to the client.
|
// name error is returned to the client.
|
||||||
func CreateEncryptedWallet(reply chan []byte, msg []byte) {
|
func CreateEncryptedWallet(reply chan []byte, msg *btcjson.Message) {
|
||||||
var v map[string]interface{}
|
params, ok := msg.Params.([]interface{})
|
||||||
json.Unmarshal(msg, &v)
|
if !ok {
|
||||||
params := v["params"].([]interface{})
|
log.Error("CreateEncryptedWallet: Cannot parse parameters.")
|
||||||
|
return
|
||||||
|
}
|
||||||
var wname string
|
var wname string
|
||||||
if len(params) != 3 {
|
if len(params) != 3 {
|
||||||
ReplyError(reply, v["id"], &InvalidParams)
|
ReplyError(reply, msg.Id, &InvalidParams)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
wname, ok1 := params[0].(string)
|
wname, ok1 := params[0].(string)
|
||||||
desc, ok2 := params[1].(string)
|
desc, ok2 := params[1].(string)
|
||||||
pass, ok3 := params[2].(string)
|
pass, ok3 := params[2].(string)
|
||||||
if !ok1 || !ok2 || !ok3 {
|
if !ok1 || !ok2 || !ok3 {
|
||||||
ReplyError(reply, v["id"], &InvalidParams)
|
ReplyError(reply, msg.Id, &InvalidParams)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -332,7 +562,7 @@ func CreateEncryptedWallet(reply chan []byte, msg []byte) {
|
||||||
if w := wallets.m[wname]; w != nil {
|
if w := wallets.m[wname]; w != nil {
|
||||||
e := WalletInvalidAccountName
|
e := WalletInvalidAccountName
|
||||||
e.Message = "Wallet already exists."
|
e.Message = "Wallet already exists."
|
||||||
ReplyError(reply, v["id"], &e)
|
ReplyError(reply, msg.Id, &e)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
wallets.RUnlock()
|
wallets.RUnlock()
|
||||||
|
@ -340,7 +570,7 @@ func CreateEncryptedWallet(reply chan []byte, msg []byte) {
|
||||||
w, err := wallet.NewWallet(wname, desc, []byte(pass))
|
w, err := wallet.NewWallet(wname, desc, []byte(pass))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error creating wallet: " + err.Error())
|
log.Error("Error creating wallet: " + err.Error())
|
||||||
ReplyError(reply, v["id"], &InternalError)
|
ReplyError(reply, msg.Id, &InternalError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -361,21 +591,22 @@ func CreateEncryptedWallet(reply chan []byte, msg []byte) {
|
||||||
wallets.Lock()
|
wallets.Lock()
|
||||||
wallets.m[wname] = bw
|
wallets.m[wname] = bw
|
||||||
wallets.Unlock()
|
wallets.Unlock()
|
||||||
ReplySuccess(reply, v["id"], nil)
|
ReplySuccess(reply, msg.Id, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WalletIsLocked returns whether the wallet used by the specified
|
// WalletIsLocked returns whether the wallet used by the specified
|
||||||
// account, or default account, is locked.
|
// account, or default account, is locked.
|
||||||
func WalletIsLocked(reply chan []byte, msg []byte) {
|
func WalletIsLocked(reply chan []byte, msg *btcjson.Message) {
|
||||||
var v map[string]interface{}
|
params, ok := msg.Params.([]interface{})
|
||||||
json.Unmarshal(msg, &v)
|
if !ok {
|
||||||
params := v["params"].([]interface{})
|
log.Error("WalletIsLocked: Cannot parse parameters.")
|
||||||
|
}
|
||||||
account := ""
|
account := ""
|
||||||
if len(params) > 0 {
|
if len(params) > 0 {
|
||||||
if acct, ok := params[0].(string); ok {
|
if acct, ok := params[0].(string); ok {
|
||||||
account = acct
|
account = acct
|
||||||
} else {
|
} else {
|
||||||
ReplyError(reply, v["id"], &InvalidParams)
|
ReplyError(reply, msg.Id, &InvalidParams)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -384,27 +615,26 @@ func WalletIsLocked(reply chan []byte, msg []byte) {
|
||||||
wallets.RUnlock()
|
wallets.RUnlock()
|
||||||
if w != nil {
|
if w != nil {
|
||||||
result := w.IsLocked()
|
result := w.IsLocked()
|
||||||
ReplySuccess(reply, v["id"], result)
|
ReplySuccess(reply, msg.Id, result)
|
||||||
} else {
|
} else {
|
||||||
ReplyError(reply, v["id"], &WalletInvalidAccountName)
|
ReplyError(reply, msg.Id, &WalletInvalidAccountName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WalletLock locks the wallet.
|
// WalletLock locks the wallet.
|
||||||
//
|
//
|
||||||
// TODO(jrick): figure out how multiple wallets/accounts will work
|
// TODO(jrick): figure out how multiple wallets/accounts will work
|
||||||
// with this.
|
// with this. Lock all the wallets, like if all accounts are locked
|
||||||
func WalletLock(reply chan []byte, msg []byte) {
|
// for one bitcoind wallet?
|
||||||
var v map[string]interface{}
|
func WalletLock(reply chan []byte, msg *btcjson.Message) {
|
||||||
json.Unmarshal(msg, &v)
|
|
||||||
wallets.RLock()
|
wallets.RLock()
|
||||||
w := wallets.m[""]
|
w := wallets.m[""]
|
||||||
wallets.RUnlock()
|
wallets.RUnlock()
|
||||||
if w != nil {
|
if w != nil {
|
||||||
if err := w.Lock(); err != nil {
|
if err := w.Lock(); err != nil {
|
||||||
ReplyError(reply, v["id"], &WalletWrongEncState)
|
ReplyError(reply, msg.Id, &WalletWrongEncState)
|
||||||
} else {
|
} else {
|
||||||
ReplySuccess(reply, v["id"], nil)
|
ReplySuccess(reply, msg.Id, nil)
|
||||||
NotifyWalletLockStateChange(reply, true)
|
NotifyWalletLockStateChange(reply, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -413,20 +643,21 @@ func WalletLock(reply chan []byte, msg []byte) {
|
||||||
// WalletPassphrase stores the decryption key for the default account,
|
// WalletPassphrase stores the decryption key for the default account,
|
||||||
// unlocking the wallet.
|
// unlocking the wallet.
|
||||||
//
|
//
|
||||||
// TODO(jrick): figure out how multiple wallets/accounts will work
|
// TODO(jrick): figure out how to do this for non-default accounts.
|
||||||
// with this.
|
func WalletPassphrase(reply chan []byte, msg *btcjson.Message) {
|
||||||
func WalletPassphrase(reply chan []byte, msg []byte) {
|
params, ok := msg.Params.([]interface{})
|
||||||
var v map[string]interface{}
|
if !ok {
|
||||||
json.Unmarshal(msg, &v)
|
log.Error("WalletPassphrase: Cannot parse parameters.")
|
||||||
params := v["params"].([]interface{})
|
return
|
||||||
|
}
|
||||||
if len(params) != 2 {
|
if len(params) != 2 {
|
||||||
ReplyError(reply, v["id"], &InvalidParams)
|
ReplyError(reply, msg.Id, &InvalidParams)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
passphrase, ok1 := params[0].(string)
|
passphrase, ok1 := params[0].(string)
|
||||||
timeout, ok2 := params[1].(float64)
|
timeout, ok2 := params[1].(float64)
|
||||||
if !ok1 || !ok2 {
|
if !ok1 || !ok2 {
|
||||||
ReplyError(reply, v["id"], &InvalidParams)
|
ReplyError(reply, msg.Id, &InvalidParams)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -435,10 +666,10 @@ func WalletPassphrase(reply chan []byte, msg []byte) {
|
||||||
wallets.RUnlock()
|
wallets.RUnlock()
|
||||||
if w != nil {
|
if w != nil {
|
||||||
if err := w.Unlock([]byte(passphrase)); err != nil {
|
if err := w.Unlock([]byte(passphrase)); err != nil {
|
||||||
ReplyError(reply, v["id"], &WalletPassphraseIncorrect)
|
ReplyError(reply, msg.Id, &WalletPassphraseIncorrect)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ReplySuccess(reply, v["id"], nil)
|
ReplySuccess(reply, msg.Id, nil)
|
||||||
NotifyWalletLockStateChange(reply, false)
|
NotifyWalletLockStateChange(reply, false)
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(time.Second * time.Duration(int64(timeout)))
|
time.Sleep(time.Second * time.Duration(int64(timeout)))
|
||||||
|
@ -448,6 +679,13 @@ func WalletPassphrase(reply chan []byte, msg []byte) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BtcdConnected is the wallet handler for the frontend
|
||||||
|
// 'btcdconnected' method. It returns to the frontend whether btcwallet
|
||||||
|
// is currently connected to btcd or not.
|
||||||
|
func BtcdConnected(reply chan []byte, msg *btcjson.Message) {
|
||||||
|
ReplySuccess(reply, msg.Id, btcdConnected.b)
|
||||||
|
}
|
||||||
|
|
||||||
// NotifyWalletLockStateChange sends a notification to all frontends
|
// NotifyWalletLockStateChange sends a notification to all frontends
|
||||||
// that the wallet has just been locked or unlocked.
|
// that the wallet has just been locked or unlocked.
|
||||||
func NotifyWalletLockStateChange(reply chan []byte, locked bool) {
|
func NotifyWalletLockStateChange(reply chan []byte, locked bool) {
|
||||||
|
|
|
@ -141,12 +141,5 @@ func loadConfig() (*config, []string, error) {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// wallet file must be valid
|
|
||||||
/*
|
|
||||||
if !fileExists(cfg.WalletFile) {
|
|
||||||
return &cfg, nil, errors.New("Wallet file does not exist.")
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
return &cfg, remainingArgs, nil
|
return &cfg, remainingArgs, nil
|
||||||
}
|
}
|
||||||
|
|
211
createtx.go
Normal file
211
createtx.go
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
/*
|
||||||
|
* 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 (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/conformal/btcscript"
|
||||||
|
"github.com/conformal/btcutil"
|
||||||
|
"github.com/conformal/btcwallet/tx"
|
||||||
|
"github.com/conformal/btcwire"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrInsufficientFunds represents an error where there are not enough
|
||||||
|
// funds from unspent tx outputs for a wallet to create a transaction.
|
||||||
|
var ErrInsufficientFunds = errors.New("insufficient funds")
|
||||||
|
|
||||||
|
// ErrUnknownBitcoinNet represents an error where the parsed or
|
||||||
|
// requested bitcoin network is invalid (neither mainnet nor testnet).
|
||||||
|
var ErrUnknownBitcoinNet = errors.New("unknown bitcoin network")
|
||||||
|
|
||||||
|
// ByAmount defines the methods needed to satisify sort.Interface to
|
||||||
|
// sort a slice of Utxos by their amount.
|
||||||
|
type ByAmount []*tx.Utxo
|
||||||
|
|
||||||
|
func (u ByAmount) Len() int {
|
||||||
|
return len(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u ByAmount) Less(i, j int) bool {
|
||||||
|
return u[i].Amt < u[j].Amt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u ByAmount) Swap(i, j int) {
|
||||||
|
u[i], u[j] = u[j], u[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectOutputs selects the minimum number possible of unspent
|
||||||
|
// outputs to use to create a new transaction that spends amt satoshis.
|
||||||
|
// Outputs with less than minconf confirmations are ignored. btcout is
|
||||||
|
// the total number of satoshis which would be spent by the combination
|
||||||
|
// of all selected outputs. err will equal ErrInsufficientFunds if there
|
||||||
|
// are not enough unspent outputs to spend amt.
|
||||||
|
func selectOutputs(s tx.UtxoStore, amt uint64, minconf int) (outputs []*tx.Utxo, btcout uint64, err error) {
|
||||||
|
height := getCurHeight()
|
||||||
|
|
||||||
|
// Create list of eligible unspent outputs to use as tx inputs, and
|
||||||
|
// sort by the amount in reverse order so a minimum number of
|
||||||
|
// inputs is needed.
|
||||||
|
var eligible []*tx.Utxo
|
||||||
|
for _, utxo := range s {
|
||||||
|
if int(height-utxo.Height) >= minconf {
|
||||||
|
eligible = append(eligible, utxo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Sort(sort.Reverse(ByAmount(eligible)))
|
||||||
|
|
||||||
|
// Iterate throguh eligible transactions, appending to coins and
|
||||||
|
// increasing btcout. This is finished when btcout is greater than the
|
||||||
|
// requested amt to spend.
|
||||||
|
for _, u := range eligible {
|
||||||
|
outputs = append(outputs, u)
|
||||||
|
if btcout += u.Amt; btcout >= amt {
|
||||||
|
return outputs, btcout, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if btcout < amt {
|
||||||
|
return nil, 0, ErrInsufficientFunds
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputs, btcout, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// txToPairs creates a raw transaction sending the amounts for each
|
||||||
|
// address/amount pair and fee to each address and the miner. minconf
|
||||||
|
// specifies the minimum number of confirmations required before an
|
||||||
|
// unspent output is eligible for spending. Leftover input funds not sent
|
||||||
|
// to addr or as a fee for the miner are sent to a newly generated
|
||||||
|
// address. ErrInsufficientFunds is returned if there are not enough
|
||||||
|
// eligible unspent outputs to create the transaction.
|
||||||
|
func (w *BtcWallet) txToPairs(pairs map[string]uint64, fee uint64, minconf int) (rawtx []byte, err error) {
|
||||||
|
// Recorded unspent transactions should not be modified until this
|
||||||
|
// finishes.
|
||||||
|
w.UtxoStore.RLock()
|
||||||
|
defer w.UtxoStore.RUnlock()
|
||||||
|
|
||||||
|
// Create a new transaction which will include all input scripts.
|
||||||
|
msgtx := btcwire.NewMsgTx()
|
||||||
|
|
||||||
|
// Calculate minimum amount needed for inputs.
|
||||||
|
var amt uint64
|
||||||
|
for _, v := range pairs {
|
||||||
|
amt += v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select unspent outputs to be used in transaction.
|
||||||
|
outputs, btcout, err := selectOutputs(w.UtxoStore.s, amt+fee, minconf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add outputs to new tx.
|
||||||
|
for addr, amt := range pairs {
|
||||||
|
addr160, _, err := btcutil.DecodeAddress(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot decode address: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spend amt to addr160
|
||||||
|
pkScript, err := btcscript.PayToPubKeyHashScript(addr160)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot create txout script: %s", err)
|
||||||
|
}
|
||||||
|
txout := btcwire.NewTxOut(int64(amt), pkScript)
|
||||||
|
msgtx.AddTxOut(txout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are leftover unspent outputs, and return coins back to
|
||||||
|
// a new address we own.
|
||||||
|
if btcout > amt+fee {
|
||||||
|
// Create a new address to spend leftover outputs to.
|
||||||
|
// TODO(jrick): use the next chained address, not the next unused.
|
||||||
|
newaddr, err := w.NextUnusedAddress()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get next unused address: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spend change
|
||||||
|
change := btcout - (amt + fee)
|
||||||
|
newaddr160, _, err := btcutil.DecodeAddress(newaddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot decode new address: %s", err)
|
||||||
|
}
|
||||||
|
pkScript, err := btcscript.PayToPubKeyHashScript(newaddr160)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot create txout script: %s", err)
|
||||||
|
}
|
||||||
|
txout := btcwire.NewTxOut(int64(change), pkScript)
|
||||||
|
msgtx.AddTxOut(txout)
|
||||||
|
}
|
||||||
|
|
||||||
|
var netID byte
|
||||||
|
switch w.Wallet.Net() {
|
||||||
|
case btcwire.MainNet:
|
||||||
|
netID = btcutil.MainNetAddr
|
||||||
|
case btcwire.TestNet:
|
||||||
|
fallthrough
|
||||||
|
case btcwire.TestNet3:
|
||||||
|
netID = btcutil.TestNetAddr
|
||||||
|
default: // wrong!
|
||||||
|
return nil, ErrUnknownBitcoinNet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selected unspent outputs become new transaction's inputs.
|
||||||
|
for _, op := range outputs {
|
||||||
|
msgtx.AddTxIn(btcwire.NewTxIn((*btcwire.OutPoint)(&op.Out), nil))
|
||||||
|
}
|
||||||
|
for i, op := range outputs {
|
||||||
|
addrstr, err := btcutil.EncodeAddress(op.Addr[:], netID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
privkey, err := w.GetAddressKey(addrstr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot get address key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(jrick): we want compressed pubkeys. Switch wallet to
|
||||||
|
// generate addresses from the compressed key. This will break
|
||||||
|
// armory wallet compat but oh well.
|
||||||
|
sigscript, err := btcscript.SignatureScript(msgtx, i,
|
||||||
|
op.Subscript, btcscript.SigHashAll, privkey, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot create sigscript: %s", err)
|
||||||
|
}
|
||||||
|
msgtx.TxIn[i].SignatureScript = sigscript
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate msgtx before returning the raw transaction.
|
||||||
|
for i, txin := range msgtx.TxIn {
|
||||||
|
engine, err := btcscript.NewScript(txin.SignatureScript, outputs[i].Subscript, i,
|
||||||
|
msgtx, time.Now().After(btcscript.Bip16Activation))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot create script engine: %s", err)
|
||||||
|
}
|
||||||
|
if err = engine.Execute(); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot validate transaction: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
msgtx.BtcEncode(buf, btcwire.ProtocolVersion)
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
69
createtx_test.go
Normal file
69
createtx_test.go
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/conformal/btcscript"
|
||||||
|
"github.com/conformal/btcutil"
|
||||||
|
"github.com/conformal/btcwallet/tx"
|
||||||
|
"github.com/conformal/btcwallet/wallet"
|
||||||
|
"github.com/conformal/btcwire"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFakeTxs(t *testing.T) {
|
||||||
|
// First we need a wallet.
|
||||||
|
w, err := wallet.NewWallet("banana wallet", "", []byte("banana"))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Can not create encrypted wallet: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
btcw := &BtcWallet{
|
||||||
|
Wallet: w,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Unlock([]byte("banana"))
|
||||||
|
|
||||||
|
// Create and add a fake Utxo so we have some funds to spend.
|
||||||
|
//
|
||||||
|
// This will pass validation because btcscript is unaware of invalid
|
||||||
|
// tx inputs, however, this example would fail in btcd.
|
||||||
|
utxo := &tx.Utxo{}
|
||||||
|
addr, err := w.NextUnusedAddress()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Cannot get next address: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addr160, _, err := btcutil.DecodeAddress(addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Cannot decode address: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
copy(utxo.Addr[:], addr160)
|
||||||
|
ophash := (btcwire.ShaHash)([...]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
|
||||||
|
12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
|
||||||
|
28, 29, 30, 31, 32})
|
||||||
|
out := btcwire.NewOutPoint(&ophash, 0)
|
||||||
|
utxo.Out = tx.OutPoint(*out)
|
||||||
|
ss, err := btcscript.PayToPubKeyHashScript(addr160)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not create utxo PkScript: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utxo.Subscript = tx.PkScript(ss)
|
||||||
|
utxo.Amt = 10000
|
||||||
|
utxo.Height = 12345
|
||||||
|
btcw.UtxoStore.s = append(btcw.UtxoStore.s, utxo)
|
||||||
|
|
||||||
|
// Fake our current block height so btcd doesn't need to be queried.
|
||||||
|
curHeight.h = 12346
|
||||||
|
|
||||||
|
// Create the transaction.
|
||||||
|
pairs := map[string]uint64{
|
||||||
|
"17XhEvq9Nahdj7Xe1nv6oRe1tEmaHUuynH": 5000,
|
||||||
|
}
|
||||||
|
rawtx, err := btcw.txToPairs(pairs, 100, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Tx creation failed: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = rawtx
|
||||||
|
}
|
256
sockets.go
256
sockets.go
|
@ -28,10 +28,21 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ConnRefused = errors.New("Connection refused")
|
// ErrConnRefused represents an error where a connection to another
|
||||||
|
// process cannot be established.
|
||||||
|
ErrConnRefused = errors.New("connection refused")
|
||||||
|
|
||||||
|
// ErrConnLost represents an error where a connection to another
|
||||||
|
// process cannot be established.
|
||||||
|
ErrConnLost = errors.New("connection lost")
|
||||||
|
|
||||||
// Channel to close to notify that connection to btcd has been lost.
|
// Channel to close to notify that connection to btcd has been lost.
|
||||||
btcdDisconnected = make(chan int)
|
btcdConnected = struct {
|
||||||
|
b bool
|
||||||
|
c chan bool
|
||||||
|
}{
|
||||||
|
c: make(chan bool),
|
||||||
|
}
|
||||||
|
|
||||||
// Channel to send messages btcwallet does not understand and requests
|
// Channel to send messages btcwallet does not understand and requests
|
||||||
// from btcwallet to btcd.
|
// from btcwallet to btcd.
|
||||||
|
@ -52,9 +63,9 @@ var (
|
||||||
// handler function to route the reply to.
|
// handler function to route the reply to.
|
||||||
replyHandlers = struct {
|
replyHandlers = struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
m map[uint64]func(interface{}, interface{}) bool
|
m map[uint64]func(interface{}, *btcjson.Error) bool
|
||||||
}{
|
}{
|
||||||
m: make(map[uint64]func(interface{}, interface{}) bool),
|
m: make(map[uint64]func(interface{}, *btcjson.Error) bool),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -88,12 +99,26 @@ func frontendListenerDuplicator() {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Duplicate all messages sent across frontendNotificationMaster to each
|
// Duplicate all messages sent across frontendNotificationMaster, as
|
||||||
// listening wallet.
|
// well as internal btcwallet notifications, to each listening wallet.
|
||||||
for {
|
for {
|
||||||
ntfn := <-frontendNotificationMaster
|
var ntfn []byte
|
||||||
|
|
||||||
|
select {
|
||||||
|
case conn := <-btcdConnected.c:
|
||||||
|
btcdConnected.b = conn
|
||||||
|
var idStr interface{} = "btcwallet:btcconnected"
|
||||||
|
r := btcjson.Reply{
|
||||||
|
Result: conn,
|
||||||
|
Id: &idStr,
|
||||||
|
}
|
||||||
|
ntfn, _ = json.Marshal(r)
|
||||||
|
|
||||||
|
case ntfn = <-frontendNotificationMaster:
|
||||||
|
}
|
||||||
|
|
||||||
mtx.Lock()
|
mtx.Lock()
|
||||||
for c, _ := range frontendListeners {
|
for c := range frontendListeners {
|
||||||
c <- ntfn
|
c <- ntfn
|
||||||
}
|
}
|
||||||
mtx.Unlock()
|
mtx.Unlock()
|
||||||
|
@ -132,14 +157,6 @@ func frontendReqsNotifications(ws *websocket.Conn) {
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-btcdDisconnected:
|
|
||||||
var idStr interface{} = "btcwallet:btcddisconnected"
|
|
||||||
r := btcjson.Reply{
|
|
||||||
Id: &idStr,
|
|
||||||
}
|
|
||||||
m, _ := json.Marshal(r)
|
|
||||||
websocket.Message.Send(ws, m)
|
|
||||||
return
|
|
||||||
case m, ok := <-jsonMsgs:
|
case m, ok := <-jsonMsgs:
|
||||||
if !ok {
|
if !ok {
|
||||||
// frontend disconnected.
|
// frontend disconnected.
|
||||||
|
@ -164,7 +181,6 @@ func BtcdHandler(ws *websocket.Conn) {
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
close(disconnected)
|
close(disconnected)
|
||||||
close(btcdDisconnected)
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Listen for replies/notifications from btcd, and decide how to handle them.
|
// Listen for replies/notifications from btcd, and decide how to handle them.
|
||||||
|
@ -210,12 +226,15 @@ func BtcdHandler(ws *websocket.Conn) {
|
||||||
// are sent to every connected frontend.
|
// are sent to every connected frontend.
|
||||||
func ProcessBtcdNotificationReply(b []byte) {
|
func ProcessBtcdNotificationReply(b []byte) {
|
||||||
// Check if the json id field was set by btcwallet.
|
// Check if the json id field was set by btcwallet.
|
||||||
var routeId uint64
|
var routeID uint64
|
||||||
var origId string
|
var origID string
|
||||||
|
|
||||||
var m map[string]interface{}
|
var r btcjson.Reply
|
||||||
json.Unmarshal(b, &m)
|
if err := json.Unmarshal(b, &r); err != nil {
|
||||||
idStr, ok := m["id"].(string)
|
log.Errorf("Unable to unmarshal btcd message: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
idStr, ok := (*r.Id).(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
// btcd should only ever be sending JSON messages with a string in
|
// btcd should only ever be sending JSON messages with a string in
|
||||||
// the id field. Log the error and drop the message.
|
// the id field. Log the error and drop the message.
|
||||||
|
@ -223,18 +242,18 @@ func ProcessBtcdNotificationReply(b []byte) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
n, _ := fmt.Sscanf(idStr, "btcwallet(%d)-%s", &routeId, &origId)
|
n, _ := fmt.Sscanf(idStr, "btcwallet(%d)-%s", &routeID, &origID)
|
||||||
if n == 1 {
|
if n == 1 {
|
||||||
// Request originated from btcwallet. Run and remove correct
|
// Request originated from btcwallet. Run and remove correct
|
||||||
// handler.
|
// handler.
|
||||||
replyHandlers.Lock()
|
replyHandlers.Lock()
|
||||||
f := replyHandlers.m[routeId]
|
f := replyHandlers.m[routeID]
|
||||||
replyHandlers.Unlock()
|
replyHandlers.Unlock()
|
||||||
if f != nil {
|
if f != nil {
|
||||||
go func() {
|
go func() {
|
||||||
if f(m["result"], m["error"]) {
|
if f(r.Result, r.Error) {
|
||||||
replyHandlers.Lock()
|
replyHandlers.Lock()
|
||||||
delete(replyHandlers.m, routeId)
|
delete(replyHandlers.m, routeID)
|
||||||
replyHandlers.Unlock()
|
replyHandlers.Unlock()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -242,9 +261,9 @@ func ProcessBtcdNotificationReply(b []byte) {
|
||||||
} else if n == 2 {
|
} else if n == 2 {
|
||||||
// Attempt to route btcd reply to correct frontend.
|
// Attempt to route btcd reply to correct frontend.
|
||||||
replyRouter.Lock()
|
replyRouter.Lock()
|
||||||
c := replyRouter.m[routeId]
|
c := replyRouter.m[routeID]
|
||||||
if c != nil {
|
if c != nil {
|
||||||
delete(replyRouter.m, routeId)
|
delete(replyRouter.m, routeID)
|
||||||
} else {
|
} else {
|
||||||
// Can't route to a frontend, drop reply.
|
// Can't route to a frontend, drop reply.
|
||||||
log.Info("Unable to route btcd reply to frontend. Dropping.")
|
log.Info("Unable to route btcd reply to frontend. Dropping.")
|
||||||
|
@ -253,15 +272,17 @@ func ProcessBtcdNotificationReply(b []byte) {
|
||||||
replyRouter.Unlock()
|
replyRouter.Unlock()
|
||||||
|
|
||||||
// Convert string back to number if possible.
|
// Convert string back to number if possible.
|
||||||
var origIdNum float64
|
var origIDNum float64
|
||||||
n, _ := fmt.Sscanf(origId, "%f", &origIdNum)
|
n, _ := fmt.Sscanf(origID, "%f", &origIDNum)
|
||||||
|
var id interface{}
|
||||||
if n == 1 {
|
if n == 1 {
|
||||||
m["id"] = origIdNum
|
id = origIDNum
|
||||||
} else {
|
} else {
|
||||||
m["id"] = origId
|
id = origID
|
||||||
}
|
}
|
||||||
|
r.Id = &id
|
||||||
|
|
||||||
b, err := json.Marshal(m)
|
b, err := json.Marshal(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error marshalling btcd reply. Dropping.")
|
log.Error("Error marshalling btcd reply. Dropping.")
|
||||||
return
|
return
|
||||||
|
@ -272,27 +293,10 @@ func ProcessBtcdNotificationReply(b []byte) {
|
||||||
// to all frontends if btcwallet can not handle it.
|
// to all frontends if btcwallet can not handle it.
|
||||||
switch idStr {
|
switch idStr {
|
||||||
case "btcd:blockconnected":
|
case "btcd:blockconnected":
|
||||||
result := m["result"].(map[string]interface{})
|
NtfnBlockConnected(r.Result)
|
||||||
hashBE := result["hash"].(string)
|
|
||||||
hash, err := btcwire.NewShaHashFromStr(hashBE)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("btcd:blockconnected handler: Invalid hash string")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
height := int64(result["height"].(float64))
|
|
||||||
|
|
||||||
// TODO(jrick): update TxStore and UtxoStore with new hash
|
|
||||||
_ = hash
|
|
||||||
var id interface{} = "btcwallet:newblockchainheight"
|
|
||||||
msgRaw := &btcjson.Reply{
|
|
||||||
Result: height,
|
|
||||||
Id: &id,
|
|
||||||
}
|
|
||||||
msg, _ := json.Marshal(msgRaw)
|
|
||||||
frontendNotificationMaster <- msg
|
|
||||||
|
|
||||||
case "btcd:blockdisconnected":
|
case "btcd:blockdisconnected":
|
||||||
// TODO(jrick): rollback txs and utxos from removed block.
|
NtfnBlockDisconnected(r.Result)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
frontendNotificationMaster <- b
|
frontendNotificationMaster <- b
|
||||||
|
@ -300,43 +304,145 @@ func ProcessBtcdNotificationReply(b []byte) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenAndServe connects to a running btcd instance over a websocket
|
// NtfnBlockConnected handles btcd notifications resulting from newly
|
||||||
|
// connected blocks to the main blockchain. Currently, this only creates
|
||||||
|
// a new notification for frontends with the new blockchain height.
|
||||||
|
func NtfnBlockConnected(r interface{}) {
|
||||||
|
result, ok := r.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
log.Error("blockconnected notification: invalid result")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hashBE, ok := result["hash"].(string)
|
||||||
|
if !ok {
|
||||||
|
log.Error("blockconnected notification: invalid hash")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hash, err := btcwire.NewShaHashFromStr(hashBE)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("btcd:blockconnected handler: invalid hash string")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
heightf, ok := result["height"].(float64)
|
||||||
|
if !ok {
|
||||||
|
log.Error("blockconnected notification: invalid height")
|
||||||
|
}
|
||||||
|
height := int64(heightf)
|
||||||
|
|
||||||
|
// TODO(jrick): update TxStore and UtxoStore with new hash
|
||||||
|
_ = hash
|
||||||
|
var id interface{} = "btcwallet:newblockchainheight"
|
||||||
|
msgRaw := &btcjson.Reply{
|
||||||
|
Result: height,
|
||||||
|
Id: &id,
|
||||||
|
}
|
||||||
|
msg, err := json.Marshal(msgRaw)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("btcd:blockconnected handler: unable to marshal reply")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
frontendNotificationMaster <- msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// NtfnBlockDisconnected handles btcd notifications resulting from
|
||||||
|
// blocks disconnected from the main chain in the event of a chain
|
||||||
|
// switch and notifies frontends of the new blockchain height.
|
||||||
|
//
|
||||||
|
// TODO(jrick): Rollback Utxo and Tx data
|
||||||
|
func NtfnBlockDisconnected(r interface{}) {
|
||||||
|
result, ok := r.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
log.Error("blockdisconnected notification: invalid result")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hashBE, ok := result["hash"].(string)
|
||||||
|
if !ok {
|
||||||
|
log.Error("blockdisconnected notification: invalid hash")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hash, err := btcwire.NewShaHashFromStr(hashBE)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("btcd:blockdisconnected handler: invalid hash string")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
heightf, ok := result["height"].(float64)
|
||||||
|
if !ok {
|
||||||
|
log.Error("blockdisconnected notification: invalid height")
|
||||||
|
}
|
||||||
|
height := int64(heightf)
|
||||||
|
|
||||||
|
// Rollback Utxo and Tx data stores.
|
||||||
|
go func() {
|
||||||
|
wallets.Rollback(height, hash)
|
||||||
|
}()
|
||||||
|
|
||||||
|
var id interface{} = "btcwallet:newblockchainheight"
|
||||||
|
msgRaw := &btcjson.Reply{
|
||||||
|
Result: height,
|
||||||
|
Id: &id,
|
||||||
|
}
|
||||||
|
msg, err := json.Marshal(msgRaw)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("btcd:blockdisconnected handler: unable to marshal reply")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
frontendNotificationMaster <- msg
|
||||||
|
}
|
||||||
|
|
||||||
|
var duplicateOnce sync.Once
|
||||||
|
|
||||||
|
// FrontendListenAndServe starts a HTTP server to provide websocket
|
||||||
|
// connections for any number of btcwallet frontends.
|
||||||
|
func FrontendListenAndServe() error {
|
||||||
|
// We'll need to duplicate replies to frontends to each frontend.
|
||||||
|
// Replies are sent to frontendReplyMaster, and duplicated to each valid
|
||||||
|
// channel in frontendReplySet. This runs a goroutine to duplicate
|
||||||
|
// requests for each channel in the set.
|
||||||
|
//
|
||||||
|
// Use a sync.Once to insure no extra duplicators run.
|
||||||
|
go duplicateOnce.Do(frontendListenerDuplicator)
|
||||||
|
|
||||||
|
// TODO(jrick): We need some sort of authentication before websocket
|
||||||
|
// connections are allowed, and perhaps TLS on the server as well.
|
||||||
|
http.Handle("/frontend", websocket.Handler(frontendReqsNotifications))
|
||||||
|
return http.ListenAndServe(fmt.Sprintf(":%d", cfg.SvrPort), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BtcdConnect connects to a running btcd instance over a websocket
|
||||||
// for sending and receiving chain-related messages, failing if the
|
// for sending and receiving chain-related messages, failing if the
|
||||||
// connection can not be established. An additional HTTP server is then
|
// connection cannot be established or is lost.
|
||||||
// started to provide websocket connections for any number of btcwallet
|
func BtcdConnect(reply chan error) {
|
||||||
// frontends.
|
|
||||||
func ListenAndServe() error {
|
|
||||||
// Attempt to connect to running btcd instance. Bail if it fails.
|
// Attempt to connect to running btcd instance. Bail if it fails.
|
||||||
btcdws, err := websocket.Dial(
|
btcdws, err := websocket.Dial(
|
||||||
fmt.Sprintf("ws://localhost:%d/wallet", cfg.BtcdPort),
|
fmt.Sprintf("ws://localhost:%d/wallet", cfg.BtcdPort),
|
||||||
"",
|
"",
|
||||||
"http://localhost/")
|
"http://localhost/")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ConnRefused
|
reply <- ErrConnRefused
|
||||||
|
return
|
||||||
}
|
}
|
||||||
go BtcdHandler(btcdws)
|
reply <- nil
|
||||||
|
|
||||||
log.Info("Established connection to btcd.")
|
// Remove all reply handlers (if any exist from an old connection).
|
||||||
|
replyHandlers.Lock()
|
||||||
|
for k := range replyHandlers.m {
|
||||||
|
delete(replyHandlers.m, k)
|
||||||
|
}
|
||||||
|
replyHandlers.Unlock()
|
||||||
|
|
||||||
// Begin tracking wallets.
|
handlerClosed := make(chan int)
|
||||||
|
go func() {
|
||||||
|
BtcdHandler(btcdws)
|
||||||
|
close(handlerClosed)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Begin tracking wallets against this btcd instance.
|
||||||
wallets.RLock()
|
wallets.RLock()
|
||||||
for _, w := range wallets.m {
|
for _, w := range wallets.m {
|
||||||
w.Track()
|
w.Track()
|
||||||
}
|
}
|
||||||
wallets.RUnlock()
|
wallets.RUnlock()
|
||||||
|
|
||||||
// We'll need to duplicate replies to frontends to each frontend.
|
<-handlerClosed
|
||||||
// Replies are sent to frontendReplyMaster, and duplicated to each valid
|
reply <- ErrConnLost
|
||||||
// channel in frontendReplySet. This runs a goroutine to duplicate
|
|
||||||
// requests for each channel in the set.
|
|
||||||
go frontendListenerDuplicator()
|
|
||||||
|
|
||||||
// TODO(jrick): We need some sort of authentication before websocket
|
|
||||||
// connections are allowed, and perhaps TLS on the server as well.
|
|
||||||
http.Handle("/frontend", websocket.Handler(frontendReqsNotifications))
|
|
||||||
if err := http.ListenAndServe(fmt.Sprintf(":%d", cfg.SvrPort), nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
220
tx/tx.go
220
tx/tx.go
|
@ -20,53 +20,61 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"code.google.com/p/go.crypto/ripemd160"
|
"code.google.com/p/go.crypto/ripemd160"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/conformal/btcwire"
|
"github.com/conformal/btcwire"
|
||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Byte headers prepending confirmed and unconfirmed serialized UTXOs.
|
|
||||||
const (
|
|
||||||
ConfirmedUtxoHeader byte = iota
|
|
||||||
UnconfirmedUtxoHeader
|
|
||||||
)
|
|
||||||
|
|
||||||
// Byte headers prepending received and sent serialized transactions.
|
// Byte headers prepending received and sent serialized transactions.
|
||||||
const (
|
const (
|
||||||
RecvTxHeader byte = iota
|
RecvTxHeader byte = iota
|
||||||
SendTxHeader
|
SendTxHeader
|
||||||
)
|
)
|
||||||
|
|
||||||
type UtxoStore struct {
|
// UtxoStore is a type used for holding all Utxo structures for all
|
||||||
Confirmed []*Utxo
|
// addresses in a wallet.
|
||||||
Unconfirmed []*Utxo
|
type UtxoStore []*Utxo
|
||||||
}
|
|
||||||
|
|
||||||
|
// Utxo is a type storing information about a single unspent
|
||||||
|
// transaction output.
|
||||||
type Utxo struct {
|
type Utxo struct {
|
||||||
Addr [ripemd160.Size]byte
|
Addr [ripemd160.Size]byte
|
||||||
Out OutPoint
|
Out OutPoint
|
||||||
Subscript PKScript
|
Subscript PkScript
|
||||||
Amt uint64 // Measured in Satoshis
|
Amt uint64 // Measured in Satoshis
|
||||||
Height int64
|
Height int64
|
||||||
|
BlockHash btcwire.ShaHash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OutPoint is a btcwire.OutPoint with custom methods for serialization.
|
||||||
type OutPoint btcwire.OutPoint
|
type OutPoint btcwire.OutPoint
|
||||||
|
|
||||||
type PKScript []byte
|
// PkScript is a custom type with methods to serialize pubkey scripts
|
||||||
|
// of variable length.
|
||||||
|
type PkScript []byte
|
||||||
|
|
||||||
// TxStore is a slice holding RecvTx and SendTx pointers.
|
// TxStore is a slice holding RecvTx and SendTx pointers.
|
||||||
type TxStore []interface{}
|
type TxStore []interface{}
|
||||||
|
|
||||||
|
// RecvTx is a type storing information about a transaction that was
|
||||||
|
// received by an address in a wallet.
|
||||||
type RecvTx struct {
|
type RecvTx struct {
|
||||||
TxHash btcwire.ShaHash
|
TxHash btcwire.ShaHash
|
||||||
|
BlockHash btcwire.ShaHash
|
||||||
|
Height int64
|
||||||
Amt uint64 // Measured in Satoshis
|
Amt uint64 // Measured in Satoshis
|
||||||
SenderAddr [ripemd160.Size]byte
|
SenderAddr [ripemd160.Size]byte
|
||||||
ReceiverAddr [ripemd160.Size]byte
|
ReceiverAddr [ripemd160.Size]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendTx is a type storing information about a transaction that was
|
||||||
|
// sent by an address in a wallet.
|
||||||
type SendTx struct {
|
type SendTx struct {
|
||||||
TxHash btcwire.ShaHash
|
TxHash btcwire.ShaHash
|
||||||
Fee int64 // Measured in Satoshis
|
BlockHash btcwire.ShaHash
|
||||||
|
Height int64
|
||||||
|
Fee uint64 // Measured in Satoshis
|
||||||
SenderAddr [ripemd160.Size]byte
|
SenderAddr [ripemd160.Size]byte
|
||||||
ReceiverAddrs []struct {
|
ReceiverAddrs []struct {
|
||||||
Addr [ripemd160.Size]byte
|
Addr [ripemd160.Size]byte
|
||||||
|
@ -85,6 +93,9 @@ func binaryRead(r io.Reader, order binary.ByteOrder, data interface{}) (n int64,
|
||||||
if read, err = r.Read(buf); err != nil {
|
if read, err = r.Read(buf); err != nil {
|
||||||
return int64(read), err
|
return int64(read), err
|
||||||
}
|
}
|
||||||
|
if read < binary.Size(data) {
|
||||||
|
return int64(read), io.EOF
|
||||||
|
}
|
||||||
return int64(read), binary.Read(bytes.NewBuffer(buf), order, data)
|
return int64(read), binary.Read(bytes.NewBuffer(buf), order, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,35 +116,17 @@ func binaryWrite(w io.Writer, order binary.ByteOrder, data interface{}) (n int64
|
||||||
func (u *UtxoStore) ReadFrom(r io.Reader) (n int64, err error) {
|
func (u *UtxoStore) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
var read int64
|
var read int64
|
||||||
for {
|
for {
|
||||||
// Read header
|
// Read Utxo
|
||||||
var header byte
|
utxo := new(Utxo)
|
||||||
read, err = binaryRead(r, binary.LittleEndian, &header)
|
read, err = utxo.ReadFrom(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// EOF here is not an error.
|
if read == 0 && err == io.EOF {
|
||||||
if err == io.EOF {
|
return n, nil
|
||||||
return n + read, nil
|
|
||||||
}
|
}
|
||||||
return n + read, err
|
return n + read, err
|
||||||
}
|
}
|
||||||
n += read
|
n += read
|
||||||
|
*u = append(*u, utxo)
|
||||||
// Read Utxo
|
|
||||||
var slicep *[]*Utxo
|
|
||||||
switch header {
|
|
||||||
case ConfirmedUtxoHeader:
|
|
||||||
slicep = &u.Confirmed
|
|
||||||
case UnconfirmedUtxoHeader:
|
|
||||||
slicep = &u.Unconfirmed
|
|
||||||
default:
|
|
||||||
return n, fmt.Errorf("Unknown Utxo header.")
|
|
||||||
}
|
|
||||||
utxo := new(Utxo)
|
|
||||||
read, err = utxo.ReadFrom(r)
|
|
||||||
if err != nil {
|
|
||||||
return n + read, err
|
|
||||||
}
|
|
||||||
n += read
|
|
||||||
*slicep = append(*slicep, utxo)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,31 +135,7 @@ func (u *UtxoStore) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
// confirmed and unconfirmed outputs.
|
// confirmed and unconfirmed outputs.
|
||||||
func (u *UtxoStore) WriteTo(w io.Writer) (n int64, err error) {
|
func (u *UtxoStore) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
var written int64
|
var written int64
|
||||||
|
for _, utxo := range *u {
|
||||||
for _, utxo := range u.Confirmed {
|
|
||||||
// Write header
|
|
||||||
written, err = binaryWrite(w, binary.LittleEndian, ConfirmedUtxoHeader)
|
|
||||||
if err != nil {
|
|
||||||
return n + written, err
|
|
||||||
}
|
|
||||||
n += written
|
|
||||||
|
|
||||||
// Write Utxo
|
|
||||||
written, err = utxo.WriteTo(w)
|
|
||||||
if err != nil {
|
|
||||||
return n + written, err
|
|
||||||
}
|
|
||||||
n += written
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, utxo := range u.Unconfirmed {
|
|
||||||
// Write header
|
|
||||||
written, err = binaryWrite(w, binary.LittleEndian, UnconfirmedUtxoHeader)
|
|
||||||
if err != nil {
|
|
||||||
return n + written, err
|
|
||||||
}
|
|
||||||
n += written
|
|
||||||
|
|
||||||
// Write Utxo
|
// Write Utxo
|
||||||
written, err = utxo.WriteTo(w)
|
written, err = utxo.WriteTo(w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -178,10 +147,43 @@ func (u *UtxoStore) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rollback removes all utxos from and after the block specified
|
||||||
|
// by a block height and hash.
|
||||||
|
//
|
||||||
|
// Correct results rely on u being sorted by block height in
|
||||||
|
// increasing order.
|
||||||
|
func (u *UtxoStore) Rollback(height int64, hash *btcwire.ShaHash) (modified bool) {
|
||||||
|
s := *u
|
||||||
|
|
||||||
|
// endlen specifies the final length of the rolled-back UtxoStore.
|
||||||
|
// Past endlen, array elements are nilled. We do this instead of
|
||||||
|
// just reslicing with a shorter length to avoid leaving elements
|
||||||
|
// in the underlying array so they can be garbage collected.
|
||||||
|
endlen := len(s)
|
||||||
|
defer func() {
|
||||||
|
modified = endlen != len(s)
|
||||||
|
for i := endlen; i < len(s); i++ {
|
||||||
|
s[i] = nil
|
||||||
|
}
|
||||||
|
*u = s[:endlen]
|
||||||
|
return
|
||||||
|
}()
|
||||||
|
|
||||||
|
for i := len(s) - 1; i >= 0; i-- {
|
||||||
|
if height > s[i].Height {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if height == s[i].Height && *hash == s[i].BlockHash {
|
||||||
|
endlen = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// ReadFrom satisifies the io.ReaderFrom interface. A Utxo is read
|
// ReadFrom satisifies the io.ReaderFrom interface. A Utxo is read
|
||||||
// from r with the format:
|
// from r with the format:
|
||||||
//
|
//
|
||||||
// [Addr (20 bytes), Out (36 bytes), Subscript (varies), Amt (8 bytes), Height (8 bytes)]
|
// [Addr (20 bytes), Out (36 bytes), Subscript (varies), Amt (8 bytes), Height (8 bytes), BlockHash (32 bytes)]
|
||||||
//
|
//
|
||||||
// Each field is read little endian.
|
// Each field is read little endian.
|
||||||
func (u *Utxo) ReadFrom(r io.Reader) (n int64, err error) {
|
func (u *Utxo) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
|
@ -191,6 +193,7 @@ func (u *Utxo) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
&u.Subscript,
|
&u.Subscript,
|
||||||
&u.Amt,
|
&u.Amt,
|
||||||
&u.Height,
|
&u.Height,
|
||||||
|
&u.BlockHash,
|
||||||
}
|
}
|
||||||
var read int64
|
var read int64
|
||||||
for _, data := range datas {
|
for _, data := range datas {
|
||||||
|
@ -210,7 +213,7 @@ func (u *Utxo) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
// WriteTo satisifies the io.WriterTo interface. A Utxo is written to
|
// WriteTo satisifies the io.WriterTo interface. A Utxo is written to
|
||||||
// w in the format:
|
// w in the format:
|
||||||
//
|
//
|
||||||
// [Addr (20 bytes), Out (36 bytes), Subscript (varies), Amt (8 bytes), Height (8 bytes)]
|
// [Addr (20 bytes), Out (36 bytes), Subscript (varies), Amt (8 bytes), Height (8 bytes), BlockHash (32 bytes)]
|
||||||
//
|
//
|
||||||
// Each field is written little endian.
|
// Each field is written little endian.
|
||||||
func (u *Utxo) WriteTo(w io.Writer) (n int64, err error) {
|
func (u *Utxo) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
|
@ -220,6 +223,7 @@ func (u *Utxo) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
&u.Subscript,
|
&u.Subscript,
|
||||||
&u.Amt,
|
&u.Amt,
|
||||||
&u.Height,
|
&u.Height,
|
||||||
|
&u.BlockHash,
|
||||||
}
|
}
|
||||||
var written int64
|
var written int64
|
||||||
for _, data := range datas {
|
for _, data := range datas {
|
||||||
|
@ -280,13 +284,13 @@ func (o *OutPoint) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadFrom satisifies the io.ReaderFrom interface. A PKScript is read
|
// ReadFrom satisifies the io.ReaderFrom interface. A PkScript is read
|
||||||
// from r with the format:
|
// from r with the format:
|
||||||
//
|
//
|
||||||
// [Length (4 byte unsigned integer), ScriptBytes (Length bytes)]
|
// [Length (4 byte unsigned integer), ScriptBytes (Length bytes)]
|
||||||
//
|
//
|
||||||
// Length is read little endian.
|
// Length is read little endian.
|
||||||
func (s *PKScript) ReadFrom(r io.Reader) (n int64, err error) {
|
func (s *PkScript) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
var scriptlen uint32
|
var scriptlen uint32
|
||||||
var read int64
|
var read int64
|
||||||
read, err = binaryRead(r, binary.LittleEndian, &scriptlen)
|
read, err = binaryRead(r, binary.LittleEndian, &scriptlen)
|
||||||
|
@ -306,13 +310,13 @@ func (s *PKScript) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteTo satisifies the io.WriterTo interface. A PKScript is written
|
// WriteTo satisifies the io.WriterTo interface. A PkScript is written
|
||||||
// to w in the format:
|
// to w in the format:
|
||||||
//
|
//
|
||||||
// [Length (4 byte unsigned integer), ScriptBytes (Length bytes)]
|
// [Length (4 byte unsigned integer), ScriptBytes (Length bytes)]
|
||||||
//
|
//
|
||||||
// Length is written little endian.
|
// Length is written little endian.
|
||||||
func (s *PKScript) WriteTo(w io.Writer) (n int64, err error) {
|
func (s *PkScript) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
var written int64
|
var written int64
|
||||||
written, err = binaryWrite(w, binary.LittleEndian, uint32(len(*s)))
|
written, err = binaryWrite(w, binary.LittleEndian, uint32(len(*s)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -359,7 +363,7 @@ func (txs *TxStore) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
case SendTxHeader:
|
case SendTxHeader:
|
||||||
tx = new(SendTx)
|
tx = new(SendTx)
|
||||||
default:
|
default:
|
||||||
return n, fmt.Errorf("Unknown Tx header")
|
return n, fmt.Errorf("unknown Tx header")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read tx
|
// Read tx
|
||||||
|
@ -395,7 +399,7 @@ func (txs *TxStore) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
}
|
}
|
||||||
n += written
|
n += written
|
||||||
default:
|
default:
|
||||||
return n, fmt.Errorf("Unknown type in TxStore")
|
return n, fmt.Errorf("unknown type in TxStore")
|
||||||
}
|
}
|
||||||
wt := tx.(io.WriterTo)
|
wt := tx.(io.WriterTo)
|
||||||
written, err = wt.WriteTo(w)
|
written, err = wt.WriteTo(w)
|
||||||
|
@ -407,15 +411,65 @@ func (txs *TxStore) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rollback removes all txs from and after the block specified by a
|
||||||
|
// block height and hash.
|
||||||
|
//
|
||||||
|
// Correct results rely on txs being sorted by block height in
|
||||||
|
// increasing order.
|
||||||
|
func (txs *TxStore) Rollback(height int64, hash *btcwire.ShaHash) (modified bool) {
|
||||||
|
s := ([]interface{})(*txs)
|
||||||
|
|
||||||
|
// endlen specifies the final length of the rolled-back TxStore.
|
||||||
|
// Past endlen, array elements are nilled. We do this instead of
|
||||||
|
// just reslicing with a shorter length to avoid leaving elements
|
||||||
|
// in the underlying array so they can be garbage collected.
|
||||||
|
endlen := len(s)
|
||||||
|
defer func() {
|
||||||
|
modified = endlen != len(s)
|
||||||
|
for i := endlen; i < len(s); i++ {
|
||||||
|
s[i] = nil
|
||||||
|
}
|
||||||
|
*txs = s[:endlen]
|
||||||
|
return
|
||||||
|
}()
|
||||||
|
|
||||||
|
for i := len(s) - 1; i >= 0; i-- {
|
||||||
|
var txheight int64
|
||||||
|
var txhash *btcwire.ShaHash
|
||||||
|
switch s[i].(type) {
|
||||||
|
case *RecvTx:
|
||||||
|
tx := s[i].(*RecvTx)
|
||||||
|
if height > tx.Height {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
txheight = tx.Height
|
||||||
|
txhash = &tx.BlockHash
|
||||||
|
case *SendTx:
|
||||||
|
tx := s[i].(*SendTx)
|
||||||
|
if height > tx.Height {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
txheight = tx.Height
|
||||||
|
txhash = &tx.BlockHash
|
||||||
|
}
|
||||||
|
if height == txheight && *hash == *txhash {
|
||||||
|
endlen = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// ReadFrom satisifies the io.ReaderFrom interface. A RecTx is read
|
// ReadFrom satisifies the io.ReaderFrom interface. A RecTx is read
|
||||||
// in from r with the format:
|
// in from r with the format:
|
||||||
//
|
//
|
||||||
// [TxHash (32 bytes), Amt (8 bytes), SenderAddr (20 bytes), ReceiverAddr (20 bytes)]
|
// [TxHash (32 bytes), BlockHash (32 bytes), Height (8 bytes), Amt (8 bytes), SenderAddr (20 bytes), ReceiverAddr (20 bytes)]
|
||||||
//
|
//
|
||||||
// Each field is read little endian.
|
// Each field is read little endian.
|
||||||
func (tx *RecvTx) ReadFrom(r io.Reader) (n int64, err error) {
|
func (tx *RecvTx) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
datas := []interface{}{
|
datas := []interface{}{
|
||||||
&tx.TxHash,
|
&tx.TxHash,
|
||||||
|
&tx.BlockHash,
|
||||||
|
&tx.Height,
|
||||||
&tx.Amt,
|
&tx.Amt,
|
||||||
&tx.SenderAddr,
|
&tx.SenderAddr,
|
||||||
&tx.ReceiverAddr,
|
&tx.ReceiverAddr,
|
||||||
|
@ -434,12 +488,14 @@ func (tx *RecvTx) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
// WriteTo satisifies the io.WriterTo interface. A RecvTx is written to
|
// WriteTo satisifies the io.WriterTo interface. A RecvTx is written to
|
||||||
// w in the format:
|
// w in the format:
|
||||||
//
|
//
|
||||||
// [TxHash (32 bytes), Amt (8 bytes), SenderAddr (20 bytes), ReceiverAddr (20 bytes)]
|
// [TxHash (32 bytes), BlockHash (32 bytes), Height (8 bytes), Amt (8 bytes), SenderAddr (20 bytes), ReceiverAddr (20 bytes)]
|
||||||
//
|
//
|
||||||
// Each field is written little endian.
|
// Each field is written little endian.
|
||||||
func (tx *RecvTx) WriteTo(w io.Writer) (n int64, err error) {
|
func (tx *RecvTx) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
datas := []interface{}{
|
datas := []interface{}{
|
||||||
&tx.TxHash,
|
&tx.TxHash,
|
||||||
|
&tx.BlockHash,
|
||||||
|
&tx.Height,
|
||||||
&tx.Amt,
|
&tx.Amt,
|
||||||
&tx.SenderAddr,
|
&tx.SenderAddr,
|
||||||
&tx.ReceiverAddr,
|
&tx.ReceiverAddr,
|
||||||
|
@ -458,13 +514,14 @@ func (tx *RecvTx) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
// ReadFrom satisifies the io.WriterTo interface. A SendTx is read
|
// ReadFrom satisifies the io.WriterTo interface. A SendTx is read
|
||||||
// from r with the format:
|
// from r with the format:
|
||||||
//
|
//
|
||||||
// [TxHash (32 bytes), Fee (8 bytes), SenderAddr (20 bytes), len(ReceiverAddrs) (4 bytes), ReceiverAddrs[Addr (20 bytes), Amt (8 bytes)]...]
|
// [TxHash (32 bytes), Height (8 bytes), Fee (8 bytes), SenderAddr (20 bytes), len(ReceiverAddrs) (4 bytes), ReceiverAddrs[Addr (20 bytes), Amt (8 bytes)]...]
|
||||||
//
|
//
|
||||||
// Each field is read little endian.
|
// Each field is read little endian.
|
||||||
func (tx *SendTx) ReadFrom(r io.Reader) (n int64, err error) {
|
func (tx *SendTx) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
var nReceivers uint32
|
var nReceivers uint32
|
||||||
datas := []interface{}{
|
datas := []interface{}{
|
||||||
&tx.TxHash,
|
&tx.TxHash,
|
||||||
|
&tx.Height,
|
||||||
&tx.Fee,
|
&tx.Fee,
|
||||||
&tx.SenderAddr,
|
&tx.SenderAddr,
|
||||||
&nReceivers,
|
&nReceivers,
|
||||||
|
@ -503,19 +560,20 @@ func (tx *SendTx) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteTo satisifies the io.WriterTo interface. A RecvTx is written to
|
// WriteTo satisifies the io.WriterTo interface. A SendTx is written to
|
||||||
// w in the format:
|
// w in the format:
|
||||||
//
|
//
|
||||||
// [TxHash (32 bytes), Fee (8 bytes), SenderAddr (20 bytes), len(ReceiverAddrs) (4 bytes), ReceiverAddrs[Addr (20 bytes), Amt (8 bytes)]...]
|
// [TxHash (32 bytes), Height (8 bytes), Fee (8 bytes), SenderAddr (20 bytes), len(ReceiverAddrs) (4 bytes), ReceiverAddrs[Addr (20 bytes), Amt (8 bytes)]...]
|
||||||
//
|
//
|
||||||
// Each field is written little endian.
|
// Each field is written little endian.
|
||||||
func (tx *SendTx) WriteTo(w io.Writer) (n int64, err error) {
|
func (tx *SendTx) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
nReceivers := uint32(len(tx.ReceiverAddrs))
|
nReceivers := uint32(len(tx.ReceiverAddrs))
|
||||||
if int64(nReceivers) != int64(len(tx.ReceiverAddrs)) {
|
if int64(nReceivers) != int64(len(tx.ReceiverAddrs)) {
|
||||||
return n, fmt.Errorf("Too many receiving addresses.")
|
return n, errors.New("too many receiving addresses")
|
||||||
}
|
}
|
||||||
datas := []interface{}{
|
datas := []interface{}{
|
||||||
&tx.TxHash,
|
&tx.TxHash,
|
||||||
|
&tx.Height,
|
||||||
&tx.Fee,
|
&tx.Fee,
|
||||||
&tx.SenderAddr,
|
&tx.SenderAddr,
|
||||||
nReceivers,
|
nReceivers,
|
||||||
|
@ -529,7 +587,7 @@ func (tx *SendTx) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
n += written
|
n += written
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, _ := range tx.ReceiverAddrs {
|
for i := range tx.ReceiverAddrs {
|
||||||
datas := []interface{}{
|
datas := []interface{}{
|
||||||
&tx.ReceiverAddrs[i].Addr,
|
&tx.ReceiverAddrs[i].Addr,
|
||||||
&tx.ReceiverAddrs[i].Amt,
|
&tx.ReceiverAddrs[i].Amt,
|
||||||
|
|
|
@ -98,8 +98,8 @@ func TestUtxoWriteRead(t *testing.T) {
|
||||||
Index: 1,
|
Index: 1,
|
||||||
},
|
},
|
||||||
Subscript: []byte{},
|
Subscript: []byte{},
|
||||||
Amt: 69,
|
Amt: 69,
|
||||||
Height: 1337,
|
Height: 1337,
|
||||||
}
|
}
|
||||||
bufWriter := &bytes.Buffer{}
|
bufWriter := &bytes.Buffer{}
|
||||||
written, err := utxo1.WriteTo(bufWriter)
|
written, err := utxo1.WriteTo(bufWriter)
|
||||||
|
@ -136,27 +136,16 @@ func TestUtxoWriteRead(t *testing.T) {
|
||||||
|
|
||||||
func TestUtxoStoreWriteRead(t *testing.T) {
|
func TestUtxoStoreWriteRead(t *testing.T) {
|
||||||
store1 := new(UtxoStore)
|
store1 := new(UtxoStore)
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 20; i++ {
|
||||||
utxo := new(Utxo)
|
utxo := new(Utxo)
|
||||||
for j, _ := range utxo.Out.Hash[:] {
|
for j := range utxo.Out.Hash[:] {
|
||||||
utxo.Out.Hash[j] = byte(i)
|
utxo.Out.Hash[j] = byte(i + 1)
|
||||||
}
|
}
|
||||||
utxo.Out.Index = uint32(i + 1)
|
utxo.Out.Index = uint32(i + 2)
|
||||||
utxo.Subscript = []byte{}
|
utxo.Subscript = []byte{}
|
||||||
utxo.Amt = uint64(i + 2)
|
utxo.Amt = uint64(i + 3)
|
||||||
utxo.Height = int64(i + 3)
|
utxo.Height = int64(i + 4)
|
||||||
store1.Confirmed = append(store1.Confirmed, utxo)
|
*store1 = append(*store1, utxo)
|
||||||
}
|
|
||||||
for i := 10; i < 20; i++ {
|
|
||||||
utxo := new(Utxo)
|
|
||||||
for j, _ := range utxo.Out.Hash[:] {
|
|
||||||
utxo.Out.Hash[j] = byte(i)
|
|
||||||
}
|
|
||||||
utxo.Out.Index = uint32(i + 1)
|
|
||||||
utxo.Subscript = []byte{}
|
|
||||||
utxo.Amt = uint64(i + 2)
|
|
||||||
utxo.Height = int64(i + 3)
|
|
||||||
store1.Unconfirmed = append(store1.Unconfirmed, utxo)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bufWriter := &bytes.Buffer{}
|
bufWriter := &bytes.Buffer{}
|
||||||
|
@ -181,14 +170,14 @@ func TestUtxoStoreWriteRead(t *testing.T) {
|
||||||
t.Error("Stores do not match.")
|
t.Error("Stores do not match.")
|
||||||
}
|
}
|
||||||
|
|
||||||
truncatedReadBuf := bytes.NewBuffer(storeBytes)
|
truncatedLen := 100
|
||||||
truncatedReadBuf.Truncate(100)
|
truncatedReadBuf := bytes.NewBuffer(storeBytes[:truncatedLen])
|
||||||
store3 := new(UtxoStore)
|
store3 := new(UtxoStore)
|
||||||
n, err = store3.ReadFrom(truncatedReadBuf)
|
n, err = store3.ReadFrom(truncatedReadBuf)
|
||||||
if err != io.EOF {
|
if err != io.EOF {
|
||||||
t.Error("Expected err = io.EOF reading from truncated buffer.")
|
t.Errorf("Expected err = io.EOF reading from truncated buffer, got: %v", err)
|
||||||
}
|
}
|
||||||
if n != 100 {
|
if int(n) != truncatedLen {
|
||||||
t.Error("Incorrect number of bytes read from truncated buffer.")
|
t.Error("Incorrect number of bytes read from truncated buffer.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
292
wallet/wallet.go
292
wallet/wallet.go
|
@ -21,6 +21,7 @@ import (
|
||||||
"code.google.com/p/go.crypto/ripemd160"
|
"code.google.com/p/go.crypto/ripemd160"
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
|
"crypto/ecdsa"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
|
@ -57,9 +58,9 @@ const (
|
||||||
|
|
||||||
// Possible errors when dealing with wallets.
|
// Possible errors when dealing with wallets.
|
||||||
var (
|
var (
|
||||||
ChecksumErr = errors.New("Checksum mismatch")
|
ErrChecksumMismatch = errors.New("checksum mismatch")
|
||||||
MalformedEntryErr = errors.New("Malformed entry")
|
ErrMalformedEntry = errors.New("malformed entry")
|
||||||
WalletDoesNotExist = errors.New("Non-existant wallet")
|
ErrWalletDoesNotExist = errors.New("non-existant wallet")
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -90,6 +91,9 @@ func binaryRead(r io.Reader, order binary.ByteOrder, data interface{}) (n int64,
|
||||||
if read, err = r.Read(buf); err != nil {
|
if read, err = r.Read(buf); err != nil {
|
||||||
return int64(read), err
|
return int64(read), err
|
||||||
}
|
}
|
||||||
|
if read < binary.Size(data) {
|
||||||
|
return int64(read), io.EOF
|
||||||
|
}
|
||||||
return int64(read), binary.Read(bytes.NewBuffer(buf), order, data)
|
return int64(read), binary.Read(bytes.NewBuffer(buf), order, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,20 +124,26 @@ func calcHash256(buf []byte) []byte {
|
||||||
return calcHash(calcHash(buf, sha256.New()), sha256.New())
|
return calcHash(calcHash(buf, sha256.New()), sha256.New())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// calculate sha512(data)
|
||||||
|
func calcSha512(buf []byte) []byte {
|
||||||
|
return calcHash(buf, sha512.New())
|
||||||
|
}
|
||||||
|
|
||||||
// First byte in uncompressed pubKey field.
|
// First byte in uncompressed pubKey field.
|
||||||
const pubkeyUncompressed = 0x4
|
const pubkeyUncompressed = 0x4
|
||||||
|
|
||||||
// pubkeyFromPrivkey creates a 65-byte encoded pubkey based on a
|
// pubkeyFromPrivkey creates a 65-byte encoded pubkey based on a
|
||||||
// 32-byte privkey.
|
// 32-byte privkey.
|
||||||
|
//
|
||||||
|
// TODO(jrick): this must be changed to a compressed pubkey.
|
||||||
func pubkeyFromPrivkey(privkey []byte) (pubkey []byte) {
|
func pubkeyFromPrivkey(privkey []byte) (pubkey []byte) {
|
||||||
x, y := btcec.S256().ScalarBaseMult(privkey)
|
x, y := btcec.S256().ScalarBaseMult(privkey)
|
||||||
|
pub := (*btcec.PublicKey)(&ecdsa.PublicKey{
|
||||||
pubkey = make([]byte, 65)
|
Curve: btcec.S256(),
|
||||||
pubkey[0] = pubkeyUncompressed
|
X: x,
|
||||||
copy(pubkey[1:33], x.Bytes())
|
Y: y,
|
||||||
copy(pubkey[33:], y.Bytes())
|
})
|
||||||
|
return pub.SerializeUncompressed()
|
||||||
return pubkey
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func keyOneIter(passphrase, salt []byte, memReqts uint64) []byte {
|
func keyOneIter(passphrase, salt []byte, memReqts uint64) []byte {
|
||||||
|
@ -141,11 +151,11 @@ func keyOneIter(passphrase, salt []byte, memReqts uint64) []byte {
|
||||||
lutbl := make([]byte, memReqts)
|
lutbl := make([]byte, memReqts)
|
||||||
|
|
||||||
// Seed for lookup table
|
// Seed for lookup table
|
||||||
seed := sha512.Sum512(saltedpass)
|
seed := calcSha512(saltedpass)
|
||||||
copy(lutbl[:sha512.Size], seed[:])
|
copy(lutbl[:sha512.Size], seed)
|
||||||
|
|
||||||
for nByte := 0; nByte < (int(memReqts) - sha512.Size); nByte += sha512.Size {
|
for nByte := 0; nByte < (int(memReqts) - sha512.Size); nByte += sha512.Size {
|
||||||
hash := sha512.Sum512(lutbl[nByte : nByte+sha512.Size])
|
hash := calcSha512(lutbl[nByte : nByte+sha512.Size])
|
||||||
copy(lutbl[nByte+sha512.Size:nByte+2*sha512.Size], hash[:])
|
copy(lutbl[nByte+sha512.Size:nByte+2*sha512.Size], hash[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,7 +177,7 @@ func keyOneIter(passphrase, salt []byte, memReqts uint64) []byte {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save new hash to x
|
// Save new hash to x
|
||||||
hash := sha512.Sum512(x)
|
hash := calcSha512(x)
|
||||||
copy(x, hash[:])
|
copy(x, hash[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,23 +208,23 @@ func leftPad(input []byte, size int) (out []byte) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChainedPrivKey deterministically generates new private key using a
|
// ChainedPrivKey deterministically generates a new private key using a
|
||||||
// previous address and chaincode. privkey and chaincode must be 32
|
// previous address and chaincode. privkey and chaincode must be 32
|
||||||
// bytes long, and pubkey may either be 65 bytes or nil (in which case it
|
// bytes long, and pubkey may either be 65 bytes or nil (in which case it
|
||||||
// is generated by the privkey).
|
// is generated by the privkey).
|
||||||
func ChainedPrivKey(privkey, pubkey, chaincode []byte) ([]byte, error) {
|
func ChainedPrivKey(privkey, pubkey, chaincode []byte) ([]byte, error) {
|
||||||
if len(privkey) != 32 {
|
if len(privkey) != 32 {
|
||||||
return nil, fmt.Errorf("Invalid privkey length %d (must be 32)",
|
return nil, fmt.Errorf("invalid privkey length %d (must be 32)",
|
||||||
len(privkey))
|
len(privkey))
|
||||||
}
|
}
|
||||||
if len(chaincode) != 32 {
|
if len(chaincode) != 32 {
|
||||||
return nil, fmt.Errorf("Invalid chaincode length %d (must be 32)",
|
return nil, fmt.Errorf("invalid chaincode length %d (must be 32)",
|
||||||
len(chaincode))
|
len(chaincode))
|
||||||
}
|
}
|
||||||
if pubkey == nil {
|
if pubkey == nil {
|
||||||
pubkey = pubkeyFromPrivkey(privkey)
|
pubkey = pubkeyFromPrivkey(privkey)
|
||||||
} else if len(pubkey) != 65 {
|
} else if len(pubkey) != 65 {
|
||||||
return nil, fmt.Errorf("Invalid pubkey length %d.", len(pubkey))
|
return nil, fmt.Errorf("invalid pubkey length %d", len(pubkey))
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a perfect example of YOLO crypto. Armory claims this XORing
|
// This is a perfect example of YOLO crypto. Armory claims this XORing
|
||||||
|
@ -226,7 +236,7 @@ func ChainedPrivKey(privkey, pubkey, chaincode []byte) ([]byte, error) {
|
||||||
// Armory's chained address generation.
|
// Armory's chained address generation.
|
||||||
xorbytes := make([]byte, 32)
|
xorbytes := make([]byte, 32)
|
||||||
chainMod := calcHash256(pubkey)
|
chainMod := calcHash256(pubkey)
|
||||||
for i, _ := range xorbytes {
|
for i := range xorbytes {
|
||||||
xorbytes[i] = chainMod[i] ^ chaincode[i]
|
xorbytes[i] = chainMod[i] ^ chaincode[i]
|
||||||
}
|
}
|
||||||
chainXor := new(big.Int).SetBytes(xorbytes)
|
chainXor := new(big.Int).SetBytes(xorbytes)
|
||||||
|
@ -272,7 +282,7 @@ func (v *varEntries) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
}
|
}
|
||||||
n += read
|
n += read
|
||||||
|
|
||||||
var wt io.WriterTo = nil
|
var wt io.WriterTo
|
||||||
switch header {
|
switch header {
|
||||||
case addrHeader:
|
case addrHeader:
|
||||||
var entry addrEntry
|
var entry addrEntry
|
||||||
|
@ -302,15 +312,13 @@ func (v *varEntries) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
}
|
}
|
||||||
n += read
|
n += read
|
||||||
default:
|
default:
|
||||||
return n, fmt.Errorf("Unknown entry header: %d", uint8(header))
|
return n, fmt.Errorf("unknown entry header: %d", uint8(header))
|
||||||
}
|
}
|
||||||
if wt != nil {
|
if wt != nil {
|
||||||
wts = append(wts, wt)
|
wts = append(wts, wt)
|
||||||
*v = wts
|
*v = wts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return n, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wallet represents an btcd/Armory wallet in memory. It
|
// Wallet represents an btcd/Armory wallet in memory. It
|
||||||
|
@ -341,7 +349,7 @@ type Wallet struct {
|
||||||
lastChainIdx int64
|
lastChainIdx int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWallet() creates and initializes a new Wallet. name's and
|
// NewWallet creates and initializes a new Wallet. name's and
|
||||||
// desc's binary representation must not exceed 32 and 256 bytes,
|
// desc's binary representation must not exceed 32 and 256 bytes,
|
||||||
// respectively. All address private keys are encrypted with passphrase.
|
// respectively. All address private keys are encrypted with passphrase.
|
||||||
// The wallet is returned unlocked.
|
// The wallet is returned unlocked.
|
||||||
|
@ -379,8 +387,8 @@ func NewWallet(name, desc string, passphrase []byte) (*Wallet, error) {
|
||||||
useEncryption: true,
|
useEncryption: true,
|
||||||
watchingOnly: false,
|
watchingOnly: false,
|
||||||
},
|
},
|
||||||
createDate: time.Now().Unix(),
|
createDate: time.Now().Unix(),
|
||||||
highestUsed: -1,
|
highestUsed: -1,
|
||||||
kdfParams: *kdfp,
|
kdfParams: *kdfp,
|
||||||
keyGenerator: *root,
|
keyGenerator: *root,
|
||||||
addrMap: make(map[[ripemd160.Size]byte]*btcAddress),
|
addrMap: make(map[[ripemd160.Size]byte]*btcAddress),
|
||||||
|
@ -421,6 +429,8 @@ func NewWallet(name, desc string, passphrase []byte) (*Wallet, error) {
|
||||||
return w, nil
|
return w, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Name returns the name of a wallet. This name is used as the
|
||||||
|
// account name for btcwallet JSON methods.
|
||||||
func (w *Wallet) Name() string {
|
func (w *Wallet) Name() string {
|
||||||
return string(w.name[:])
|
return string(w.name[:])
|
||||||
}
|
}
|
||||||
|
@ -471,7 +481,7 @@ func (w *Wallet) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if id != fileID {
|
if id != fileID {
|
||||||
return n, errors.New("Unknown File ID.")
|
return n, errors.New("unknown file ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add root address to address map
|
// Add root address to address map
|
||||||
|
@ -496,7 +506,7 @@ func (w *Wallet) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
e := wt.(*txCommentEntry)
|
e := wt.(*txCommentEntry)
|
||||||
w.txCommentMap[e.txHash] = &e.comment
|
w.txCommentMap[e.txHash] = &e.comment
|
||||||
default:
|
default:
|
||||||
return n, errors.New("Unknown appended entry")
|
return n, errors.New("unknown appended entry")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -575,12 +585,11 @@ func (w *Wallet) Unlock(passphrase []byte) error {
|
||||||
// Attempt unlocking root address
|
// Attempt unlocking root address
|
||||||
if err := w.keyGenerator.unlock(key); err != nil {
|
if err := w.keyGenerator.unlock(key); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
|
||||||
w.key.Lock()
|
|
||||||
w.key.secret = key
|
|
||||||
w.key.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
w.key.Lock()
|
||||||
|
w.key.secret = key
|
||||||
|
w.key.Unlock()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lock does a best effort to zero the keys.
|
// Lock does a best effort to zero the keys.
|
||||||
|
@ -594,12 +603,12 @@ func (w *Wallet) Lock() (err error) {
|
||||||
|
|
||||||
w.key.Lock()
|
w.key.Lock()
|
||||||
if w.key.secret != nil {
|
if w.key.secret != nil {
|
||||||
for i, _ := range w.key.secret {
|
for i := range w.key.secret {
|
||||||
w.key.secret[i] = 0
|
w.key.secret[i] = 0
|
||||||
}
|
}
|
||||||
w.key.secret = nil
|
w.key.secret = nil
|
||||||
} else {
|
} else {
|
||||||
err = fmt.Errorf("Wallet already locked")
|
err = fmt.Errorf("wallet already locked")
|
||||||
}
|
}
|
||||||
w.key.Unlock()
|
w.key.Unlock()
|
||||||
|
|
||||||
|
@ -615,7 +624,7 @@ func (w *Wallet) IsLocked() (locked bool) {
|
||||||
return locked
|
return locked
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns wallet version as string and int.
|
// Version returns a wallet's version as a string and int.
|
||||||
// TODO(jrick)
|
// TODO(jrick)
|
||||||
func (w *Wallet) Version() (string, int) {
|
func (w *Wallet) Version() (string, int) {
|
||||||
return "", 0
|
return "", 0
|
||||||
|
@ -624,24 +633,77 @@ func (w *Wallet) Version() (string, int) {
|
||||||
// NextUnusedAddress attempts to get the next chained address. It
|
// NextUnusedAddress attempts to get the next chained address. It
|
||||||
// currently relies on pre-generated addresses and will return an empty
|
// currently relies on pre-generated addresses and will return an empty
|
||||||
// string if the address pool has run out. TODO(jrick)
|
// string if the address pool has run out. TODO(jrick)
|
||||||
func (w *Wallet) NextUnusedAddress() string {
|
func (w *Wallet) NextUnusedAddress() (string, error) {
|
||||||
_ = w.lastChainIdx
|
_ = w.lastChainIdx
|
||||||
w.highestUsed++
|
w.highestUsed++
|
||||||
new160, err := w.addr160ForIdx(w.highestUsed)
|
new160, err := w.addr160ForIdx(w.highestUsed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return "", errors.New("cannot find generated address")
|
||||||
}
|
}
|
||||||
addr := w.addrMap[*new160]
|
addr := w.addrMap[*new160]
|
||||||
if addr != nil {
|
if addr == nil {
|
||||||
return btcutil.Base58Encode(addr.pubKeyHash[:])
|
return "", errors.New("cannot find generated address")
|
||||||
} else {
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
return addr.paymentAddress(w.net)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAddressKey returns the private key for a payment address stored
|
||||||
|
// in a wallet. This can fail if the payment address for a different
|
||||||
|
// Bitcoin network than what this wallet uses, the address is not
|
||||||
|
// contained in the wallet, the address does not include a public and
|
||||||
|
// private key, or if the wallet is locked.
|
||||||
|
func (w *Wallet) GetAddressKey(addr string) (key *ecdsa.PrivateKey, err error) {
|
||||||
|
addr160, net, err := btcutil.DecodeAddress(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case net == btcutil.MainNetAddr && w.net != btcwire.MainNet:
|
||||||
|
fallthrough
|
||||||
|
case net == btcutil.TestNetAddr && w.net != btcwire.TestNet:
|
||||||
|
return nil, errors.New("wallet and address networks mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
addrHash := new([ripemd160.Size]byte)
|
||||||
|
copy(addrHash[:], addr160)
|
||||||
|
|
||||||
|
btcaddr, ok := w.addrMap[*addrHash]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("address not in wallet")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !btcaddr.flags.hasPubKey {
|
||||||
|
return nil, errors.New("no public key for address")
|
||||||
|
}
|
||||||
|
if !btcaddr.flags.hasPrivKey {
|
||||||
|
return nil, errors.New("no private key for address")
|
||||||
|
}
|
||||||
|
|
||||||
|
pubkey, err := btcec.ParsePubKey(btcaddr.pubKey[:], btcec.S256())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = btcaddr.unlock(w.key.secret); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
d := new(big.Int).SetBytes(btcaddr.privKeyCT)
|
||||||
|
key = &ecdsa.PrivateKey{
|
||||||
|
PublicKey: *pubkey,
|
||||||
|
D: d,
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Net returns the bitcoin network identifier for this wallet.
|
||||||
|
func (w *Wallet) Net() btcwire.BitcoinNet {
|
||||||
|
return w.net
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Wallet) addr160ForIdx(idx int64) (*[ripemd160.Size]byte, error) {
|
func (w *Wallet) addr160ForIdx(idx int64) (*[ripemd160.Size]byte, error) {
|
||||||
if idx > w.lastChainIdx {
|
if idx > w.lastChainIdx {
|
||||||
return nil, errors.New("Chain index out of range")
|
return nil, errors.New("chain index out of range")
|
||||||
}
|
}
|
||||||
return w.chainIdxMap[idx], nil
|
return w.chainIdxMap[idx], nil
|
||||||
}
|
}
|
||||||
|
@ -657,7 +719,11 @@ func (w *Wallet) GetActiveAddresses() []string {
|
||||||
return addrs
|
return addrs
|
||||||
}
|
}
|
||||||
addr := w.addrMap[*addr160]
|
addr := w.addrMap[*addr160]
|
||||||
addrs = append(addrs, btcutil.Base58Encode(addr.pubKeyHash[:]))
|
addrstr, err := addr.paymentAddress(w.net)
|
||||||
|
// TODO(jrick): propigate error
|
||||||
|
if err != nil {
|
||||||
|
addrs = append(addrs, addrstr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return addrs
|
return addrs
|
||||||
}
|
}
|
||||||
|
@ -708,7 +774,7 @@ func (af *addrFlags) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
af.hasPubKey = true
|
af.hasPubKey = true
|
||||||
}
|
}
|
||||||
if b[0]&(1<<2) == 0 {
|
if b[0]&(1<<2) == 0 {
|
||||||
return n, errors.New("Address flag specifies unencrypted address.")
|
return n, errors.New("address flag specifies unencrypted address")
|
||||||
}
|
}
|
||||||
af.encrypted = true
|
af.encrypted = true
|
||||||
|
|
||||||
|
@ -725,7 +791,7 @@ func (af *addrFlags) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
}
|
}
|
||||||
if !af.encrypted {
|
if !af.encrypted {
|
||||||
// We only support encrypted privkeys.
|
// We only support encrypted privkeys.
|
||||||
return n, errors.New("Address must be encrypted.")
|
return n, errors.New("address must be encrypted")
|
||||||
}
|
}
|
||||||
b[0] |= 1 << 2
|
b[0] |= 1 << 2
|
||||||
|
|
||||||
|
@ -753,13 +819,13 @@ type btcAddress struct {
|
||||||
// randomly generated).
|
// randomly generated).
|
||||||
func newBtcAddress(privkey, iv []byte) (addr *btcAddress, err error) {
|
func newBtcAddress(privkey, iv []byte) (addr *btcAddress, err error) {
|
||||||
if len(privkey) != 32 {
|
if len(privkey) != 32 {
|
||||||
return nil, errors.New("Private key is not 32 bytes.")
|
return nil, errors.New("private key is not 32 bytes")
|
||||||
}
|
}
|
||||||
if iv == nil {
|
if iv == nil {
|
||||||
iv = make([]byte, 16)
|
iv = make([]byte, 16)
|
||||||
rand.Read(iv)
|
rand.Read(iv)
|
||||||
} else if len(iv) != 16 {
|
} else if len(iv) != 16 {
|
||||||
return nil, errors.New("Init vector must be nil or 16 bytes large.")
|
return nil, errors.New("init vector must be nil or 16 bytes large")
|
||||||
}
|
}
|
||||||
|
|
||||||
addr = &btcAddress{
|
addr = &btcAddress{
|
||||||
|
@ -784,7 +850,7 @@ func newBtcAddress(privkey, iv []byte) (addr *btcAddress, err error) {
|
||||||
// address.
|
// address.
|
||||||
func newRootBtcAddress(privKey, iv, chaincode []byte) (addr *btcAddress, err error) {
|
func newRootBtcAddress(privKey, iv, chaincode []byte) (addr *btcAddress, err error) {
|
||||||
if len(chaincode) != 32 {
|
if len(chaincode) != 32 {
|
||||||
return nil, errors.New("Chaincode is not 32 bytes.")
|
return nil, errors.New("chaincode is not 32 bytes")
|
||||||
}
|
}
|
||||||
|
|
||||||
addr, err = newBtcAddress(privKey, iv)
|
addr, err = newBtcAddress(privKey, iv)
|
||||||
|
@ -799,7 +865,7 @@ func newRootBtcAddress(privKey, iv, chaincode []byte) (addr *btcAddress, err err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadFrom reads an encrypted address from an io.Reader.
|
// ReadFrom reads an encrypted address from an io.Reader.
|
||||||
func (addr *btcAddress) ReadFrom(r io.Reader) (n int64, err error) {
|
func (a *btcAddress) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
var read int64
|
var read int64
|
||||||
|
|
||||||
// Checksums
|
// Checksums
|
||||||
|
@ -811,24 +877,24 @@ func (addr *btcAddress) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
|
|
||||||
// Read serialized wallet into addr fields and checksums.
|
// Read serialized wallet into addr fields and checksums.
|
||||||
datas := []interface{}{
|
datas := []interface{}{
|
||||||
&addr.pubKeyHash,
|
&a.pubKeyHash,
|
||||||
&chkPubKeyHash,
|
&chkPubKeyHash,
|
||||||
make([]byte, 4), // version
|
make([]byte, 4), // version
|
||||||
&addr.flags,
|
&a.flags,
|
||||||
&addr.chaincode,
|
&a.chaincode,
|
||||||
&chkChaincode,
|
&chkChaincode,
|
||||||
&addr.chainIndex,
|
&a.chainIndex,
|
||||||
&addr.chainDepth,
|
&a.chainDepth,
|
||||||
&addr.initVector,
|
&a.initVector,
|
||||||
&chkInitVector,
|
&chkInitVector,
|
||||||
&addr.privKey,
|
&a.privKey,
|
||||||
&chkPrivKey,
|
&chkPrivKey,
|
||||||
&addr.pubKey,
|
&a.pubKey,
|
||||||
&chkPubKey,
|
&chkPubKey,
|
||||||
&addr.firstSeen,
|
&a.firstSeen,
|
||||||
&addr.lastSeen,
|
&a.lastSeen,
|
||||||
&addr.firstBlock,
|
&a.firstBlock,
|
||||||
&addr.lastBlock,
|
&a.lastBlock,
|
||||||
}
|
}
|
||||||
for _, data := range datas {
|
for _, data := range datas {
|
||||||
if rf, ok := data.(io.ReaderFrom); ok {
|
if rf, ok := data.(io.ReaderFrom); ok {
|
||||||
|
@ -847,13 +913,13 @@ func (addr *btcAddress) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
data []byte
|
data []byte
|
||||||
chk uint32
|
chk uint32
|
||||||
}{
|
}{
|
||||||
{addr.pubKeyHash[:], chkPubKeyHash},
|
{a.pubKeyHash[:], chkPubKeyHash},
|
||||||
{addr.chaincode[:], chkChaincode},
|
{a.chaincode[:], chkChaincode},
|
||||||
{addr.initVector[:], chkInitVector},
|
{a.initVector[:], chkInitVector},
|
||||||
{addr.privKey[:], chkPrivKey},
|
{a.privKey[:], chkPrivKey},
|
||||||
{addr.pubKey[:], chkPubKey},
|
{a.pubKey[:], chkPubKey},
|
||||||
}
|
}
|
||||||
for i, _ := range checks {
|
for i := range checks {
|
||||||
if err = verifyAndFix(checks[i].data, checks[i].chk); err != nil {
|
if err = verifyAndFix(checks[i].data, checks[i].chk); err != nil {
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
@ -862,28 +928,28 @@ func (addr *btcAddress) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (addr *btcAddress) WriteTo(w io.Writer) (n int64, err error) {
|
func (a *btcAddress) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
var written int64
|
var written int64
|
||||||
|
|
||||||
datas := []interface{}{
|
datas := []interface{}{
|
||||||
&addr.pubKeyHash,
|
&a.pubKeyHash,
|
||||||
walletHash(addr.pubKeyHash[:]),
|
walletHash(a.pubKeyHash[:]),
|
||||||
make([]byte, 4), //version
|
make([]byte, 4), //version
|
||||||
&addr.flags,
|
&a.flags,
|
||||||
&addr.chaincode,
|
&a.chaincode,
|
||||||
walletHash(addr.chaincode[:]),
|
walletHash(a.chaincode[:]),
|
||||||
&addr.chainIndex,
|
&a.chainIndex,
|
||||||
&addr.chainDepth,
|
&a.chainDepth,
|
||||||
&addr.initVector,
|
&a.initVector,
|
||||||
walletHash(addr.initVector[:]),
|
walletHash(a.initVector[:]),
|
||||||
&addr.privKey,
|
&a.privKey,
|
||||||
walletHash(addr.privKey[:]),
|
walletHash(a.privKey[:]),
|
||||||
&addr.pubKey,
|
&a.pubKey,
|
||||||
walletHash(addr.pubKey[:]),
|
walletHash(a.pubKey[:]),
|
||||||
&addr.firstSeen,
|
&a.firstSeen,
|
||||||
&addr.lastSeen,
|
&a.lastSeen,
|
||||||
&addr.firstBlock,
|
&a.firstBlock,
|
||||||
&addr.lastBlock,
|
&a.lastBlock,
|
||||||
}
|
}
|
||||||
for _, data := range datas {
|
for _, data := range datas {
|
||||||
if wt, ok := data.(io.WriterTo); ok {
|
if wt, ok := data.(io.WriterTo); ok {
|
||||||
|
@ -904,10 +970,10 @@ func (addr *btcAddress) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
// not 32 bytes. If successful, the encryption flag is set.
|
// not 32 bytes. If successful, the encryption flag is set.
|
||||||
func (a *btcAddress) encrypt(key []byte) error {
|
func (a *btcAddress) encrypt(key []byte) error {
|
||||||
if a.flags.encrypted {
|
if a.flags.encrypted {
|
||||||
return errors.New("Address already encrypted.")
|
return errors.New("address already encrypted")
|
||||||
}
|
}
|
||||||
if len(a.privKeyCT) != 32 {
|
if len(a.privKeyCT) != 32 {
|
||||||
return errors.New("Invalid clear text private key.")
|
return errors.New("invalid clear text private key")
|
||||||
}
|
}
|
||||||
|
|
||||||
aesBlockEncrypter, err := aes.NewCipher(key)
|
aesBlockEncrypter, err := aes.NewCipher(key)
|
||||||
|
@ -926,7 +992,7 @@ func (a *btcAddress) encrypt(key []byte) error {
|
||||||
// private key. This function fails if the address is not encrypted.
|
// private key. This function fails if the address is not encrypted.
|
||||||
func (a *btcAddress) lock() error {
|
func (a *btcAddress) lock() error {
|
||||||
if !a.flags.encrypted {
|
if !a.flags.encrypted {
|
||||||
return errors.New("Unable to lock unencrypted address.")
|
return errors.New("unable to lock unencrypted address")
|
||||||
}
|
}
|
||||||
|
|
||||||
a.privKeyCT = nil
|
a.privKeyCT = nil
|
||||||
|
@ -938,7 +1004,7 @@ func (a *btcAddress) lock() error {
|
||||||
// incorrect.
|
// incorrect.
|
||||||
func (a *btcAddress) unlock(key []byte) error {
|
func (a *btcAddress) unlock(key []byte) error {
|
||||||
if !a.flags.encrypted {
|
if !a.flags.encrypted {
|
||||||
return errors.New("Unable to unlock unencrypted address.")
|
return errors.New("unable to unlock unencrypted address")
|
||||||
}
|
}
|
||||||
|
|
||||||
aesBlockDecrypter, err := aes.NewCipher(key)
|
aesBlockDecrypter, err := aes.NewCipher(key)
|
||||||
|
@ -951,11 +1017,11 @@ func (a *btcAddress) unlock(key []byte) error {
|
||||||
|
|
||||||
pubKey, err := btcec.ParsePubKey(a.pubKey[:], btcec.S256())
|
pubKey, err := btcec.ParsePubKey(a.pubKey[:], btcec.S256())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ParsePubKey faild:", err)
|
return fmt.Errorf("cannot parse pubkey: %s", err)
|
||||||
}
|
}
|
||||||
x, y := btcec.S256().ScalarBaseMult(ct)
|
x, y := btcec.S256().ScalarBaseMult(ct)
|
||||||
if x.Cmp(pubKey.X) != 0 || y.Cmp(pubKey.Y) != 0 {
|
if x.Cmp(pubKey.X) != 0 || y.Cmp(pubKey.Y) != 0 {
|
||||||
return errors.New("Decryption failed.")
|
return errors.New("decryption failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
a.privKeyCT = ct
|
a.privKeyCT = ct
|
||||||
|
@ -963,8 +1029,25 @@ func (a *btcAddress) unlock(key []byte) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(jrick)
|
// TODO(jrick)
|
||||||
func (addr *btcAddress) changeEncryptionKey(oldkey, newkey []byte) error {
|
func (a *btcAddress) changeEncryptionKey(oldkey, newkey []byte) error {
|
||||||
return nil
|
return errors.New("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// paymentAddress returns a human readable payment address string for
|
||||||
|
// an address.
|
||||||
|
func (a *btcAddress) paymentAddress(net btcwire.BitcoinNet) (string, error) {
|
||||||
|
var netID byte
|
||||||
|
switch net {
|
||||||
|
case btcwire.MainNet:
|
||||||
|
netID = btcutil.MainNetAddr
|
||||||
|
case btcwire.TestNet:
|
||||||
|
fallthrough
|
||||||
|
case btcwire.TestNet3:
|
||||||
|
netID = btcutil.TestNetAddr
|
||||||
|
default: // wrong!
|
||||||
|
return "", errors.New("unknown bitcoin network")
|
||||||
|
}
|
||||||
|
return btcutil.EncodeAddress(a.pubKeyHash[:], netID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func walletHash(b []byte) uint32 {
|
func walletHash(b []byte) uint32 {
|
||||||
|
@ -975,7 +1058,7 @@ func walletHash(b []byte) uint32 {
|
||||||
// TODO(jrick) add error correction.
|
// TODO(jrick) add error correction.
|
||||||
func verifyAndFix(b []byte, chk uint32) error {
|
func verifyAndFix(b []byte, chk uint32) error {
|
||||||
if walletHash(b) != chk {
|
if walletHash(b) != chk {
|
||||||
return ChecksumErr
|
return ErrChecksumMismatch
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1138,7 +1221,7 @@ func (e *addrCommentEntry) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
|
|
||||||
// Comments shall not overflow their entry.
|
// Comments shall not overflow their entry.
|
||||||
if len(e.comment) > maxCommentLen {
|
if len(e.comment) > maxCommentLen {
|
||||||
return n, MalformedEntryErr
|
return n, ErrMalformedEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write header
|
// Write header
|
||||||
|
@ -1193,7 +1276,7 @@ func (e *txCommentEntry) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
|
|
||||||
// Comments shall not overflow their entry.
|
// Comments shall not overflow their entry.
|
||||||
if len(e.comment) > maxCommentLen {
|
if len(e.comment) > maxCommentLen {
|
||||||
return n, MalformedEntryErr
|
return n, ErrMalformedEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write header
|
// Write header
|
||||||
|
@ -1244,18 +1327,9 @@ func (e *deletedEntry) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
n += read
|
n += read
|
||||||
|
|
||||||
unused := make([]byte, ulen)
|
unused := make([]byte, ulen)
|
||||||
if nRead, err := r.Read(unused); err == io.EOF {
|
nRead, err := r.Read(unused)
|
||||||
|
if err == io.EOF {
|
||||||
return n + int64(nRead), nil
|
return n + int64(nRead), nil
|
||||||
} else {
|
|
||||||
return n + int64(nRead), err
|
|
||||||
}
|
}
|
||||||
}
|
return n + int64(nRead), err
|
||||||
|
|
||||||
type UTXOStore struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
type utxo struct {
|
|
||||||
pubKeyHash [ripemd160.Size]byte
|
|
||||||
*btcwire.TxOut
|
|
||||||
block int64
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue