aboutsummaryrefslogtreecommitdiffstats
path: root/static/js/wallet.coffee
diff options
context:
space:
mode:
Diffstat (limited to 'static/js/wallet.coffee')
-rw-r--r--static/js/wallet.coffee456
1 files changed, 456 insertions, 0 deletions
diff --git a/static/js/wallet.coffee b/static/js/wallet.coffee
new file mode 100644
index 00000000..1beefd23
--- /dev/null
+++ b/static/js/wallet.coffee
@@ -0,0 +1,456 @@
+g = exports ? this
+g.proceed = false
+g.api = '/blockcypher/v1/btc/main'
+
+validators =
+ address: (e) ->
+ return true if e.val() is ''
+ try
+ bitcoin.address.fromBase58Check(e.val())
+ catch
+ return false
+
+ key: (e) ->
+ $('#keytype').val() != 'unknown'
+
+errors =
+ address: 'Invalid address.'
+ key: 'Could not detect key type.'
+
+$(->
+ getUser()
+
+ $('#types').attr('data-content', """
+ Bitcoin Address (starts with 1 or 3)<br />
+ Private Key (starts with 5 or L)<br />
+ HD Wallet pubkey (starts with xpub)<br />
+ HD Wallet private key (starts with xprv)<br />
+ AES Encrypted Private Key (starts with U)<br />
+ BIP38 Encrypted Private Key (starts with 6)<br />
+ BIP39 Mnemonic (series of 12-24 english words)
+ """)
+ .popover(html: true)
+ .on("show.bs.popover", -> $(this).data("bs.popover").tip().css(minWidth: "400px"))
+
+ $('form').validator(custom: validators, errors: errors, delay: 1200)
+ $('[data-toggle=tooltip]').tooltip(trigger: 'hover')
+
+ $('#key').keyup(->
+ val = $(this).val()
+
+ $('#keytype').val('unknown')
+
+ switch val[0]
+ when '1'
+ try
+ bitcoin.address.fromBase58Check(val)
+ $('#keytype').val('address')
+ when 'm'
+ try
+ bitcoin.address.fromBase58Check(val)
+ $('#keytype').val('address')
+ when '5', \
+ 'L', \
+ 'K'
+ try
+ bitcoin.ECPair.fromWIF(val)
+ $('#keytype').val('wif')
+ when 'U'
+ try
+ if CryptoJS.AES.decrypt(val, g.password).toString(CryptoJS.enc.Utf8)
+ $('#keytype').val('aes')
+ when '6'
+ if bip38().verify(val)
+ $('#keytype').val('bip38')
+ when 'x'
+ if $(this).val()[3] is 'b'
+ try
+ bitcoin.HDNode.fromBase58(val)
+ $('#keytype').val('xpub')
+ else
+ try
+ bitcoin.HDNode.fromBase58(val)
+ $('#keytype').val('xprv')
+ else
+ if val.split(' ').length in [12, 15, 18, 21, 24] and bip39.validateMnemonic(val)
+ $('#keytype').val('bip39')
+ )
+
+ $('#save').click(->
+ $('.form-control').blur()
+ if $('#keys .has-error').length > 0
+ $('#keys .has-error').effect('shake', 500)
+ return
+
+ key = $('#key').val()
+ proceed = true
+
+ switch $('#keytype').val()
+ when 'address'
+ $('#pubkey').val(key)
+ $('#privkey').val('')
+ when 'wif'
+ pubkey = bitcoin.ECPair.fromWIF(key).getAddress()
+ privkey = CryptoJS.AES.encrypt(key, g.password)
+ $('#pubkey').val(pubkey)
+ $('#privkey').val(privkey)
+ when 'aes'
+ try
+ pubkey = bitcoin.HDNode.fromBase58(CryptoJS.AES.decrypt(key, g.password).toString(CryptoJS.enc.Utf8)).neutered().toString()
+ $('#pubkey').val(pubkey)
+ $('#privkey').val(key)
+ catch
+ try
+ pubkey = bitcoin.ECPair.fromWIF(CryptoJS.AES.decrypt(key, g.password).toString(CryptoJS.enc.Utf8)).getAddress()
+ $('#pubkey').val(pubkey)
+ $('#privkey').val(key)
+ catch
+ proceed = false
+ when 'bip38'
+ wif = bip38().decrypt(key, g.password)
+ pubkey = bitcoin.ECPair.fromWIF(wif).getAddress()
+ privkey = CryptoJS.AES.encrypt(wif, g.password)
+ $('#pubkey').val(pubkey)
+ $('#privkey, #key').val(privkey)
+ when 'xpub'
+ $('#pubkey').val(key)
+ $('#privkey').val('')
+ when 'xprv'
+ pubkey = bitcoin.HDNode.fromBase58(key).neutered().toString()
+ privkey = CryptoJS.AES.encrypt(key, g.password)
+ $('#pubkey').val(pubkey)
+ $('#privkey').val(privkey)
+ when 'bip39'
+ master = bitcoin.HDNode.fromSeedBuffer(bip39.mnemonicToSeed(key)).deriveHardened(44).deriveHardened(0)
+ pubkey = master.neutered().toString()
+ privkey = CryptoJS.AES.encrypt(master.toString(), g.password)
+ $('#pubkey').val(pubkey)
+ $('#privkey').val(privkey)
+
+ if proceed
+ updateUser()
+ $('#keys').hide()
+ $('#keys_updated').fadeIn()
+ $('#balances').hide()
+ $('#fetching').show()
+ )
+
+
+ $('#withdrawal form input[type=button]').click(sendTransaction)
+
+ $('#currency_toggle').click(->
+ if $(this).html() is g.user.unit
+ $(this).html(g.user.currency)
+ g.amount = parseFloat($('#amount').val()).toFixed(precision())
+ amount = (g.amount * g.exchange / multiplier()).toFixed(2)
+ $('#amount').val(amount)
+ $('#amount').attr('step', 0.01)
+ $('#amount').attr('max', (g.balance * g.exchange / multiplier()).toFixed(2))
+ else
+ $(this).html(g.user.unit)
+ $('#amount').val(convertedAmount())
+ $('#amount').attr('step', 0.00000001 * multiplier())
+ $('#amount').attr('max', g.balance)
+ )
+
+ $('#max').click(->
+ if $('#currency_toggle').html() is g.user.unit
+ $('#amount').val(g.balance).blur()
+ else
+ g.amount = parseFloat(g.balance).toFixed(precision())
+ amount = (g.balance / multiplier() * g.exchange).toFixed(2)
+ $('#amount').val(amount).blur()
+ )
+
+ $('#amount').change(->
+ if $('#currency_toggle').html() is g.user.unit
+ $(this).val(parseFloat($(this).val()).toFixed(precision()))
+ else
+ $(this).val(parseFloat($(this).val()).toFixed(2))
+
+ if parseFloat($(this).val()) > parseFloat($(this).attr('max'))
+ $(this).val($(this).attr('max'))
+ )
+
+ $('#password').keyup(->
+ try
+ if g.user.privkey
+ g.privkey = CryptoJS.AES.decrypt(g.user.privkey, $(this).val()).toString(CryptoJS.enc.Utf8)
+
+ if g.privkey[0] is 'x'
+ g.key = bitcoin.HDNode.fromBase58(g.privkey)
+ else
+ g.key = bitcoin.ECPair.fromWIF(g.privkey)
+
+ $('#key').val(g.privkey).keyup()
+ else
+ $('#key').val(g.user.pubkey).keyup()
+
+ $(this).closest('.form-group').hide()
+ $('.wallet').fadeIn()
+ g.password = $(this).val()
+ )
+
+ $('#manage').click(->
+ $('#withdrawal').hide()
+ $('#keys').show()
+ $('#privkey').val(g.user.privkey)
+ )
+
+ $('#withdraw').click(->
+ $('#withdrawal').show()
+ $('#amount').focus()
+ $('#keys').hide()
+ $('#withdrawal form').validator('destroy')
+ $('#withdrawal form').validator(custom: validators, errors: errors, delay: 1200)
+ )
+
+ $('#cancel').click(->
+ $('#keys').hide()
+ if g.balance > 0 and g.user.privkey
+ $('#withdrawal form').validator('destroy')
+ $('#withdrawal form').validator(custom: validators, errors: errors, delay: 1200)
+ $('#withdrawal').show()
+ $('#amount').focus()
+ )
+
+ $('#backup').click(->
+ url = 'data:application/json;base64,' + btoa(JSON.stringify(g.user.privkey))
+ a = document.createElement('a')
+ a.setAttribute('href', url)
+ a.setAttribute('download', 'wallet.json.aes')
+ a.click()
+ )
+
+ $('#generate').click(->
+ bootbox.confirm('<h3>Are you sure?</h3> <p>This will overwrite your existing wallet so make sure that you have the backup we sent to your email in case you want to restore it.</p>', (result) ->
+ if result
+ mnemonic = bip39.generateMnemonic()
+ key = bitcoin.HDNode.fromSeedBuffer(bip39.mnemonicToSeed(mnemonic)).deriveHardened(44).deriveHardened(0)
+ $('#key').val(key.toString()).effect('highlight', {}, 2000).keyup()
+ )
+ )
+
+ $('.close').on('click', -> $(this).closest('.alert').fadeOut())
+)
+
+getUser = ->
+ $.getJSON("/#{$('#username').val()}.json", (user) ->
+ g.user = user
+ $('#key').val(user.pubkey)
+ $('#pubkey').val(user.pubkey)
+ $('#privkey').val(user.privkey)
+ $('#address').val(user.address)
+ $('.address').html(user.address)
+ $('#unit').html(user.unit)
+ $('#currency_toggle').html(user.unit)
+ $('#amount').attr('step', 0.00000001 * multiplier())
+ $('#password').closest('.form-group').show()
+ $('#password').focus()
+
+ getExchangeRate()
+ $('#password').keyup()
+ )
+
+getExchangeRate = ->
+ $.get("/ticker?currency=#{g.user.currency}&symbol=#{g.user.symbol}&type=bid", (exchange) ->
+ g.exchange = exchange
+ createWallet()
+ )
+
+createWallet = ->
+ $.get("#{g.api}/wallets", (data) ->
+ if g.user.username in data.wallet_names
+ getBalance()
+ else
+ params = name: g.user.username
+
+ if isBip32(g.user.pubkey)
+ params.extended_public_key = g.user.pubkey
+ params.subchain_indexes = [0,1]
+
+ $.post("#{g.api}/wallets/hd", JSON.stringify(params)).done(getBalance).fail(-> $('.wallet').fadeIn())
+ else
+ params.addresses = [g.user.pubkey]
+ $.post("#{g.api}/wallets", JSON.stringify(params)).done(getBalance).fail(-> $('.wallet').fadeIn())
+ )
+
+
+getBalance = ->
+ $.get("#{g.api}/addrs/#{g.user.username}/balance?omitWalletAddresses=true", (data) ->
+ balance = data.final_balance
+ g.balance = parseFloat(balance.toBTC())
+ fiat = balance.toFiat()
+ $('#balance').html(g.balance)
+ $('#fiat').html("#{fiat} #{g.user.currency}")
+ $('#amount').attr('max', g.balance)
+ $('#balances').show()
+ $('#fetching').hide()
+
+ if g.balance > 0 and g.user.privkey
+ $('#keys').hide()
+ $('#withdrawal form').validator('destroy')
+ $('#withdrawal form').validator(custom: validators, errors: errors, delay: 1200)
+ $('#withdrawal').show()
+ $('#amount').focus()
+ )
+
+updateUser = ->
+ data = $('#keys form').serializeObject()
+ delete data['key']
+
+ $.post("/#{g.user.username}", data, ->
+ $.ajax(
+ url: "#{g.api}/wallets/#{g.user.username}"
+ type: 'DELETE'
+ ).always(->
+ $.ajax(
+ url: "#{g.api}/wallets/hd/#{g.user.username}"
+ type: 'DELETE'
+ ).always(->
+ getUser()
+ )
+ )
+ )
+
+sendTransaction = ->
+ if g.key and not (typeof g.key.d is 'undefined' and typeof g.key.keyPair.d is 'undefined')
+ dialog = new BootstrapDialog(
+ title: '<h3>Confirm Transaction</h3>'
+ message: '<i class="fa fa-spinner fa-spin"></i> Calculating fee...</i>'
+ buttons: [
+ label: 'Send'
+ cssClass: 'btn-primary'
+ ,
+ label: ' Cancel'
+ cssClass: 'btn-default'
+ action: (dialogItself) -> dialogItself.close()
+ icon: 'glyphicon glyphicon-ban-circle'
+ ]
+ ).open()
+
+ params =
+ inputs: [{wallet_name: g.user.username, wallet_token: g.token}]
+ outputs: [{addresses: [$('#recipient').val()], value: 1}]
+ preference: $('#priority').val()
+
+ $.post("#{g.api}/txs/new", JSON.stringify(params)).done((data) ->
+ if $('#currency_toggle').html() is g.user.unit
+ amount = $('#amount').val()
+ else
+ amount = convertedAmount()
+
+ value = parseInt(amount * 100000000 / multiplier())
+
+ params.fees = data.tx.fees
+ params.outputs[0].value = value
+ if value > parseFloat(g.balance).toSatoshis() - params.fees
+ params.outputs[0].value -= params.fees
+
+ $.post("#{g.api}/txs/new", JSON.stringify(params)).done((data) ->
+ data.pubkeys = []
+ if g.key instanceof bitcoin.ECPair
+ data.signatures = data.tosign.map((tosign, i) ->
+ data.pubkeys.push(g.key.getPublicKeyBuffer().toString('hex'))
+ g.key.sign(new buffer.Buffer(tosign, "hex")).toDER().toString("hex")
+ )
+ else
+ data.signatures = data.tosign.map((tosign, i) ->
+ path = data.tx.inputs[i].hd_path.split('/')
+ key = g.key.derive(parseInt(path[1])).derive(parseInt(path[2]))
+ data.pubkeys.push(key.keyPair.getPublicKeyBuffer().toString('hex'))
+ key.keyPair.sign(new buffer.Buffer(tosign, "hex")).toDER().toString("hex")
+ )
+
+ g.data = data
+
+ amount = data.tx.outputs[0].value
+ fee = data.tx.fees
+ total = amount
+ if value > parseFloat(g.balance).toSatoshis() - data.tx.fees
+ total += fee
+
+
+ $('.dialog .amount').html("#{(amount.toBTC())} #{g.user.unit} (#{amount.toFiat()} #{g.user.currency})")
+ $('.dialog .fee').html("#{(fee.toBTC())} #{g.user.unit} (#{fee.toFiat()} #{g.user.currency})")
+ $('.dialog .total').html("#{(total.toBTC())} #{g.user.unit} (#{total.toFiat()} #{g.user.currency})")
+ $('.dialog .address').html(data.tx.outputs[0].addresses[0])
+
+ dialog.getModalBody().html($('.dialog').html())
+
+ if amount.toBTC() < 0.00000534
+ displayErrors({errors: [error: 'Amount left over after fee is too small to send']}, dialog)
+
+ dialog.getModal().find('.btn-primary').click(->
+ $.post("#{g.api}/txs/send", JSON.stringify(g.data)).then((finaltx) ->
+ $('#transaction_sent').fadeIn()
+ balance = g.balance.toSatoshis() - finaltx.tx.outputs[0].value - finaltx.tx.fees
+ g.balance = balance.toBTC()
+ fiat = balance.toFiat()
+ $('#balance').html(g.balance)
+ $('#fiat').html("#{fiat} #{g.user.currency}")
+ $('#blockchain').off('click').on('click', -> window.open('https://blockchain.info/tx/' + finaltx.tx.hash, '_blank'))
+ dialog.close()
+ )
+ )
+ ).fail((data) ->
+ displayErrors(data.responseJSON, dialog)
+ )
+ ).fail((data) ->
+ displayErrors(data.responseJSON, dialog)
+ )
+
+displayErrors = (data, dialog) ->
+ if data.errors
+ dialog.getModalBody().html('')
+ dialog.getModal().find('.btn-primary').hide()
+ for e in data.errors
+ dialog.getModalBody().append("<div class='alert alert-danger'>#{e.error}</div>")
+
+
+convertedAmount = ->
+ g.amount = amount unless g.amount and !isNaN(parseFloat(g.amount))
+ amount = parseFloat($('#amount').val() * multiplier() / g.exchange).toFixed(precision())
+ difference = parseFloat(Math.abs(g.amount - amount).toFixed(precision()))
+ tolerance = parseFloat((.00000002 * g.exchange * multiplier()).toFixed(precision()))
+ if difference > tolerance
+ return amount
+ else
+ return g.amount
+
+
+check_address = (address) ->
+ try
+ bitcoin.address.fromBase58Check(address)
+ return true
+ catch
+ return isBip32(address)
+
+isBip32 = (address) ->
+ try
+ bitcoin.HDNode.fromBase58(address)
+ return true
+ catch
+ return false
+
+multiplier = ->
+ switch g.user.unit
+ when 'BTC' then 1
+ when 'mBTC' then 1000
+ when 'µBTC' then 1000000
+ when 'bits' then 1000000
+ when 'satoshis' then 100000000
+
+precision = ->
+ 9 - multiplier().toString().length
+
+Number.prototype.toBTC = ->
+ parseFloat((this / 100000000 * multiplier())).toFixed(precision())
+
+Number.prototype.toFiat = ->
+ parseFloat((this * g.exchange / 100000000)).toFixed(2)
+
+Number.prototype.toSatoshis = ->
+ parseInt(this * 100000000 / multiplier())
+
+