github.com/decred/politeia@v1.4.0/politeiawww/legacy/proposals.go (about) 1 // Copyright (c) 2017-2021 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package legacy 6 7 import ( 8 "context" 9 "encoding/base64" 10 "encoding/hex" 11 "encoding/json" 12 "errors" 13 "io" 14 "strconv" 15 "strings" 16 17 pdv2 "github.com/decred/politeia/politeiad/api/v2" 18 "github.com/decred/politeia/politeiad/backend/gitbe/decredplugin" 19 piplugin "github.com/decred/politeia/politeiad/plugins/pi" 20 "github.com/decred/politeia/politeiad/plugins/ticketvote" 21 tkplugin "github.com/decred/politeia/politeiad/plugins/ticketvote" 22 "github.com/decred/politeia/politeiad/plugins/usermd" 23 umplugin "github.com/decred/politeia/politeiad/plugins/usermd" 24 rcv1 "github.com/decred/politeia/politeiawww/api/records/v1" 25 www "github.com/decred/politeia/politeiawww/api/www/v1" 26 "github.com/decred/politeia/politeiawww/legacy/user" 27 "github.com/decred/politeia/util" 28 "github.com/google/uuid" 29 ) 30 31 func (p *Politeiawww) proposals(ctx context.Context, reqs []pdv2.RecordRequest) (map[string]www.ProposalRecord, error) { 32 // Break the requests up so that they do not exceed the politeiad 33 // records page size. 34 var startIdx int 35 proposals := make(map[string]www.ProposalRecord, len(reqs)) 36 for startIdx < len(reqs) { 37 // Setup a page of requests 38 endIdx := startIdx + int(pdv2.RecordsPageSize) 39 if endIdx > len(reqs) { 40 endIdx = len(reqs) 41 } 42 43 page := reqs[startIdx:endIdx] 44 records, err := p.politeiad.Records(ctx, page) 45 if err != nil { 46 return nil, err 47 } 48 49 // Get records' comment counts 50 tokens := make([]string, 0, len(page)) 51 for _, r := range page { 52 tokens = append(tokens, r.Token) 53 } 54 counts, err := p.politeiad.CommentCount(ctx, tokens) 55 if err != nil { 56 return nil, err 57 } 58 59 for k, v := range records { 60 // Legacy www routes are only for vetted records 61 if v.State == pdv2.RecordStateUnvetted { 62 continue 63 } 64 65 // Convert to a proposal 66 pr, err := convertRecordToProposal(v) 67 if err != nil { 68 return nil, err 69 } 70 71 count := counts[k] 72 pr.NumComments = uint(count) 73 74 // Get submissions list if this is an RFP 75 if pr.LinkBy != 0 { 76 subs, err := p.politeiad.TicketVoteSubmissions(ctx, 77 pr.CensorshipRecord.Token) 78 if err != nil { 79 return nil, err 80 } 81 pr.LinkedFrom = subs 82 } 83 84 // Fill in user data 85 userID := userIDFromMetadataStreams(v.Metadata) 86 uid, err := uuid.Parse(userID) 87 if err != nil { 88 return nil, err 89 } 90 u, err := p.db.UserGetById(uid) 91 if err != nil { 92 return nil, err 93 } 94 pr.Username = u.Username 95 96 proposals[k] = *pr 97 } 98 99 // Update the index 100 startIdx = endIdx 101 } 102 103 return proposals, nil 104 } 105 106 func (p *Politeiawww) processTokenInventory(ctx context.Context, isAdmin bool) (*www.TokenInventoryReply, error) { 107 log.Tracef("processTokenInventory") 108 109 // Get record inventory 110 ir, err := p.politeiad.Inventory(ctx, pdv2.RecordStateInvalid, 111 pdv2.RecordStatusInvalid, 0) 112 if err != nil { 113 return nil, err 114 } 115 116 // Get vote inventory 117 ti := ticketvote.Inventory{} 118 vir, err := p.politeiad.TicketVoteInventory(ctx, ti) 119 if err != nil { 120 return nil, err 121 } 122 123 var ( 124 // Unvetted 125 statusUnreviewed = pdv2.RecordStatuses[pdv2.RecordStatusUnreviewed] 126 statusCensored = pdv2.RecordStatuses[pdv2.RecordStatusCensored] 127 statusArchived = pdv2.RecordStatuses[pdv2.RecordStatusArchived] 128 129 unreviewed = ir.Unvetted[statusUnreviewed] 130 censored = ir.Unvetted[statusCensored] 131 132 // Human readable vote statuses 133 statusUnauth = tkplugin.VoteStatuses[tkplugin.VoteStatusUnauthorized] 134 statusAuth = tkplugin.VoteStatuses[tkplugin.VoteStatusAuthorized] 135 statusStarted = tkplugin.VoteStatuses[tkplugin.VoteStatusStarted] 136 statusApproved = tkplugin.VoteStatuses[tkplugin.VoteStatusApproved] 137 statusRejected = tkplugin.VoteStatuses[tkplugin.VoteStatusRejected] 138 139 // Vetted 140 unauth = vir.Tokens[statusUnauth] 141 auth = vir.Tokens[statusAuth] 142 pre = append(unauth, auth...) 143 active = vir.Tokens[statusStarted] 144 approved = vir.Tokens[statusApproved] 145 rejected = vir.Tokens[statusRejected] 146 abandoned = ir.Vetted[statusArchived] 147 ) 148 149 // Only return unvetted tokens to admins 150 if isAdmin { 151 unreviewed = []string{} 152 censored = []string{} 153 } 154 155 // Return empty arrays and not nils 156 if unreviewed == nil { 157 unreviewed = []string{} 158 } 159 if censored == nil { 160 censored = []string{} 161 } 162 if pre == nil { 163 pre = []string{} 164 } 165 if active == nil { 166 active = []string{} 167 } 168 if approved == nil { 169 approved = []string{} 170 } 171 if rejected == nil { 172 rejected = []string{} 173 } 174 if abandoned == nil { 175 abandoned = []string{} 176 } 177 178 return &www.TokenInventoryReply{ 179 Unreviewed: unreviewed, 180 Censored: censored, 181 Pre: pre, 182 Active: active, 183 Approved: approved, 184 Rejected: rejected, 185 Abandoned: abandoned, 186 }, nil 187 } 188 189 func (p *Politeiawww) processAllVetted(ctx context.Context, gav www.GetAllVetted) (*www.GetAllVettedReply, error) { 190 log.Tracef("processAllVetted: %v %v", gav.Before, gav.After) 191 192 // NOTE: this route is not scalable and needs to be removed ASAP. 193 // It only needs to be supported to give dcrdata a change to switch 194 // to the records API. 195 196 // The Before and After arguments are NO LONGER SUPPORTED. This 197 // route will only return a single page of vetted tokens. The 198 // records API InventoryOrdered command should be used instead. 199 tokens, err := p.politeiad.InventoryOrdered(ctx, pdv2.RecordStateVetted, 1) 200 if err != nil { 201 return nil, err 202 } 203 204 // Get the proposals without any files 205 reqs := make([]pdv2.RecordRequest, 0, pdv2.RecordsPageSize) 206 for _, v := range tokens { 207 reqs = append(reqs, pdv2.RecordRequest{ 208 Token: v, 209 Filenames: []string{ 210 piplugin.FileNameProposalMetadata, 211 tkplugin.FileNameVoteMetadata, 212 }, 213 }) 214 } 215 props, err := p.proposals(ctx, reqs) 216 if err != nil { 217 return nil, err 218 } 219 220 // Covert proposal map to an slice 221 proposals := make([]www.ProposalRecord, 0, len(props)) 222 for _, v := range tokens { 223 pr, ok := props[v] 224 if !ok { 225 continue 226 } 227 proposals = append(proposals, pr) 228 } 229 230 return &www.GetAllVettedReply{ 231 Proposals: proposals, 232 }, nil 233 } 234 235 func (p *Politeiawww) processProposalDetails(ctx context.Context, pd www.ProposalsDetails, u *user.User) (*www.ProposalDetailsReply, error) { 236 log.Tracef("processProposalDetails: %v", pd.Token) 237 238 // Parse version 239 var version uint64 240 var err error 241 if pd.Version != "" { 242 version, err = strconv.ParseUint(pd.Version, 10, 64) 243 if err != nil { 244 return nil, www.UserError{ 245 ErrorCode: www.ErrorStatusProposalNotFound, 246 } 247 } 248 } 249 250 // Get proposal 251 reqs := []pdv2.RecordRequest{ 252 { 253 Token: pd.Token, 254 Version: uint32(version), 255 }, 256 } 257 prs, err := p.proposals(ctx, reqs) 258 if err != nil { 259 return nil, err 260 } 261 pr, ok := prs[pd.Token] 262 if !ok { 263 return nil, www.UserError{ 264 ErrorCode: www.ErrorStatusProposalNotFound, 265 } 266 } 267 268 return &www.ProposalDetailsReply{ 269 Proposal: pr, 270 }, nil 271 } 272 273 func (p *Politeiawww) processBatchProposals(ctx context.Context, bp www.BatchProposals, u *user.User) (*www.BatchProposalsReply, error) { 274 log.Tracef("processBatchProposals: %v", bp.Tokens) 275 276 if len(bp.Tokens) > www.ProposalListPageSize { 277 return nil, www.UserError{ 278 ErrorCode: www.ErrorStatusMaxProposalsExceededPolicy, 279 } 280 } 281 282 // Get the proposals batch 283 reqs := make([]pdv2.RecordRequest, 0, len(bp.Tokens)) 284 for _, v := range bp.Tokens { 285 reqs = append(reqs, pdv2.RecordRequest{ 286 Token: v, 287 Filenames: []string{ 288 piplugin.FileNameProposalMetadata, 289 tkplugin.FileNameVoteMetadata, 290 }, 291 }) 292 } 293 props, err := p.proposals(ctx, reqs) 294 if err != nil { 295 return nil, err 296 } 297 298 // Return the proposals in the same order they were requests in. 299 proposals := make([]www.ProposalRecord, 0, len(props)) 300 for _, v := range bp.Tokens { 301 pr, ok := props[v] 302 if !ok { 303 continue 304 } 305 proposals = append(proposals, pr) 306 } 307 308 return &www.BatchProposalsReply{ 309 Proposals: proposals, 310 }, nil 311 } 312 313 func (p *Politeiawww) processBatchVoteSummary(ctx context.Context, bvs www.BatchVoteSummary) (*www.BatchVoteSummaryReply, error) { 314 log.Tracef("processBatchVoteSummary: %v", bvs.Tokens) 315 316 if len(bvs.Tokens) > www.ProposalListPageSize { 317 return nil, www.UserError{ 318 ErrorCode: www.ErrorStatusMaxProposalsExceededPolicy, 319 } 320 } 321 322 // Get vote summaries 323 vs, err := p.politeiad.TicketVoteSummaries(ctx, bvs.Tokens) 324 if err != nil { 325 return nil, err 326 } 327 328 // Prepare reply 329 var bestBlock uint32 330 summaries := make(map[string]www.VoteSummary, len(vs)) 331 for token, v := range vs { 332 bestBlock = v.BestBlock 333 results := make([]www.VoteOptionResult, len(v.Results)) 334 for k, r := range v.Results { 335 results[k] = www.VoteOptionResult{ 336 VotesReceived: r.Votes, 337 Option: www.VoteOption{ 338 Id: r.ID, 339 Description: r.Description, 340 Bits: r.VoteBit, 341 }, 342 } 343 } 344 summaries[token] = www.VoteSummary{ 345 Status: convertVoteStatusToWWW(v.Status), 346 Type: convertVoteTypeToWWW(v.Type), 347 Approved: v.Status == tkplugin.VoteStatusApproved, 348 EligibleTickets: v.EligibleTickets, 349 Duration: v.Duration, 350 EndHeight: uint64(v.EndBlockHeight), 351 QuorumPercentage: v.QuorumPercentage, 352 PassPercentage: v.PassPercentage, 353 Results: results, 354 } 355 } 356 357 return &www.BatchVoteSummaryReply{ 358 Summaries: summaries, 359 BestBlock: uint64(bestBlock), 360 }, nil 361 } 362 363 func (p *Politeiawww) processVoteStatus(ctx context.Context, token string) (*www.VoteStatusReply, error) { 364 log.Tracef("processVoteStatus") 365 366 // Get vote summaries 367 summaries, err := p.politeiad.TicketVoteSummaries(ctx, []string{token}) 368 if err != nil { 369 return nil, err 370 } 371 s, ok := summaries[token] 372 if !ok { 373 return nil, www.UserError{ 374 ErrorCode: www.ErrorStatusProposalNotFound, 375 } 376 } 377 vsr := convertVoteStatusReply(token, s) 378 379 return &vsr, nil 380 } 381 382 func (p *Politeiawww) processAllVoteStatus(ctx context.Context) (*www.GetAllVoteStatusReply, error) { 383 log.Tracef("processAllVoteStatus") 384 385 // NOTE: This route is suppose to return the vote status of all 386 // public proposals. This is horrendously unscalable. We are only 387 // supporting this route until dcrdata has a chance to update and 388 // use the ticketvote API. Until then, we only return a single page 389 // of vote statuses. 390 391 // Get a page of vetted records 392 tokens, err := p.politeiad.InventoryOrdered(ctx, pdv2.RecordStateVetted, 1) 393 if err != nil { 394 return nil, err 395 } 396 397 // Get vote summaries 398 vs, err := p.politeiad.TicketVoteSummaries(ctx, tokens) 399 if err != nil { 400 return nil, err 401 } 402 403 // Prepare reply 404 statuses := make([]www.VoteStatusReply, 0, len(vs)) 405 for token, v := range vs { 406 statuses = append(statuses, convertVoteStatusReply(token, v)) 407 } 408 409 return &www.GetAllVoteStatusReply{ 410 VotesStatus: statuses, 411 }, nil 412 } 413 414 func convertVoteDetails(vd tkplugin.VoteDetails) (www.StartVote, www.StartVoteReply) { 415 options := make([]www.VoteOption, 0, len(vd.Params.Options)) 416 for _, v := range vd.Params.Options { 417 options = append(options, www.VoteOption{ 418 Id: v.ID, 419 Description: v.Description, 420 Bits: v.Bit, 421 }) 422 } 423 sv := www.StartVote{ 424 Vote: www.Vote{ 425 Token: vd.Params.Token, 426 Mask: vd.Params.Mask, 427 Duration: vd.Params.Duration, 428 QuorumPercentage: vd.Params.QuorumPercentage, 429 PassPercentage: vd.Params.PassPercentage, 430 Options: options, 431 }, 432 PublicKey: vd.PublicKey, 433 Signature: vd.Signature, 434 } 435 svr := www.StartVoteReply{ 436 StartBlockHeight: strconv.FormatUint(uint64(vd.StartBlockHeight), 10), 437 StartBlockHash: vd.StartBlockHash, 438 EndHeight: strconv.FormatUint(uint64(vd.EndBlockHeight), 10), 439 EligibleTickets: vd.EligibleTickets, 440 } 441 442 return sv, svr 443 } 444 445 func (p *Politeiawww) processActiveVote(ctx context.Context) (*www.ActiveVoteReply, error) { 446 log.Tracef("processActiveVotes") 447 448 // Get a page of ongoing votes. This route is deprecated and should 449 // be deleted before the time comes when more than a page of ongoing 450 // votes is required. 451 i := ticketvote.Inventory{} 452 ir, err := p.politeiad.TicketVoteInventory(ctx, i) 453 if err != nil { 454 return nil, err 455 } 456 s := ticketvote.VoteStatuses[ticketvote.VoteStatusStarted] 457 started := ir.Tokens[s] 458 459 if len(started) == 0 { 460 // No active votes 461 return &www.ActiveVoteReply{ 462 Votes: []www.ProposalVoteTuple{}, 463 }, nil 464 } 465 466 // Get proposals 467 reqs := make([]pdv2.RecordRequest, 0, pdv2.RecordsPageSize) 468 for _, v := range started { 469 reqs = append(reqs, pdv2.RecordRequest{ 470 Token: v, 471 Filenames: []string{ 472 piplugin.FileNameProposalMetadata, 473 tkplugin.FileNameVoteMetadata, 474 }, 475 }) 476 } 477 props, err := p.proposals(ctx, reqs) 478 if err != nil { 479 return nil, err 480 } 481 482 // Get vote details 483 voteDetails := make(map[string]tkplugin.VoteDetails, len(started)) 484 for _, v := range started { 485 dr, err := p.politeiad.TicketVoteDetails(ctx, v) 486 if err != nil { 487 return nil, err 488 } 489 if dr.Vote == nil { 490 continue 491 } 492 voteDetails[v] = *dr.Vote 493 } 494 495 // Prepare reply 496 votes := make([]www.ProposalVoteTuple, 0, len(started)) 497 for _, v := range started { 498 var ( 499 proposal www.ProposalRecord 500 sv www.StartVote 501 svr www.StartVoteReply 502 ok bool 503 ) 504 proposal, ok = props[v] 505 if !ok { 506 continue 507 } 508 vd, ok := voteDetails[v] 509 if ok { 510 sv, svr = convertVoteDetails(vd) 511 votes = append(votes, www.ProposalVoteTuple{ 512 Proposal: proposal, 513 StartVote: sv, 514 StartVoteReply: svr, 515 }) 516 } 517 } 518 519 return &www.ActiveVoteReply{ 520 Votes: votes, 521 }, nil 522 } 523 524 func (p *Politeiawww) processCastVotes(ctx context.Context, ballot *www.Ballot) (*www.BallotReply, error) { 525 log.Tracef("processCastVotes") 526 527 // Verify there is work to do 528 if len(ballot.Votes) == 0 { 529 return &www.BallotReply{ 530 Receipts: []www.CastVoteReply{}, 531 }, nil 532 } 533 534 // Prepare plugin command 535 votes := make([]tkplugin.CastVote, 0, len(ballot.Votes)) 536 var token string 537 for _, v := range ballot.Votes { 538 token = v.Token 539 votes = append(votes, tkplugin.CastVote{ 540 Token: v.Token, 541 Ticket: v.Ticket, 542 VoteBit: v.VoteBit, 543 Signature: v.Signature, 544 }) 545 } 546 cb := tkplugin.CastBallot{ 547 Ballot: votes, 548 } 549 550 // Send plugin command 551 cbr, err := p.politeiad.TicketVoteCastBallot(ctx, token, cb) 552 if err != nil { 553 return nil, err 554 } 555 556 // Prepare reply 557 receipts := make([]www.CastVoteReply, 0, len(cbr.Receipts)) 558 for k, v := range cbr.Receipts { 559 receipts = append(receipts, www.CastVoteReply{ 560 ClientSignature: ballot.Votes[k].Signature, 561 Signature: v.Receipt, 562 Error: v.ErrorContext, 563 ErrorStatus: convertVoteErrorCodeToWWW(v.ErrorCode), 564 }) 565 } 566 567 return &www.BallotReply{ 568 Receipts: receipts, 569 }, nil 570 } 571 572 func (p *Politeiawww) processVoteResults(ctx context.Context, token string) (*www.VoteResultsReply, error) { 573 log.Tracef("processVoteResults: %v", token) 574 575 // Get vote details 576 dr, err := p.politeiad.TicketVoteDetails(ctx, token) 577 if err != nil { 578 return nil, err 579 } 580 if dr.Vote == nil { 581 return &www.VoteResultsReply{}, nil 582 } 583 sv, svr := convertVoteDetails(*dr.Vote) 584 585 // Get cast votes 586 rr, err := p.politeiad.TicketVoteResults(ctx, token) 587 if err != nil { 588 return nil, err 589 } 590 591 // Convert to www 592 votes := make([]www.CastVote, 0, len(rr.Votes)) 593 for _, v := range rr.Votes { 594 votes = append(votes, www.CastVote{ 595 Token: v.Token, 596 Ticket: v.Ticket, 597 VoteBit: v.VoteBit, 598 Signature: v.Signature, 599 }) 600 } 601 602 return &www.VoteResultsReply{ 603 StartVote: sv, 604 StartVoteReply: svr, 605 CastVotes: votes, 606 }, nil 607 } 608 609 // userMetadataDecode decodes and returns the UserMetadata from the provided 610 // metadata streams. If a UserMetadata is not found, nil is returned. 611 func userMetadataDecode(ms []pdv2.MetadataStream) (*umplugin.UserMetadata, error) { 612 var userMD *umplugin.UserMetadata 613 for _, v := range ms { 614 if v.PluginID != usermd.PluginID || 615 v.StreamID != umplugin.StreamIDUserMetadata { 616 // This is not user metadata 617 continue 618 } 619 var um umplugin.UserMetadata 620 err := json.Unmarshal([]byte(v.Payload), &um) 621 if err != nil { 622 return nil, err 623 } 624 userMD = &um 625 break 626 } 627 return userMD, nil 628 } 629 630 // userIDFromMetadataStreams searches for a UserMetadata and parses the user ID 631 // from it if found. An empty string is returned if no UserMetadata is found. 632 func userIDFromMetadataStreams(ms []pdv2.MetadataStream) string { 633 um, err := userMetadataDecode(ms) 634 if err != nil { 635 return "" 636 } 637 if um == nil { 638 return "" 639 } 640 return um.UserID 641 } 642 643 func convertStatusToWWW(status pdv2.RecordStatusT) www.PropStatusT { 644 switch status { 645 case pdv2.RecordStatusInvalid: 646 return www.PropStatusInvalid 647 case pdv2.RecordStatusPublic: 648 return www.PropStatusPublic 649 case pdv2.RecordStatusCensored: 650 return www.PropStatusCensored 651 case pdv2.RecordStatusArchived: 652 return www.PropStatusAbandoned 653 default: 654 return www.PropStatusInvalid 655 } 656 } 657 658 func convertRecordToProposal(r pdv2.Record) (*www.ProposalRecord, error) { 659 // Decode metadata 660 var ( 661 um *umplugin.UserMetadata 662 statuses = make([]umplugin.StatusChangeMetadata, 0, 16) 663 ) 664 for _, v := range r.Metadata { 665 if v.PluginID != umplugin.PluginID { 666 continue 667 } 668 669 // This is a usermd plugin metadata stream 670 switch v.StreamID { 671 case umplugin.StreamIDUserMetadata: 672 var m umplugin.UserMetadata 673 err := json.Unmarshal([]byte(v.Payload), &m) 674 if err != nil { 675 return nil, err 676 } 677 um = &m 678 case umplugin.StreamIDStatusChanges: 679 d := json.NewDecoder(strings.NewReader(v.Payload)) 680 for { 681 var sc umplugin.StatusChangeMetadata 682 err := d.Decode(&sc) 683 if errors.Is(err, io.EOF) { 684 break 685 } else if err != nil { 686 return nil, err 687 } 688 statuses = append(statuses, sc) 689 } 690 } 691 } 692 693 // Convert files 694 var ( 695 name, linkTo string 696 linkBy int64 697 files = make([]www.File, 0, len(r.Files)) 698 ) 699 for _, v := range r.Files { 700 switch v.Name { 701 case piplugin.FileNameProposalMetadata: 702 b, err := base64.StdEncoding.DecodeString(v.Payload) 703 if err != nil { 704 return nil, err 705 } 706 var pm piplugin.ProposalMetadata 707 err = json.Unmarshal(b, &pm) 708 if err != nil { 709 return nil, err 710 } 711 name = pm.Name 712 713 case tkplugin.FileNameVoteMetadata: 714 b, err := base64.StdEncoding.DecodeString(v.Payload) 715 if err != nil { 716 return nil, err 717 } 718 var vm tkplugin.VoteMetadata 719 err = json.Unmarshal(b, &vm) 720 if err != nil { 721 return nil, err 722 } 723 linkTo = vm.LinkTo 724 linkBy = vm.LinkBy 725 726 default: 727 files = append(files, www.File{ 728 Name: v.Name, 729 MIME: v.MIME, 730 Digest: v.Digest, 731 Payload: v.Payload, 732 }) 733 } 734 } 735 736 // Setup user defined metadata 737 pm := www.ProposalMetadata{ 738 Name: name, 739 LinkTo: linkTo, 740 LinkBy: linkBy, 741 } 742 b, err := json.Marshal(pm) 743 if err != nil { 744 return nil, err 745 } 746 metadata := []www.Metadata{ 747 { 748 Digest: hex.EncodeToString(util.Digest(b)), 749 Hint: www.HintProposalMetadata, 750 Payload: base64.StdEncoding.EncodeToString(b), 751 }, 752 } 753 754 var ( 755 publishedAt, censoredAt, abandonedAt int64 756 changeMsg string 757 changeMsgTimestamp int64 758 ) 759 for _, v := range statuses { 760 if v.Timestamp > changeMsgTimestamp { 761 changeMsg = v.Reason 762 changeMsgTimestamp = v.Timestamp 763 } 764 switch rcv1.RecordStatusT(v.Status) { 765 case rcv1.RecordStatusPublic: 766 publishedAt = v.Timestamp 767 case rcv1.RecordStatusCensored: 768 censoredAt = v.Timestamp 769 case rcv1.RecordStatusArchived: 770 abandonedAt = v.Timestamp 771 } 772 } 773 774 return &www.ProposalRecord{ 775 Name: pm.Name, 776 State: www.PropStateVetted, 777 Status: convertStatusToWWW(r.Status), 778 Timestamp: r.Timestamp, 779 UserId: um.UserID, 780 Username: "", // Intentionally omitted 781 PublicKey: um.PublicKey, 782 Signature: um.Signature, 783 Version: strconv.FormatUint(uint64(r.Version), 10), 784 StatusChangeMessage: changeMsg, 785 PublishedAt: publishedAt, 786 CensoredAt: censoredAt, 787 AbandonedAt: abandonedAt, 788 LinkTo: pm.LinkTo, 789 LinkBy: pm.LinkBy, 790 LinkedFrom: []string{}, 791 Files: files, 792 Metadata: metadata, 793 CensorshipRecord: www.CensorshipRecord{ 794 Token: r.CensorshipRecord.Token, 795 Merkle: r.CensorshipRecord.Merkle, 796 Signature: r.CensorshipRecord.Signature, 797 }, 798 }, nil 799 } 800 801 func convertVoteStatusToWWW(status tkplugin.VoteStatusT) www.PropVoteStatusT { 802 switch status { 803 case tkplugin.VoteStatusInvalid: 804 return www.PropVoteStatusInvalid 805 case tkplugin.VoteStatusUnauthorized: 806 return www.PropVoteStatusNotAuthorized 807 case tkplugin.VoteStatusAuthorized: 808 return www.PropVoteStatusAuthorized 809 case tkplugin.VoteStatusStarted: 810 return www.PropVoteStatusStarted 811 case tkplugin.VoteStatusFinished: 812 return www.PropVoteStatusFinished 813 case tkplugin.VoteStatusApproved: 814 return www.PropVoteStatusFinished 815 case tkplugin.VoteStatusRejected: 816 return www.PropVoteStatusFinished 817 default: 818 return www.PropVoteStatusInvalid 819 } 820 } 821 822 func convertVoteTypeToWWW(t tkplugin.VoteT) www.VoteT { 823 switch t { 824 case tkplugin.VoteTypeInvalid: 825 return www.VoteTypeInvalid 826 case tkplugin.VoteTypeStandard: 827 return www.VoteTypeStandard 828 case tkplugin.VoteTypeRunoff: 829 return www.VoteTypeRunoff 830 default: 831 return www.VoteTypeInvalid 832 } 833 } 834 835 func convertVoteErrorCodeToWWW(e *tkplugin.VoteErrorT) decredplugin.ErrorStatusT { 836 if e == nil { 837 return decredplugin.ErrorStatusInvalid 838 } 839 switch *e { 840 case tkplugin.VoteErrorInvalid: 841 return decredplugin.ErrorStatusInvalid 842 case tkplugin.VoteErrorInternalError: 843 return decredplugin.ErrorStatusInternalError 844 case tkplugin.VoteErrorRecordNotFound: 845 return decredplugin.ErrorStatusProposalNotFound 846 case tkplugin.VoteErrorMultipleRecordVotes: 847 // There is not decredplugin error code for this 848 case tkplugin.VoteErrorVoteStatusInvalid: 849 return decredplugin.ErrorStatusVoteHasEnded 850 case tkplugin.VoteErrorVoteBitInvalid: 851 return decredplugin.ErrorStatusInvalidVoteBit 852 case tkplugin.VoteErrorSignatureInvalid: 853 // There is not decredplugin error code for this 854 case tkplugin.VoteErrorTicketNotEligible: 855 return decredplugin.ErrorStatusIneligibleTicket 856 case tkplugin.VoteErrorTicketAlreadyVoted: 857 return decredplugin.ErrorStatusDuplicateVote 858 default: 859 } 860 return decredplugin.ErrorStatusInternalError 861 } 862 863 func convertVoteStatusReply(token string, s tkplugin.SummaryReply) www.VoteStatusReply { 864 results := make([]www.VoteOptionResult, 0, len(s.Results)) 865 var totalVotes uint64 866 for _, v := range s.Results { 867 totalVotes += v.Votes 868 results = append(results, www.VoteOptionResult{ 869 VotesReceived: v.Votes, 870 Option: www.VoteOption{ 871 Id: v.ID, 872 Description: v.Description, 873 Bits: v.VoteBit, 874 }, 875 }) 876 } 877 return www.VoteStatusReply{ 878 Token: token, 879 Status: convertVoteStatusToWWW(s.Status), 880 TotalVotes: totalVotes, 881 OptionsResult: results, 882 EndHeight: strconv.FormatUint(uint64(s.EndBlockHeight), 10), 883 BestBlock: strconv.FormatUint(uint64(s.BestBlock), 10), 884 NumOfEligibleVotes: int(s.EligibleTickets), 885 QuorumPercentage: s.QuorumPercentage, 886 PassPercentage: s.PassPercentage, 887 } 888 }