見出し画像

Lisk Elements で遊ぼう #番外編

はじめに

・今回は、前回のソースコードに@liskholderさんデザインを適用したのを載せているだけです。

・説明とかは無し。

・あんまりきれいじゃないので、サービス化するとかアプリ化するとかの時はキレイキレイするのをお勧めします。

HTML (vote.html)

<!DOCTYPE html>
<html>
 <head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Vote</title>
   <script src="https://js.lisk.io/lisk-elements-2.0.0.min.js"></script>
   <script src="https://cdn.jsdelivr.net/npm/sweetalert2@8"></script>
   <script src="vote.js"></script>
   <link rel="stylesheet" href="vote.css">
 </head>
 <body class="lisk-font-bold">
   <div id="area-search">
     <input class="lisk-form lisk-font-bold" type="text" id="liskAddress" placeholder="Lisk Address"/>
     <button class="lisk-button" onclick="search()">Search</button>
     <button class="lisk-button-help" onclick="help()">?</button>
     <div id="target-address" ></div>
     <div id="vote-status" ></div>
   </div>
   <div id="area-contents"></div>
   <div id="area-footer">
     <input type="checkbox" id="lisknet-check" style="display:none;" onchange="changeNet()" checked/>
     <label id="lisknet-toggle" for="lisknet-check"><span></span></label>
     <button class="lisk-button-exec" id="btn-vote" style="display:none;" onclick="castvote()" >vote</button>
   </div>
 </body>
</html>

CSS (vote.css)

