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 }