### # htdocs/coffee/bill/10_common.coffee ### # shared with billing, tipping and options ADDRMATCH = /(?:^|\s|bitcoin:|>|\/)([13][a-km-zA-HJ-NP-Z0-9]{26,33})(?:$|\"|\&|<|\s)/ # default values DEFAULT_OPTS = { default: '1000', # 1000 bits ($.37 12/10/14) tipreply: "I am sending you {AMT} bits via https://www.syndicoin.co/", notipreply: 'I tried to send you {AMT} bits, but could not find your address. Please see https://www.syndicoin.co/#!tipping' } LOADEDTYPE = 'DOMContentLoaded' ADDEVENTLISTENER = 'addEventListener' if not document[ADDEVENTLISTENER] # bail if IE<9 return # when document is ready ready = (readycb) -> state = document.readyState if state == 'loading' document[ADDEVENTLISTENER] LOADEDTYPE, () -> document.removeEventListener LOADEDTYPE, self readycb() else readycb() HTMLCollection.prototype.each = (cb) -> i = 0 while node = @[i++] cb node return null ### # htdocs/coffee/bill/12_constants.coffee ### # to aid minification - things used in bill and badge DBLUE = '#082F63' LBLUE = '#116DDE' GRAY = '#C8D6D4' doc=document win=window NAMESPACE='syndicoin' DOMAIN=NAMESPACE + '.co' # syndicoin.co MSGID=NAMESPACE + '-message' ROOT='https://www.' + DOMAIN FONT = 'font-family:sans-serif;font-size:13px;font-weight:bold;text-decoration:none;line-height:1.2;' BITCOIN='bitcoin:' ### # htdocs/coffee/bill/20_bug.coffee ### W3 = 'http://www.w3.org/' svgImg = (size) -> scale = size/200 '' ### # htdocs/coffee/bill/30_fmt.coffee ### formatsatoshi = (sat) -> rnd = Math.round if sat > 100000000000 return rnd(sat / 100000000000) + 'BTC' else if sat > 100000000 return rnd(sat / 100000000) + 'M bits' else if sat > 100000 return rnd(sat / 100000) + 'K bits' else return rnd(sat/100) + ' bits' ### # htdocs/coffee/bill/40_parseqs.coffee ### # parse query string DECODE = decodeURIComponent parseqs = (query) -> params = {} if query for val in query.split /[&;]/ pair = val.split('=') params[DECODE(pair[0])] = DECODE(pair[1]) return params ### # htdocs/coffee/bill/5_ajax.coffee ### ajax = (url, callback) -> xhr = new XMLHttpRequest() xhr.onreadystatechange = () -> if xhr.readyState == 4 callback(if xhr.status == 200 then xhr.responseText else '') xhr.open "GET", url, true xhr.send() ### # htdocs/coffee/bill/badge.coffee ### ilblock = () -> span = doc.createElement('span') span.style.cssText = 'display:inline-block;vertical-align:top;' return span # Detect visibility to suppress animation # https://developer.mozilla.org/en-US/docs/Web/Guide/User_experience/Using_the_Page_Visibility_API visibilityChange = hidden = null visible = true for prefix in ['h', 'mozH', 'msH', 'webkitH'] hidden = prefix + 'idden' if doc[hidden]? visibilityChange = prefix.slice(0, -1) + 'visibilitychange' break if visibilityChange doc[ADDEVENTLISTENER] visibilityChange, () -> visible = not doc[hidden] getaddr = (badge) -> href = badge.href if not href or href[0..7] != BITCOIN return qs = parseqs(href.split('?')[1]) or {} addr = href.split(/[:?]/)[1] return addr initval = {} ws = null addrs = [] afterInitVal = (elements) -> elements.each (badge) -> addr = getaddr badge if not addr console.error 'cannot find address', badge return degrees = 0 badge.style.cssText = FONT + 'display:inline-block;height:18px;width:88px;border:1px solid ' + LBLUE + ';line-height: 18px;text-align:left;background-color:#fff;word-spacing:-2px;color:' + DBLUE + ';white-space:nowrap;overflow:hidden;border-radius:2px;box-shadow:' + GRAY + ' 1px 1px 1px;text-shadow: 1px 1px ' + GRAY + ';transition:background-color .5s;' badge.innerHTML = '' icon = ilblock() icon.style.cssText += 'padding:0 3px;transition:transform 1s;' badge.appendChild icon icon.innerHTML = svgImg(18) text = ilblock() badge.appendChild text badge.onmouseover = () -> @.style.color= LBLUE @.style.borderColor = DBLUE badge.onmouseout = () -> @.style.color = DBLUE @.style.borderColor = LBLUE badge.setAttribute 'title', 'SyndiCoin.co - contributions to date' subscribe = (satoshi) -> text.innerHTML = if satoshi then formatsatoshi(satoshi) else 'Firehose' if not WebSocket? return if not ws? ws = new WebSocket 'wss://socket.blockcypher.com/v1/btc/main?token=' + CHAINAPI timer = null ws[ADDEVENTLISTENER] 'message', (ev) -> msg = JSON.parse(ev.data) amt = 0 for output in msg.outputs if addr == '*' or output.addresses[0] == addr amt += output.value satoshi += amt # animation only happens if page is visible if not visible or amt == 0 return text.innerHTML = '+ ' + formatsatoshi(amt) degrees += 360 badge.style.backgroundColor = GRAY icon.style.transform = "rotate(#{ degrees }deg)" if timer clearTimeout timer timer = setTimeout () -> badge.style.backgroundColor = 'transparent' text.innerHTML = formatsatoshi(satoshi) , 2000 addrs.push addr if addr == '*' # test mode subscribe(0) else subscribe(initval[addr]) if ws ws[ADDEVENTLISTENER] 'open', () -> req = {event: 'unconfirmed-tx'} if '*' in addrs ws.send JSON.stringify req return for addr in addrs req.address = addr ws.send JSON.stringify req CHAINAPI='bc19915c188d1f28afd984919a388d58' GEBCN = 'getElementsByClassName' setupBadge = (root) -> cnt = 0 elements = root[GEBCN]('syndicoin-badge') for badge in elements addr = getaddr badge if addr and addr != '*' cnt++ ajax "https://api.blockcypher.com/v1/btc/main/addrs/" + addr + '/balance?token=' + CHAINAPI, (response) -> response = JSON.parse(response) initval[response.address] = response.total_received if --cnt == 0 afterInitVal(elements) ready () -> if syndicoin.badgeinit return syndicoin.badgeinit = true if not doc[GEBCN]? return setupBadge doc ### # htdocs/coffee/bill/bill.coffee ### # IE needs it in base64 format MSGCSS = FONT + "display:inline-block;position:fixed;top:0;right:0;background:#C8D6D4 url('" + (if window.btoa then ('data:image/svg+xml;base64,' + window.btoa(svgImg(24))) else (ROOT + '/img/bug_sm.png')) + "') no-repeat 10px 3px;border-bottom-left-radius:1em;padding: .5em 1em .7em 40px;color: #116DDE;display:none;opacity:1;transition:opacity 1.5s;box-shadow:0 0 3px #353D3B inset;z-index:2000000000;max-height:3em;overflow:hidden;" origin = doc.location.protocol + '//' + doc.location.host if origin == 'http://test.' + DOMAIN or origin == 'https://testnet.' + DOMAIN ROOT = origin INVOICE = ROOT + '/#!invoice' opts = { scantext: true, default: DEFAULT_OPTS['default'] } msg = null # Walk the dom # thanks http://www.javascriptcookbook.com/article/Traversing-DOM-subtrees-with-a-recursive-walk-the-DOM-function walk = (node, cb) -> cb node node = node.firstChild while node walk(node, cb) node = node.nextSibling # set up communication frame frame = null frameready = false onready = [] win[ADDEVENTLISTENER] 'message', (ifrmsg) -> if ifrmsg.origin != ROOT return if ifrmsg.data != 'ready' and ifrmsg.data != 'first' return if ifrmsg.data == 'first' INVOICE = ROOT + '/#!' if msg msg.setAttribute 'href', INVOICE frameready = true while cb = onready.pop() cb() # when frame is ready, call callback whenready = (cb) -> if frameready cb() return onready.push cb setupFrame = (callback) -> # no duplication - find frame inserted by my brother # (document loaded vs. extension loaded) # message window if not msg msg = doc.getElementById(MSGID) if not msg msg=doc.createElement('a') msg.setAttribute 'id', MSGID msg.style.cssText = MSGCSS msg.setAttribute 'href', INVOICE msg.setAttribute 'target', '_blank' doc.body.appendChild msg # communication frame if not frame frame = doc.getElementById(NAMESPACE) if not frame frame=doc.createElement('iframe') frame.setAttribute 'id', NAMESPACE frame.setAttribute 'src', ROOT + '/endpoint.html' frame.style.cssText = "position:absolute;width:1px;height:1px;left:-9999px;" doc.body.appendChild frame whenready callback fadeout = null fadedone = null setupBill = () -> pushData = () -> for d in arguments do (d) -> data = d if not data.amount data.amount = opts.default data.currency = 'bits' setupFrame () -> data.url = win.location.href data.title = doc.title # preview iframe does not save data: if not document.location.href.match(/^(about:|javascript:)/) frame.contentWindow.postMessage JSON.stringify(data), ROOT if data.hide == 'hide' return msg.innerHTML = "SyndiCoin #{ data.wallet[0..5] }... #{ data.amount } #{ data.currency }
" + msg.innerHTML msg.style.display = 'inline-block'; msg.style.opacity='1'; if fadeout clearTimeout fadeout clearTimeout fadedone fadeout = setTimeout () -> msg.style.opacity='0'; , 3000 fadedone = setTimeout () -> msg.style.display = 'none'; msg.innerHTML = '' , 4500 if win[NAMESPACE] pushData(win[NAMESPACE]...) win[NAMESPACE] = { push: pushData, } # on tipping site, no scan if doc.location.host.match /^([a-z]*\.reddit\.com|bitcointalk\.org|www\.facebook\.com|www\.tradingview\.com|twitter\.com)/ return scan = () -> matches = [] items = doc.getElementsByTagName('a') btclinks = [] for link in items if link.href[0..7] == BITCOIN link.onclick = (ev) -> ev.preventDefault() win.open INVOICE for link in items href = link.href if href[0..7] == BITCOIN qs = parseqs(href.split('?')[1]) or {} data = { wallet: href.split(/[:?]/)[1], amount: link.getAttribute('data-amount') or qs.amount, currency: link.getAttribute('data-currency') or 'BTC', hide: link.getAttribute('data-hide') or null, } pushData data return # https://blockchain.info/address/1LEuvYqBwU9DW7gGkZtaPnVEJqd9jvNpYy bcmatch = href.match /https\:\/\/blockchain.info\/address\/([a-zA-Z0-9]+)/ if bcmatch matches.push bcmatch[1] # https://github.com/priestc/Autotip if matches.length < 1 for meta in doc.getElementsByTagName('meta') if meta.name == 'microtip' data = { wallet: meta.content, amount: meta.getAttribute('data-amount'), currency: (meta.getAttribute('data-currency') or 'btc').toLowerCase(), description: meta.getAttribute('data-recipient'), } ratio = parseFloat(meta.getAttribute('data-ratio')) if ratio data.amount = parseInt(opts.default * ratio) data.currency = 'bits' pushData data if matches.length < 1 and opts['scantext'] walk document.body, (node) -> pname = node.parentNode.tagName.toLowerCase() if node.nodeType == 3 and pname != 'script' and pname != 'style' text = node.data match = text.match(ADDRMATCH) if match and matches[0] != match[1] matches.push match[1] if matches.length == 1 pushData { wallet: matches[0], } if doc.readyState == 'loading' win[ADDEVENTLISTENER] 'load', scan, false else scan() ready () -> # already loaded / on home site, bail out if doc.getElementById(NAMESPACE) or (origin == ROOT and doc.getElementById('intro')) return if chrome? and chrome.storage chrome.storage.sync.get null, (items) -> for k, v of items opts[k] = v setupBill() else setupBill()