diff --git a/src/js/controllers/shapeshift.js b/src/js/controllers/shapeshift.js new file mode 100644 index 000000000..29e5a70a2 --- /dev/null +++ b/src/js/controllers/shapeshift.js @@ -0,0 +1,3 @@ +'use strict'; + +angular.module('copayApp.controllers').controller('shapeshiftController', function($scope, $interval){ }); diff --git a/src/js/directives/shapeshiftCoinDepositInfo.js b/src/js/directives/shapeshiftCoinDepositInfo.js new file mode 100644 index 000000000..58bec7674 --- /dev/null +++ b/src/js/directives/shapeshiftCoinDepositInfo.js @@ -0,0 +1,17 @@ +'use strict'; + +angular.module('copayApp.directives').directive('shapeshiftCoinDepositInfo', function(shapeshiftApiService) { + return { + require:['^shapeshiftCoinTrader'], + restrict: 'E', + transclude: true, + scope: { + depositInfo : '=depositInfo', + DepositStatus :'=depositStatus' + }, + link: function(scope, element, attrs, controllers) { + + }, + templateUrl: 'views/includes/shapeshift-coin-deposit-info.html' + } +}); diff --git a/src/js/directives/shapeshiftCoinError.js b/src/js/directives/shapeshiftCoinError.js new file mode 100644 index 000000000..5a8d7c04b --- /dev/null +++ b/src/js/directives/shapeshiftCoinError.js @@ -0,0 +1,16 @@ +'use strict'; + +angular.module('copayApp.directives').directive('shapeshiftCoinError', function(shapeshiftApiService) { + return { + require:['^shapeshiftCoinTrader'], + restrict: 'E', + transclude: true, + scope: { + depositInfo : '=ssError' + }, + link: function(scope, element, attrs, controllers) { + + }, + templateUrl: 'views/includes/shapeshift-coin-error.html' + } +}); diff --git a/src/js/directives/shapeshiftCoinSelector.js b/src/js/directives/shapeshiftCoinSelector.js new file mode 100644 index 000000000..a5601be6b --- /dev/null +++ b/src/js/directives/shapeshiftCoinSelector.js @@ -0,0 +1,37 @@ +'use strict'; + +angular.module('copayApp.directives').directive('shapeshiftCoinSelector', function(shapeshiftApiService) { + return { + require:['^shapeshiftCoinTrader'], + restrict: 'E', + transclude: false, + scope: { + coins: '=coins', + label:'=label', + selectedCoin:'=selectedCoin', + getMarketData: '=getMarketData', + amount:'=amount', + marketData:'=marketData', + coinAddress:'=coinAddress', + direction:'=direction', + }, + link: function(scope, element, attrs, controllers) { + var coinTraderCtrl = controllers[0]; + + scope.selectedCoinModel = { + coin: scope.selectedCoin + } + + scope.$watch('coinAddress', function(newVal){ + if(scope.direction === 'in') + coinTraderCtrl.returnAddress(newVal); + else if(scope.direction === 'out') + coinTraderCtrl.withdrawalAddress(newVal); + }); + scope.$watch('amount', function(newVal){ + coinTraderCtrl.amount(newVal) + }); + }, + templateUrl: 'views/includes/shapeshift-coin-selector.html' + } +}); diff --git a/src/js/directives/shapeshiftCoinShiftButton.js b/src/js/directives/shapeshiftCoinShiftButton.js new file mode 100644 index 000000000..7e70efaae --- /dev/null +++ b/src/js/directives/shapeshiftCoinShiftButton.js @@ -0,0 +1,17 @@ +'use strict'; + +angular.module('copayApp.directives').directive('shapeshiftCoinShiftButton', function(shapeshiftApiService) { + return { + require:['^shapeshiftCoinTrader'], + restrict: 'E', + transclude: true, + scope: { + ShiftState : '=shiftState', + shiftIt : '=shiftIt' + }, + link: function(scope, element, attrs, controllers) { + console.log(scope.ShiftState) + }, + templateUrl: 'views/includes/shapeshift-coin-shift-button.html' + } +}); diff --git a/src/js/directives/shapeshiftCoinTrader.js b/src/js/directives/shapeshiftCoinTrader.js new file mode 100644 index 000000000..deab44dfe --- /dev/null +++ b/src/js/directives/shapeshiftCoinTrader.js @@ -0,0 +1,152 @@ +'use strict'; + +angular.module('copayApp.directives').directive('shapeshiftCoinTrader', function($interval, shapeshiftApiService, profileService) { + return { + restrict: 'E', + transclude: true, + controller: function($scope, $q) { + $scope.ShiftState = 'Shift'; + $scope.withdrawalAddress = '' + $scope.returnAddress = '' + $scope.amount = ''; + $scope.marketData = {} + this.withdrawalAddress = function(address) { + $scope.withdrawalAddress = address; + }; + this.returnAddress = function(address) { + $scope.returnAddress = address; + }; + this.amount = function(amount) { + $scope.amount = amount; + }; + + $scope.getMarketDataIn = function(coin) { + if(coin === $scope.coinOut) return $scope.getMarketData($scope.coinOut, $scope.coinIn); + return $scope.getMarketData(coin, $scope.coinOut); + }; + $scope.getMarketDataOut = function(coin) { + if(coin === $scope.coinIn) return $scope.getMarketData($scope.coinOut, $scope.coinIn); + return $scope.getMarketData($scope.coinIn, coin); + }; + $scope.getMarketData = function(coinIn, coinOut) { + $scope.coinIn = coinIn; + $scope.coinOut= coinOut; + if($scope.coinIn === undefined || $scope.coinOut === undefined) return; + shapeshiftApiService + .marketInfo($scope.coinIn, $scope.coinOut) + .then(function(marketData){ + $scope.marketData = marketData; + }); + }; + + /*shapeshiftApiService.coins().then(function(coins){ + $scope.coins = coins; + $scope.coinIn = coins['BTC'].symbol; + $scope.coinOut = coins['BCH'].symbol; + $scope.getMarketData($scope.coinIn, $scope.coinOut); + });*/ + + $scope.coins = { + 'BTC': { name: 'Bitcoin', symbol: 'BTC' }, + 'BCH': { name: 'Bitcoin Cash', symbol: 'BCH' } + }; + $scope.coinIn = $scope.coins['BTC'].symbol; + $scope.coinOut = $scope.coins['BCH'].symbol; + $scope.getMarketData($scope.coinIn, $scope.coinOut); + + function checkForError(data){ + if(data.error) return true; + return false; + } + + $scope.shiftIt = function(){ + console.log($scope.coinOut) + var validate=shapeshiftApiService.ValidateAddress($scope.withdrawalAddress, $scope.coinOut); + validate.then(function(valid){ + console.log($scope.withdrawalAddress) + console.log(valid) + var tx = ShapeShift(); + tx.then(function(txData){ + if(txData['fixedTxData']){ + txData = txData.fixedTxData; + if(checkForError(txData)) return; + console.log(txData) + var coinPair=txData.pair.split('_'); + txData.depositType = coinPair[0].toUpperCase(); + txData.withdrawalType = coinPair[1].toUpperCase(); + var coin = $scope.coins[txData.depositType].name.toLowerCase(); + console.log(coin) + txData.depositQR = coin + ":" + txData.deposit + "?amount=" + txData.depositAmount + $scope.txFixedPending = true; + } else if(txData['normalTxData']){ + txData = txData.normalTxData; + if(checkForError(txData)) return; + var coin = $scope.coins[txData.depositType.toUpperCase()].name.toLowerCase(); + txData.depositQR = coin + ":" + txData.deposit; + + } else if(txData['cancelTxData']){ + if(checkForError(txData.cancelTxData)) return; + if($scope.txFixedPending) { + $interval.cancel($scope.txInterval); + $scope.txFixedPending = false; + } + $scope.ShiftState = 'Shift'; + return; + } + $scope.depositInfo = txData; + console.log($scope.depositInfo) + $scope.ShiftState = 'Cancel'; + $scope.GetStatus(); + $scope.txInterval=$interval($scope.GetStatus, 8000); + }); + }) + }; + + function ShapeShift() { + if($scope.ShiftState === 'Cancel') return shapeshiftApiService.CancelTx($scope); + if(parseFloat($scope.amount) > 0) return shapeshiftApiService.FixedAmountTx($scope); + return shapeshiftApiService.NormalTx($scope); + } + + $scope.GetStatus = function(){ + var address = $scope.depositInfo.deposit + shapeshiftApiService.GetStatusOfDepositToAddress(address).then(function(data){ + $scope.DepositStatus = data; + if($scope.DepositStatus.status === 'complete'){ + $interval.cancel($scope.txInterval); + $scope.depositInfo = null; + $scope.ShiftState = 'Shift' + } + }); + } + + $scope.walletsBtc = profileService.getWallets({coin: 'btc'}); + $scope.walletsBch = profileService.getWallets({coin: 'bch'}); + $scope.fromWallet = $scope.walletsBtc[0]; + $scope.toWallet = $scope.walletsBch[0]; + $scope.fromWalletSelectorTitle = 'From'; + $scope.toWalletSelectorTitle = 'To'; + $scope.showFromWallets = false; + $scope.showFromWalletSelector = function() { + $scope.showFromWallets = true; + } + $scope.showToWallets = false; + $scope.showToWalletSelector = function() { + $scope.showToWallets = true; + } + + $scope.onFromWalletSelect = function(wallet) { + $scope.fromWallet = wallet; + //setProtocolHandler(); + //$scope.setAddress(); + }; + + $scope.onToWalletSelect = function(wallet) { + $scope.toWallet = wallet; + //setProtocolHandler(); + //$scope.setAddress(); + } + }, + templateUrl: 'views/includes/shapeshift-coin-trader.html' + } +}); diff --git a/src/js/services/shapeShiftApiService.js b/src/js/services/shapeShiftApiService.js new file mode 100644 index 000000000..cb3664a3d --- /dev/null +++ b/src/js/services/shapeShiftApiService.js @@ -0,0 +1,370 @@ +var ShapeShift = (function() { + var JP = JSON.parse; + var JS = JSON.stringify; + + function CreateXmlHttp(){ + var xmlhttp; + if (window.XMLHttpRequest) {// code for IE7+, Firefox, Chrome, Opera, Safari + xmlhttp = new XMLHttpRequest(); + } + else {// code for IE6, IE5 + xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); + } + return xmlhttp; + } + + function AjaxRequest(xmlhttp, apiEp, data, cb) { + if(cb === undefined){ + cb = data; + } + + xmlhttp.onreadystatechange = function() { + if (xmlhttp.readyState == 4) { + if (xmlhttp.status == 200) { + var parsedResponse = JP(xmlhttp.responseText); + cb.apply(null, [parsedResponse]); + } else { + cb.apply(null, [new Error('Request Failed')]) + } + } + }; + + var url='https://shapeshift.io/'+apiEp.path; + var type = apiEp.method; + + xmlhttp.open(apiEp.method, url, true); + if(type.toUpperCase() === 'POST') { + xmlhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + xmlhttp.send(JS(data)); + } else if(type.toUpperCase() === 'GET') { + xmlhttp.send(); + } + } + + var endPoints = { + Rate : { path : 'rate', method : 'GET' } + , DepositLimit : { path : 'limit', method : 'GET' } + , MarketInfo : { path : 'marketinfo', method : 'GET' } + , RecentTxList : { path : 'recenttx', method : 'GET' } + , StatusOfDepositToAddress : { path : 'txStat', method : 'GET' } + , TimeRemainingFixedAmountTx : { path : 'timeremaining', method : 'GET' } + , GetCoins : { path : 'getcoins', method : 'GET' } + , GetTxListWithKey : { path : 'txbyapikey', method : 'GET' } + , GetTxToAddressWithKey : { path : 'txbyaddress', method : 'GET' } + , ValidateAddress : { path : 'validateAddress', method : 'GET' } + , NormalTx : { path : 'shift', method : 'POST'} + , RequestEmailReceipt : { path : 'mail', method : 'POST'} + , FixedAmountTx : { path: 'sendamount', method : 'POST'} + , QuoteSendExactPrice : { path: 'sendamount', method : 'POST'} + , CancelPendingTx : { path: 'cancelpending', method : 'POST'} + }; + + function coinPairer(coin1, coin2){ + var pair = null; + + if(coin1 === undefined && coin2 === undefined) return ''; + if(typeof(coin1) === 'function') return ''; + if(typeof(coin2) === 'function') return coin1.toLowerCase(); + if(coin1 === undefined) return pair; + if(coin2 === undefined) return coin1.toLowerCase(); + return coin1.toLowerCase()+'_'+coin2.toLowerCase(); + } + + function getArgsAdder(endPoint, args){ + var clone = { + path : endPoint.path, + method : endPoint.method + }; + if(args !== undefined && args[0] !== null){ + for(var i = 0; i < args.length; i++) { + clone.path = clone.path + '/' + args[i]; + } + } + + return clone; + } + + function cbProtector(cb, data){ + if(cb === undefined) return; + if(typeof(cb) === 'function') cb(data); + } + + function ShapeShiftApi(publicApiKey) { this.apiPubKey = publicApiKey; } + + var SS=ShapeShiftApi.prototype; + + SS.GetRate = function(coin1, coin2, cb) { + var pair = coinPairer(coin1, coin2); + var apiEp = getArgsAdder(endPoints.Rate, pair); + var xmlhttp = CreateXmlHttp(); + AjaxRequest(xmlhttp, apiEp, function(response) { + cbProtector(cb, response); + }); + }; + + SS.GetDepositLimit = function(coin1, coin2, cb) { + var pair = coinPairer(coin1, coin2); + var apiEp = getArgsAdder(endPoints.DepositLimit, [pair]); + var xmlhttp = CreateXmlHttp(); + AjaxRequest(xmlhttp, apiEp, function(response) { + cbProtector(cb, response); + }); + }; + + SS.GetMarketInfo = function(coin1, coin2, cb) { + var pair = coinPairer(coin1, coin2); + if(typeof(coin1) === 'function') cb = coin1; + if(typeof(coin2) === 'function') cb = coin2; + var apiEp = getArgsAdder(endPoints.MarketInfo, [pair]); + var xmlhttp = CreateXmlHttp(); + AjaxRequest(xmlhttp, apiEp, function(response) { + cbProtector(cb, response); + }); + }; + + SS.GetRecentTxList = function(max, cb) { + if(typeof(max) === 'function') cb = max; + var apiEp = getArgsAdder(endPoints.RecentTxList, [max]); + var xmlhttp = CreateXmlHttp(); + AjaxRequest(xmlhttp, apiEp, function(response) { + cbProtector(cb, response); + }); + }; + + SS.GetStatusOfDepositToAddress = function(address, cb){ + if(address === undefined) throw new Error('no address provided'); + var apiEp = getArgsAdder(endPoints.StatusOfDepositToAddress, [address]); + var xmlhttp = CreateXmlHttp(); + AjaxRequest(xmlhttp, apiEp, function(response) { + cbProtector(cb, response); + }); + }; + + SS.GetTimeRemainingFxiedAmountTx = function(address, cb){ + if(address === undefined) throw new Error('no address provided'); + var apiEp = getArgsAdder(endPoints.TimeRemainingFixedAmountTx, [address]); + var xmlhttp = CreateXmlHttp(); + AjaxRequest(xmlhttp, apiEp, function(response) { + cbProtector(cb, response); + + }); + }; + + SS.GetCoins = function(cb) { + var apiEp = getArgsAdder(endPoints.GetCoins); + var xmlhttp = CreateXmlHttp(); + AjaxRequest(xmlhttp, apiEp, function(response) { + cbProtector(cb, response); + }); + }; + + SS.GetTxListWithKey = function() { + //TODO do we care about exposing private api key functions? + }; + + SS.GetTxToAddressWithKey = function() { + //TODO do we care about exposing private api key functions? + }; + + SS.ValidateAdddress = function(address, coinSymbol, cb) { + if(address === undefined) throw new Error('no address provided'); + if(coinSymbol === undefined) throw new Error('no coin symbol provided'); + var apiEp = getArgsAdder(endPoints.ValidateAddress, [address, coinSymbol]); + var xmlhttp = CreateXmlHttp(); + AjaxRequest(xmlhttp, apiEp, function(response) { + cbProtector(cb, response); + }); + }; + + function NormalTxValidate(data, ss) { + if(data.withdrawal === undefined) throw new Error('no withdrawal address'); + if(data.pair === undefined) throw new Error('no pair given'); + //TODO check if valid pair + //TODO check if any other data in there is valid + if(ss.apiKey) data.apiKey = ss.apiPubKey; + return data; + } + + SS.CreateNormalTx = function(withdrawalAddress, coin1, coin2){ + var NormalTx = { + withdrawal : withdrawalAddress, + pair: coinPairer(coin1, coin2) + }; + return NormalTx; + }; + SS.NormalTx = function(data, cb) { + data = NormalTxValidate(data, this); + var apiEp = getArgsAdder(endPoints.NormalTx, []); + var xmlhttp = CreateXmlHttp(); + AjaxRequest(xmlhttp, apiEp, data, function(response) { + cbProtector(cb, response); + }); + }; + + function RequestEmailValidate(data, ss) { + if(data.email === undefined) throw new Error('no email given'); + if(data.txid === undefined) throw new Error('no txid given'); + //TODO check if valid pair + //TODO check if any other data in there is valid + + data.apiPubKey = ss.apiPubKey; + return data; + } + + SS.RequestEmailReceipt = function(data, cb) { + //TODO validateData(data); + data = RequestEmailValidate(data, this); + var apiEp = getArgsAdder(endPoints.RequestEmailReceipt); + var xmlhttp = CreateXmlHttp(); + AjaxRequest(xmlhttp, apiEp, data, function(response) { + cbProtector(cb, response); + }); + }; + + function FixedAmountValidate(data, ss) { + if(data.withdrawal === undefined) throw new Error('no withdrawal address'); + if(data.pair === undefined) throw new Error('no pair given'); + if(data.amount === undefined) throw new Error('no amount given'); + //TODO check if valid pair + //TODO check if any other data in there is valid + + data.apiPubKey = ss.apiPubKey; + return data; + } + + SS.CreateFixedTx = function(amount, withdrawalAddress, coin1, coin2){ + var NormalTx = { + amount : amount, + withdrawal : withdrawalAddress, + pair: coinPairer(coin1, coin2) + }; + return NormalTx; + }; + + SS.FixedAmountTx = function(data, cb) { + //TODO validateData(data); + data = FixedAmountValidate(data, this); + var apiEp = getArgsAdder(endPoints.FixedAmountTx); + var xmlhttp = CreateXmlHttp(); + console.log(data); + AjaxRequest(xmlhttp, apiEp, data, function(response) { + cbProtector(cb, response); + }); + }; + + function QuoteSendValidate(data, ss) { + if(data.pair === undefined) throw new Error('no pair given'); + if(data.amount === undefined) throw new Error('no amount given'); + //ss.GetMarketInfo(data.pair, function(mkinfo){ + //TODO implement check of min of the market + //}); + if(ss.apiKey) data.apiKey = ss.apiPubKey; + return data; + } + + SS.QuoteSendExactPrice = function(data, cb) { + //TODO validateData(data); + data = QuoteSendValidate(data, this); + var apiEp = getArgsAdder(endPoints.QuoteSendExactPrice); + var xmlhttp = CreateXmlHttp(); + AjaxRequest(xmlhttp, apiEp, data, function(response) { + cbProtector(cb, response); + }); + }; + + function CancelPendingValidate(data, ss) { + if(typeof(data) === 'object') return data; + if(data.address === undefined) throw new Error('no address given'); + if(typeof(data) === 'String') { + var address = data; + data = { address : address } + } + if(ss.apiKey) data.apiKey = ss.apiPubKey; + return data; + } + + SS.CancelPendingTx = function(data, cb) { + data = CancelPendingValidate(data, this); + var apiEp = getArgsAdder(endPoints.CancelPendingTx); + var xmlhttp = CreateXmlHttp(); + AjaxRequest(xmlhttp, apiEp, data, function(response) { + cbProtector(cb, response); + }); + }; + + return { + ShapeShiftApi: ShapeShiftApi + } +})(); +var PUBLIC_API_KEY = '08ef330fe264f674ddd4943a5156cfb1ea06f10b95d5db54781afa3d8b108100874083d53b28afa5ce58bf3e834158a3114db725bce5b49da9454ef036753599' +var SSA = new ShapeShift.ShapeShiftApi(PUBLIC_API_KEY); + +angular.module('copayApp.services').factory('shapeshiftApiService', function($q) { + return { + coins : function(){ + var promise = $q.defer(); + var coins = null; + if(coins === null) { + SSA.GetCoins(function (data) { + coins = data; + promise.resolve(coins); + }); + } else { + promise.resolve(coins); + } + return promise.promise; + }, + marketInfo : function(coinIn, coinOut){ + var promise = $q.defer(); + SSA.GetMarketInfo(coinIn, coinOut, function (data) { + promise.resolve(data) + }); + return promise.promise; + }, + FixedAmountTx : function($scope){ + var promise = $q.defer(); + $scope.ssError = null; + var fixedTx = SSA.CreateFixedTx( + $scope.amount, $scope.withdrawalAddress, + $scope.coinIn, $scope.coinOut + ); + console.log(fixedTx); + SSA.FixedAmountTx(fixedTx, function (data) { + console.log(data) + return promise.resolve({ fixedTxData : data.success }); + }); + return promise.promise; + }, + NormalTx : function($scope){ + var promise = $q.defer(); + var normalTx = SSA.CreateNormalTx($scope.withdrawalAddress, $scope.coinIn, $scope.coinOut); + SSA.NormalTx(normalTx, function (data) { + promise.resolve({ normalTxData : data }); + }); + return promise.promise; + }, + CancelTx : function ($scope) { + var promise = $q.defer(); + SSA.CancelPendingTx( + { address:$scope.depositInfo.deposit }, + function(data){ + promise.resolve({ cancelTxData : data }); + }); + return promise.promise; + }, + GetStatusOfDepositToAddress : function(address){ + var promise = $q.defer(); + SSA.GetStatusOfDepositToAddress(address, function(data){ + promise.resolve(data); + }); + return promise.promise; + }, + ValidateAddress : function(address, coin) { + var promise = $q.defer(); + SSA.ValidateAdddress(address, coin, function(data){ + promise.resolve(data); + }); + return promise.promise; + } + }; +}); diff --git a/www/views/includes/shapeshift-coin-deposit-info.html b/www/views/includes/shapeshift-coin-deposit-info.html new file mode 100644 index 000000000..7745e2a31 --- /dev/null +++ b/www/views/includes/shapeshift-coin-deposit-info.html @@ -0,0 +1,19 @@ +