github.com/aarzilli/tools@v0.0.0-20151123112009-0d27094f75e0/net/http/coinbase/coinbase.go (about)

     1  // Implements  coinbase.com integration
     2  package coinbase
     3  
     4  /*
     5  
     6  requestPay is unused.
     7  
     8  We use payment buttons instead.
     9  See https://developers.coinbase.com/docs/merchants/payment-buttons
    10  
    11  
    12  
    13  Oauth is also not used.
    14  Look here for a preconfigured app with oauth:
    15  	https://www.coinbase.com/oauth/applications/560fbcaca4221973720002c7
    16  
    17  */
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"fmt"
    23  	"io/ioutil"
    24  	"net/http"
    25  	"net/url"
    26  	"time"
    27  
    28  	"github.com/pbberlin/tools/dsu"
    29  	"github.com/pbberlin/tools/net/http/fetch"
    30  	"github.com/pbberlin/tools/net/http/htmlfrag"
    31  	"github.com/pbberlin/tools/net/http/loghttp"
    32  	"github.com/pbberlin/tools/stringspb"
    33  	"google.golang.org/appengine"
    34  )
    35  
    36  const uriRequestPayment = "/coinbase-integr/request" // unused
    37  const uriConfirmPayment = "/coinbase-integr/confirm"
    38  const uriRedirectSuccess = "/coinbase-integr/redir-success1"
    39  
    40  const coinbaseHost = "www.coinbase.com"
    41  
    42  const walletAddress = "1E37asSURuvPDjjvPGSwAgDMnNgZDJdMDY" // for the entire account
    43  
    44  // look into exclude.go
    45  const (
    46  	XX_apiKey    = "----------------------"
    47  	XX_apiSecret = "------------------------" // salt for SHA256 signing
    48  )
    49  
    50  var wpf = fmt.Fprintf
    51  
    52  // InitHandlers is called from outside,
    53  // and makes the EndPoints available.
    54  func InitHandlers() {
    55  	http.HandleFunc(uriRequestPayment, loghttp.Adapter(requestPay))
    56  	http.HandleFunc(uriConfirmPayment, loghttp.Adapter(confirmPay))
    57  	http.HandleFunc(uriRedirectSuccess, loghttp.Adapter(paymentSuccess))
    58  }
    59  
    60  // BackendUIRendered returns a userinterface rendered to HTML
    61  func BackendUIRendered() *bytes.Buffer {
    62  	var b1 = new(bytes.Buffer)
    63  	htmlfrag.Wb(b1, "Coinbase integration", uriRequestPayment, "request payment")
    64  	htmlfrag.Wb(b1, "Confirm", uriConfirmPayment, "")
    65  	return b1
    66  }
    67  
    68  const BtnTestFormat = `
    69  					<a class="coinbase-button"
    70  						data-code="0025d69ea925b48ba2b7adeb2a911ca2"
    71  						data-custom="productID=%v&uID=%v"
    72  						data-env="sandbox"
    73  						href="https://sandbox.coinbase.com/checkouts/0025d69ea925b48ba2b7adeb2a911ca2"
    74  					>Pay With Bitcoin</a>
    75  					<script src="https://sandbox.coinbase.com/assets/button.js" type="text/javascript"></script>
    76  					`
    77  
    78  const BtnLiveFormat = `
    79  					<a class="coinbase-button" 
    80  						data-code="aa4e03abbc5e2f5321d27df32756a932" 
    81  						data-custom="productID=%v&uID=%v" 
    82  						href="https://www.coinbase.com/checkouts/aa4e03abbc5e2f5321d27df32756a932" 
    83  					>Pay With Bitcoin</a>
    84  					<script src="https://www.coinbase.com/assets/button.js" type="text/javascript"></script>
    85  
    86  				`
    87  
    88  //
    89  //
    90  // requestPay is unused
    91  func requestPay(w http.ResponseWriter, r *http.Request, m map[string]interface{}) {
    92  
    93  	lg, b := loghttp.BuffLoggerUniversal(w, r)
    94  	closureOverBuf := func(bUnused *bytes.Buffer) {
    95  		loghttp.Pf(w, r, b.String())
    96  	}
    97  	defer closureOverBuf(b) // the argument is ignored,
    98  	r.Header.Set("X-Custom-Header-Counter", "nocounter")
    99  
   100  	protoc := "https://"
   101  	if appengine.IsDevAppServer() {
   102  		protoc = "http://"
   103  	}
   104  
   105  	host := appengine.DefaultVersionHostname(appengine.NewContext(r))
   106  	if appengine.IsDevAppServer() {
   107  		host = "not-loclhost"
   108  	}
   109  
   110  	confirmURL := fmt.Sprintf("%v%v%v", protoc, host, uriConfirmPayment)
   111  	confirmURL = url.QueryEscape(confirmURL)
   112  
   113  	addrURL := fmt.Sprintf("https://%v/api/receive?method=create&address=%v&callback=%v&customsecret=49&api_code=%v",
   114  		coinbaseHost, walletAddress, confirmURL, apiKey)
   115  
   116  	req, err := http.NewRequest("GET", addrURL, nil)
   117  	lg(err)
   118  	if err != nil {
   119  		return
   120  	}
   121  	bts, inf, err := fetch.UrlGetter(r, fetch.Options{Req: req})
   122  	bts = bytes.Replace(bts, []byte(`","`), []byte(`", "`), -1)
   123  	if err != nil {
   124  		lg(err)
   125  		lg(inf.Msg)
   126  		return
   127  	}
   128  
   129  	lg("response body 1:\n")
   130  	lg("%s\n", string(bts))
   131  
   132  	lg("response body 2:\n")
   133  	var data1 map[string]interface{}
   134  	err = json.Unmarshal(bts, &data1)
   135  	lg(err)
   136  	lg(stringspb.IndentedDumpBytes(data1))
   137  
   138  	// Response body contains the suggested bitcoin address for payment.
   139  	// And the minimum recommended fee percentage
   140  	inputAddress, ok := data1["input_address"].(string)
   141  	if !ok {
   142  		lg("input address could not be casted to string; is type %T", data1["input_address"])
   143  		return
   144  	}
   145  	feePercent, ok := data1["fee_percent"].(float64)
   146  	if !ok {
   147  		lg("fee percent could not be casted to float64; is type %T", data1["fee_percent"])
   148  		return
   149  	}
   150  
   151  	lg("Input Adress will be %q; fee percent will be %4.2v", inputAddress, feePercent)
   152  
   153  }
   154  
   155  /*
   156  
   157  https://developers.coinbase.com/docs/merchants/callbacks
   158  
   159  
   160  id				Order number used to uniquely identify an order on Coinbase
   161  completed_at	ISO 8601 timestamp when the order completed
   162  status			[completed, mispaid, expired]
   163  event			[completed, mispayment]. If mispayment => check key mispayment_id. Distinction from status ...
   164  total_btc		Total amount of the order in ‘satoshi’ (1 BTC = 100,000,000 Satoshi). Note the use of the word ‘cents’ in the callback really means satoshi in this context. The btc amount will be calculated at the current exchange rate at the time the order is placed (current to within 15 minutes).
   165  total_native	Units of local currency. 1 unit = 100 cents. Equal to the price from creating the button.
   166  total_payout	Units of local currency deposited using instant payout.
   167  custom			Custom parameter from data-custom attribute of button. Usually an Order, User, or Product ID
   168  receive_address	Bitcoin address associated with this order. This is where the payment was sent.
   169  button			Button details. ID matches the data-code parameter in your embedded HTML code.
   170  transaction		Hash and number of confirmations of underlying transaction.
   171  				Number of confirmations typically zero at the time of the first callback.
   172  customer		Customer information from order form. Can include email xor shipping address.
   173  refund_address	Experimental parameter that is subject to change.
   174  
   175  
   176  */
   177  func confirmPay(w http.ResponseWriter, r *http.Request, m map[string]interface{}) {
   178  
   179  	lg, b := loghttp.BuffLoggerUniversal(w, r)
   180  	closureOverBuf := func(bUnused *bytes.Buffer) {
   181  		// loghttp.Pf(w, r, b.String())
   182  	}
   183  	defer closureOverBuf(b) // the argument is ignored,
   184  	r.Header.Set("X-Custom-Header-Counter", "nocounter")
   185  
   186  	htmlfrag.SetNocacheHeaders(w)
   187  
   188  	//____________________________________________________________________
   189  
   190  	bts, err := ioutil.ReadAll(r.Body)
   191  	if err != nil {
   192  		lg("cannot read resp body: %v", err)
   193  		return
   194  	}
   195  	defer r.Body.Close()
   196  
   197  	// lg("bytes are -%s-", stringspb.ToLen(string(bts), 20))
   198  
   199  	if len(bts) < 1 {
   200  		lg("lo empty post body")
   201  		w.WriteHeader(http.StatusOK)
   202  		b = new(bytes.Buffer)
   203  		return
   204  	}
   205  
   206  	var mp map[string]interface{}
   207  	err = json.Unmarshal(bts, &mp)
   208  	lg(err)
   209  
   210  	mpPayout := submap(mp, "payout", lg)
   211  	if len(mpPayout) > 0 {
   212  		lg("lo " + stringspb.IndentedDump(mpPayout))
   213  	}
   214  	mpAddress := submap(mp, "address", lg)
   215  	if len(mpAddress) > 0 {
   216  		lg("lo " + stringspb.IndentedDump(mpAddress))
   217  	}
   218  
   219  	var cents, BTC float64
   220  	var status string
   221  
   222  	mpOrder := submap(mp, "order", lg)
   223  	if len(mpOrder) < 1 {
   224  		w.WriteHeader(http.StatusLengthRequired)
   225  		lg("mpOrder not present %v", status)
   226  		return
   227  	} else {
   228  		lg("lo " + stringspb.IndentedDump(mpOrder))
   229  
   230  		mpBTC := submap(mpOrder, "total_btc", lg)
   231  		// lg("lo " + stringspb.IndentedDump(mpBTC))
   232  
   233  		if icents, ok := mpBTC["cents"]; ok {
   234  			cents, ok = icents.(float64)
   235  			if !ok {
   236  				lg(" mpBTC[cents] is of unexpected type %T ", mpBTC["cents"])
   237  			}
   238  			BTC = cents / (1000 * 1000 * 100)
   239  
   240  		} else {
   241  			lg(" mpBTC[cents] not present")
   242  		}
   243  		lg("received %18.2f satoshi, %2.9v BTC ", cents, BTC)
   244  
   245  		if _, ok := mpOrder["status"]; ok {
   246  			status, ok = mpOrder["status"].(string)
   247  			if !ok {
   248  				lg(" mpOrder[status] is of unexpected type %T ", mpOrder["status"])
   249  			}
   250  		}
   251  
   252  		lg("status    %v  ", status)
   253  		lg("custom   %#v  ", mpOrder["custom"])
   254  		lg("customer %#v - mostly empty", mpOrder["customer"])
   255  
   256  		var values url.Values
   257  		if _, ok := mpOrder["custom"]; ok {
   258  			if mpOrder["custom"] == "123456789" {
   259  				lg("test request recognized")
   260  				values = url.Values{}
   261  				values.Add("uID", "testUser123")
   262  				values.Add("productID", "/member/somearticle")
   263  			} else {
   264  				var err error
   265  				values, err = url.ParseQuery(mpOrder["custom"].(string))
   266  				lg(err)
   267  				if err != nil {
   268  					w.WriteHeader(http.StatusLengthRequired)
   269  					lg("unsatisfactory query in custom string %v", mpOrder["custom"])
   270  					return
   271  				}
   272  			}
   273  		} else {
   274  			w.WriteHeader(http.StatusLengthRequired)
   275  			lg("custom string not present")
   276  			return
   277  		}
   278  
   279  		//  save
   280  		if status == "completed" {
   281  			lg("status 'completed'")
   282  			blob := dsu.WrapBlob{
   283  				VByte: stringspb.IndentedDumpBytes(mpOrder),
   284  			}
   285  			blob.Name = values.Get("uID")
   286  			blob.Category = "invoice"
   287  			blob.S = values.Get("productID")
   288  			blob.Desc = status
   289  			blob.F = BTC
   290  			blob.I = int(time.Now().Unix())
   291  
   292  			// blob.VVByte, _ = conv.String_to_VVByte(string(blob.VByte)) // just to make it readable
   293  
   294  			newKey, err := dsu.BufPut(appengine.NewContext(r), blob, blob.Name+blob.S)
   295  			lg("key is %v", newKey)
   296  			lg(err)
   297  
   298  			retrieveAgain, err := dsu.BufGet(appengine.NewContext(r), "dsu.WrapBlob__"+blob.Name+blob.S)
   299  			lg(err)
   300  			lg("retrieved %v %v %v", retrieveAgain.Name, retrieveAgain.Desc, retrieveAgain.F)
   301  
   302  		} else {
   303  			w.WriteHeader(http.StatusLengthRequired)
   304  			lg("unsatisfactory status %v", status)
   305  			return
   306  		}
   307  
   308  	}
   309  	w.WriteHeader(http.StatusOK)
   310  	b = new(bytes.Buffer)
   311  
   312  }
   313  
   314  func submap(mpArg map[string]interface{}, key string, lg loghttp.FuncBufUniv) map[string]interface{} {
   315  
   316  	var mp map[string]interface{}
   317  
   318  	if branchTemp, ok := mpArg[key]; ok {
   319  		var okConv bool
   320  		mp, okConv = branchTemp.(map[string]interface{})
   321  		if !okConv {
   322  			lg(" mp[%v] of type %T ", key, branchTemp)
   323  		}
   324  
   325  	} else {
   326  		// lg("mp[%v] not present", key)
   327  	}
   328  
   329  	return mp
   330  }
   331  
   332  /*
   333  https://tec-news.appspot.com/coinbase-integr/redir-success1?
   334  order[button][description]=When and how Bitcoin decline will start.&
   335  order[button][id]=0025d69ea925b48ba2b7adeb2a911ca2&
   336  order[button][name]=Bitcoin Analysis&
   337  order[button][repeat]=&
   338  order[button][resource_path]=/v2/checkouts/4f1e5ecc-c8fc-56fc-926c-15a7eebd8314&
   339  order[button][subscription]=false&
   340  order[button][type]=buy_now&
   341  order[button][uuid]=4f1e5ecc-c8fc-56fc-926c-15a7eebd8314&
   342  order[created_at]=2015-10-26 08:03:17 -0700&
   343  order[custom]=productID=/member/tec-news/crypto-experts-neglect-one-vital-aspect&uID=14952300052240127534&
   344  order[event]=&
   345  order[id]=GAB5VN36&
   346  order[metadata]=&
   347  order[receive_address]=myL84ofiymQpzzmJ7Foc9F2wQ4GMuSuQ3f&
   348  order[refund_address]=mwaz3wxMbnZrBZUSZpVHr51xjQ6Swx756b&
   349  order[resource_path]=/v2/orders/9bbf6fde-530a-53a4-bf94-d54fc3f43d40&
   350  order[status]=completed&
   351  order[total_btc][cents]=5600.0&
   352  order[total_btc][currency_iso]=BTC&
   353  order[total_native][cents]=50.0&
   354  order[total_native][currency_iso]=EUR&
   355  order[total_payout][cents]=0.0&
   356  order[total_payout][currency_iso]=USD&
   357  order[transaction][confirmations]=0&
   358  order[transaction][hash]=ada26d75ff1e16b4febf539433d5260441171560c57adfff2ac968be37108112&
   359  order[transaction][id]=562e40dede472f26be000018&
   360  order[uuid]=9bbf6fde-530a-53a4-bf94-d54fc3f43d40
   361  */
   362  func paymentSuccess(w http.ResponseWriter, r *http.Request, m map[string]interface{}) {
   363  
   364  	r.Header.Set("X-Custom-Header-Counter", "nocounter")
   365  	lg, _ := loghttp.BuffLoggerUniversal(w, r)
   366  
   367  	err := r.ParseForm()
   368  	if err != nil {
   369  		lg(err)
   370  		http.Error(w, err.Error(), http.StatusInternalServerError)
   371  		return
   372  	}
   373  
   374  	custom := r.Form.Get("order[custom]")
   375  	// w.Write([]byte("custom=" + custom + "<br>\n"))
   376  
   377  	values, err := url.ParseQuery(custom)
   378  	if err != nil {
   379  		lg(err)
   380  		http.Error(w, err.Error(), http.StatusInternalServerError)
   381  		return
   382  	}
   383  
   384  	productID := values.Get("productID")
   385  	uID := values.Get("uID")
   386  
   387  	if productID != "" {
   388  		lg("about to redirect to %v", productID)
   389  		http.Redirect(w, r, productID+"?redirected-from=paymentsucc", http.StatusFound)
   390  		return
   391  	}
   392  
   393  	w.Write([]byte("productID=" + productID + " uID=" + uID + "<br>\n"))
   394  
   395  }