diff --git a/client/client.go b/client/client.go index 08fdd84..449bd75 100644 --- a/client/client.go +++ b/client/client.go @@ -11,6 +11,7 @@ import ( "net/http" "regexp" "strconv" + "time" ) // The Client struct maintains the state of the current client. To use a client from session to session, the Pem and Token will need to be saved and used in the next client. The ClientId can be recreated by using the key_util.GenerateSinFromPem func, and the ApiUri will generally be https://bitpay.com. Insecure should generally be set to false or not set at all, there are a limited number of test scenarios in which it must be set to true. @@ -32,6 +33,142 @@ type Token struct { PairingCode string } +type Invoice struct { + Addresses *PaymentDisplay `json:"addresses,omitempty"` + AmountPaid int `json:"amountPaid,omitempty"` + BitcoinAddress string `json:"bitcoinAddress,omitempty"` + BtcDue string `json:"btcDue,omitempty"` + BtcPaid string `json:"btcPaid,omitempty"` + BtcPrice string `json:"btcPrice,omitempty"` + Buyer *Buyer `json:"buyer,omitempty"` + BuyerPaidBtcMinerFee interface{} `json:"buyerPaidBtcMinerFee,omitempty"` + BuyerTotalBtcAmount interface{} `json:"buyerTotalBtcAmount,omitempty"` + CryptoInfo []struct { + Address string `json:"address,omitempty"` + CryptoCode string `json:"cryptoCode,omitempty"` + CryptoPaid string `json:"cryptoPaid,omitempty"` + Due string `json:"due,omitempty"` + ExRates struct { + USD int `json:"USD,omitempty"` + } `json:"exRates,omitempty"` + NetworkFee string `json:"networkFee,omitempty"` + Paid string `json:"paid,omitempty"` + PaymentType string `json:"paymentType,omitempty"` + PaymentUrls struct { + BIP21 string `json:"BIP21,omitempty"` + BIP72 interface{} `json:"BIP72,omitempty"` + BIP72B interface{} `json:"BIP72b,omitempty"` + BIP73 interface{} `json:"BIP73,omitempty"` + BOLT11 interface{} `json:"BOLT11,omitempty"` + } `json:"paymentUrls,omitempty"` + Payments []interface{} `json:"payments,omitempty"` + Price string `json:"price,omitempty"` + Rate float64 `json:"rate,omitempty"` + TotalDue string `json:"totalDue,omitempty"` + TxCount int `json:"txCount,omitempty"` + URL string `json:"url,omitempty"` + } `json:"cryptoInfo,omitempty"` + Currency string `json:"currency,omitempty"` + CurrentTime int64 `json:"currentTime,omitempty"` + /* + ExRates struct { + USD int `json:"USD,omitempty"` + } `json:"exRates,omitempty"` + */ + + ExceptionStatus bool `json:"exceptionStatus,omitempty"` + /* + ExchangeRates struct { + BTC struct { + USD int `json:"USD,omitempty"` + } `json:"BTC,omitempty"` + } `json:"exchangeRates,omitempty"` + */ + + ExpirationTime int64 `json:"expirationTime,omitempty"` + /* + Flags struct { + Refundable bool `json:"refundable,omitempty"` + } `json:"flags,omitempty"` + */ + GUID string `json:"guid,omitempty"` + ID string `json:"id,omitempty"` + InvoiceTime int64 `json:"invoiceTime,omitempty"` + ItemCode interface{} `json:"itemCode,omitempty"` + ItemDesc interface{} `json:"itemDesc,omitempty"` + LowFeeDetected bool `json:"lowFeeDetected,omitempty"` + /*MinerFees struct { + BTC struct { + SatoshisPerByte int `json:"satoshisPerByte,omitempty"` + TotalFee int `json:"totalFee,omitempty"` + } `json:"BTC,omitempty"` + } `json:"minerFees,omitempty"` + */ + OrderID interface{} `json:"orderId,omitempty"` + /*PaymentCodes struct { + BTC struct { + BIP21 string `json:"BIP21,omitempty"` + BIP72 interface{} `json:"BIP72,omitempty"` + BIP72B interface{} `json:"BIP72b,omitempty"` + BIP73 interface{} `json:"BIP73,omitempty"` + BOLT11 interface{} `json:"BOLT11,omitempty"` + } `json:"BTC,omitempty"` + } `json:"paymentCodes,omitempty"` + */ + /*PaymentSubtotals struct { + BTC int `json:"BTC,omitempty"` + } `json:"paymentSubtotals,omitempty"` + PaymentTotals struct { + BTC int `json:"BTC,omitempty"` + } `json:"paymentTotals,omitempty"` + PaymentUrls struct { + BIP21 string `json:"BIP21,omitempty"` + BIP72 interface{} `json:"BIP72,omitempty"` + BIP72B interface{} `json:"BIP72b,omitempty"` + BIP73 interface{} `json:"BIP73,omitempty"` + BOLT11 interface{} `json:"BOLT11,omitempty"` + } `json:"paymentUrls,omitempty"` + */ + PosData interface{} `json:"posData,omitempty"` + Price float64 `json:"price,omitempty"` + Rate float64 `json:"rate,omitempty"` + RefundAddressRequestPending bool `json:"refundAddressRequestPending,omitempty"` + Status string `json:"status,omitempty"` + /*SupportedTransactionCurrencies struct { + BTC struct { + Enabled bool `json:"enabled"` + Reason interface{} `json:"reason"` + } `json:"BTC"` + } `json:"supportedTransactionCurrencies,omitempty"` + */ + PaymentCurrencies []string `json:"paymentCurrencies,omitempty"` + Token string `json:"token,omitempty"` + URL string `json:"url,omitempty"` +} + +type PaymentDisplay struct { + Btc string `json:"BTC,omitempty"` + Bch string `json:"BCH,omitempty"` + Eth string `json:"ETH,omitempty"` + Gusd string `json:"GUSD,omitempty"` + Pax string `json:"PAX,omitempty"` + Busd string `json:"BUSD,omitempty"` + Usdc string `json:"USDC,omitempty"` + Xrp string `json:"XRP,omitempty"` +} + +type Buyer struct { + Address1 interface{} `json:"address1,omitempty"` + Address2 interface{} `json:"address2,omitempty"` + Country interface{} `json:"country,omitempty"` + Email interface{} `json:"email,omitempty"` + Locality interface{} `json:"locality,omitempty"` + Name interface{} `json:"name,omitempty"` + Phone interface{} `json:"phone,omitempty"` + PostalCode interface{} `json:"postalCode,omitempty"` + Region interface{} `json:"region,omitempty"` +} + // Go struct mapping the JSON returned from the BitPay server when sending a POST or GET request to /invoices. type invoice struct { @@ -54,28 +191,70 @@ type invoice struct { Token string } +type Payout struct { + Id string + Account string + Reference string + SupportPhone string + Status string + Amount float64 + PercentFee float64 + Fee float64 + DepositTotal float64 + Btc float64 + Currency string + RequestDate time.Time + EffectiveDate time.Time + NotificationUrl string + NotificationEmail string + Instructions []Instruction + Token string +} + +type Btc struct { + Unpaid int + Paid int +} + +type Instruction struct { + Id string + Amount float64 + Btc Btc + Address string + Label string + Status string + WalletProvider string + Receiverinfo Receiverinfo +} + +type Receiverinfo struct { + Name string + EmailAddress string + Address Address +} + +type Address struct { + StreetAddress1 string + StreetAddress2 string + Locality string + Region string + PostalCode string + Country string +} + // CreateInvoice returns an invoice type or pass the error from the server. The method will create an invoice on the BitPay server. -func (client *Client) CreateInvoice(price float64, currency string) (inv invoice, err error) { - match, _ := regexp.MatchString("^[[:upper:]]{3}$", currency) +func (client *Client) CreateInvoice(i Invoice) (inv Invoice, err error) { + match, _ := regexp.MatchString("^[[:upper:]]{3}$", i.Currency) if !match { err = errors.New("BitPayArgumentError: invalid currency code") return inv, err } - paylo := make(map[string]string) - var floatPrec int - if currency == "BTC" { - floatPrec = 8 - } else { - floatPrec = 2 - } - priceString := strconv.FormatFloat(price, 'f', floatPrec, 64) - paylo["price"] = priceString - paylo["currency"] = currency - paylo["token"] = client.Token.Token - paylo["id"] = client.ClientId - response, _ := client.Post("invoices", paylo) - inv, err = processInvoice(response) - return inv, err + + i.Token = client.Token.Token + response, _ := client.Post("invoices", i) + var invoice Invoice + invoice, err = processInvoice(response) + return invoice, err } // PairWithFacade @@ -124,7 +303,7 @@ func (client *Client) PairClient(paylo map[string]string) (tok Token, err error) return tok, err } -func (client *Client) Post(path string, paylo map[string]string) (response *http.Response, err error) { +func (client *Client) Post(path string, paylo interface{}) (response *http.Response, err error) { url := client.ApiUri + "/" + path htclient := setHttpClient(client) payload, _ := json.Marshal(paylo) @@ -141,10 +320,20 @@ func (client *Client) Post(path string, paylo map[string]string) (response *http } // GetInvoice is a public facade method, any client which has the ApiUri field set can retrieve an invoice from that endpoint, provided they have the invoice id. -func (client *Client) GetInvoice(invId string) (inv invoice, err error) { +func (client *Client) GetInvoice(invId string) (inv Invoice, err error) { url := client.ApiUri + "/invoices/" + invId htclient := setHttpClient(client) - response, _ := htclient.Get(url) + req, _ := http.NewRequest("GET", url, nil) + req.Header.Add("content-type", "application/json") + req.Header.Add("X-accept-version", "2.0.0") + publ := ku.ExtractCompressedPublicKey(client.Pem) + req.Header.Add("X-Identity", publ) + sig := ku.Sign(url, client.Pem) + req.Header.Add("X-Signature", sig) + response, err := htclient.Do(req) + if err != nil { + return inv, err + } inv, err = processInvoice(response) return inv, err } @@ -160,9 +349,15 @@ func (client *Client) GetTokens() (tokes []map[string]string, err error) { req.Header.Add("X-Identity", publ) sig := ku.Sign(url, client.Pem) req.Header.Add("X-Signature", sig) - response, _ := htclient.Do(req) + response, err := htclient.Do(req) + if err != nil { + return tokes, err + } defer response.Body.Close() - contents, _ := ioutil.ReadAll(response.Body) + contents, err := ioutil.ReadAll(response.Body) + if err != nil { + return tokes, err + } var jsonContents map[string]interface{} json.Unmarshal(contents, &jsonContents) if response.StatusCode/100 != 2 { @@ -202,7 +397,12 @@ func setHttpClient(client *Client) *http.Client { func processErrorMessage(response *http.Response, jsonContents map[string]interface{}) error { responseStatus := strconv.Itoa(response.StatusCode) - contentError := responseStatus + ": " + jsonContents["error"].(string) + var contentError string + if jsonContents != nil { + contentError = responseStatus + ": " + jsonContents["error"].(string) + } else { + contentError = responseStatus + } return errors.New(contentError) } @@ -213,7 +413,7 @@ func processToken(response *http.Response, jsonContents map[string]interface{}) return tok, nil } -func processInvoice(response *http.Response) (inv invoice, err error) { +func processInvoice(response *http.Response) (inv Invoice, err error) { defer response.Body.Close() contents, _ := ioutil.ReadAll(response.Body) var jsonContents map[string]interface{} @@ -227,3 +427,17 @@ func processInvoice(response *http.Response) (inv invoice, err error) { } return inv, err } + +// CreatePayout create and returns a PayOut. +func (client *Client) CreatePayout(p Payout) (payout Payout, err error) { + match, _ := regexp.MatchString("^[[:upper:]]{3}$", p.Currency) + if !match { + err = errors.New("BitPayArgumentError: invalid currency code") + return payout, err + } + p.Token = client.Token.Token + response, err := client.Post("payouts", p) + body, err := ioutil.ReadAll(response.Body) + json.Unmarshal(body, &payout) + return payout, err +}