diff --git a/LICENSE b/LICENSE index b0cde50..bb5a40d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ ISC License -Copyright (c) 2020-2021 The Decred developers +Copyright (c) 2020-2022 The Decred developers Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above diff --git a/background/background.go b/background/background.go index daa2084..f453143 100644 --- a/background/background.go +++ b/background/background.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2021 The Decred developers +// Copyright (c) 2020-2022 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -244,6 +244,25 @@ func blockConnected() { } } } + + // Set tspend policy on voting wallets. + for tspend, policy := range ticket.TSpendPolicy { + err = walletClient.SetTSpendPolicy(tspend, policy, ticket.Hash) + if err != nil { + log.Errorf("%s: dcrwallet.SetTSpendPolicy failed (wallet=%s, ticketHash=%s): %v", + funcName, walletClient.String(), ticket.Hash, err) + } + } + + // Set treasury policy on voting wallets. + for key, policy := range ticket.TreasuryPolicy { + err = walletClient.SetTreasuryPolicy(key, policy, ticket.Hash) + if err != nil { + log.Errorf("%s: dcrwallet.SetTreasuryPolicy failed (wallet=%s, ticketHash=%s): %v", + funcName, walletClient.String(), ticket.Hash, err) + } + } + log.Infof("%s: Ticket added to voting wallet (wallet=%s, ticketHash=%s)", funcName, walletClient.String(), ticket.Hash) } @@ -557,6 +576,8 @@ func checkWalletConsistency() { } } } + + // TODO - tspend and treasury policy consistency checking. } } } diff --git a/database/ticket.go b/database/ticket.go index 35541d2..18c7443 100644 --- a/database/ticket.go +++ b/database/ticket.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2021 The Decred developers +// Copyright (c) 2020-2022 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -51,6 +51,8 @@ var ( confirmedK = []byte("Confirmed") votingWIFK = []byte("VotingWIF") voteChoicesK = []byte("VoteChoices") + tSpendPolicyK = []byte("TSpendPolicy") + treasuryPolicyK = []byte("TreasuryPolicy") feeTxHexK = []byte("FeeTxHex") feeTxHashK = []byte("FeeTxHash") feeTxStatusK = []byte("FeeTxStatus") @@ -72,9 +74,11 @@ type Ticket struct { // VotingWIF is set in /payfee. VotingWIF string - // VoteChoices is initially set in /payfee, but can be updated in - // /setvotechoices. - VoteChoices map[string]string + // VoteChoices, TSpendPolicy and TreasuryPolicy are initially set in + // /payfee, but can be updated in /setvotechoices. + VoteChoices map[string]string + TSpendPolicy map[string]string + TreasuryPolicy map[string]string // FeeTxHex and FeeTxHash will be set when the fee tx has been received. FeeTxHex string @@ -176,6 +180,22 @@ func putTicketInBucket(bkt *bolt.Bucket, ticket Ticket) error { return err } + jsonTSpend, err := json.Marshal(ticket.TSpendPolicy) + if err != nil { + return err + } + if err = bkt.Put(tSpendPolicyK, jsonTSpend); err != nil { + return err + } + + jsonTreasury, err := json.Marshal(ticket.TreasuryPolicy) + if err != nil { + return err + } + if err = bkt.Put(treasuryPolicyK, jsonTreasury); err != nil { + return err + } + jsonVoteChoices, err := json.Marshal(ticket.VoteChoices) if err != nil { return err @@ -202,10 +222,30 @@ func getTicketFromBkt(bkt *bolt.Bucket) (Ticket, error) { ticket.Confirmed = bytesToBool(bkt.Get(confirmedK)) - ticket.VoteChoices = make(map[string]string) - err := json.Unmarshal(bkt.Get(voteChoicesK), &ticket.VoteChoices) - if err != nil { - return ticket, err + var err error + + voteChoicesB := bkt.Get(voteChoicesK) + if voteChoicesB != nil { + err = json.Unmarshal(voteChoicesB, &ticket.VoteChoices) + if err != nil { + return ticket, fmt.Errorf("unmarshal VoteChoices err: %w", err) + } + } + + tSpendPolicyB := bkt.Get(tSpendPolicyK) + if tSpendPolicyB != nil { + err = json.Unmarshal(tSpendPolicyB, &ticket.TSpendPolicy) + if err != nil { + return ticket, fmt.Errorf("unmarshal TSpendPolicy err: %w", err) + } + } + + treasuryPolicyB := bkt.Get(treasuryPolicyK) + if treasuryPolicyB != nil { + err = json.Unmarshal(treasuryPolicyB, &ticket.TreasuryPolicy) + if err != nil { + return ticket, fmt.Errorf("unmarshal TreasuryPolicy err: %w", err) + } } return ticket, nil diff --git a/database/ticket_test.go b/database/ticket_test.go index fe78f62..0ea523d 100644 --- a/database/ticket_test.go +++ b/database/ticket_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2021 The Decred developers +// Copyright (c) 2020-2022 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -22,6 +22,8 @@ func exampleTicket() Ticket { FeeExpiration: 4, Confirmed: false, VoteChoices: map[string]string{"AgendaID": "yes"}, + TSpendPolicy: map[string]string{randString(64, hexCharset): "no"}, + TreasuryPolicy: map[string]string{randString(66, hexCharset): "abstain"}, VotingWIF: randString(53, addrCharset), FeeTxHex: randString(504, hexCharset), FeeTxHash: randString(64, hexCharset), diff --git a/database/upgrade_v3.go b/database/upgrade_v3.go index 2d81dc2..9fe0084 100644 --- a/database/upgrade_v3.go +++ b/database/upgrade_v3.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021 The Decred developers +// Copyright (c) 2021-2022 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -52,7 +52,22 @@ func ticketBucketUpgrade(db *bolt.DB) error { return fmt.Errorf("could not create new ticket bucket: %w", err) } - err = putTicketInBucket(newBkt, Ticket(ticket)) + err = putTicketInBucket(newBkt, Ticket{ + Hash: ticket.Hash, + PurchaseHeight: ticket.PurchaseHeight, + CommitmentAddress: ticket.CommitmentAddress, + FeeAddressIndex: ticket.FeeAddressIndex, + FeeAddress: ticket.FeeAddress, + FeeAmount: ticket.FeeAmount, + FeeExpiration: ticket.FeeExpiration, + Confirmed: ticket.Confirmed, + VotingWIF: ticket.VotingWIF, + VoteChoices: ticket.VoteChoices, + FeeTxHex: ticket.FeeTxHex, + FeeTxHash: ticket.FeeTxHash, + FeeTxStatus: ticket.FeeTxStatus, + Outcome: ticket.Outcome, + }) if err != nil { return fmt.Errorf("could not put new ticket in bucket: %w", err) } diff --git a/docs/api.md b/docs/api.md index 6d90001..ceea78d 100644 --- a/docs/api.md +++ b/docs/api.md @@ -161,7 +161,9 @@ for the specified ticket. "tickethash":"484a68f7148e55d05f0b64a29fe7b148572cb5272d1ce2438cf15466d347f4f4", "feetx":"010000000125...737b266ffb9a93", "votingkey":"PtWUJWhSXsM9ztPkdtH8REe91z7uoidX8dsMChJUZ2spagm7YvrNm", - "votechoices":{"headercommitments":"yes"} + "votechoices":{"headercommitments":"yes"}, + "tspendpolicy":{"":"yes"}, + "treasurypolicy":{"":"no"} } ``` @@ -212,6 +214,8 @@ its `feetxstatus` is `confirmed`. "feetxhash":"e1c02b04b5bbdae66cf8e3c88366c4918d458a2d27a26144df37f54a2bc956ac", "altsignaddress":"Tsfkn6k9AoYgVZRV6ZzcgmuVSgCdJQt9JY2", "votechoices":{"headercommitments":"no"}, + "tspendpolicy":{"":"yes"}, + "treasurypolicy":{"":"no"}, "request": {""} } ``` @@ -220,6 +224,8 @@ its `feetxstatus` is `confirmed`. Clients can update the voting preferences of their ticket at any time after after calling `/payfee`. +This call can be used to update consensus, treasury spend key, and tspend voting +preferences. - `POST /api/v3/setvotechoices` @@ -229,7 +235,9 @@ after calling `/payfee`. { "timestamp":1590509066, "tickethash":"484a68f7148e55d05f0b64a29fe7b148572cb5272d1ce2438cf15466d347f4f4", - "votechoices":{"headercommitments":"no"} + "votechoices":{"headercommitments":"no"}, + "tspendpolicy":{"":"yes"}, + "treasurypolicy":{"":"no"} } ``` diff --git a/go.mod b/go.mod index 66805fa..03473f5 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,12 @@ module github.com/decred/vspd go 1.16 require ( - decred.org/dcrwallet/v2 v2.0.0-rc3 + decred.org/dcrwallet/v2 v2.0.0 github.com/decred/dcrd/blockchain/stake/v4 v4.0.0 github.com/decred/dcrd/blockchain/v4 v4.0.0 github.com/decred/dcrd/chaincfg/chainhash v1.0.3 github.com/decred/dcrd/chaincfg/v3 v3.1.1 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 github.com/decred/dcrd/dcrutil/v4 v4.0.0 github.com/decred/dcrd/hdkeychain/v3 v3.1.0 github.com/decred/dcrd/rpc/jsonrpc/types/v3 v3.0.0 diff --git a/go.sum b/go.sum index 08f9435..791df16 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -decred.org/cspp/v2 v2.0.0-20220117153402-4f26c92d52a3/go.mod h1:USyJS44Kqxz2wT/VaNsf9iTAONegO/qKXRdLg1nvrWI= -decred.org/dcrwallet/v2 v2.0.0-rc3 h1:xxmZndkTheyfORD16nf8fUsevR3dBHdI8hhdAUFumZE= -decred.org/dcrwallet/v2 v2.0.0-rc3/go.mod h1:NJnnnOrPISrCt5oxrkXgTu0feCnxcLUsbqsvdXtFVBI= +decred.org/cspp/v2 v2.0.0/go.mod h1:0shJWKTWY3LxZEWGxtbER1Y45+HVjC0WZtj4bctSzCI= +decred.org/dcrwallet/v2 v2.0.0 h1:nOGChwR5RM4QLmbm/Krit6/eQryv/RvcpUxxmUThfKI= +decred.org/dcrwallet/v2 v2.0.0/go.mod h1:lZXgx5OcLDaWyNWFkBekqER1gdqiVwua1w68SFC1/Nk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= diff --git a/rpc/dcrwallet.go b/rpc/dcrwallet.go index c8ff286..08c91cd 100644 --- a/rpc/dcrwallet.go +++ b/rpc/dcrwallet.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020 The Decred developers +// Copyright (c) 2020-2022 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -219,3 +219,15 @@ func (c *WalletRPC) TicketInfo(startHeight int64) (map[string]*wallettypes.Ticke func (c *WalletRPC) RescanFrom(fromHeight int64) error { return c.Call(c.ctx, "rescanwallet", nil, fromHeight) } + +// SetTreasuryPolicy sets the specified tickets voting policy for all tspends +// published by the given treasury key. +func (c *WalletRPC) SetTreasuryPolicy(key, policy, ticket string) error { + return c.Call(c.ctx, "settreasurypolicy", nil, key, policy, ticket) +} + +// SetTSpendPolicy sets the specified tickets voting policy for a single tspend +// identified by its hash. +func (c *WalletRPC) SetTSpendPolicy(tSpend, policy, ticket string) error { + return c.Call(c.ctx, "settspendpolicy", nil, tSpend, policy, ticket) +} diff --git a/webapi/helpers.go b/webapi/helpers.go index 201f954..7734adc 100644 --- a/webapi/helpers.go +++ b/webapi/helpers.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020 The Decred developers +// Copyright (c) 2020-2022 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -10,7 +10,9 @@ import ( "fmt" "github.com/decred/dcrd/blockchain/stake/v4" + "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/chaincfg/v3" + "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrutil/v4" "github.com/decred/dcrd/wire" "github.com/gin-gonic/gin" @@ -52,6 +54,57 @@ agendaLoop: return nil } +func validTreasuryPolicy(policy map[string]string) error { + for key, choice := range policy { + pikey, err := hex.DecodeString(key) + if err != nil { + return fmt.Errorf("error decoding treasury key %q: %w", key, err) + } + if len(pikey) != secp256k1.PubKeyBytesLenCompressed { + return fmt.Errorf("treasury key %q is not 33 bytes", key) + } + + err = validPolicyOption(choice) + if err != nil { + return err + } + } + + return nil +} + +func validTSpendPolicy(policy map[string]string) error { + for hash, choice := range policy { + if len(hash) != chainhash.MaxHashStringSize { + return fmt.Errorf("wrong tspend hash length, expected %d got %d", + chainhash.MaxHashStringSize, len(hash)) + } + + _, err := chainhash.NewHashFromStr(hash) + if err != nil { + return fmt.Errorf("error decoding tspend hash %q: %w", hash, err) + } + + err = validPolicyOption(choice) + if err != nil { + return err + } + } + + return nil +} + +// validPolicyOption checks that policy is one of the valid values accepted by +// dcrwallet RPCs. Invalid values return an error. +func validPolicyOption(policy string) error { + switch policy { + case "yes", "no", "abstain", "invalid", "": + return nil + default: + return fmt.Errorf("%q is not a valid policy option", policy) + } +} + func validateSignature(reqBytes []byte, commitmentAddress string, c *gin.Context) error { // Ensure a signature is provided. signature := c.GetHeader("VSP-Client-Signature") diff --git a/webapi/helpers_test.go b/webapi/helpers_test.go index 97971db..8ecd221 100644 --- a/webapi/helpers_test.go +++ b/webapi/helpers_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020 The Decred developers +// Copyright (c) 2020-2022 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -28,7 +28,7 @@ func TestIsValidVoteChoices(t *testing.T) { {map[string]string{"lnsupport": "yes"}, true}, {map[string]string{"sdiffalgorithm": "no", "lnsupport": "yes"}, true}, - // Invalid agenda. + // Invalid agenda, valid vote choice. {map[string]string{"": "yes"}, false}, {map[string]string{"Fake agenda": "yes"}, false}, @@ -48,7 +48,98 @@ func TestIsValidVoteChoices(t *testing.T) { for _, test := range tests { err := validConsensusVoteChoices(params, voteVersion, test.voteChoices) if (err == nil) != test.valid { - t.Fatalf("isValidVoteChoices failed for votechoices '%v'.", test.voteChoices) + t.Fatalf("isValidVoteChoices failed for votechoices '%v': %v", + test.voteChoices, err) + } + } +} + +func TestIsValidTSpendPolicy(t *testing.T) { + + // A valid tspend hash is 32 bytes (64 characters). + validHash := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + anotherValidHash := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + + var tests = []struct { + tspendPolicy map[string]string + valid bool + }{ + // Empty vote choices are allowed. + {map[string]string{}, true}, + + // Valid tspend hash, valid vote choice. + {map[string]string{validHash: "yes"}, true}, + {map[string]string{validHash: ""}, true}, + {map[string]string{validHash: "no", anotherValidHash: "yes"}, true}, + + // Invalid tspend hash. + {map[string]string{"": "yes"}, false}, + {map[string]string{"a": "yes"}, false}, + {map[string]string{"non hex characters": "yes"}, false}, + {map[string]string{validHash + "a": "yes"}, false}, + + // Valid tspend hash, invalid vote choice. + {map[string]string{validHash: "1234"}, false}, + + // // One valid choice, one invalid choice. + {map[string]string{validHash: "no", anotherValidHash: "1234"}, false}, + {map[string]string{validHash: "1234", anotherValidHash: "no"}, false}, + + // One valid tspend hash, one invalid tspend hash. + {map[string]string{"fake": "abstain", anotherValidHash: "no"}, false}, + {map[string]string{validHash: "abstain", "": "no"}, false}, + } + + for _, test := range tests { + err := validTSpendPolicy(test.tspendPolicy) + if (err == nil) != test.valid { + t.Fatalf("validTSpendPolicy failed for policy '%v': %v", + test.tspendPolicy, err) + } + } +} + +func TestIsValidTreasuryPolicy(t *testing.T) { + + // A valid treasury key is 33 bytes (66 characters). + validKey := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + anotherValidKey := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + + var tests = []struct { + treasuryPolicy map[string]string + valid bool + }{ + // Empty vote choices are allowed. + {map[string]string{}, true}, + + // Valid treasury key, valid vote choice. + {map[string]string{validKey: "yes"}, true}, + {map[string]string{validKey: ""}, true}, + {map[string]string{validKey: "no", anotherValidKey: "yes"}, true}, + + // Invalid treasury key. + {map[string]string{"": "yes"}, false}, + {map[string]string{"a": "yes"}, false}, + {map[string]string{"non hex characters": "yes"}, false}, + {map[string]string{validKey + "a": "yes"}, false}, + + // Valid treasury key, invalid vote choice. + {map[string]string{validKey: "1234"}, false}, + + // // One valid choice, one invalid choice. + {map[string]string{validKey: "no", anotherValidKey: "1234"}, false}, + {map[string]string{validKey: "1234", anotherValidKey: "no"}, false}, + + // One valid treasury key, one invalid treasury key. + {map[string]string{"fake": "abstain", anotherValidKey: "no"}, false}, + {map[string]string{validKey: "abstain", "": "no"}, false}, + } + + for _, test := range tests { + err := validTreasuryPolicy(test.treasuryPolicy) + if (err == nil) != test.valid { + t.Fatalf("validTreasuryPolicy failed for policy '%v': %v", + test.treasuryPolicy, err) } } } diff --git a/webapi/payfee.go b/webapi/payfee.go index 68d1b16..d735aca 100644 --- a/webapi/payfee.go +++ b/webapi/payfee.go @@ -103,8 +103,9 @@ func payFee(c *gin.Context) { return } - // Validate VoteChoices. Just log a warning if vote choices are not valid - // for the current vote version - the ticket should still be registered. + // Validate voting prefences. Just log a warning if anything is invalid - + // the ticket should still be registered. + validVoteChoices := true err = validConsensusVoteChoices(cfg.NetParams, currentVoteVersion(cfg.NetParams), request.VoteChoices) if err != nil { @@ -113,6 +114,22 @@ func payFee(c *gin.Context) { funcName, c.ClientIP(), ticket.Hash, err) } + validTreasury := true + err = validTreasuryPolicy(request.TreasuryPolicy) + if err != nil { + validTreasury = false + log.Warnf("%s: Invalid treasury policy (clientIP=%s, ticketHash=%s): %v", + funcName, c.ClientIP(), ticket.Hash, err) + } + + validTSpend := true + err = validTSpendPolicy(request.TSpendPolicy) + if err != nil { + validTSpend = false + log.Warnf("%s: Invalid tspend policy (clientIP=%s, ticketHash=%s): %v", + funcName, c.ClientIP(), ticket.Hash, err) + } + // Validate FeeTx. feeTx, err := decodeTransaction(request.FeeTx) if err != nil { @@ -217,6 +234,14 @@ func payFee(c *gin.Context) { ticket.VoteChoices = request.VoteChoices } + if validTSpend { + ticket.TSpendPolicy = request.TSpendPolicy + } + + if validTreasury { + ticket.TreasuryPolicy = request.TreasuryPolicy + } + err = db.UpdateTicket(ticket) if err != nil { log.Errorf("%s: db.UpdateTicket error, failed to set fee tx (ticketHash=%s): %v", diff --git a/webapi/setvotechoices.go b/webapi/setvotechoices.go index 9a527c5..f08348b 100644 --- a/webapi/setvotechoices.go +++ b/webapi/setvotechoices.go @@ -92,6 +92,8 @@ func setVoteChoices(c *gin.Context) { } } + // Validate vote choices (consensus, tspend policy and treasury policy). + err = validConsensusVoteChoices(cfg.NetParams, currentVoteVersion(cfg.NetParams), request.VoteChoices) if err != nil { log.Warnf("%s: Invalid consensus vote choices (clientIP=%s, ticketHash=%s): %v", @@ -100,12 +102,35 @@ func setVoteChoices(c *gin.Context) { return } - // Update VoteChoices in the database before updating the wallets. DB is the - // source of truth, and also is less likely to error. + err = validTreasuryPolicy(request.TreasuryPolicy) + if err != nil { + log.Warnf("%s: Invalid treasury policy (clientIP=%s, ticketHash=%s): %v", + funcName, c.ClientIP(), ticket.Hash, err) + sendErrorWithMsg(err.Error(), errInvalidVoteChoices, c) + } + + err = validTSpendPolicy(request.TSpendPolicy) + if err != nil { + log.Warnf("%s: Invalid tspend policy (clientIP=%s, ticketHash=%s): %v", + funcName, c.ClientIP(), ticket.Hash, err) + sendErrorWithMsg(err.Error(), errInvalidVoteChoices, c) + } + + // Update voting preferences in the database before updating the wallets. DB + // is the source of truth, and also is less likely to error. + for newAgenda, newChoice := range request.VoteChoices { ticket.VoteChoices[newAgenda] = newChoice } + for newTSpend, newChoice := range request.TSpendPolicy { + ticket.TSpendPolicy[newTSpend] = newChoice + } + + for newTreasuryKey, newChoice := range request.TreasuryPolicy { + ticket.TreasuryPolicy[newTreasuryKey] = newChoice + } + err = db.UpdateTicket(ticket) if err != nil { log.Errorf("%s: db.UpdateTicket error, failed to set vote choices (ticketHash=%s): %v", @@ -131,6 +156,23 @@ func setVoteChoices(c *gin.Context) { } } + // Update tspend policy. + for tspend, policy := range ticket.TSpendPolicy { + err = walletClient.SetTSpendPolicy(tspend, policy, ticket.Hash) + if err != nil { + log.Errorf("%s: dcrwallet.SetTSpendPolicy failed (wallet=%s, ticketHash=%s): %v", + funcName, walletClient.String(), ticket.Hash, err) + } + } + + // Update treasury policy. + for key, policy := range ticket.TreasuryPolicy { + err = walletClient.SetTreasuryPolicy(key, policy, ticket.Hash) + if err != nil { + log.Errorf("%s: dcrwallet.SetTreasuryPolicy failed (wallet=%s, ticketHash=%s): %v", + funcName, walletClient.String(), ticket.Hash, err) + } + } } } diff --git a/webapi/templates/footer.html b/webapi/templates/footer.html index b29fdaa..cc65586 100644 --- a/webapi/templates/footer.html +++ b/webapi/templates/footer.html @@ -15,7 +15,7 @@