ghissuemarket-cli.go
1 // ghissuemarket-cli.go 2 3 package main 4 5 import ( 6 "encoding/json" 7 "fmt" 8 "log" 9 "os" 10 "os/exec" 11 "time" 12 13 "github.com/google/uuid" 14 "github.com/spf13/cobra" 15 ) 16 17 // Log file paths 18 var ( 19 logFile = "/var/log/ghissuemarket/ghissuemarket.log" 20 privateLogFile = "/var/log/ghissuemarket/private.log" 21 sysLogFile = "/var/log/ghissuemarket/sys.log" 22 ) 23 24 // Fixed LND paths for cert and macaroon 25 const ( 26 lndTlsCertPath = "/home/lnd/.lnd/tls.cert" // Path to tls.cert 27 lndMacaroonPath = "/home/lnd/.lnd/data/chain/bitcoin/regtest/admin.macaroon" // Path to macaroon 28 ) 29 30 // Auction structure 31 type Auction struct { 32 UUID string `json:"uuid"` // UUID for grouping events 33 AuctionID string `json:"auction_id"` 34 IssueID string `json:"issue_id"` // Added field for issue ID 35 Issue string `json:"issue"` 36 StartingPrice float64 `json:"starting_price"` 37 OpenTime int64 `json:"open_time"` 38 CloseTime int64 `json:"close_time"` 39 AnnouncementTime int64 `json:"announcement_time"` 40 Metadata string `json:"metadata"` 41 State string `json:"state"` 42 Timestamp int64 `json:"timestamp"` 43 AuctioneerPubKey string `json:"auctioneer_pubkey"` 44 } 45 46 // Bid structure 47 type Bid struct { 48 UUID string `json:"uuid"` // UUID for grouping events 49 AuctionID string `json:"auction_id"` 50 BidderID string `json:"bidder_id"` 51 Amount float64 `json:"amount"` 52 Metadata string `json:"metadata"` 53 State string `json:"state"` 54 Timestamp int64 `json:"timestamp"` 55 BidderPubKey string `json:"bidder_pubkey"` 56 } 57 58 // Invoice structure 59 type Invoice struct { 60 UUID string `json:"uuid"` // UUID for the invoice 61 InvoiceID string `json:"invoice_id"` 62 AuctionID string `json:"auction_id"` 63 Amount int64 `json:"amount"` // in satoshis 64 Memo string `json:"memo"` 65 State string `json:"state"` 66 Timestamp int64 `json:"timestamp"` 67 } 68 69 // Issue structure 70 type Issue struct { 71 UUID string `json:"uuid"` // UUID for the issue 72 IssueID string `json:"issue_id"` 73 IssueDescription string `json:"issue_description"` 74 EstimatedCost float64 `json:"estimated_cost"` 75 Metadata string `json:"metadata"` 76 Timestamp int64 `json:"timestamp"` 77 State string `json:"state"` // e.g., Open, Resolved 78 } 79 80 // Main function 81 func main() { 82 var rootCmd = &cobra.Command{ 83 Use: "ghissuemarket", 84 Short: "ghissuemarket is a CLI for managing decentralized auctions, bids, and issues", 85 Run: func(cmd *cobra.Command, args []string) { 86 fmt.Println("Error: Unknown command or invalid usage.") 87 showUsage(cmd) 88 }, 89 SilenceErrors: true, 90 SilenceUsage: true, 91 } 92 93 // Register all commands 94 rootCmd.AddCommand(openAuctionCmd, closeAuctionCmd, placeBidCmd, announceWinnerCmd, addInvoiceCmd, payInvoiceCmd, walletBalanceCmd, queryCmd, addIssueCmd, resolveIssueCmd) 95 96 if err := rootCmd.Execute(); err != nil { 97 fmt.Println("Error: Unknown command or invalid usage. Refer to help below:") 98 rootCmd.Help() 99 os.Exit(1) 100 } 101 } 102 103 // open-auction command 104 var openAuctionCmd = &cobra.Command{ 105 Use: "open-auction", 106 Short: "Auctioneer opens a new auction with specific open and close times", 107 Run: func(cmd *cobra.Command, args []string) { 108 auctionID, _ := cmd.Flags().GetString("auction-id") 109 issueID, _ := cmd.Flags().GetString("issue-id") // Added field for issue ID 110 issue, _ := cmd.Flags().GetString("issue") 111 startingPrice, _ := cmd.Flags().GetFloat64("starting-price") 112 openTime, _ := cmd.Flags().GetInt64("open-time") 113 closeTime, _ := cmd.Flags().GetInt64("close-time") 114 metadata, _ := cmd.Flags().GetString("metadata") 115 116 announcementTime := time.Now().Unix() 117 auctioneerPubKey, err := getPublicKey("open-auction") 118 if err != nil { 119 logError(fmt.Sprintf("Failed to retrieve auctioneer public key for open-auction: %v", err)) 120 return 121 } 122 123 auction := &Auction{ 124 UUID: uuid.New().String(), // Generate UUID for the auction 125 AuctionID: auctionID, 126 IssueID: issueID, 127 Issue: issue, 128 StartingPrice: startingPrice, 129 OpenTime: openTime, 130 CloseTime: closeTime, 131 AnnouncementTime: announcementTime, 132 Metadata: metadata, 133 State: "Open", 134 Timestamp: announcementTime, 135 AuctioneerPubKey: auctioneerPubKey, 136 } 137 138 saveDataToLog(auction, "auction-opened", logFile) 139 fmt.Println(toJSON(auction)) 140 }, 141 } 142 143 // close-auction command 144 var closeAuctionCmd = &cobra.Command{ 145 Use: "close-auction", 146 Short: "Auctioneer closes the auction", 147 Run: func(cmd *cobra.Command, args []string) { 148 auctionID, _ := cmd.Flags().GetString("auction-id") 149 150 auctioneerPubKey, err := getPublicKey("close-auction") 151 if err != nil { 152 logError(fmt.Sprintf("Failed to retrieve auctioneer public key for close-auction: %v", err)) 153 return 154 } 155 156 entry := map[string]interface{}{ 157 "type": "auction-closed", 158 "auction_id": auctionID, 159 "auctioneer_pubkey": auctioneerPubKey, 160 "timestamp": time.Now().Unix(), 161 } 162 saveEntryToLog(entry, logFile) 163 fmt.Printf("Auction %s closed.\n", auctionID) 164 }, 165 } 166 167 // place-bid command 168 var placeBidCmd = &cobra.Command{ 169 Use: "place-bid", 170 Short: "Bidder places a bid on an auction", 171 Run: func(cmd *cobra.Command, args []string) { 172 auctionID, _ := cmd.Flags().GetString("auction-id") 173 bidderID, _ := cmd.Flags().GetString("bidder-id") 174 bidAmount, _ := cmd.Flags().GetFloat64("bid-amount") 175 metadata, _ := cmd.Flags().GetString("metadata") 176 177 bidderPubKey, err := getPublicKey("place-bid") 178 if err != nil { 179 logError(fmt.Sprintf("Failed to retrieve bidder public key for place-bid: %v", err)) 180 return 181 } 182 183 bid := &Bid{ 184 UUID: uuid.New().String(), // Generate UUID for the bid 185 AuctionID: auctionID, 186 BidderID: bidderID, 187 Amount: bidAmount, 188 Metadata: metadata, 189 State: "Placed", 190 Timestamp: time.Now().Unix(), 191 BidderPubKey: bidderPubKey, 192 } 193 194 saveDataToLog(bid, "bid-placed", logFile) 195 fmt.Println(toJSON(bid)) 196 }, 197 } 198 199 // announce-winner command 200 var announceWinnerCmd = &cobra.Command{ 201 Use: "announce-winner", 202 Short: "Auctioneer announces the winner of an auction", 203 Run: func(cmd *cobra.Command, args []string) { 204 auctionID, _ := cmd.Flags().GetString("auction-id") 205 bidderID, _ := cmd.Flags().GetString("bidder-id") 206 207 auctioneerPubKey, err := getPublicKey("announce-winner") 208 if err != nil { 209 logError(fmt.Sprintf("Failed to retrieve auctioneer public key for announce-winner: %v", err)) 210 return 211 } 212 213 entry := map[string]interface{}{ 214 "type": "winner-announced", 215 "auction_id": auctionID, 216 "bidder_id": bidderID, 217 "auctioneer_pubkey": auctioneerPubKey, 218 "timestamp": time.Now().Unix(), 219 } 220 saveEntryToLog(entry, logFile) 221 fmt.Printf("Winner announced for auction %s: Bidder %s\n", auctionID, bidderID) 222 }, 223 } 224 225 // add-invoice command 226 var addInvoiceCmd = &cobra.Command{ 227 Use: "add-invoice", 228 Short: "Bidder creates an invoice for the auctioneer to pay", 229 Run: func(cmd *cobra.Command, args []string) { 230 auctionID, _ := cmd.Flags().GetString("auction-id") 231 amountFloat, _ := cmd.Flags().GetFloat64("amount") 232 memo, _ := cmd.Flags().GetString("memo") 233 234 // Convert amount to satoshis 235 amount := int64(amountFloat * 1000) // Assuming amount is in millisatoshis 236 237 lncliCmd := exec.Command("lncli", 238 "--tlscertpath", lndTlsCertPath, 239 "--macaroonpath", lndMacaroonPath, 240 "addinvoice", 241 fmt.Sprintf("%d", amount), 242 ) 243 lncliOutput, err := lncliCmd.CombinedOutput() 244 if err != nil { 245 logError(fmt.Sprintf("Failed to create invoice using lncli: %v, Output: %s", err, string(lncliOutput))) 246 return 247 } 248 249 var lncliResult map[string]interface{} 250 if err := json.Unmarshal(lncliOutput, &lncliResult); err != nil { 251 logError(fmt.Sprintf("Failed to parse lncli output: %v", err)) 252 return 253 } 254 255 invoiceID := fmt.Sprintf("invoice-%s-%s", auctionID, uuid.New().String()) 256 invoice := &Invoice{ 257 UUID: uuid.New().String(), // Generate UUID for the invoice 258 InvoiceID: invoiceID, 259 AuctionID: auctionID, 260 Amount: amount, 261 Memo: memo, 262 State: "Created", 263 Timestamp: time.Now().Unix(), 264 } 265 266 saveDataToLogWithMetadata(invoice, "invoice-created", lncliResult, logFile) 267 fmt.Println(toJSON(invoice)) 268 }, 269 } 270 271 // pay-invoice command 272 var payInvoiceCmd = &cobra.Command{ 273 Use: "pay-invoice", 274 Short: "Auctioneer pays an invoice, opens/closes channels", 275 Run: func(cmd *cobra.Command, args []string) { 276 paymentRequest, _ := cmd.Flags().GetString("payment-request") 277 bidderPubKey, _ := cmd.Flags().GetString("bidder-pubkey") 278 279 // Open channel 280 openChannelCmd := exec.Command("lncli", 281 "--tlscertpath", lndTlsCertPath, 282 "--macaroonpath", lndMacaroonPath, 283 "openchannel", 284 bidderPubKey, 285 "1000000000000", // Adjust the channel size as needed 286 ) 287 openChannelOutput, err := openChannelCmd.CombinedOutput() 288 if err != nil { 289 logError(fmt.Sprintf("Failed to open channel: %v, Output: %s", err, string(openChannelOutput))) 290 return 291 } 292 293 // Pay invoice 294 lncliPayCmd := exec.Command("lncli", 295 "--tlscertpath", lndTlsCertPath, 296 "--macaroonpath", lndMacaroonPath, 297 "payinvoice", 298 paymentRequest, 299 ) 300 lncliPayOutput, err := lncliPayCmd.CombinedOutput() 301 if err != nil { 302 logError(fmt.Sprintf("Failed to pay invoice: %v, Output: %s", err, string(lncliPayOutput))) 303 return 304 } 305 306 // Confirm payment using lookupinvoice 307 lookupInvoiceCmd := exec.Command("lncli", 308 "--tlscertpath", lndTlsCertPath, 309 "--macaroonpath", lndMacaroonPath, 310 "lookupinvoice", 311 paymentRequest, 312 ) 313 lookupOutput, err := lookupInvoiceCmd.CombinedOutput() 314 if err != nil { 315 logError(fmt.Sprintf("Failed to lookup invoice: %v, Output: %s", err, string(lookupOutput))) 316 return 317 } 318 319 // Parse lookup output to check payment status 320 var lookupResult map[string]interface{} 321 if err := json.Unmarshal(lookupOutput, &lookupResult); err != nil { 322 logError(fmt.Sprintf("Failed to parse lookup invoice output: %v", err)) 323 return 324 } 325 326 // Check the payment status 327 if paid, ok := lookupResult["state"].(string); ok && paid == "SETTLED" { 328 fmt.Printf("Invoice paid successfully. Payment request: %s\n", paymentRequest) 329 } else { 330 fmt.Println("Payment not settled yet or failed.") 331 } 332 333 // Close channel 334 closeChannelCmd := exec.Command("lncli", 335 "--tlscertpath", lndTlsCertPath, 336 "--macaroonpath", lndMacaroonPath, 337 "closechannel", 338 bidderPubKey, 339 ) 340 closeChannelOutput, err := closeChannelCmd.CombinedOutput() 341 if err != nil { 342 logError(fmt.Sprintf("Failed to close channel: %v, Output: %s", err, string(closeChannelOutput))) 343 return 344 } 345 346 // Log the transaction 347 event := map[string]interface{}{ 348 "type": "invoice-paid", 349 "payment_request": paymentRequest, 350 "metadata": string(lncliPayOutput), 351 "timestamp": time.Now().Unix(), 352 } 353 saveEntryToLog(event, privateLogFile) 354 355 // Automatically log wallet balance after transaction 356 logWalletBalance() 357 }, 358 } 359 360 // walletbalance command 361 var walletBalanceCmd = &cobra.Command{ 362 Use: "walletbalance", 363 Short: "Check wallet balance and log to private.log", 364 Run: func(cmd *cobra.Command, args []string) { 365 balance := logWalletBalance() 366 fmt.Printf("Wallet balance: %s\n", balance) 367 }, 368 } 369 370 // query command 371 var queryCmd = &cobra.Command{ 372 Use: "query [query-string]", 373 Short: "Query the environment for feedback", 374 Args: cobra.MinimumNArgs(1), 375 Run: func(cmd *cobra.Command, args []string) { 376 queryStr := args[0] 377 fmt.Printf("Executing query: %s\n", queryStr) 378 379 // Execute the ghissuemarket-feedback_engine command with the query 380 out, err := exec.Command("/usr/local/bin/ghissuemarket-feedback_engine", queryStr).Output() 381 382 if err != nil { 383 logError(fmt.Sprintf("Error executing feedback engine: %v, Output: %s", err, string(out))) 384 fmt.Println("Error executing query. Please check the logs for more details.") 385 return 386 } 387 388 // If output is empty, notify the user 389 if len(out) == 0 { 390 fmt.Println("No response from the feedback engine.") 391 return 392 } 393 394 // Display the query response 395 fmt.Println("Query response:", string(out)) 396 }, 397 } 398 399 // add-issue command 400 var addIssueCmd = &cobra.Command{ 401 Use: "add-issue", 402 Short: "Creates a new issue with an estimated cost", 403 Run: func(cmd *cobra.Command, args []string) { 404 issueID, _ := cmd.Flags().GetString("issue-id") 405 issueDescription, _ := cmd.Flags().GetString("issue-description") 406 estimatedCost, _ := cmd.Flags().GetFloat64("estimated-cost") 407 metadata, _ := cmd.Flags().GetString("metadata") 408 409 issue := &Issue{ 410 UUID: uuid.New().String(), // Generate UUID for the issue 411 IssueID: issueID, 412 IssueDescription: issueDescription, 413 EstimatedCost: estimatedCost, 414 Metadata: metadata, 415 Timestamp: time.Now().Unix(), 416 State: "Open", 417 } 418 419 saveDataToLog(issue, "issue-created", logFile) 420 fmt.Println(toJSON(issue)) 421 }, 422 } 423 424 // resolve-issue command 425 var resolveIssueCmd = &cobra.Command{ 426 Use: "resolve-issue", 427 Short: "Resolve an issue directly without outsourcing", 428 Run: func(cmd *cobra.Command, args []string) { 429 issueID, _ := cmd.Flags().GetString("issue-id") 430 resolutionDetails, _ := cmd.Flags().GetString("resolution-details") 431 432 event := map[string]interface{}{ 433 "type": "issue-resolved", 434 "issue_id": issueID, 435 "resolution_details": resolutionDetails, 436 "timestamp": time.Now().Unix(), 437 } 438 saveEntryToLog(event, logFile) 439 fmt.Printf("Issue %s resolved with details: %s\n", issueID, resolutionDetails) 440 }, 441 } 442 443 // Utility functions 444 445 // saveDataToLog appends a JSON-encoded entry to the specified log file. 446 func saveDataToLog(data interface{}, eventType string, logFile string) { 447 entry := map[string]interface{}{ 448 "type": eventType, 449 "timestamp": time.Now().Unix(), 450 "data": data, 451 } 452 saveEntryToLog(entry, logFile) 453 } 454 455 // saveDataToLogWithMetadata logs data with additional metadata. 456 func saveDataToLogWithMetadata(data interface{}, eventType string, metadata map[string]interface{}, logFile string) { 457 entry := map[string]interface{}{ 458 "type": eventType, 459 "timestamp": time.Now().Unix(), 460 "data": data, 461 "metadata": metadata, 462 } 463 saveEntryToLog(entry, logFile) 464 } 465 466 // saveEntryToLog appends a JSON-encoded entry to the specified log file. 467 func saveEntryToLog(entry map[string]interface{}, logFile string) { 468 logFileHandle, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 469 if err != nil { 470 log.Fatalf("Failed to open log file %s: %v", logFile, err) 471 } 472 defer logFileHandle.Close() 473 474 eventJSON, err := json.Marshal(entry) 475 if err != nil { 476 log.Fatalf("Failed to marshal log entry: %v", err) 477 } 478 479 logFileHandle.Write(eventJSON) 480 logFileHandle.Write([]byte("\n")) 481 } 482 483 // toJSON returns a pretty-printed JSON string of the given data. 484 func toJSON(data interface{}) string { 485 jsonData, err := json.MarshalIndent(data, "", " ") 486 if err != nil { 487 log.Fatalf("Failed to marshal to JSON: %v", err) 488 } 489 return string(jsonData) 490 } 491 492 // logError logs an error message to sys.log with a timestamp and unique event ID. 493 func logError(message string) { 494 event := map[string]interface{}{ 495 "type": "error", 496 "timestamp": time.Now().Unix(), 497 "message": message, 498 } 499 saveEntryToLog(event, sysLogFile) 500 } 501 502 // getPublicKey retrieves the public key using lncli. 503 func getPublicKey(command string) (string, error) { 504 cmd := exec.Command("lncli", 505 "--tlscertpath", lndTlsCertPath, 506 "--macaroonpath", lndMacaroonPath, 507 "getinfo", 508 ) 509 out, err := cmd.Output() 510 if err != nil { 511 return "", fmt.Errorf("lncli getinfo failed for %s: %v", command, err) 512 } 513 514 var result map[string]interface{} 515 if err := json.Unmarshal(out, &result); err != nil { 516 return "", fmt.Errorf("failed to parse lncli getinfo output for %s: %v", command, err) 517 } 518 519 pubKey, ok := result["identity_pubkey"].(string) 520 if !ok { 521 return "", fmt.Errorf("failed to extract public key from lncli getinfo response for %s", command) 522 } 523 524 return pubKey, nil 525 } 526 527 // logWalletBalance retrieves the wallet balance using lncli and logs it to private.log. 528 func logWalletBalance() string { 529 lncliCmd := exec.Command("lncli", 530 "--tlscertpath", lndTlsCertPath, 531 "--macaroonpath", lndMacaroonPath, 532 "walletbalance", 533 ) 534 lncliOutput, err := lncliCmd.Output() 535 if err != nil { 536 logError(fmt.Sprintf("Failed to fetch wallet balance: %v", err)) 537 return "" 538 } 539 540 event := map[string]interface{}{ 541 "type": "wallet-balance", 542 "data": string(lncliOutput), 543 "timestamp": time.Now().Unix(), 544 } 545 saveEntryToLog(event, privateLogFile) 546 547 return string(lncliOutput) 548 } 549 550 // showUsage displays the usage information for the given command. 551 func showUsage(cmd *cobra.Command) { 552 cmd.Help() 553 } 554 555 // Initialize flags 556 func init() { 557 openAuctionCmd.Flags().String("auction-id", "", "Unique ID for the auction") 558 openAuctionCmd.Flags().String("issue-id", "", "Unique ID for the issue") // Added field for issue ID 559 openAuctionCmd.Flags().String("issue", "", "Issue to be auctioned") 560 openAuctionCmd.Flags().Float64("starting-price", 0, "Starting price of the auction") 561 openAuctionCmd.Flags().Int64("open-time", 0, "Unix timestamp for when the auction opens") 562 openAuctionCmd.Flags().Int64("close-time", 0, "Unix timestamp for when the auction closes") 563 openAuctionCmd.Flags().String("metadata", "", "Additional information (e.g., required skills)") 564 565 closeAuctionCmd.Flags().String("auction-id", "", "ID of the auction to be closed") 566 567 placeBidCmd.Flags().String("auction-id", "", "ID of the auction to place a bid on") 568 placeBidCmd.Flags().String("bidder-id", "", "Unique bidder ID") 569 placeBidCmd.Flags().Float64("bid-amount", 0, "Bid amount (in satoshis)") 570 placeBidCmd.Flags().String("metadata", "", "Additional information (e.g., skills)") 571 572 announceWinnerCmd.Flags().String("auction-id", "", "ID of the auction for which to announce the winner") 573 announceWinnerCmd.Flags().String("bidder-id", "", "ID of the winning bidder") 574 575 addInvoiceCmd.Flags().String("auction-id", "", "ID of the auction for which to create an invoice") 576 addInvoiceCmd.Flags().Float64("amount", 0, "Amount of the invoice (in millisatoshis)") 577 addInvoiceCmd.Flags().String("memo", "", "Memo describing the invoice") 578 579 payInvoiceCmd.Flags().String("payment-request", "", "Payment request for the invoice to be paid") 580 payInvoiceCmd.Flags().String("bidder-pubkey", "", "Public key of the bidder") 581 582 addIssueCmd.Flags().String("issue-id", "", "Unique ID for the issue") 583 addIssueCmd.Flags().String("issue-description", "", "Description of the issue") 584 addIssueCmd.Flags().Float64("estimated-cost", 0, "Estimated cost to resolve the issue") 585 addIssueCmd.Flags().String("metadata", "", "Additional information about the issue") 586 587 resolveIssueCmd.Flags().String("issue-id", "", "ID of the issue to resolve") 588 resolveIssueCmd.Flags().String("resolution-details", "", "Details of how the issue was resolved") 589 }