Skip to content
262 changes: 238 additions & 24 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}

Expand All @@ -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{}
Expand All @@ -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
}