diff --git a/www/js/controllers/blockchain-controllers.js b/www/js/controllers/blockchain-controllers.js
new file mode 100644
index 0000000000000000000000000000000000000000..72966d049fb93ec457deb8b0fa269bbb3d7bbaaf
--- /dev/null
+++ b/www/js/controllers/blockchain-controllers.js
@@ -0,0 +1,639 @@
+
+angular.module('cesium.blockchain.controllers', ['cesium.services'])
+
+  .config(function($stateProvider) {
+    'ngInject';
+
+    $stateProvider
+
+      .state('app.blockchain', {
+        url: "/blockchain",
+        views: {
+          'menuContent': {
+            templateUrl: "templates/blockchain/lookup.html",
+            controller: 'BlockLookupCtrl'
+          }
+        },
+        data: {
+          large: 'app.blockchain_lg'
+        }
+      })
+
+      .state('app.blockchain_lg', {
+        url: "/blockchain/lg",
+        views: {
+          'menuContent': {
+            templateUrl: "templates/blockchain/lookup_lg.html",
+            controller: 'BlockLookupCtrl'
+          }
+        }
+      })
+
+      .state('app.view_currency_block_hash', {
+        url: "/:currency/block/:number/:hash",
+        views: {
+          'menuContent': {
+            templateUrl: "templates/blockchain/view_block.html",
+            controller: 'BlockViewCtrl'
+          }
+        }
+      })
+
+      .state('app.view_block', {
+        url: "/block/:number",
+        views: {
+          'menuContent': {
+            templateUrl: "templates/blockchain/view_block.html",
+            controller: 'BlockViewCtrl'
+          }
+        }
+      })
+
+      .state('app.view_block_hash', {
+        url: "/block/:number/:hash",
+        views: {
+          'menuContent': {
+            templateUrl: "templates/blockchain/view_block.html",
+            controller: 'BlockViewCtrl'
+          }
+        }
+      });
+  })
+
+  .controller('BlockLookupCtrl', BlockLookupController)
+
+  .controller('BlockViewCtrl', BlockViewController)
+
+;
+
+function BlockLookupController($scope, $timeout, $focus, $filter, $state, $anchorScroll, UIUtils, BMA, csCurrency, csWot, csSettings) {
+  'ngInject';
+
+  $scope.search = {
+    result: [],
+    total: 0,
+    loading: true,
+    loadingMore: false,
+    hasMore: false,
+    type: 'last'
+  };
+  $scope.currency = false;
+  $scope.entered = false;
+  $scope.searchTextId = null;
+  $scope.ionItemClass = 'item-border-large';
+  $scope.defaultSizeLimit = UIUtils.screen.isSmall() ? 50 : 100;
+
+  /**
+   * Enter into the view
+   * @param e
+   * @param state
+   */
+  $scope.enter = function(e, state) {
+    if (!$scope.entered) {
+      if (state && state.stateParams && state.stateParams.q) { // Query parameter
+        $scope.search.text = state.stateParams.q;
+        if ($scope.search.text && $scope.search.text.trim().length) {
+          $scope.search.type='text';
+        }
+      }
+      if (state && state.stateParams && state.stateParams.currency) { // Currency parameter
+        $scope.currency = state.stateParams.currency;
+      }
+      // Load currency if need
+      if (!$scope.currency) {
+        csCurrency.get()
+          .then(function(currency) {
+            $scope.currency = currency ? currency.name : null;
+            $scope.node = !BMA.node.same(currency.node.host, currency.node.port) ?
+              BMA.instance(currency.node.host, currency.node.port) : BMA;
+
+            if (!$scope.currency) {
+              UIUtils.alert.error('ERROR.GET_CURRENCY_FAILED');
+              return;
+            }
+            $scope.enter(); // back to enter()
+          })
+          .catch(UIUtils.onError('ERROR.GET_CURRENCY_FAILED'));
+        return;
+      }
+
+      $scope.compactMode = angular.isDefined($scope.compactMode) ? $scope.compactMode : true;
+      $scope.expertMode = angular.isDefined($scope.expertMode) ? $scope.expertMode : !UIUtils.screen.isSmall() && csSettings.data.expertMode;
+
+      $scope.doSearch();
+
+      // removeIf(device)
+      // Focus on search text (only if NOT device, to avoid keyboard opening)
+      if ($scope.searchTextId) {
+        $timeout(function(){
+          $focus($scope.searchTextId);
+        }, 100);
+      }
+      // endRemoveIf(device)
+
+      $scope.startListenBlock();
+
+      $scope.entered = true;
+
+      $scope.showHelpTip();
+    }
+    else {
+      $scope.startListenBlock();
+    }
+  };
+  $scope.$on('$ionicView.enter', $scope.enter);
+  $scope.$on('$ionicParentView.enter', $scope.enter);
+
+  /**
+   * Leave the view
+   * @param e
+   * @param state
+   */
+  $scope.leave = function() {
+    if ($scope.wsBlock) {
+      console.info('[block] Stopping websocket on block');
+      $scope.wsBlock.close();
+      delete $scope.wsBlock;
+    }
+  };
+  $scope.$on('$ionicView.leave', $scope.leave);
+  $scope.$on('$ionicParentView.leave', $scope.leave);
+  $scope.$on('$destroy', $scope.leave);
+
+  $scope.doSearchLast = function() {
+    $scope.search.type = 'last';
+    return $scope.doSearch();
+  };
+
+  $scope.doSearch = function(from) {
+    from = angular.isDefined(from) ? from : 0;
+
+    $scope.search.loading = (from === 0);
+    $scope.search.hasMore = false;
+
+    var promise;
+
+    // get blocks
+    if (from === 0) {
+      promise = $scope.node.blockchain.current()
+        .then(function(current) {
+          var size = current.number < $scope.defaultSizeLimit ? current.number : $scope.defaultSizeLimit;
+          return $scope.node.blockchain.blocksSlice({count: size, from: current.number-size})
+            .then(function(blocks) {
+              blocks.splice(0,0,current);
+              return blocks;
+            });
+        })
+        .catch(function(err) {
+          // Special case when block #0 not written yet
+          if (err && err.ucode == BMA.errorCodes.NO_CURRENT_BLOCK) {
+            return [];
+          }
+          throw err;
+        });
+    }
+    else {
+      var oldestNumber = $scope.search.results[$scope.search.results.length-1].number;
+      var size = oldestNumber < $scope.defaultSizeLimit ? oldestNumber : $scope.defaultSizeLimit;
+      promise = $scope.node.blockchain.blocksSlice({count: size, from: oldestNumber-size});
+    }
+
+    // process blocks
+    return promise
+      .then(function(blocks) {
+        // If no result
+        if (!blocks || !blocks.length) {
+          $scope.doDisplayResult([], from, 0);
+          $scope.search.loading = false;
+          return;
+        }
+
+        // Transform to entities
+        blocks = blocks.reduce(function(res, json){
+          var block = new Block(json);
+          block.cleanData(); // release arrays content
+          return res.concat(block);
+        }, []);
+
+        // Order by number (desc)
+        blocks = _.sortBy(blocks, function(b) {
+          return -1 * b.number;
+        });
+
+        // Prepare then display results
+        var total = ((from===0) ? blocks[0].number: $scope.search.results[0].number) + 1;
+        return $scope.doPrepareResult(blocks, from)
+          .then(function() {
+            $scope.doDisplayResult(blocks, from, total);
+            $scope.search.loading = false;
+          });
+      })
+
+      .catch(function(err) {
+        UIUtils.onError('BLOCKCHAIN.ERROR.SEARCH_BLOCKS_FAILED')(err);
+        $scope.search.loading = false;
+      });
+  };
+
+  $scope.doPrepareResult = function(blocks, offset) {
+    offset = angular.isDefined(offset) ? offset : 0;
+
+    if ($scope.search.type=='last') {
+
+      var previousEmptyBlockDay;
+      if (offset > 0 && $scope.search.results.length) {
+        var lastBlock = $scope.search.results[$scope.search.results.length-1];
+        previousEmptyBlockDay = lastBlock.empty ? lastBlock.day : undefined;
+      }
+
+      _.forEach(blocks, function(block, index){
+        // If empty
+        if (block.empty) {
+          // compute the day
+          var blockDay = $filter('formatDateShort')(block.medianTime);
+          var notFirstEmpty = (index !== 0) || (offset !== 0);
+          var previousNotEmptyOrSameDay = !previousEmptyBlockDay || (previousEmptyBlockDay == blockDay);
+          block.compacted = notFirstEmpty && previousNotEmptyOrSameDay;
+          previousEmptyBlockDay = blockDay;
+        }
+        else {
+          previousEmptyBlockDay = undefined;
+        }
+      });
+    }
+
+    return csWot.extendAll(blocks, 'issuer');
+  };
+
+  $scope.doDisplayResult = function(res, offset, total) {
+    if (!offset) {
+      $scope.search.results = res || [];
+    }
+    else {
+      $scope.search.results = $scope.search.results.concat(res);
+    }
+    $scope.search.hasMore = total && $scope.search.results.length < total;
+    $scope.search.total = total || $scope.search.total;
+
+    $scope.smallscreen = UIUtils.screen.isSmall();
+
+    // Set Motion
+    if (res && res.length) {
+      $scope.motion.show({selector: '.list-blocks .item-block'});
+    }
+
+    $scope.$broadcast('$$rebind::rebind'); // notify binder
+  };
+
+  $scope.showMore = function() {
+    var from = $scope.search.results ? $scope.search.results.length : 0;
+
+    $scope.search.loadingMore = true;
+
+    return $scope.doSearch(from)
+      .then(function() {
+        $scope.search.loadingMore = false;
+        $scope.$broadcast('scroll.infiniteScrollComplete');
+      })
+      .catch(function(err) {
+        console.error(err);
+        $scope.search.loadingMore = false;
+        $scope.search.hasMore = false;
+        $scope.$broadcast('scroll.infiniteScrollComplete');
+      });
+  };
+
+  $scope.startListenBlock = function() {
+    if (!$scope.wsBlock) {
+      console.info('[block] Starting websocket on block');
+      $scope.wsBlock = $scope.node.websocket.block();
+    }
+
+    var showBlock = function(block){
+      // Force rebind
+      $scope.$broadcast('$$rebind::rebind');
+      $scope.motion.show({selector: '#block-'+block.number});
+    };
+
+    $scope.wsBlock.on(function(json) {
+      // Skip if still loading or if filter/sort is not the default (not last blocks)
+      if ($scope.search.loading || !json || $scope.search.type != 'last' ||
+        ($scope.search.sort && $scope.search.sort != 'desc')) return; // skip
+
+      var block = new Block(json);
+      block.cleanData(); // release arrays content
+
+      // Make sure results is init
+      $scope.search.results = $scope.search.results || [];
+
+      if (!$scope.search.results.length) {
+        console.debug('[ES] [blockchain] new block #{0} received (by websocket)'.format(block.number));
+        // add it to result
+        $scope.search.total++;
+        $scope.search.results.push(block);
+
+        // Prepare the new block, then show it
+        $scope.doPrepareResult([block])
+          .then(function() {
+            return showBlock(block);
+          });
+      }
+      else {
+        // Find existing block, by number
+        var existingBlock = _.findWhere($scope.search.results, {number: block.number});
+
+        // replace existing block (fork could have replaced previous block)
+        if (existingBlock) {
+          if (existingBlock.hash != block.hash) {
+            console.debug('[ES] [blockchain] block #{0} updated (by websocket)'.format(block.number));
+            // Replace existing content
+            angular.copy(block, existingBlock);
+            // Prepare the new block, then show it
+            $scope.doPrepareResult([block, $scope.search.results[1]])
+              .then(function() {
+                return showBlock(existingBlock);
+              });
+          }
+        }
+        else {
+          console.debug('[ES] [blockchain] new block #{0} received (by websocket)'.format(block.number));
+          // Insert at index 0
+          $scope.search.total++;
+          $scope.search.results.splice(0, 0, block);
+
+          // Prepare the new block, then show it
+          $scope.doPrepareResult([block, $scope.search.results[1]])
+            .then(function() {
+              return showBlock(block);
+            });
+        }
+      }
+    });
+  };
+
+  $scope.selectBlock = function(block) {
+    if (block.compacted && $scope.compactMode) {
+      $scope.toggleCompactMode();
+      $timeout(function(){
+        $anchorScroll('block-' + block.number);
+      }, 900);
+    }
+    else {
+      $state.go('app.view_block_hash', {number: block.number, hash: block.hash});
+    }
+  };
+
+  $scope.toggleCompactMode = function() {
+    $scope.compactMode = !$scope.compactMode;
+    $scope.doDisplayResult($scope.search.results, 0, $scope.search.total/*keep previous total*/);
+
+    // Workaround to re-initialized the <ion-infinite-loop>
+    if (!$scope.search.hasMore && $scope.search.results.length && $scope.search.type == 'last') {
+      var lastBlock = $scope.search.results[$scope.search.results.length-1];
+      if (lastBlock && lastBlock.number > 0) {
+        $timeout(function() {
+          $scope.search.hasMore = true;
+        }, 500);
+      }
+    }
+  };
+
+  $scope.toggleSort = function(sort){
+    if ($scope.search.sort === sort && !$scope.search.asc) {
+      $scope.search.asc = undefined;
+      $scope.search.sort = undefined;
+    }
+    else {
+      $scope.search.asc = ($scope.search.sort === sort) ? !$scope.search.asc : true;
+      $scope.search.sort = sort;
+    }
+    $scope.doSearch();
+  };
+
+  $scope.showHelpTip = function() {
+
+  };
+}
+
+
+function BlockViewController($scope, $ionicPopover, $state, UIUtils, BMA, csCurrency, csWot, csSettings) {
+  'ngInject';
+
+  $scope.loading = true;
+  $scope.formData = {};
+  $scope.compactMode = true; // TODO change to true
+
+  /**
+   * Enter on view
+   */
+  $scope.enter = function(e, state) {
+    if (!$scope.loading) return; // call once
+
+    $scope.currency = state && state.stateParams ? state.stateParams.currency : undefined;
+    $scope.number = state && state.stateParams && angular.isDefined(state.stateParams.number) ? state.stateParams.number : 'current';
+    $scope.hash = state && state.stateParams && state.stateParams.hash ? state.stateParams.hash : undefined;
+
+    if (!$scope.currency) {
+      csCurrency.get()
+        .then(function (currency) {
+          if (currency) {
+            $scope.currency = currency.name;
+            $scope.node = currency.node;
+            $scope.load();
+          }
+        })
+        .catch(UIUtils.onError('ERROR.GET_CURRENCY_FAILED'));
+    }
+    else {
+      $scope.node = BMA;
+      $scope.load();
+    }
+  };
+  $scope.$on('$ionicView.enter', $scope.enter);
+
+  /**
+   * Leave the view
+   */
+  $scope.leave = function() {
+    //console.debug("Leaving view peer...");
+  };
+  $scope.$on('$ionicParentView.beforeLeave', $scope.leave);
+
+  $scope.load = function() {
+    if (!$scope.number) return;
+
+    var promise = $scope.number == 'current' ?
+      $scope.node.blockchain.current() :
+      $scope.node.blockchain.block({block: $scope.number});
+
+    return  promise
+      .then(function(json) {
+        var block = new Block(json);
+        block.parseData();
+        if (!block || !angular.isDefined(block.number) || !block.hash) {
+          $scope.loading = false;
+          UIUtils.alert.error('ERROR.GET_BLOCK_FAILED');
+          return;
+        }
+        if ($scope.hash && block.hash != $scope.hash) {
+          $scope.loading = false;
+          UIUtils.alert.error('ERROR.INVALID_BLOCK_HASH');
+          return;
+        }
+
+        var users = [];
+        if (block.joiners.length) {
+          users = users.concat(block.joiners);
+        }
+        if (block.certifications.length) {
+          users = block.certifications.reduce(function(res, cert) {
+            cert.to = {
+              pubkey: cert.to
+            };
+            cert.from = {
+              pubkey: cert.from
+            };
+            return res.concat(cert.to , cert.from);
+          }, users);
+          block.certifications = _.groupBy(block.certifications, function(cert) {
+            return cert.to.pubkey;
+          });
+        }
+        if (block.transactions.length) {
+          users = block.transactions.reduce(function(res, tx) {
+            tx.issuers = tx.issuers.reduce(function(res, issuer) {
+              return res.concat({pubkey: issuer});
+            }, []);
+
+            // Parse unlockConditions
+            _.forEach(tx.outputs||[], function(output) {
+              if (output.unlockCondition) {
+                angular.merge(output, BMA.tx.parseUnlockCondition(output.unlockCondition));
+              }
+            });
+
+            return res.concat(tx.issuers.concat(tx.outputs||[]));
+          }, users);
+        }
+
+        var issuer = {pubkey: block.issuer};
+        users.push(issuer);
+        return csWot.extendAll(users)
+          .then(function() {
+            $scope.updateView({block: block, issuer: issuer});
+          });
+      })
+      .catch(function(err) {
+        $scope.loading = false;
+        UIUtils.onError('ERROR.GET_BLOCK_FAILED')(err);
+      });
+  };
+
+  $scope.updateView = function(data) {
+    $scope.formData = data.block;
+    //angular.copy(data.block, $scope.formData);
+    $scope.issuer = data.issuer;
+    $scope.loading = false;
+  };
+
+  $scope.toggleCompactMode = function() {
+    $scope.compactMode = !$scope.compactMode;
+  };
+
+  /* -- popover -- */
+
+  var paddingIndent = 10;
+
+  $scope.toUnlockUIArray = function(unlockTreeItem, leftPadding, operator) {
+    leftPadding = leftPadding || paddingIndent;
+
+    // If operator (AND, OR)
+    if (unlockTreeItem.children && (unlockTreeItem.type == 'AND' || unlockTreeItem.type == 'OR')) {
+      return unlockTreeItem.children.reduce(function(res, child, index){
+        if (child.children && index > 0) {
+          // Add space between expression block
+          res = res.concat({
+            style: {
+              'padding-left': leftPadding + 'px',
+              'padding-top': '10px',
+              'padding-bottom': '10px'
+            },
+            operator: unlockTreeItem.type
+          });
+
+          return res.concat($scope.toUnlockUIArray(child, leftPadding + paddingIndent));
+        }
+        return res.concat($scope.toUnlockUIArray(child, leftPadding + paddingIndent, index && unlockTreeItem.type));
+      }, []);
+    }
+
+    return {
+      style: {
+        'padding-left': leftPadding + 'px'
+      },
+      operator: operator,
+      type: unlockTreeItem.type,
+      value: unlockTreeItem.value
+    };
+  };
+
+  $scope.showUnlockConditionPopover = function(output, event) {
+    if (!output.unlockTree) return;
+
+    // Convert condition into UI array
+    $scope.popoverData = $scope.popoverData || {};
+    $scope.popoverData.unlockConditions = $scope.toUnlockUIArray(output.unlockTree);
+
+    // Open popover
+    if (!$scope.unlockConditionPopover) {
+      $ionicPopover.fromTemplateUrl('templates/blockchain/unlock_condition_popover.html', {
+        scope: $scope
+      }).then(function(popover) {
+        $scope.unlockConditionPopover = popover;
+        //Cleanup the popover when we're done with it!
+        $scope.$on('$destroy', function() {
+          $scope.unlockConditionPopover.remove();
+        });
+        $scope.unlockConditionPopover.show(event);
+      });
+    }
+    else {
+      $scope.unlockConditionPopover.show(event);
+    }
+  };
+
+  $scope.hideUnlockConditionsPopover = function() {
+    if ($scope.unlockConditionPopover) {
+      $scope.unlockConditionPopover.hide();
+      if ($scope.popoverData) {
+        delete $scope.popoverData.unlockConditions;
+      }
+    }
+  };
+
+  $scope.goState = function(stateName, stateParams) {
+    $scope.hideUnlockConditionsPopover();
+    $state.go(stateName, stateParams);
+  };
+
+  /* -- help tip -- */
+
+  // Show help tip
+  $scope.showHelpTip = function() {
+    if (!$scope.isLogin()) return;
+    index = csSettings.data.helptip.block;
+    if (index < 0) return;
+
+    // Create a new scope for the tour controller
+    var helptipScope = $scope.createHelptipScope();
+    if (!helptipScope) return; // could be undefined, if a global tour already is already started
+
+    /*return helptipScope.startBlockTour(index, false)
+     .then(function(endIndex) {
+     helptipScope.$destroy();
+     csSettings.data.helptip.block = endIndex;
+     csSettings.store();
+     });*/
+  };
+}
+