###
# 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()