diff --git a/.gitignore b/.gitignore index a5c7e6bb66..07000a4fe4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ btcwallet vendor .idea +*.exe \ No newline at end of file diff --git a/README.md b/README.md index 8a68448b29..3a56663cbe 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +Check use case for transferring a transaction as a whole +[Transaction transfer use case](/wallet/txtransfer/README-SampleUsage.md) + btcwallet ========= diff --git a/internal/rpchelp/methods.go b/internal/rpchelp/methods.go index cdc4fad4a8..b49cdbab75 100644 --- a/internal/rpchelp/methods.go +++ b/internal/rpchelp/methods.go @@ -52,6 +52,7 @@ var Methods = []struct { {"sendfrom", returnsString}, {"sendmany", returnsString}, {"sendtoaddress", returnsString}, + {"transfertransaction", returnsString}, {"settxfee", returnsBool}, {"signmessage", returnsString}, {"signrawtransaction", []interface{}{(*btcjson.SignRawTransactionResult)(nil)}}, diff --git a/rpc/legacyrpc/methods.go b/rpc/legacyrpc/methods.go index 9251a91670..f14d945b3e 100644 --- a/rpc/legacyrpc/methods.go +++ b/rpc/legacyrpc/methods.go @@ -101,6 +101,8 @@ var rpcHandlers = map[string]struct { "lockunspent": {handler: lockUnspent}, "sendfrom": {handlerWithChain: sendFrom}, "sendmany": {handler: sendMany}, + "sendPostDatedTx": {handler: sendPostDatedTx}, + "transfertransaction": {handler: transferTransaction}, "sendtoaddress": {handler: sendToAddress}, "settxfee": {handler: setTxFee}, "signmessage": {handler: signMessage}, @@ -1492,6 +1494,63 @@ func sendMany(icmd interface{}, w *wallet.Wallet) (interface{}, error) { return sendPairs(w, pairs, account, minConf, txrules.DefaultRelayFeePerKb) } +// TODO : write summary +func sendPostDatedTx(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.SendPostDatedTxCmd) + + redeemTxHash, _ := w.SendPostDated(cmd.Address, cmd.Amount, cmd.LockTime, waddrmgr.DefaultAccountNum) //txHash + // TODO : Check for error + + txHashStr := redeemTxHash.String() + log.Infof("Successfully transferred transaction %v", txHashStr) + return txHashStr, nil +} + +// TODO : write summary +func transferTransaction(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.TransferTransactionCmd) + + address := cmd.Address + txHash, err := chainhash.NewHashFromStr(cmd.TxId) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCDecodeHexString, + Message: "Transaction hash string decode failed: " + err.Error(), + } + } + + return transferToAddress(w, address, *txHash, + waddrmgr.DefaultAccountNum, 1, txrules.DefaultRelayFeePerKb) +} + +func transferToAddress(w *wallet.Wallet, addrStr string, txHash chainhash.Hash, + account uint32, minconf int32, feeSatPerKb btcutil.Amount) (string, error) { + + redeemTxHash, err := w.TransferTx(addrStr, txHash, account, minconf, feeSatPerKb) //txHash + if err != nil { + // TODO : check for tx is not found error + // TODO : check for tx is not mature error + // TODO : check for fee not enough + + if waddrmgr.IsError(err, waddrmgr.ErrLocked) { + return "", &ErrWalletUnlockNeeded + } + switch err.(type) { + case btcjson.RPCError: + return "", err + } + + return "", &btcjson.RPCError{ + Code: btcjson.ErrRPCInternal.Code, + Message: err.Error(), + } + } + + txHashStr := redeemTxHash.String() + log.Infof("Successfully transferred transaction %v", txHashStr) + return txHashStr, nil +} + // sendToAddress handles a sendtoaddress RPC request by creating 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 diff --git a/rpc/legacyrpc/rpcserverhelp.go b/rpc/legacyrpc/rpcserverhelp.go index f78fb6731d..aaaa304ba2 100644 --- a/rpc/legacyrpc/rpcserverhelp.go +++ b/rpc/legacyrpc/rpcserverhelp.go @@ -32,6 +32,7 @@ func helpDescsEnUS() map[string]string { "lockunspent": "lockunspent unlock [{\"txid\":\"value\",\"vout\":n},...]\n\nLocks or unlocks an unspent output.\nLocked outputs are not chosen for transaction inputs of authored transactions and are not included in 'listunspent' results.\nLocked outputs are volatile and are not saved across wallet restarts.\nIf unlock is true and no transaction outputs are specified, all locked outputs are marked unlocked.\n\nArguments:\n1. unlock (boolean, required) True to unlock outputs, false to lock\n2. transactions (array of object, required) Transaction outputs to lock or unlock\n[{\n \"txid\": \"value\", (string) The transaction hash of the referenced output\n \"vout\": n, (numeric) The output index of the referenced output\n},...]\n\nResult:\ntrue|false (boolean) The boolean 'true'\n", "sendfrom": "sendfrom \"fromaccount\" \"toaddress\" amount (minconf=1 \"comment\" \"commentto\")\n\nDEPRECATED -- Authors, signs, and sends a transaction that outputs some amount to a payment address.\nA change output is automatically included to send extra output value back to the original account.\n\nArguments:\n1. fromaccount (string, required) Account to pick unspent outputs from\n2. toaddress (string, required) Address to pay\n3. amount (numeric, required) Amount to send to the payment address valued in bitcoin\n4. minconf (numeric, optional, default=1) Minimum number of block confirmations required before a transaction output is eligible to be spent\n5. comment (string, optional) Unused\n6. commentto (string, optional) Unused\n\nResult:\n\"value\" (string) The transaction hash of the sent transaction\n", "sendmany": "sendmany \"fromaccount\" {\"address\":amount,...} (minconf=1 \"comment\")\n\nAuthors, signs, and sends a transaction that outputs to many payment addresses.\nA change output is automatically included to send extra output value back to the original account.\n\nArguments:\n1. fromaccount (string, required) DEPRECATED -- Account to pick unspent outputs from\n2. amounts (object, required) Pairs of payment addresses and the output amount to pay each\n{\n \"Address to pay\": Amount to send to the payment address valued in bitcoin, (object) JSON object using payment addresses as keys and output amounts valued in bitcoin to send to each address\n ...\n}\n3. minconf (numeric, optional, default=1) Minimum number of block confirmations required before a transaction output is eligible to be spent\n4. comment (string, optional) Unused\n\nResult:\n\"value\" (string) The transaction hash of the sent transaction\n", + "transfertransaction": "transfertransaction", "sendtoaddress": "sendtoaddress \"address\" amount (\"comment\" \"commentto\")\n\nAuthors, signs, and sends a transaction that outputs some amount to a payment address.\nUnlike sendfrom, outputs are always chosen from the default account.\nA change output is automatically included to send extra output value back to the original account.\n\nArguments:\n1. address (string, required) Address to pay\n2. amount (numeric, required) Amount to send to the payment address valued in bitcoin\n3. comment (string, optional) Unused\n4. commentto (string, optional) Unused\n\nResult:\n\"value\" (string) The transaction hash of the sent transaction\n", "settxfee": "settxfee amount\n\nModify the increment used each time more fee is required for an authored transaction.\n\nArguments:\n1. amount (numeric, required) The new fee increment valued in bitcoin\n\nResult:\ntrue|false (boolean) The boolean 'true'\n", "signmessage": "signmessage \"address\" \"message\"\n\nSigns a message using the private key of a payment address.\n\nArguments:\n1. address (string, required) Payment address of private key used to sign the message with\n2. message (string, required) Message to sign\n\nResult:\n\"value\" (string) The signed message encoded as a base64 string\n", diff --git a/wallet/coincasetx.go b/wallet/coincasetx.go new file mode 100644 index 0000000000..0283d4d577 --- /dev/null +++ b/wallet/coincasetx.go @@ -0,0 +1,66 @@ +package wallet + +import ( + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" +) + +const ( + coincaseTxFlags = "/POSTDATED/" + + // TODO : Need to be in wire package + PostDatedTxVersion = 2 +) + +func createCoincaseScript() ([]byte, error) { + return txscript.NewScriptBuilder().AddData([]byte(coincaseTxFlags)).Script() +} + +// Reference : btcsuite\btcd\mining\mining.go:253 +func newCoincaseTransaction(pkScript []byte, amount int64, lockTime uint32) ( + *btcutil.Tx, error) { + var err error + + postDatedScript, err := createCoincaseScript() + if err != nil { + return nil, err + } + + tx := wire.NewMsgTx(PostDatedTxVersion) + + tx.AddTxIn(&wire.TxIn{ + // Coincase transactions have no inputs, so previous outpoint is + // zero hash and max index. + PreviousOutPoint: *wire.NewOutPoint(&chainhash.Hash{}, + wire.MaxPrevOutIndex), + SignatureScript: postDatedScript, + Sequence: wire.MaxTxInSequenceNum, + }) + tx.AddTxOut(&wire.TxOut{ + Value: amount, + PkScript: pkScript, + }) + + tx.LockTime = lockTime + + // Reference : btcsuite\btcd\mining\mining.go:805 + // TODO : Check segwit related codes + + return btcutil.NewTx(tx), nil +} + +func (w *Wallet) createCoincase( + coincaseAddr btcutil.Address, + amount int64, lockTime uint32) (coincaseTx *btcutil.Tx, err error) { + + // Create coincase + pkScript, err := txscript.PayToAddrScript(coincaseAddr) + if err != nil { + return + } + + coincaseTx, err = newCoincaseTransaction(pkScript, amount, lockTime) + return +} diff --git a/wallet/postdated.go b/wallet/postdated.go new file mode 100644 index 0000000000..bcf333d031 --- /dev/null +++ b/wallet/postdated.go @@ -0,0 +1,153 @@ +// TODO : License related notes + +package wallet + +import ( + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wallet/txauthor" + "github.com/btcsuite/btcwallet/wallet/txrules" + "github.com/btcsuite/btcwallet/walletdb" +) + +// wallet/wallet.go +func (w *Wallet) SendPostDated(addrStr string, amount int64, lockTime uint32, + account uint32) (*chainhash.Hash, error) { + createdTx, err := w.createSimplePostDatedTx(addrStr, amount, lockTime, account) + if err != nil { + return nil, err + } + + return w.publishTransaction(createdTx.Tx) +} + +// wallet/wallet.go +type ( + createPostDatedTxRequest struct { + account uint32 + address string + amount int64 + lockTime uint32 + feeSatPerKB btcutil.Amount + resp chan createPostDatedTxResponse + } + createPostDatedTxResponse struct { + tx *txauthor.AuthoredTx + err error + } +) + +// wallet/wallet.go +func (w *Wallet) createSimplePostDatedTx(address string, amount int64, lockTime uint32, + account uint32) (*txauthor.AuthoredTx, error) { + req := createPostDatedTxRequest{ + account: account, + address: address, + lockTime: lockTime, + amount: amount, + resp: make(chan createPostDatedTxResponse), + } + + // TODO : use channels instead of direct call + //w.createPostDatedTxRequests <- req + //resp := <-req.resp + resp := w.createPostDatedTx(req) + return resp.tx, resp.err +} + +// TODO : move to wallet/wallet.go +func (w *Wallet) postDatedTxCreator() { + quit := w.quitChan() +out: + for { + select { + case txr := <-w.createPostDatedTxRequests: + heldUnlock, err := w.holdUnlock() + if err != nil { + txr.resp <- createPostDatedTxResponse{nil, err} + continue + } + res := w.createPostDatedTx(txr) + heldUnlock.release() + txr.resp <- res + case <-quit: + break out + } + } + w.wg.Done() +} + +func NewUnsignedTransactionFromCoincase(coincaseTx *btcutil.Tx, output *wire.TxOut) (*txauthor.AuthoredTx, error) { + // Create unsigned tx + unsignedTransaction := &wire.MsgTx{ + Version: PostDatedTxVersion, + } + + outpoint := wire.NewOutPoint(coincaseTx.Hash(), 0) + txIn := wire.NewTxIn(outpoint, nil, nil) + + unsignedTransaction.AddTxIn(txIn) + unsignedTransaction.AddTxOut(output) + unsignedTransaction.LockTime = coincaseTx.MsgTx().LockTime + + // Get amount from coincase + amount := btcutil.Amount(coincaseTx.MsgTx().TxOut[0].Value) + currentInputValues := []btcutil.Amount{amount} + + // Get pkScript from coincase + currentScripts := [][]byte{coincaseTx.MsgTx().TxOut[0].PkScript} + + return &txauthor.AuthoredTx{ + Tx: unsignedTransaction, + PrevScripts: currentScripts, + PrevInputValues: currentInputValues, + TotalInput: 1, + ChangeIndex: -1, + }, nil +} + +// +func (w *Wallet) createPostDatedTx(req createPostDatedTxRequest) createPostDatedTxResponse { + amount := btcutil.Amount(req.amount) + redeemOutput, err := makeOutput(req.address, amount, w.ChainParams()) + if err != nil { + return createPostDatedTxResponse{nil, err} + } + + if err := txrules.CheckOutput(redeemOutput, req.feeSatPerKB); err != nil { + return createPostDatedTxResponse{nil, err} + } + + // Reference server.go:236 + var coincaseAddr btcutil.Address + coincaseAddr, err = w.NewChangeAddress(req.account, waddrmgr.KeyScopeBIP0044) + + coincaseTx, err := w.createCoincase(coincaseAddr, req.amount, req.lockTime) + if err != nil { + return createPostDatedTxResponse{nil, err} + } + + var tx *txauthor.AuthoredTx + err = walletdb.Update(w.db, func(dbtx walletdb.ReadWriteTx) error { + addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + + tx, err = NewUnsignedTransactionFromCoincase(coincaseTx, redeemOutput) + if err != nil { + return err + } + + return tx.AddAllInputScripts(secretSource{w.Manager, addrmgrNs}) + }) + if err != nil { + return createPostDatedTxResponse{nil, err} + } + + err = validateMsgTx(tx.Tx, tx.PrevScripts, tx.PrevInputValues) + if err != nil { + return createPostDatedTxResponse{nil, err} + } + + return createPostDatedTxResponse{tx, nil} +} diff --git a/wallet/transferTx.go b/wallet/transferTx.go new file mode 100644 index 0000000000..e7111083bb --- /dev/null +++ b/wallet/transferTx.go @@ -0,0 +1,287 @@ +// TODO : License related notes + +package wallet + +import ( + "fmt" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wallet/txauthor" + "github.com/btcsuite/btcwallet/wallet/txrules" + "github.com/btcsuite/btcwallet/walletdb" + "github.com/btcsuite/btcwallet/wtxmgr" +) + +// TODO : Write summary +type ( + createTxTransferRequest struct { + account uint32 + address string + txHash chainhash.Hash + minconf int32 + feeSatPerKB btcutil.Amount + resp chan createTxTransferResponse + } + createTxTransferResponse struct { + tx *txauthor.AuthoredTx + err error + } +) + +// TODO : Write summary +func (w *Wallet) txTransferCreator() { + quit := w.quitChan() +out: + for { + select { + case txr := <-w.createTxTransferRequests: + heldUnlock, err := w.holdUnlock() + if err != nil { + txr.resp <- createTxTransferResponse{nil, err} + continue + } + tx, err := w.txTransferToOutputs(txr.address, txr.txHash, txr.account, + txr.minconf, txr.feeSatPerKB) + heldUnlock.release() + txr.resp <- createTxTransferResponse{tx, err} + case <-quit: + break out + } + } + w.wg.Done() +} + +// TODO : Write summary +func (w *Wallet) CreateSimpleTxTransfer(account uint32, address string, txHash chainhash.Hash, minconf int32, feeSatPerKb btcutil.Amount) (*txauthor.AuthoredTx, error) { + req := createTxTransferRequest{ + account: account, + address: address, + txHash: txHash, + minconf: minconf, + feeSatPerKB: feeSatPerKb, + resp: make(chan createTxTransferResponse), + } + w.createTxTransferRequests <- req + resp := <-req.resp + return resp.tx, resp.err +} + +// TODO : Write summary +func (w *Wallet) TransferTx(address string, txHash chainhash.Hash, account uint32, minconf int32, feeSatPerKb btcutil.Amount) (*chainhash.Hash, error) { + createdTx, err := w.CreateSimpleTxTransfer(account, address, txHash, minconf, feeSatPerKb) + if err != nil { + return nil, err + } + + return w.publishTransaction(createdTx.Tx) +} + +// TODO : Write summary +func (w *Wallet) txTransferToOutputs(address string, txHash chainhash.Hash, account uint32, + minconf int32, feeSatPerKb btcutil.Amount) (tx *txauthor.AuthoredTx, err error) { + log.Infof("Transaction transfer requested. Address : %s, Tx hash: %s, Account: %d", address, txHash, account) + + chainClient, err := w.requireChainClient() + if err != nil { + return nil, err + } + + // Find tx to be transferred + var txToBoTransferred *wtxmgr.Credit + // Open a database read transaction and executes the function f + // Used to find transaction with hash 'txHash' + err = walletdb.View(w.db, func(dbtx walletdb.ReadTx) error { + // Get current block's height and hash. + bs, err := chainClient.BlockStamp() + if err != nil { + return err + } + + // Find only the transaction with hash 'txHash' that belong to 'account' + // If not found any, or the found one isn't eligible, throw a relevant exception + // Eligible if : has 'minconf' confirmation & unspent + // Eventually will return only post-dated cheques + txToBoTransferred, err = w.findTheTransaction(dbtx, txHash, account, minconf, bs) + return err + }) + if err != nil { + return nil, err + } + + amount := txToBoTransferred.Amount + // Make outputs for tx to be transferred + redeemOutput, err := makeOutput(address, amount, w.ChainParams()) + if err != nil { + return nil, err + } + + // Ensure the outputs to be created adhere to the network's consensus + // rules. + if err := txrules.CheckOutput(redeemOutput, feeSatPerKb); err != nil { + return nil, err + } + + // Open a database read/write transaction and executes the function f + err = walletdb.Update(w.db, func(dbtx walletdb.ReadWriteTx) error { + addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + _ = addrmgrNs + + // Get current block's height and hash. + bs, err := chainClient.BlockStamp() + if err != nil { + return err + } + + eligible, err := w.findEligibleOutputs(dbtx, account, minconf, bs) + if err != nil { + return err + } + + // Remove transaction to be transferred from eligible inputs + // If we do not, it causes duplicate inputs error + for idx, item := range eligible { + if item.Hash == txHash { + eligible = append(eligible[:idx], eligible[idx+1:]...) + } + } + + inputSource := makeInputSource(eligible) + changeSource := func() ([]byte, error) { + // Derive the change output script. As a hack to allow + // spending from the imported account, change addresses + // are created from account 0. + var changeAddr btcutil.Address + var err error + if account == waddrmgr.ImportedAddrAccount { + changeAddr, err = w.newChangeAddress(addrmgrNs, 0) + } else { + changeAddr, err = w.newChangeAddress(addrmgrNs, account) + } + if err != nil { + return nil, err + } + return txscript.PayToAddrScript(changeAddr) + } + + tx, err = txauthor.NewUnsignedTransactionFromInput(txToBoTransferred, redeemOutput, feeSatPerKb, + inputSource, changeSource) + if err != nil { + return err + } + + // Randomize change position, if change exists, before signing. + // This doesn't affect the serialize size, so the change amount + // will still be valid. + if tx.ChangeIndex >= 0 { + tx.RandomizeChangePosition() + } + + return tx.AddAllInputScripts(secretSource{w.Manager, addrmgrNs}) + }) + if err != nil { + return nil, err + } + + err = validateMsgTx(tx.Tx, tx.PrevScripts, tx.PrevInputValues) + if err != nil { + return nil, err + } + + if tx.ChangeIndex >= 0 && account == waddrmgr.ImportedAddrAccount { + changeAmount := btcutil.Amount(tx.Tx.TxOut[tx.ChangeIndex].Value) + log.Warnf("Spend from imported account produced change: moving"+ + " %v from imported account into default account.", changeAmount) + } + + return tx, nil +} + +// makeOutput creates a transaction output from a pair of address +// strings to amounts. This is used to create the outputs to include in newly +// created transactions from a JSON object describing the output destinations +// and amounts. +func makeOutput(addrStr string, amt btcutil.Amount, chainParams *chaincfg.Params) (*wire.TxOut, error) { + addr, err := btcutil.DecodeAddress(addrStr, chainParams) + if err != nil { + return nil, fmt.Errorf("cannot decode address: %s", err) + } + + pkScript, err := txscript.PayToAddrScript(addr) + if err != nil { + return nil, fmt.Errorf("cannot create txout script: %s", err) + } + + output := wire.NewTxOut(int64(amt), pkScript) + return output, nil +} + +// findTransaction is modified from 'findEligibleOutputs' in 'createtx.go' +func (w *Wallet) findTheTransaction(dbtx walletdb.ReadTx, txHash chainhash.Hash, + account uint32, minconf int32, bs *waddrmgr.BlockStamp) (*wtxmgr.Credit, error) { + + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + + // TODO : Eventually get from post-dated transactions (POST-DATED feature) + unspent, err := w.TxStore.UnspentOutputs(txmgrNs) + if err != nil { + return nil, err + } + + // TODO: Eventually all of these filters (except perhaps output locking) + // should be handled by the call to UnspentOutputs (or similar). + // Because one of these filters requires matching the output script to + // the desired account, this change depends on making wtxmgr a waddrmgr + // dependancy and requesting unspent outputs for a single account. + var eligible *wtxmgr.Credit + for i := range unspent { + output := &unspent[i] + + // For post-dated cheques, we want to transfer only tx with txHash + if output.Hash != txHash { + continue + } + + // Only include this output if it meets the required number of + // confirmations. Coinbase transactions must have have reached + // maturity before their outputs may be spent. + if !confirmed(minconf, output.Height, bs.Height) { + return nil, txNotMatureError{} + } + if output.FromCoinBase { + target := int32(w.chainParams.CoinbaseMaturity) + if !confirmed(target, output.Height, bs.Height) { + return nil, txCoinbaseNotMatureError{} + } + } + + // Locked unspent outputs are skipped. + if w.LockedOutpoint(output.OutPoint) { + return nil, txIsLockedError{} + } + + // Only include the output if it is associated with the passed + // account. + // + // TODO: Handle multisig outputs by determining if enough of the + // addresses are controlled. + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + output.PkScript, w.chainParams) + if err != nil || len(addrs) != 1 { + continue + } + _, addrAcct, err := w.Manager.AddrAccount(addrmgrNs, addrs[0]) + if err != nil || addrAcct != account { + return nil, txIsNotOwnedError{} + } + eligible = output + + return eligible, nil + } + + return nil, txNotFoundError{} +} diff --git a/wallet/transferTxErrors.go b/wallet/transferTxErrors.go new file mode 100644 index 0000000000..eacabc2f32 --- /dev/null +++ b/wallet/transferTxErrors.go @@ -0,0 +1,46 @@ +package wallet + +type TransactionTransferError interface { + error + TransactionTransferError() +} + +// Transaction not found error +type txNotFoundError struct{} + +func (txNotFoundError) Error() string { + return "transaction not found to transfer" +} +func (txNotFoundError) TransactionTransferError() {} + +// Transaction in not mature error +type txNotMatureError struct{} + +func (txNotMatureError) Error() string { + return "transaction is not mature" +} +func (txNotMatureError) TransactionTransferError() {} + +// Coinbase transaction in not mature error +type txCoinbaseNotMatureError struct{} + +func (txCoinbaseNotMatureError) Error() string { + return "Coin transaction is not mature" +} +func (txCoinbaseNotMatureError) TransactionTransferError() {} + +// transaction is locked error +type txIsLockedError struct{} + +func (txIsLockedError) Error() string { + return "Transaction is locked" +} +func (txIsLockedError) TransactionTransferError() {} + +// transaction is not owned by account error +type txIsNotOwnedError struct{} + +func (txIsNotOwnedError) Error() string { + return "Transaction is not owned by this account" +} +func (txIsNotOwnedError) TransactionTransferError() {} diff --git a/wallet/txauthor/author.go b/wallet/txauthor/author.go index 467153c924..09fdc400cb 100644 --- a/wallet/txauthor/author.go +++ b/wallet/txauthor/author.go @@ -7,6 +7,7 @@ package txauthor import ( "errors" + "github.com/btcsuite/btcwallet/wtxmgr" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/txscript" @@ -152,6 +153,103 @@ func NewUnsignedTransaction(outputs []*wire.TxOut, relayFeePerKb btcutil.Amount, } } +// TODO : Summary +// TODO : Unit test +// TODO : Test zero fee scenario. It should be +// one input, one output transaction if there is no fee +// +// NewUnsignedTransactionFromInput ... +// Uses credit as a first input and finds another one for the fee +func NewUnsignedTransactionFromInput(credit *wtxmgr.Credit, output *wire.TxOut, relayFeePerKb btcutil.Amount, + fetchInputs InputSource, fetchChange ChangeSource) (*AuthoredTx, error) { + + outputs := []*wire.TxOut{output} + + // Create input from transaction to be transferred + nextInput := wire.NewTxIn(&credit.OutPoint, nil, nil) + + targetAmount := credit.Amount + estimatedSize := txsizes.EstimateVirtualSize(0, 1, 0, outputs, true) + targetFee := txrules.FeeForSerializeSize(relayFeePerKb, estimatedSize) + + for { + // Fetch input only for fee + feeInputAmount, feeInputs, feeInputValues, feeScripts, err := fetchInputs(targetFee) + if err != nil { + return nil, err + } + + // Couldn't find eligible input for fee + if feeInputAmount < targetFee { + return nil, insufficientFundsError{} + } + + // Calculate input values from fee & transaction to be transferred + inputAmount := feeInputAmount + credit.Amount + inputs := append(feeInputs, nextInput) + inputValues := append(feeInputValues, credit.Amount) + scripts := append(feeScripts, credit.PkScript) + + // We count the types of inputs, which we'll use to estimate + // the vsize of the transaction. + var nested, p2wpkh, p2pkh int + for _, pkScript := range scripts { + switch { + // If this is a p2sh output, we assume this is a + // nested P2WKH. + case txscript.IsPayToScriptHash(pkScript): + nested++ + case txscript.IsPayToWitnessPubKeyHash(pkScript): + p2wpkh++ + default: + p2pkh++ + } + } + + maxSignedSize := txsizes.EstimateVirtualSize(p2pkh, p2wpkh, + nested, outputs, true) + maxRequiredFee := txrules.FeeForSerializeSize(relayFeePerKb, maxSignedSize) + remainingAmount := inputAmount - targetAmount + if remainingAmount < maxRequiredFee { + targetFee = maxRequiredFee + continue + } + + // TODO : Should version change for post-dated cheqeues + unsignedTransaction := &wire.MsgTx{ + Version: wire.TxVersion, + TxIn: inputs, + TxOut: outputs, + LockTime: 0, + } + changeIndex := -1 + changeAmount := inputAmount - targetAmount - maxRequiredFee + if changeAmount != 0 && !txrules.IsDustAmount(changeAmount, + txsizes.P2WPKHPkScriptSize, relayFeePerKb) { + changeScript, err := fetchChange() + if err != nil { + return nil, err + } + if len(changeScript) > txsizes.P2WPKHPkScriptSize { + return nil, errors.New("fee estimation requires change " + + "scripts no larger than P2WPKH output scripts") + } + change := wire.NewTxOut(int64(changeAmount), changeScript) + l := len(outputs) + unsignedTransaction.TxOut = append(outputs[:l:l], change) + changeIndex = l + } + + return &AuthoredTx{ + Tx: unsignedTransaction, + PrevScripts: scripts, + PrevInputValues: inputValues, + TotalInput: inputAmount, + ChangeIndex: changeIndex, + }, nil + } +} + // RandomizeOutputPosition randomizes the position of a transaction's output by // swapping it with a random output. The new index is returned. This should be // done before signing. diff --git a/wallet/txtransfer/README-SampleUsage.md b/wallet/txtransfer/README-SampleUsage.md new file mode 100644 index 0000000000..d8b80ba6ab --- /dev/null +++ b/wallet/txtransfer/README-SampleUsage.md @@ -0,0 +1,213 @@ +``` +btcd -C .\btcd.conf + +btcwallet -C .\btcwallet.conf --create +-> Wallet seed (SimNet) 51b63e594eafd12d1393f49936bcee0b8a54c12e506309f21c0d52af6ad0ea47 + +btcwallet -C .\btcwallet.conf + +btcctl -C .\btcctl.conf --wallet walletpassphrase "[password]" 600 + +btcctl -C .\btcctl.conf --wallet getnewaddress +-> SYYFCKLX38x6aZRsxuLmdNXSBDZ3BaJczq + +btcd -C .\btcd.conf --miningaddr=SYYFCKLX38x6aZRsxuLmdNXSBDZ3BaJczq + +btcctl -C .\btcctl.conf --wallet createnewaccount account1 + +btcctl -C .\btcctl.conf --wallet getnewaddress account1 +-> SZMEzGtDCgxmm1WKjbSUqBie3xcU7ovuyG + +btcctl -C .\btcctl.conf --wallet getblockhash 0 +-> 683e86bd5c6d110d91b94b97137ba6bfe02dbbdb8e3dff722a669b5d69d77af6 + +btcctl -C .\btcctl.conf --wallet getblock 683e86bd5c6d110d91b94b97137ba6bfe02dbbdb8e3dff722a669b5d69d77af6 +-> +{ + "hash": "683e86bd5c6d110d91b94b97137ba6bfe02dbbdb8e3dff722a669b5d69d77af6", + "confirmations": 1, + "strippedsize": 285, + "size": 285, + "weight": 1140, + "height": 0, + "version": 1, + "versionHex": "00000001", + "merkleroot": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", + "tx": [ + "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b" + ], + "time": 1401292357, + "nonce": 2, + "bits": "207fffff", + "difficulty": 1, + "previousblockhash": "0000000000000000000000000000000000000000000000000000000000000000" +} + +btcctl -C .\btcctl.conf generate 109 +-> +[ + "2a666bba893f07ce7e9ebfa2d0aa0379b0b54bc6487b3715ab5200b74fda9154", + "1641c6a0d5916cfc2f70b7c89fcd27dfb85fe11886516dc6f1de36b8f4761212", + .... +] + +btcctl -C .\btcctl.conf --wallet getblockhash 8 +-> 20c703d4ff8b515c634073ebe99cc01ac9f7739dee0507f89d01dbd0fe76c853 + +btcctl -C .\btcctl.conf --wallet getblock 20c703d4ff8b515c634073ebe99cc01ac9f7739dee0507f89d01dbd0fe76c853 +-> +{ + "hash": "20c703d4ff8b515c634073ebe99cc01ac9f7739dee0507f89d01dbd0fe76c853", + "confirmations": 100, + "strippedsize": 188, + "size": 188, + "weight": 752, + "height": 8, + "version": 536870912, + "versionHex": "20000000", + "merkleroot": "de14e6d076a217558dd23191ee6ae3b652297d359f92c6973f3139fe8d757359", + "tx": [ + "de14e6d076a217558dd23191ee6ae3b652297d359f92c6973f3139fe8d757359" + ], + "time": 1539433122, + "nonce": 0, + "bits": "207fffff", + "difficulty": 1, + "previousblockhash": "4d2d4f8d266c329a24576ab314edace636e376b0cb33d661b18e52c03085f06a", + "nextblockhash": "3294318179979673976745ab8b83bf5fc33c0399aa42db0299b33dab81ded9ac" +} + +btcctl -C .\btcctl.conf --wallet listunspent +-> +[ + { + "txid": "e8511bd42a9bbdac69655fd3bb0e5cb2f9c42cc9b51476c65682ba18a18e4c0e", + "vout": 0, + "address": "SYYFCKLX38x6aZRsxuLmdNXSBDZ3BaJczq", + "account": "default", + "scriptPubKey": "76a9147b5b07b286ef494549db8630aeb37301bf99663c88ac", + "amount": 50, + "confirmations": 100, + "spendable": true + }, + { + "txid": "de14e6d076a217558dd23191ee6ae3b652297d359f92c6973f3139fe8d757359", + "vout": 0, + "address": "SYYFCKLX38x6aZRsxuLmdNXSBDZ3BaJczq", + "account": "default", + "scriptPubKey": "76a9147b5b07b286ef494549db8630aeb37301bf99663c88ac", + "amount": 50, + "confirmations": 101, + "spendable": true + } +] + +btcctl -C .\btcctl.conf --wallet transfertransaction SZMEzGtDCgxmm1WKjbSUqBie3xcU7ovuyG de14e6d076a217558dd23191ee6ae3b652297d359f92c6973f3139fe8d757359 +-> d7ff90d4a3a07148446cc98cae786efe3e394e9660614fac262ab1354575f4c4 + +btcctl -C .\btcctl.conf --wallet listunspent +-> [] (bug here) + +btcctl -C .\btcctl.conf generate 1 +-> +[ + "47b7d70a2088854f80a39fa57f392715c2ad7abf8f04e2c7938f6fc683f618c8" +] + +btcctl -C .\btcctl.conf --wallet listunspent +-> +[ + { + "txid": "d7ff90d4a3a07148446cc98cae786efe3e394e9660614fac262ab1354575f4c4", + "vout": 1, + "address": "SZMEzGtDCgxmm1WKjbSUqBie3xcU7ovuyG", + "account": "account1", + "scriptPubKey": "76a914843e682e2cad833c2b4056a473423cf4458f38f388ac", + "amount": 50, + "confirmations": 1, + "spendable": true + }, + { + "txid": "d7ff90d4a3a07148446cc98cae786efe3e394e9660614fac262ab1354575f4c4", + "vout": 0, + "address": "sb1qt05j4gu54adsxrvqfwg9e2wfwn2yzazy39x8hm", + "account": "default", + "scriptPubKey": "00145be92aa394af5b030d804b905ca9c974d4417444", + "amount": 49.99999627, + "confirmations": 1, + "spendable": true + }, + { + "txid": "33c956a67f0d9423d376fc8fcb27426da727baf162b63e501ed8bb4ff4679aee", + "vout": 0, + "address": "SYYFCKLX38x6aZRsxuLmdNXSBDZ3BaJczq", + "account": "default", + "scriptPubKey": "76a9147b5b07b286ef494549db8630aeb37301bf99663c88ac", + "amount": 50, + "confirmations": 100, + "spendable": true + } +] + +btcctl -C .\btcctl.conf --wallet gettransaction d7ff90d4a3a07148446cc98cae786efe3e394e9660614fac262ab1354575f4c4 +(This tx includes transferred transaction and the change) +-> +{ + "amount": 50, + "fee": 0.00000373, + "confirmations": 1, + "blockhash": "47b7d70a2088854f80a39fa57f392715c2ad7abf8f04e2c7938f6fc683f618c8", + "blockindex": 0, + "blocktime": 1539549532, + "txid": "d7ff90d4a3a07148446cc98cae786efe3e394e9660614fac262ab1354575f4c4", + "walletconflicts": [], + "time": 1539549533, + "timereceived": 1539549533, + "details": [ + { + "account": "", + "amount": -100, + "category": "send", + "fee": 0.00000373, + "vout": 0 + }, + { + "account": "account1", + "address": "SZMEzGtDCgxmm1WKjbSUqBie3xcU7ovuyG", + "amount": 50, + "category": "receive", + "vout": 1 + } + ], + "hex": "01000000020e4c8ea118ba8256c67614b5c92cc4f9b25c0ebbd35f6569acbd9b2ad41b51e8000000006a4730440220101c9a344296bd8fb49e48878b0342ba578ab92c38269c72915d79b53763c0440220446b54721493bff502a39eba0b69b5faa4a349bc1f7a6ff7cbbaabb00f4c3f9d01210223ac6a75f94e60de9ad1642982170c958f7eba2d8640830b28fb67e1fe57561dffffffff5973758dfe39313f97c6929f357d2952b6e36aee9131d28d5517a276d0e614de000000006b483045022100856c05b2136a03362aa5f44892caabd90efac09780409deb19955c1bca36858d02205a7563e0f265ab55c4c75e5508fa2e987c8977873a30aa62788985cd5fccc05901210223ac6a75f94e60de9ad1642982170c958f7eba2d8640830b28fb67e1fe57561dffffffff028bf0052a010000001600145be92aa394af5b030d804b905ca9c974d441744400f2052a010000001976a914843e682e2cad833c2b4056a473423cf4458f38f388ac00000000" +} + +btcctl -C .\btcctl.conf --wallet gettransaction 33c956a67f0d9423d376fc8fcb27426da727baf162b63e501ed8bb4ff4679aee +(This is the reward of 10. block) +-> +{ + "amount": 50, + "confirmations": 100, + "blockhash": "5887b49fd44146eb24447ae3c8eeb93bb80c76f8b1e931e445ecc1066427cb59", + "blockindex": 0, + "blocktime": 1539433122, + "txid": "33c956a67f0d9423d376fc8fcb27426da727baf162b63e501ed8bb4ff4679aee", + "walletconflicts": [], + "time": 1539433120, + "timereceived": 1539433120, + "details": [ + { + "account": "default", + "address": "SYYFCKLX38x6aZRsxuLmdNXSBDZ3BaJczq", + "amount": 50, + "category": "generate", + "vout": 0 + } + ], + "hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff165a08830f409bcb227e3c0b2f503253482f627463642fffffffff0100f2052a010000001976a9147b5b07b286ef494549db8630aeb37301bf99663c88ac00000000" +} + +(Block height of 47b7d70a2088854f80a39fa57f392715c2ad7abf8f04e2c7938f6fc683f618c8 : 109) +(Block height of 5887b49fd44146eb24447ae3c8eeb93bb80c76f8b1e931e445ecc1066427cb59 : 10) + +``` \ No newline at end of file diff --git a/wallet/txtransfer/README.md b/wallet/txtransfer/README.md new file mode 100644 index 0000000000..34d7d91e9e --- /dev/null +++ b/wallet/txtransfer/README.md @@ -0,0 +1,25 @@ +// TODO : Change this for transaction transfer rules +(Taken from https://en.bitcoin.it/wiki/Protocol_rules) + +These messages hold a single transaction. + +Check syntactic correctness +Make sure neither in or out lists are empty +Size in bytes <= MAX_BLOCK_SIZE +Each output value, as well as the total, must be in legal money range +Make sure none of the inputs have hash=0, n=-1 (coinbase transactions) +Check that nLockTime <= INT_MAX, size in bytes >= 100, and sig opcount <= 2 +Reject "nonstandard" transactions: scriptSig doing anything other than pushing numbers on the stack, or scriptPubkey not matching the two usual forms +Reject if we already have matching tx in the pool, or in a block in the main branch +For each input, if the referenced output exists in any other tx in the pool, reject this transaction. +For each input, look in the main branch and the transaction pool to find the referenced output transaction. If the output transaction is missing for any input, this will be an orphan transaction. Add to the orphan transactions, if a matching transaction is not in there already. +For each input, if the referenced output transaction is coinbase (i.e. only 1 input, with hash=0, n=-1), it must have at least COINBASE_MATURITY (100) confirmations; else reject this transaction +For each input, if the referenced output does not exist (e.g. never existed or has already been spent), reject this transaction +Using the referenced output transactions to get input values, check that each input value, as well as the sum, are in legal money range +Reject if the sum of input values < sum of output values +Reject if transaction fee (defined as sum of input values minus sum of output values) would be too low to get into an empty block +Verify the scriptPubKey accepts for each input; reject if any are bad +Add to transaction pool +"Add to wallet if mine" +Relay transaction to peers +For each orphan transaction that uses this one as one of its inputs, run all these steps (including this one) recursively on that orphan \ No newline at end of file diff --git a/wallet/wallet.go b/wallet/wallet.go index b202d69021..bf9d4218c8 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -97,6 +97,12 @@ type Wallet struct { // Channel for transaction creation requests. createTxRequests chan createTxRequest + // Channel for transaction transfer requests + createTxTransferRequests chan createTxTransferRequest + + // Channel for postdated transactions + createPostDatedTxRequests chan createPostDatedTxRequest + // Channels for the manager locker. unlockRequests chan unlockRequest lockRequests chan struct{} @@ -138,9 +144,11 @@ func (w *Wallet) Start() { } w.quitMu.Unlock() - w.wg.Add(2) + w.wg.Add(4) go w.txCreator() + go w.txTransferCreator() go w.walletLocker() + go w.postDatedTxCreator() } // SynchronizeRPC associates the wallet with the consensus RPC client, @@ -3433,26 +3441,28 @@ func Open(db walletdb.DB, pubPass []byte, cbs *waddrmgr.OpenCallbacks, log.Infof("Opened wallet") // TODO: log balance? last sync height? w := &Wallet{ - publicPassphrase: pubPass, - db: db, - Manager: addrMgr, - TxStore: txMgr, - lockedOutpoints: map[wire.OutPoint]struct{}{}, - recoveryWindow: recoveryWindow, - rescanAddJob: make(chan *RescanJob), - rescanBatch: make(chan *rescanBatch), - rescanNotifications: make(chan interface{}), - rescanProgress: make(chan *RescanProgressMsg), - rescanFinished: make(chan *RescanFinishedMsg), - createTxRequests: make(chan createTxRequest), - unlockRequests: make(chan unlockRequest), - lockRequests: make(chan struct{}), - holdUnlockRequests: make(chan chan heldUnlock), - lockState: make(chan bool), - changePassphrase: make(chan changePassphraseRequest), - changePassphrases: make(chan changePassphrasesRequest), - chainParams: params, - quit: make(chan struct{}), + publicPassphrase: pubPass, + db: db, + Manager: addrMgr, + TxStore: txMgr, + lockedOutpoints: map[wire.OutPoint]struct{}{}, + recoveryWindow: recoveryWindow, + rescanAddJob: make(chan *RescanJob), + rescanBatch: make(chan *rescanBatch), + rescanNotifications: make(chan interface{}), + rescanProgress: make(chan *RescanProgressMsg), + rescanFinished: make(chan *RescanFinishedMsg), + createTxRequests: make(chan createTxRequest), + createTxTransferRequests: make(chan createTxTransferRequest), + createPostDatedTxRequests: make(chan createPostDatedTxRequest), + unlockRequests: make(chan unlockRequest), + lockRequests: make(chan struct{}), + holdUnlockRequests: make(chan chan heldUnlock), + lockState: make(chan bool), + changePassphrase: make(chan changePassphraseRequest), + changePassphrases: make(chan changePassphrasesRequest), + chainParams: params, + quit: make(chan struct{}), } w.NtfnServer = newNotificationServer(w) w.TxStore.NotifyUnspent = func(hash *chainhash.Hash, index uint32) {