Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions cgo/transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,22 @@ func sendRawTransaction(cName, cTxHex *C.char) *C.char {
return successCResponse("%s", txHash)
}

//export abandonTransaction
func abandonTransaction(cName, cTxID *C.char) *C.char {
w, exists := loadedWallet(cName)
if !exists {
return errCResponse("wallet with name %q does not exist", goString(cName))
}
txHash, err := chainhash.NewHashFromStr(goString(cTxID))
if err != nil {
return errCResponse("invalid tx hash: %v", err)
}
if err := w.AbandonTransaction(w.ctx, txHash); err != nil {
return errCResponse("unable to abandon transaction: %v", err)
}
return successCResponse("%s", txHash)
}

//export listUnspents
func listUnspents(cName *C.char) *C.char {
w, exists := loadedWallet(cName)
Expand Down
65 changes: 65 additions & 0 deletions dcr/dcr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/davecgh/go-spew/spew"
"github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/dcrd/hdkeychain/v3"
"github.com/decred/dcrd/wire"
)

func TestAddrFromExtendedKey(t *testing.T) {
Expand Down Expand Up @@ -144,6 +145,70 @@ func TestDecodeTx(t *testing.T) {
}
}

func TestIsDust(t *testing.T) {
// Standard P2PKH script (25 bytes). Output serialize size is 36.
// Dust threshold = 10 * (36 + 165) * 3 = 6030 atoms.
p2pkhScript := []byte{
0x76, 0xa9, 0x14, // OP_DUP OP_HASH160 OP_DATA_20
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // 20-byte pubkey hash
0x88, 0xac, // OP_EQUALVERIFY OP_CHECKSIG
}
opReturnScript := []byte{0x6a, 0x04, 0x01, 0x02, 0x03, 0x04}

tests := []struct {
name string
value int64
script []byte
want bool
}{{
name: "zero value is dust",
value: 0,
script: p2pkhScript,
want: true,
}, {
name: "below threshold is dust",
value: 6029,
script: p2pkhScript,
want: true,
}, {
name: "at threshold is not dust",
value: 6030,
script: p2pkhScript,
want: false,
}, {
name: "above threshold is not dust",
value: 6031,
script: p2pkhScript,
want: false,
}, {
name: "OP_RETURN is always dust",
value: 100000,
script: opReturnScript,
want: true,
}}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
txOut := &wire.TxOut{Value: test.value, PkScript: test.script}
if got := isDust(txOut); got != test.want {
t.Fatalf("isDust(%d) = %v, want %v", test.value, got, test.want)
}
})
}
}

func TestDustThreshold(t *testing.T) {
// For a P2PKH output (serialize size 36):
// threshold = 10 * (36 + 165) * 3 = 6030
const p2pkhSerSize = 36
got := dustThreshold(p2pkhSerSize)
if got != 6030 {
t.Fatalf("dustThreshold(%d) = %d, want 6030", p2pkhSerSize, got)
}
}

func TestDecryptSeed(t *testing.T) {
metaData := new(walletData)
w := &Wallet{metaData: metaData}
Expand Down
35 changes: 35 additions & 0 deletions dcr/transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,30 @@ const (
// sstxCommitmentString is the string to insert when a verbose
// transaction output's pkscript type is a ticket commitment.
sstxCommitmentString = "sstxcommitment"
// defaultRelayFeePerByte is the default relay fee in atoms per byte.
defaultRelayFeePerByte = defaultRelayFeePerKb / 1000
)

// isDust returns whether the given output value is considered dust at the
// default relay fee rate. See dcrd/internal/mempool/policy.go for the dust
// policy documentation. An output is dust when:
//
// value / (3 * (outputSize + 165)) < relayFeePerByte
func isDust(txOut *wire.TxOut) bool {
if txscript.IsUnspendable(txOut.Value, txOut.PkScript) {
return true
}
totalSize := uint64(txOut.SerializeSize()) + 165
return uint64(txOut.Value)/(3*totalSize) < defaultRelayFeePerByte
}

// dustThreshold returns the minimum non-dust value in atoms for the given
// output size.
func dustThreshold(txOutSize int) int64 {
totalSize := uint64(txOutSize) + 165
return int64(defaultRelayFeePerByte * totalSize * 3)
}

// newTxOut returns a new transaction output with the given parameters.
func newTxOut(amount int64, pkScriptVer uint16, pkScript []byte) *wire.TxOut {
return &wire.TxOut{
Expand Down Expand Up @@ -217,6 +239,12 @@ func (w *Wallet) CreateTransaction(ctx context.Context, outputs []*Output,
}
payScriptVer, payScript := addr.PaymentScript()
txOut := newTxOut(int64(out.Amount), payScriptVer, payScript)
if !sendAll && isDust(txOut) {
minVal := dustThreshold(txOut.SerializeSize())
return nil, nil, 0, fmt.Errorf("output %d is dust: payment of %d atoms "+
"is below the dust threshold of %d atoms for address %s",
i, out.Amount, minVal, out.Address)
}
outs[i] = txOut
}

Expand Down Expand Up @@ -288,6 +316,13 @@ func (w *Wallet) SendRawTransaction(ctx context.Context, txHex string) (*chainha
return w.mainWallet.PublishTransaction(ctx, msgTx, w.syncer)
}

// AbandonTransaction removes an unmined transaction, identified by its hash,
// from the wallet. All transaction spend chains deriving from the transaction's
// outputs are also removed.
func (w *Wallet) AbandonTransaction(ctx context.Context, txHash *chainhash.Hash) error {
return w.mainWallet.AbandonTransaction(ctx, txHash)
}

// createVinList returns a slice of JSON objects for the inputs of the passed
// transaction.
func createVinList(mtx *wire.MsgTx, isTreasuryEnabled bool) []dcrdtypes.Vin {
Expand Down
Loading