@font-face{font-family:'Gilroy-ExtraBold';src:url(Gilroy-ExtraBold.otf);}
@font-face{font-family:'OpenSans-Regular';src:url(OpenSans-Regular.ttf);}
body{margin:0;padding-bottom:15px;overflow-y: scroll !important;}
#area-search{height:6rem;width:calc(100% - 40px);padding:10px 20px;position:fixed;top:0;background-color:#fff;}
#area-contents{padding:20px;padding-top:calc(6rem + 20px);padding-bottom:calc(3rem + 10px);}
#area-footer{height:3rem;width:calc(100% - 40px);padding:10px 20px;position:fixed;bottom:0;background-color:#fff;}
#liskAddress{width:calc(100% - 9rem - 30px);font-size:1rem;}
#target-address{padding-top:10px;}
#vote-status{font-family:OpenSans-Regular;font-size:0.9rem;}
#lisknet-toggle {position:fixed;bottom:20px;left:20px;width:5.5rem;height:2rem;border-radius:3px;background-color:#fd8888;}
#lisknet-toggle::before {content:"Testnet";color:#fff;position:absolute;top:0.4rem;left:auto;right:5px;font-family:OpenSans-Regular;font-size:0.8em;}
#lisknet-toggle > span {position:absolute;top:2px;left:2px;width:1.5rem;height:calc(2rem - 4px);border-radius:3px;background-color:#fff;transition:0.2s;}
#lisknet-check:checked + #lisknet-toggle {background-color:#4db4e4;}
#lisknet-check:checked + #lisknet-toggle::before {content:"Mainnet";position:absolute;left:5px;right:auto;color:#fff;font-family:OpenSans-Regular;font-size:0.8em;}
#lisknet-check:checked + #lisknet-toggle > span {left:calc(4rem - 2px)}
.lisk-font-bold{font-family:Gilroy-ExtraBold;}
.lisk-font-normal{font-family:OpenSans-Regular;font-size:0.8em;}
.lisk-button{width:6rem;background-color:#848686;border:2px solid #848686;border-radius:3px;padding:8px 20px;color:#fff;font-family:OpenSans-Regular;font-size:1rem;}
.lisk-button-help{position:absolute;top:10px;right:20px;width:2rem;height:2rem;background-color:#fff;border:1px solid #848686;border-radius:1rem;color:#848686;font-family:OpenSans-Regular;font-size:0.8rem;}
.lisk-button-exec{margin:0 33%;width:33%;background-color:#2475b9;border:2px solid #2475b9;border-radius:3px;padding:8px 20px;color:#fff;font-family:OpenSans-Regular;font-size:1rem;}
.lisk-form{padding:10px;border-radius:3px;background-color:#fff;border:1px solid #9E9E9E;}
.lisk-border{border-bottom:1px solid #ddd;}
.lisk-delegate-wrapper{width:190px;display:inline-block;}
.lisk-delegate-on{padding:5px 10px;border-radius:3px;background-color:#c7ffec;color:#009688;font-size:0.8em;letter-spacing:0.05em;display:inline-block;}
.lisk-delegate-off{padding:5px 10px;border-radius:3px;background-color:#eee;color:#777;font-size:0.8em;letter-spacing:0.05em;display:inline-block;}
.lisk-rank-wrapper-on{padding:15px 10px;}
.lisk-rank-wrapper-off{padding:15px 10px;background-color:#ccc;}
.lisk-rank-checkbox{display:inline-block;width:1.2rem;}
.lisk-rank-checkbox>input[type="checkbox"]{display:none;}
.lisk-rank-text{vertical-align:middle;width:5rem;display:inline-block;text-align:center;padding:0 10px;color:#4c4c4c;}
.lisk-rank-number{text-align:center;font-size:2em;letter-spacing:0.05em;color:#000;}
.lisk-address-text{display:inline-block;padding:0px;font-family:Gilroy-ExtraBold;font-size:1rem;}
.lisk-address-number{font-family:OpenSans-Regular;font-size:0.9rem;}
.lisk-rank-checkbox.add-vote::after {content:'+';font-weight:bold;}
.lisk-rank-checkbox.remove-vote::after {content:'-';font-weight:bold;}
.swal2-popup{font-family:arial,sans-serif;}
.swal2-popup ul{text-align: left;font-size:0.7rem;}

JavaScript (vote.js)

let client = lisk.APIClient.createMainnetAPIClient();
let explorerUrl = "https://explorer.lisk.io";
let userInfo;
let canInfiniteScroll = true;
let targetAddress = '';
let votes = [];
let delegates = [];
let addDelegates = [];
let removeDelegates = [];
let offset = 0;

/**
 * スクロールイベント
 */
window.onscroll = async() => {
  const delegatesCount = delegates.length;
  if (canInfiniteScroll && document.documentElement.scrollHeight - window.innerHeight <= document.documentElement.scrollTop) {
    offset += 101;
    document.querySelector('#area-contents').innerHTML += await getContent();
    if (delegatesCount === delegates.length) canInfiniteScroll = false;
  }
}

/**
 * クリア処理
 */
const clear = () => {
  canInfiniteScroll = true;
  targetAddress = '';
  userInfo = null;
  votes = [];
  delegates = [];
  addDelegates = [];
  removeDelegates = [];
  offset = 0;
  document.querySelector('#btn-vote').style = "display:none";
  document.querySelector('#area-contents').innerHTML = "";
  document.querySelector('#target-address').innerHTML = "";
  document.querySelector('#vote-status').innerHTML = "";
}

/**
 * 接続Net切替取得
 */
const changeNet = () => {
  if (document.querySelector('#lisknet-check').checked) {
    clear();
    client = lisk.APIClient.createMainnetAPIClient();
    explorerUrl = "https://explorer.lisk.io";
    document.querySelector('#liskAddress').value = '';
  } else {
    clear();
    client = lisk.APIClient.createTestnetAPIClient();
    explorerUrl = "https://testnet-explorer.lisk.io/";
    document.querySelector('#liskAddress').value = '3905013786800090105L';
  }
}

/**
 * デリゲート情報取得
 */
const getDelegates = async() => {
  try {
    // ランクの昇順で101人分のデリゲート情報を取得
    const result = await client.delegates.get({offset:offset, limit:101,sort:'rank:asc'});
    return result.data;
  } catch (err) {
    console.log(err);
    return [];
  }
}

/**
 * ユーザー情報取得
 */
const getUser = async() => {
  try {
    return await client.accounts.get({address:targetAddress});
  } catch(err) {
    console.log(err);
    return null;
  }
}

/**
 * vote情報取得
 */
const getVotes = async() => {
  try {
    const result = await client.votes.get({address:targetAddress, limit:101});
    
    // 取得した結果からpublicKeyだけの配列を作る
    let voteAddress = [];
    result.data.votes.forEach(function(val){
      voteAddress.push(val.publicKey);
    });
    return voteAddress;
  } catch(err) {
    console.log(err);
    return [];
  }
}

/**
 * vote追加
 */
const vote = (elem) => {
  const chkElem = elem.querySelector('.chk');
  if (!chkElem.checked) {
    if (addDelegates.indexOf(chkElem.value) < 0) {
      addDelegates.push(chkElem.value);
    }
    chkElem.checked = true;
    elem.querySelector('.lisk-rank-checkbox').className = 'lisk-rank-checkbox add-vote';
    elem.style="background-color:#c7e4ff;color:#004d96;";
  } else {
    addDelegates = addDelegates.filter((v) => v !== chkElem.value);
    chkElem.checked = false;
    elem.querySelector('.lisk-rank-checkbox').className = 'lisk-rank-checkbox';
    elem.style="";
  }
  document.querySelector('#vote-status').innerHTML = `voted: ${votes.length}, add: ${addDelegates.length}, remove: ${removeDelegates.length}`;
  if ((removeDelegates.length + addDelegates.length > 33) ||
    (votes.length - removeDelegates.length + addDelegates.length > 101)) {
      document.querySelector('#btn-vote').style="background-color:#b92424;border:2px solid #b92424;";
  } else {
      document.querySelector('#btn-vote').style="";
  }
}

/**
 * vote解除
 */
const unvote = (elem) => {
  const chkElem = elem.querySelector('.chk');
  if (chkElem.checked) {
    if (removeDelegates.indexOf(chkElem.value) < 0) {
      removeDelegates.push(chkElem.value);
    }
    chkElem.checked = false;
    elem.querySelector('.lisk-rank-checkbox').className = 'lisk-rank-checkbox remove-vote';
    elem.style="background-color:#ffc7c7;color:#960000";
  } else {
    removeDelegates = removeDelegates.filter((v) => v !== chkElem.value);
    chkElem.checked = true;
    elem.querySelector('.lisk-rank-checkbox').className = 'lisk-rank-checkbox';
    elem.style="";
  }
  document.querySelector('#vote-status').innerHTML = `voted: ${votes.length}, add: ${addDelegates.length}, remove: ${removeDelegates.length}`;
  if ((removeDelegates.length + addDelegates.length > 33) ||
    (votes.length - removeDelegates.length + addDelegates.length > 101)) {
      document.querySelector('#btn-vote').style="background-color:#b92424;border:2px solid #b92424;";
  } else {
      document.querySelector('#btn-vote').style="";
  }
}

/**
 * 表示内容を取得
 */
const getContent = async() => {
  // デリゲート情報取得
  const _delegates = await getDelegates();
  
  // 画面描画
  let content = "";
  
  // 取得したデリゲート情報と取得済のvote情報を比較しながらHTML生成
  _delegates.forEach(function(delegate){
    const data = delegates.filter(function(item, index){
      if (item.publicKey == delegate.account.publicKey) return true;
    });
    if (data.length === 0) delegates.push({publicKey:delegate.account.publicKey, username:delegate.username});

    if (votes.indexOf(delegate.account.publicKey) >= 0) {
      content += `<div class="lisk-rank-wrapper-on" onclick="unvote(this);">`;
      content += `<div class="lisk-rank-checkbox"><input type="checkbox" class="chk" value="${delegate.account.publicKey}" checked></div>`;
      content += `<div class="lisk-font-normal lisk-rank-text">Rank<div class="lisk-font-bold lisk-rank-number">${delegate.rank}</div></div>`;
      content += `<div class="lisk-delegate-wrapper"><span class="lisk-delegate-on">${delegate.username}</span></div>`;
      content += `<div class="lisk-address-text">Address: <span class="lisk-address-number">${delegate.account.address}</span></div>`;
      content += `</div>`;
      content += `<div class="lisk-border"></div>`;
    } else {
      content += `<div class="lisk-rank-wrapper-off" onclick="vote(this);">`;
      content += `<div class="lisk-rank-checkbox"><input type="checkbox" class="chk" value="${delegate.account.publicKey}"></div>`;
      content += `<div class="lisk-font-normal lisk-rank-text">Rank<div class="lisk-font-bold lisk-rank-number">${delegate.rank}</div></div>`;
      content += `<div class="lisk-delegate-wrapper"><span class="lisk-delegate-off">${delegate.username}</span></div>`;
      content += `<div class="lisk-address-text">Address: <span class="lisk-address-number">${delegate.account.address}</span></div>`;
      content += `</div>`;
      content += `<div class="lisk-border"></div>`;
    }
  });
  return content;
}

/**
 * 検索ボタン処理
 */
const search = async() => {
  clear();
  targetAddress = document.querySelector('#liskAddress').value;
  document.querySelector('#target-address').innerHTML = "Address: Searching..";

  // vote情報取得
  let targetAddressContent = `Address: none`;
  if (targetAddress) {
    const ret = await getUser();
    votes = await getVotes();
    if (ret && ret.data.length > 0) {
      targetAddressContent = `Address: <a href="${explorerUrl}/address/${targetAddress}" target="_blank">${targetAddress}</a>`;
      userInfo = ret.data[0];
    }
  }
  
  // 表示内容を取得して出力
  document.querySelector('#area-contents').innerHTML = await getContent();
  document.querySelector('#target-address').innerHTML = targetAddressContent;
  document.querySelector('#vote-status').innerHTML = `voted: ${votes.length}, add: ${addDelegates.length}, remove: ${removeDelegates.length}`;
  document.querySelector('#btn-vote').style="background-color:#b92424;border:2px solid #b92424;";
}

/**
 * voteボタン処理
 */
const castvote = async() => {
  let passphrase = null;
  let secondpassphrase = null;
  let transactionId = null;

  // 追加・取消をしているかをチェック
  if (removeDelegates.length <= 0 && addDelegates.length <= 0) {
    Swal.fire({
      title: 'Error',
      type: 'error',
      text: '変更がありません'
    });
    return;
  } 

  // 1度に可能なvote上限を超過しているかをチェック
  if (removeDelegates.length + addDelegates.length > 33) {
    Swal.fire({
      title: 'Error',
      type: 'error',
      text: 'add・removeの合計が33を超えてはいけません'
    });
    return;
  } 

  // voteの上限数を超過しているかをチェック
  if (votes.length - removeDelegates.length + addDelegates.length > 101) {
    Swal.fire({
      title: 'Error',
      type: 'error',
      text: 'vote上限数(101)を超えています'
    });
    return;
  } 

  // パスフレーズ入力
  await Swal.fire({
    title: 'passphraseを入力して下さい',
    type: 'info',
    input: 'password',
    showCancelButton: true,
    preConfirm: (val) => {
      val = val.replace(/^\s+|\s+$/g,'');
      // 入力されているかをチェック
      if (!val) {
        document.querySelector('.swal2-input').value = "";
        Swal.showValidationMessage('passphraseは必須です');
        return;
      }
      // 入力されたパスフレーズが検索時のLiskアドレスのものかをチェック
      if (targetAddress != lisk.cryptography.getAddressFromPassphrase(val)) {
        document.querySelector('.swal2-input').value = "";
        Swal.showValidationMessage('passphraseが違います');
        return;
      }
      passphrase = val;
    }
  });
  if (!passphrase) return;

  // セカンドパスフレーズ入力
  if (userInfo.secondPublicKey) {
    await Swal.fire({
      title: 'second passphraseを入力して下さい',
      type: 'info',
      input: 'password',
      showCancelButton: true,
      preConfirm: (val) => {
        val = val.replace(/^\s+|\s+$/g,'');
        // 入力されているかをチェック
        if (!val) {
          document.querySelector('.swal2-input').value = "";
          Swal.showValidationMessage('second passphraseは必須です');
          return;
        }
        secondpassphrase = val;
      }
    });
    if (!secondpassphrase) return;
  }

  try {
    // トランザクション生成
    let params = {passphrase:passphrase,votes:addDelegates,unvotes:removeDelegates}
    if (secondpassphrase) params['secondPassphrase'] = secondpassphrase;
    const transaction = await lisk.transaction.castVotes(params);
    if (!transaction.id) {
      Swal.fire({
        title: 'Error',
        type: 'error',
        text: 'transactionの生成に失敗しました'
      });
      return;
    }
    transactionId = transaction.id;

    // ブロードキャスト
    await client.transactions.broadcast(transaction);
    Swal.fire({
      title: 'Success',
      type: 'success',
      html: `voteしました。<br>反映まで15秒ほどかかります。<br>続けてvoteする場合は<br>反映を確認してから行って下さい。<br>` +
            `ID: <a href="${explorerUrl}/tx/${transactionId}" target="_blank">${transactionId}</a>`
    });
  } catch (err) {
    Swal.fire({
      title: 'Error',
      type: 'error',
      text: 'voteに失敗しました'
    });
    console.log(err);
  }
}

/**
* helpボタン処理
*/
const help = () => {
  Swal.fire({
    title: 'Help',
    type: 'question',
    html: '<ul>' +
          '<li>Searchボタンを押すとDelegate一覧を表示します。</li>' +
          '<li>Lisk Addressを入力してから押すとvote状況も表示します。</li>' +
          '<li>voteボタンを押すとvote出来ます。</li>' +
          '<li>画面左下のトグルでメインネットとテストネットを切り替えられます。</li>' +
          '<li>テストネットに切り替えるとサンプル用のアドレスが設定されます。</li>' +
          '<li>サンプル用のパスフレーズは「chicken problem whip mobile shield angry hard toast disease chronic code category」</li>' +
          '<li>メインネットでやって問題が起こっても責任は取りません。</li>' +
          '</ul>'
  });
}

注意

・全部同じ階層においてください。
(階層変える場合はHTMLファイル内のパスの変更忘れ注意。)

・ファイルエンコードはUTF-8です。

・Gilroy-ExtraBold.otf と OpenSans-Regular.ttf のフォントファイルが必要です。
(ググるなりして取得して下さい。)

画像

こんなかんじですー。

上のやつはこちら

応援

「おもろいやんけ!」と思ってもらえたら、コメントや万博おじへtweetしてくれると嬉しいです。
Lisk Japanもよろしくね!

この記事が気に入ったらサポートをしてみませんか?