/ src / ghissuemarket-cli.go
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  }