LoginSignup
24

Symbol × SSS Extension で作る dApps 入門

Last updated at Posted at 2022-05-01

はじめに

GWいかがお過ごしでしょうか?速習symbolは読みましたか?

もう読み終わって暇?仕方ないね、GW後半戦のコンテンツの供給だ!!!

どうも、いなたつです。SSS Extension作ってる人です。Symbol Draw作ってる人です。

どうやら巷にHTML / JavaScriptだけでdAppsが作れるブロックチェーンがあるらしいですよ、Symbolって言うんですけど

はい、ってことで今回はSymbolブロックチェーンでdAppsを作ってSSS Extensionに接続しようってことでやっていきます。

本記事では簡単なウォレットを作っていきます。とはいっても 手抜きシンプルな見た目なのでかっこいいオリジナルウォレットを作ってくれたらなぁって思っています。

ウォレットとして最低限の機能しかないのでいろんなトランザクションを使えてかっちょいいブラウザウォレットを誰か作ってください(他力本願寺)

本記事の目標

  • Symbol ブロックチェーンを利用したアプリケーション(dApps)を作るための第一歩を踏み出す。
  • とSSS Extensionとの連携の方法を身に着ける。
  • Symbolのすごさ・楽さ・楽しさを知る。

の、三本です!(SAZAE SAN)

対象読者

  • ブロックチェーン使ってみたい人
  • 他のブロックチェーンで開発をしたことがある人
  • Symbol は使ってるけど SSS Extension をどうやって導入したらいいかわからない人

前提とする知識

本記事では以下の内容については、説明いたしません。とはいっても書いているマークアップやプログラムの文法がわかれば問題ないです。
Git, GitHubに関しては、init, add, commit, pushができれば今回の内容に関しては問題なく進めれるかと思います。

  • HTML / CSS / JavaScript の基本
  • Git / GitHub の基本的な使い方

Symbol とは

Symbol とは、2021 年 3 月 17 日に、ブロックチェーンシステム NEM の次世代版としてローンチされました。
Symbol は、スマートコントラクトを持たず、豊富な API・SDK を持ち、スマートサイニングコントラクトという概念を持ちブロックチェーン上の機能を誰でも利用することができます。

Symbolで開発すれば、全てJavaScript / TypeScriptで完結させることも可能です。
コントラクトを書くために別の言語(Solidity等)を使用する必要はありません。

SSS Extension とは

Web アプリケーションと連携しWebアプリケーション上で秘密鍵を扱うことなくトランザクションへと署名することができるブラウザ拡張機能です。

これにより、

  • コントラクトの内容を署名前に確認する
  • Webアプリケーションに秘密鍵を知らせずにトランザクションに署名する

ことができるようになります。

Ethereumを扱ったことがある人向けに一言でいうとSymbol版のMetaMaskのようなものです。

2022 年 3 月 17 日 (Symbol一周年) に正式リリースされました。

SSS Extension Twitter

SSS Extension Chrome Web Store

dApps とは

ブロックチェーンを扱うアプリケーションのことです。Ethereumのスマートコントラクトを利用することが前提とされている雰囲気を感じますが、Symbolではブロックチェーンを使うことはすなわちスマートサイニングコントラクトを扱うことと同義と言っていいと思います。なのでSymbol使ったアプリケーションは全部dAppsといっていいと思います。知らんけど。

とりあえず、本記事ではdAppsとは「ブロックチェーンを扱うアプリケーション」と定義します。

前準備

作業ディレクトリを作成してください。記事内では SSS-dAppsを作業ディレクトとします。

GitHub Pages で html ファイルを公開する

まずSSS-dApps内にindex.htmlを作成してください。

<!DOCTYPE html>
<html lang="ja">
<head>
  <title>SSS My Wallet</title>
</head>
<body>
  <h1>SSS My Wallet</h1>
</body>
</html>

GitHubにpushしたら、リポジトリの設定ページを開いてください。

サイドメニューからPagesを選択してください
kiji1.png

ブランチはmaster(main) ディレクトリは/(root)を選択し保存してください。
Inkedkiji2_LI.jpg

すこし待てば反映されるので、
緑でハイライトされている公開されたリンクを開いてください。

image.png

公開されましたね。以降はpushすると反映されるので、確認したいときは都度pushしてください。
とはいっても、SSS Extensionが登場するまでは普通にローカルのファイルをブラウザで開いても問題ないです。

Symbol Desktop Wallet でテストアカウント作成

Symbol Desktop Walletを使ってテスト用のアカウントを作ります。

テストネットのアカウント作成と蛇口からテスト用の通貨を獲得しましょう。

このあたりのコンテンツが参考になります。

1つ目の動画でデスクトップウォレットを用いてテストネット(通貨価値のないテスト用のネットワーク)のアカウントを作成し、蛇口から通貨を獲得するまで解説されています。
https://www.youtube.com/playlist?list=PLifx63Bv-tdM27OZnne3-5IOvGyI_zijX

デスクトップウォレットの使い方の記事です。記事内ではメインネットを用いてるので注意してください。
https://note.com/nembear/n/n56d2c9a28e8a

こんな感じでテスト用の通貨をもらいます。
image.png

こうなればOK!
image.png

Hello Symbol

Symbol SDKをhtmlファイルに読み込みます。また、JavaScriptファイルを作成しSymbol SDKを読み込みます。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <title>SSS My Wallet</title>
</head>
<body>
  <h1>SSS My Wallet</h1>

 <script type="text/javascript" src="https://xembook.github.io/nem2-browserify/symbol-sdk-pack-1.0.3.js"></script>
 <script type="text/javascript" src="script.js"></script>
</body>
</html>
script.js
const symbol = require('/node_modules/symbol-sdk')

console.log("Hello Symbol")

image.png

さらに、作成したSymbolAccountのアドレスを表示しましょう。

image.png

Symbol Walletからアドレスをコピーしてください。

次はJavaScriptでコンソールに表示しましょう。

script.js
const symbol = require('/node_modules/symbol-sdk')

const address = symbol.Address.createFromRawAddress("TAD7Q3FEN5CZRZFE3WX6TWEESATVTMJDS2ETVTY")
console.log("Hello Symbol")
console.log(`Your Address : ${address.plain()}`)

image.png

コンソールに表示されましたね、アドレスの情報を表示することができました。これでSymbol SDKがうまく扱えていることまで確認できましたね、では早速dApps開発に着手しちゃいましょう。

アカウントの情報を確認する

アカウントのアドレスと所有するxymの量を表示させてみましょう。

まずはindex.htmlにアドレスとxymを表示させる場所を作ってください。

背景が白いと記事上で画像のエリアがわかりにくいので少し背景色をつけときます。これは別に真似しなくて大丈夫です。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <title>SSS My Wallet</title>
</head>
<body style="background: rgb(242, 242, 242);">
  <h1>SSS My Wallet :<span id="wallet-addr"></span></h1>
  <div id="wallet-xym"></div>

  <script type="text/javascript" src="https://xembook.github.io/nem2-browserify/symbol-sdk-pack-1.0.3.js"></script>
  <script type="text/javascript" src="script.js"></script>
</body>
</html>

アドレスを表示しましょう

const dom_addr = document.getElementById('wallet-addr')
dom_addr.innerText = address.pretty()

image.png

表示されましたね、

Symbol-SDKのAddress.pretty()を使うとアドレスがハイフンで区切られた文字列で表示されるため見やすいですね。

Symbolのテストネットで使用する設定値をプログラムのはじめに定義しておきます。

const GENERATION_HASH = '49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4'
const EPOCH = 1667250467
const XYM_ID = '72C0212E67A08BCE'
const NODE_URL = 'https://sym-test-03.opening-line.jp:3001'
const NET_TYPE = symbol.NetworkType.TEST_NET

次は、XYMの所持量を表示しましょう。

const repositoryFactory = new symbol.RepositoryFactoryHttp(NODE_URL)
const accountHttp = repositoryFactory.createAccountRepository()

accountHttp.getAccountInfo(address)
  .toPromise()
  .then((accountInfo) => {
    for (let m of accountInfo.mosaics) {
      if (m.id.id.toHex() === XYM_ID) {
        const dom_xym = document.getElementById('wallet-xym')
        dom_xym.innerText = `XYM Balance : ${m.amount.compact() / Math.pow(10, 6)}`
      }
    }
  })

RepositoryFactoryはSymbol-SDKで提供されるアカウントやモザイク等の機能を提供するRepositoryを作成するためのものです。
RepositoryFactoryからアカウントの情報を入手するためにAccountRepositoryを作ります。

AccountRepositoryからアカウントの所持しているモザイク(トークン)の情報を取得します。今回はxymの所有量のみを使うのでモザイクのIDがXYMのIDと一致するかを確認しています。

XYMの可分性(小数点以下の桁数)は6なので取得した値を10^6で割った値を表示します。他のモザイクで可分性を考慮した表示を行う場合、MosaicRepositoryからモザイクの情報を取得し、そのモザイクに設定された可分性(divisibility)を用いて表示すると良い感じです。

この辺でちょっとUIの整理をします。style.cssを作ります。

style.css
body {
  background: rgb(242, 242, 242);
}

h1 {
  color:rgba(22, 22, 22, 0.4);
  font-size: 96px;
  text-align: right;
  margin-right: 80px;
}

.account-card {
  background: white;
  border-radius: 16px;
  padding: 32px;
  width: 600px;
  margin: 16px;
}
.tx-card {
  background: white;
  border-radius: 16px;
  padding: 32px;
  width: 100%;
  margin: 16px;
}

#wallet-addr {
  font-size: 24px;
}

.container {
  font-size: 24px;
  margin: 16px;
}

.wrapper {
  display: flex;
  justify-content: space-between;
  margin: 0px 80px;
}
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <title>SSS My Wallet</title>
  <link rel="stylesheet" href="style.css" type="text/css">
</head>
<body>
  <h1>SSS My Wallet</h1>
  
  <div class="wrapper">
      <div class="account-card">
        <h2>Account Info</h2>
        <div class="container">
          <div>Address</div>
          <div id="wallet-addr"></div>
        </div>
        <div class="container">
          <div id="wallet-xym"></div>
        </div>
      </div>
      <div class="tx-card">
        <h2>Transactions</h2>
      </div>
  </div>

  <script type="text/javascript" src="https://xembook.github.io/nem2-browserify/symbol-sdk-pack-1.0.3.js"></script>
  <script type="text/javascript" src="script.js"></script>
</body>
</html>

script.js
const symbol = require('/node_modules/symbol-sdk')

const GENERATION_HASH = '49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4'
const EPOCH = 1667250467
const XYM_ID = '72C0212E67A08BCE'
const NODE_URL = 'https://sym-test-03.opening-line.jp:3001'
const NET_TYPE = symbol.NetworkType.TEST_NET

const address = symbol.Address.createFromRawAddress("TAD7Q3FEN5CZRZFE3WX6TWEESATVTMJDS2ETVTY")

console.log("Hello Symbol")
console.log(`Your Address : ${address.plain()}`)

const dom_addr = document.getElementById('wallet-addr')
dom_addr.innerText = address.pretty()

const repositoryFactory = new symbol.RepositoryFactoryHttp(NODE_URL)
const accountHttp = repositoryFactory.createAccountRepository()

accountHttp.getAccountInfo(address)
  .toPromise()
  .then((accountInfo) => {
    for (let m of accountInfo.mosaics) {
      if (m.id.id.toHex() === XYM_ID) {
        const dom_xym = document.getElementById('wallet-xym')
        dom_xym.innerText = `XYM Balance : ${m.amount.compact() / Math.pow(10, 6)}`
      }
    }
  })

ここまで来るとこんな感じになります。ちょっとそれっぽくなりましたね。

image.png

トランザクション履歴の取得

次はトランザクションの履歴を取得していきましょう。

まずはTransactionRepositoryを作ります。
そして、searchCriteriaを作成します。これは、Transactionの検索する設定のようなものです。
指定したアドレスの承認済みのトランザクションを20個取得するようにしています。

const transactionHttp = repositoryFactory.createTransactionRepository()
const searchCriteria = {
  group: symbol.TransactionGroup.Confirmed,
  address,
  pageNumber: 1,
  pageSize: 20,
  order: symbol.Order.Desc,
}

では、トランザクションを探します。

さっき作った検索の設定でTransactionRepositoryのsearchメソッドで検索します。

入手した情報からDOMを作っていきます。

まずトランザクションを表示するエリアにトランザクションのリストを作るための要素を追加します。

indec.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <title>SSS My Wallet</title>
  <link rel="stylesheet" href="style.css" type="text/css">
</head>
<body>
  <h1>SSS My Wallet</h1>
  
  <div class="wrapper">
      <div class="account-card">
        <h2>Account Info</h2>
        <div class="container">
          <div>Address</div>
          <div id="wallet-addr"></div>
        </div>
        <div class="container">
          <div id="wallet-xym"></div>
        </div>
      </div>
      <div class="tx-card">
        <h2>Transactions</h2>
        <div id="wallet-transactions"></div>
      </div>
  </div>

  <script type="text/javascript" src="https://xembook.github.io/nem2-browserify/symbol-sdk-pack-1.0.3.js"></script>
  <script type="text/javascript" src="script.js"></script>
</body>
</html>
transactionHttp
  .search(searchCriteria)
  .toPromise()
  .then((txs) => {
    console.log(txs)
    const dom_txInfo = document.getElementById('wallet-transactions')
    for (let tx of txs.data) {
      console.log(tx)
      const dom_tx = document.createElement('div')
      const dom_txType = document.createElement('div')
      const dom_hash = document.createElement('div')

      dom_txType.innerText = `Transaction Type : ${getTransactionType(tx.type)}`
      dom_hash.innerText = `Transaction Hash : ${tx.transactionInfo.hash}`

      dom_tx.appendChild(dom_txType)
      dom_tx.appendChild(dom_hash)
      dom_tx.appendChild(document.createElement('hr'))

      dom_txInfo.appendChild(dom_tx)
    }
  })

Transactionのtypeは数字で得られるので文字列に変換します。どのトランザクションがどの値かはhttps://symbol.github.io/symbol-sdk-typescript-javascript/1.0.3/enums/TransactionType.htmlを参照してください。今回はとりあえずTransferTransactionをとりあえず表示できるようにします。

function getTransactionType (type) {
  if (type === 16724) return 'TRANSFER TRANSACTION'
  return 'OTHER TRANSACTION'
}

image.png

こんな感じですね。トランザクションが1つだと見にくいのでちょっとデスクトップウォレットでトランザクションを発生させてみましょう。

https://www.youtube.com/playlist?list=PLifx63Bv-tdM27OZnne3-5IOvGyI_zijX 2本目の動画で解説されてますね。

僕のテスト用アカウントのアドレスを貼っておくので困ったら宛先にしてみてください。

Address : TDEC5VUUAUYHKI2Y45WBDMGODAS42P3PPCTMGUY

Feeの欄をSlowestのままだと時間がかかるのでテストネットはとりあえずFastでいいと思います。

こんなかんじです。SEND -> パスワード入力 -> ConfirmでOK!
image.png
image.png

何回かトランザクションを発生させました。
image.png

作ったWallet側でも見てみましょう。トランザクションが並びましたね!

image.png

ここまでのプログラム

確認用 & コピペ用

indec.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <title>SSS My Wallet</title>
  <link rel="stylesheet" href="style.css" type="text/css">
</head>
<body>
  <h1>SSS My Wallet</h1>
  
  <div class="wrapper">
      <div class="account-card">
        <h2>Account Info</h2>
        <div class="container">
          <div>Address</div>
          <div id="wallet-addr"></div>
        </div>
        <div class="container">
          <div id="wallet-xym"></div>
        </div>
      </div>
      <div class="tx-card">
        <h2>Transactions</h2>
        <div id="wallet-transactions"></div>
      </div>
  </div>

  <script type="text/javascript" src="https://xembook.github.io/nem2-browserify/symbol-sdk-pack-1.0.3.js"></script>
  <script type="text/javascript" src="script.js"></script>
</body>
</html>
script.js
const symbol = require('/node_modules/symbol-sdk')

const GENERATION_HASH = '7FCCD304802016BEBBCD342A332F91FF1F3BB5E902988B352697BE245F48E836'
const EPOCH = 1637848847
const XYM_ID = '3A8416DB2D53B6C8'
const NODE_URL = 'https://sym-test.opening-line.jp:3001'
const NET_TYPE = symbol.NetworkType.TEST_NET

const address = symbol.Address.createFromRawAddress("TAD7Q3FEN5CZRZFE3WX6TWEESATVTMJDS2ETVTY")

console.log("Hello Symbol")
console.log(`Your Address : ${address.plain()}`)

const dom_addr = document.getElementById('wallet-addr')
dom_addr.innerText = address.pretty()

const repositoryFactory = new symbol.RepositoryFactoryHttp(NODE_URL)
const accountHttp = repositoryFactory.createAccountRepository()

accountHttp.getAccountInfo(address)
  .toPromise()
  .then((accountInfo) => {
    for (let m of accountInfo.mosaics) {
      if (m.id.id.toHex() === XYM_ID) {
        const dom_xym = document.getElementById('wallet-xym')
        dom_xym.innerText = `XYM Balance : ${m.amount.compact() / Math.pow(10, 6)}`
      }
    }
  })

const transactionHttp = repositoryFactory.createTransactionRepository()
const searchCriteria = {
  group: symbol.TransactionGroup.Confirmed,
  address,
  pageNumber: 1,
  pageSize: 20,
  order: symbol.Order.Desc,
}

transactionHttp
  .search(searchCriteria)
  .toPromise()
  .then((txs) => {
    console.log(txs)
    const dom_txInfo = document.getElementById('wallet-transactions')
    for (let tx of txs.data) {
      console.log(tx)
      const dom_tx = document.createElement('div')
      const dom_txType = document.createElement('div')
      const dom_hash = document.createElement('div')

      dom_txType.innerText = `Transaction Type : ${getTransactionType(tx.type)}`
      dom_hash.innerText = `Transaction Hash : ${tx.transactionInfo.hash}`

      dom_tx.appendChild(dom_txType)
      dom_tx.appendChild(dom_hash)
      dom_tx.appendChild(document.createElement('hr'))

      dom_txInfo.appendChild(dom_tx)
    }
  })

function getTransactionType (type) { // https://symbol.github.io/symbol-sdk-typescript-javascript/1.0.3/enums/TransactionType.html
  if (type === 16724) return 'TRANSFER TRANSACTION'
  return 'OTHER TRANSACTION'
}

style.css
body {
  background: rgb(242, 242, 242);
}

h1 {
  color:rgba(22, 22, 22, 0.4);
  font-size: 96px;
  text-align: right;
  margin-right: 80px;
}

.account-card {
  background: white;
  border-radius: 16px;
  padding: 32px;
  width: 600px;
  margin: 16px;
}
.tx-card {
  background: white;
  border-radius: 16px;
  padding: 32px;
  width: 100%;
  margin: 16px;
}

#wallet-addr {
  font-size: 24px;
}

.container {
  font-size: 24px;
  margin: 16px;
}

.wrapper {
  display: flex;
  justify-content: space-between;
  margin: 0px 80px;
}

Symbol でトランザクションを送る

送信先、送信量、メッセージを指定してトランザクションを作ってみましょう。ついでに少し見た目を調整しました。

秘密鍵はSymbol WalletでPrivate Keyの右にあるShowボタンを押してパスワードを入力すれば表示されます。
image.png

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <title>SSS My Wallet</title>
  <link rel="stylesheet" href="style.css" type="text/css">
</head>
<body>
  <h1>SSS My Wallet</h1>
  
  <div class="wrapper">
      <div class="left">
        <div class="account-card">
          <h2>Account Info</h2>
          <div class="container">
            <div>Address</div>
            <div id="wallet-addr"></div>
          </div>
          <div class="container">
            <div id="wallet-xym"></div>
          </div>
        </div>
        <div class="sender-card">
          <h2>SEND</h2>
          <div class="textfiled-wrapper">
            <div>TO: </div>
            <input type="text" id="form-addr" class="textfiled" />
          </div>
          <div class="textfiled-wrapper">
            <div>AMOUNT: </div>
            <input type="text" id="form-amount" class="textfiled" />
          </div>
          <div class="textfiled-wrapper">
            <div>MESSAGE: </div>
            <input type="text" id="form-message" class="textfiled" />
          </div>
          <div class="textfiled-wrapper">
            <div>PRIVATE KEY: </div>
            <input type="text" id="form-pk" class="textfiled" />
          </div>
          <input type="button" onclick="handleClick();" value="秘密鍵で署名"/>
        </div>
      </div>
      <div class="right">
        <div class="tx-card">
          <h2>Transactions</h2>
          <div id="wallet-transactions"></div>
        </div>
      </div>
  </div>

  <script type="text/javascript" src="https://xembook.github.io/nem2-browserify/symbol-sdk-pack-1.0.3.js"></script>
  <script type="text/javascript" src="script.js"></script>
</body>
</html>
script.js
const symbol = require('/node_modules/symbol-sdk')

const GENERATION_HASH = '7FCCD304802016BEBBCD342A332F91FF1F3BB5E902988B352697BE245F48E836'
const EPOCH = 1637848847
const XYM_ID = '3A8416DB2D53B6C8'
const NODE_URL = 'https://sym-test.opening-line.jp:3001'
const NET_TYPE = symbol.NetworkType.TEST_NET

const address = symbol.Address.createFromRawAddress("TAD7Q3FEN5CZRZFE3WX6TWEESATVTMJDS2ETVTY")

console.log("Hello Symbol")
console.log(`Your Address : ${address.plain()}`)

const dom_addr = document.getElementById('wallet-addr')
dom_addr.innerText = address.pretty()

const repositoryFactory = new symbol.RepositoryFactoryHttp(NODE_URL)
const accountHttp = repositoryFactory.createAccountRepository()

accountHttp.getAccountInfo(address)
  .toPromise()
  .then((accountInfo) => {
    for (let m of accountInfo.mosaics) {
      if (m.id.id.toHex() === XYM_ID) {
        const dom_xym = document.getElementById('wallet-xym')
        dom_xym.innerText = `XYM Balance : ${m.amount.compact() / Math.pow(10, 6)}`
      }
    }
  })

const transactionHttp = repositoryFactory.createTransactionRepository()
const searchCriteria = {
  group: symbol.TransactionGroup.Confirmed,
  address,
  pageNumber: 1,
  pageSize: 20,
  order: symbol.Order.Desc,
}

transactionHttp
  .search(searchCriteria)
  .toPromise()
  .then((txs) => {
    console.log(txs)
    const dom_txInfo = document.getElementById('wallet-transactions')
    for (let tx of txs.data) {
      console.log(tx)
      const dom_tx = document.createElement('div')
      const dom_txType = document.createElement('div')
      const dom_hash = document.createElement('div')

      dom_txType.innerText = `Transaction Type : ${getTransactionType(tx.type)}`
      dom_hash.innerText = `Transaction Hash : ${tx.transactionInfo.hash}`

      dom_tx.appendChild(dom_txType)
      dom_tx.appendChild(dom_hash)
      dom_tx.appendChild(document.createElement('hr'))

      dom_txInfo.appendChild(dom_tx)
    }
  })

function getTransactionType (type) { // https://symbol.github.io/symbol-sdk-typescript-javascript/1.0.3/enums/TransactionType.html
  if (type === 16724) return 'TRANSFER TRANSACTION'
  return 'OTHER TRANSACTION'
}

function handleClick() {
  const addr = document.getElementById('form-addr').value
  const amount = document.getElementById('form-amount').value
  const message = document.getElementById('form-message').value
  const pk = document.getElementById('form-pk').value
  
  const tx = symbol.TransferTransaction.create(
    symbol.Deadline.create(EPOCH),
    symbol.Address.createFromRawAddress(addr),
    [
      new symbol.Mosaic(
        new symbol.MosaicId(XYM_ID),
        symbol.UInt64.fromUint(Number(amount))
      )
    ],
    symbol.PlainMessage.create(message),
    NET_TYPE,
    symbol.UInt64.fromUint(2000000)
  )

  const acc = symbol.Account.createFromPrivateKey(pk, NET_TYPE)
  
  const signedTx = acc.sign(tx, GENERATION_HASH)

  transactionHttp.announce(signedTx)
}
style.css
body {
  background: rgb(242, 242, 242);
}

h1 {
  color:rgba(22, 22, 22, 0.4);
  font-size: 96px;
  text-align: right;
  margin-right: 80px;
}

.left {
  width: 800px;
}
.right {
  width: 100%;
}

.account-card {
  background: white;
  border-radius: 16px;
  margin: 16px;
  padding: 16px;
}

.sender-card {
  background: white;
  border-radius: 16px;
  margin: 16px;
  padding: 16px;
}

.tx-card {
  background: white;
  border-radius: 16px;
  padding: 32px;
  width: 1200px;
  margin: 16px;
}

.textfiled-wrapper {
  display: flex;
  justify-content: space-between;
  margin: 8px;
}

.textfiled {
  width: 400px;
}

#wallet-addr {
  font-size: 24px;
}

.container {
  font-size: 24px;
  margin: 16px;
}

.wrapper {
  display: flex;
  justify-content: space-between;
  margin: 80px;
}

TO 送信先
AMOUNT 送信量
MESSAGE メッセージ
PRIVATE KEY 秘密鍵
を入力して送信ボタンを押せばトランザクションが作られて 秘密鍵で署名されて ノードにアナウンスされます。

トランザクションを生成

  const tx = symbol.TransferTransaction.create(
    symbol.Deadline.create(EPOCH),
    symbol.Address.createFromRawAddress(addr),
    [
      new symbol.Mosaic(
        new symbol.MosaicId(XYM_ID),
        symbol.UInt64.fromUint(Number(amount))
      )
    ],
    symbol.PlainMessage.create(message),
    NET_TYPE,
    symbol.UInt64.fromUint(2000000)
  )

署名

  const pk = 'CAB93A8E8966A5DAD9D9956DABEEE184624AD7BBE3EAEF469871563322E8D6CB'

  const acc = symbol.Account.createFromPrivateKey(pk, NET_TYPE)
  
  const signedTx = acc.sign(tx, GENERATION_HASH)

アナウンス

transactionHttp.announce(signedTx)

こんな感じになりますね。
image.png

普通にブラウザウォレットを作るってだけならとりあえず

  • 残高確認
  • 履歴確認
  • トランザクションの作成

の機能を持った自分だけのウォレットができたね~ってなります。

しかし今回はそれでは終わりません。

秘密鍵を入力するのって怖いんです。なぜかって秘密鍵が盗まれると終わりだから。

ここで登場するのが SSS Extension です

SSS-Extension_white_logo_typeMark.png

SSS Extension をインストール

install YORO! https://chrome.google.com/webstore/detail/sss-extension/llildiojemakefgnhhkmiiffonembcan

SSS 設定動画

SSS ExtensionにSymbolブロックチェーンのアカウントを登録しましょう。

SSS Extension と連携する

ではSSS Extensionと作ったWebアプリケーションを連携しましょう。

連携する方法は2つあります。

1つは、設定画面のALLOW LISTタブからドメイン名を入力する (書いてて気が付いたけどこれver 1.1.0だと一箇所も連携してないと入力欄が表示されないわ 修正しときます。。。)

もう1つは、連携したいページで右クリックしてSSSと連携を選択する
image.png

設定画面に表示されればOKです
image.png

システムに SSS Extension を組み込む

まずSSSで署名ボタンを作ります。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <title>SSS My Wallet</title>
  <link rel="stylesheet" href="style.css" type="text/css">
</head>
<body>
  <h1>SSS My Wallet</h1>
  
  <div class="wrapper">
      <div class="left">
        <div class="account-card">
          <h2>Account Info</h2>
          <div class="container">
            <div>Address</div>
            <div id="wallet-addr"></div>
          </div>
          <div class="container">
            <div id="wallet-xym"></div>
          </div>
        </div>
        <div class="sender-card">
          <h2>SEND</h2>
          <div class="textfiled-wrapper">
            <div>TO: </div>
            <input type="text" id="form-addr" class="textfiled" />
          </div>
          <div class="textfiled-wrapper">
            <div>AMOUNT: </div>
            <input type="text" id="form-amount" class="textfiled" />
          </div>
          <div class="textfiled-wrapper">
            <div>MESSAGE: </div>
            <input type="text" id="form-message" class="textfiled" />
          </div>
          <input type="button" onclick="handleSSS();" value="SSSで署名"/>
        </div>
      </div>
      <div class="right">
        <div class="tx-card">
          <h2>Transactions</h2>
          <div id="wallet-transactions"></div>
        </div>
      </div>
  </div>

  <script type="text/javascript" src="https://xembook.github.io/nem2-browserify/symbol-sdk-pack-1.0.3.js"></script>
  <script type="text/javascript" src="script.js"></script>
</body>
</html>

handleSSS関数を作ります。

handleSSS関数はトランザクションを作成し、
window.SSS.setTransaction関数を実行しSSSにトランザクションを登録します。
そしてwindow.SSS.requestSign関数を実行し、SSSを用いた署名をユーザ-に要求します。

requestSign関数は非同期関数になっているのでthenで戻りを受けてやって受け取ったsignedTxをノードに流すだけですね。あら簡単。

function handleSSS() {
  console.log('handle sss')
  const addr = document.getElementById('form-addr').value
  const amount = document.getElementById('form-amount').value
  const message = document.getElementById('form-message').value
  
  const tx = symbol.TransferTransaction.create(
    symbol.Deadline.create(EPOCH),
    symbol.Address.createFromRawAddress(addr),
    [
      new symbol.Mosaic(
        new symbol.MosaicId(XYM_ID),
        symbol.UInt64.fromUint(Number(amount))
      )
    ],
    symbol.PlainMessage.create(message),
    NET_TYPE,
    symbol.UInt64.fromUint(2000000)
  )

  window.SSS.setTransaction(tx)

  window.SSS.requestSign().then(signedTx => {
    console.log('signedTx', signedTx)
    transactionHttp.announce(signedTx)
  })
}

これで完成!ではないです。

表示しているアドレスありましたよね、あれベタ書きなのを最後に直しましょう。

Repositoryの処理を外に出してアドレスに関する処理は全部setTimeoutで囲みます。

addressをベタ書きからwindow.SSS.activeAddressに変更しました。これは、SSS上でアクティブアカウントに設定されているアカウントのアドレスを取得するってことですね

注意
ページが開いたときに実行されるプログラムでSSSオブジェクトにアクセスする場合setTimeoutで一定時間待たないとSSSのプログラムが読み込まれる前に実行されてエラーになるため注意が必要です。

const repositoryFactory = new symbol.RepositoryFactoryHttp(NODE_URL)
const accountHttp = repositoryFactory.createAccountRepository()
const transactionHttp = repositoryFactory.createTransactionRepository()

setTimeout(() => {
  
const address = symbol.Address.createFromRawAddress(window.SSS.activeAddress)

const dom_addr = document.getElementById('wallet-addr')
dom_addr.innerText = address.pretty()

accountHttp.getAccountInfo(address)
  .toPromise()
  .then((accountInfo) => {
    for (let m of accountInfo.mosaics) {
      if (m.id.id.toHex() === XYM_ID) {
        const dom_xym = document.getElementById('wallet-xym')
        dom_xym.innerText = `XYM Balance : ${m.amount.compact() / Math.pow(10, 6)}`
      }
    }
  })
const searchCriteria = {
  group: symbol.TransactionGroup.Confirmed,
  address,
  pageNumber: 1,
  pageSize: 20,
  order: symbol.Order.Desc,
}

transactionHttp
  .search(searchCriteria)
  .toPromise()
  .then((txs) => {
    console.log(txs)
    const dom_txInfo = document.getElementById('wallet-transactions')
    for (let tx of txs.data) {
      console.log(tx)
      const dom_tx = document.createElement('div')
      const dom_txType = document.createElement('div')
      const dom_hash = document.createElement('div')

      dom_txType.innerText = `Transaction Type : ${getTransactionType(tx.type)}`
      dom_hash.innerText = `Transaction Hash : ${tx.transactionInfo.hash}`

      dom_tx.appendChild(dom_txType)
      dom_tx.appendChild(dom_hash)
      dom_tx.appendChild(document.createElement('hr'))

      dom_txInfo.appendChild(dom_tx)
    }
  })
}, 500)

SSS Extensionをつかうメリット

使用者

  • Webアプリケーションに秘密鍵を入力しなくて良くなる
  • 秘密鍵の漏洩リスクの低下(使う以上0にはなりません)

開発者

  • 秘密鍵を預かるリスクを負う必要がない
  • 導入が簡単

なぜ導入が簡単なのか、SSS Extensionは署名だけを担当します。なので、アプリケーション側で用意したエラー処理やアプリケーションの流れを阻害しません。
そして既存の他の署名方法と並列させることが容易です。

なんたって、署名する箇所で非同期関数を呼ぶだけですから。

アプリケーションの幅が広がります。良ければ皆さん導入してやってくださいな

今回作ったのはここに公開されてます。
GitHub
GitHub Pages

作成したプログラム
script.js
const symbol = require('/node_modules/symbol-sdk')

const GENERATION_HASH = '7FCCD304802016BEBBCD342A332F91FF1F3BB5E902988B352697BE245F48E836'
const EPOCH = 1637848847
const XYM_ID = '3A8416DB2D53B6C8'
const NODE_URL = 'https://sym-test.opening-line.jp:3001'
const NET_TYPE = symbol.NetworkType.TEST_NET

const repositoryFactory = new symbol.RepositoryFactoryHttp(NODE_URL)
const accountHttp = repositoryFactory.createAccountRepository()
const transactionHttp = repositoryFactory.createTransactionRepository()

setTimeout(() => {
  
const address = symbol.Address.createFromRawAddress(window.SSS.activeAddress)

const dom_addr = document.getElementById('wallet-addr')
dom_addr.innerText = address.pretty()

accountHttp.getAccountInfo(address)
  .toPromise()
  .then((accountInfo) => {
    for (let m of accountInfo.mosaics) {
      if (m.id.id.toHex() === XYM_ID) {
        const dom_xym = document.getElementById('wallet-xym')
        dom_xym.innerText = `XYM Balance : ${m.amount.compact() / Math.pow(10, 6)}`
      }
    }
  })
const searchCriteria = {
  group: symbol.TransactionGroup.Confirmed,
  address,
  pageNumber: 1,
  pageSize: 20,
  order: symbol.Order.Desc,
}

transactionHttp
  .search(searchCriteria)
  .toPromise()
  .then((txs) => {
    console.log(txs)
    const dom_txInfo = document.getElementById('wallet-transactions')
    for (let tx of txs.data) {
      console.log(tx)
      const dom_tx = document.createElement('div')
      const dom_txType = document.createElement('div')
      const dom_hash = document.createElement('div')

      dom_txType.innerText = `Transaction Type : ${getTransactionType(tx.type)}`
      dom_hash.innerText = `Transaction Hash : ${tx.transactionInfo.hash}`

      dom_tx.appendChild(dom_txType)
      dom_tx.appendChild(dom_hash)
      dom_tx.appendChild(document.createElement('hr'))

      dom_txInfo.appendChild(dom_tx)
    }
  })
}, 500)

function getTransactionType (type) { // https://symbol.github.io/symbol-sdk-typescript-javascript/1.0.3/enums/TransactionType.html
  if (type === 16724) return 'TRANSFER TRANSACTION'
  return 'OTHER TRANSACTION'
}

function handleSSS() {
  console.log('handle sss')
  const addr = document.getElementById('form-addr').value
  const amount = document.getElementById('form-amount').value
  const message = document.getElementById('form-message').value
  
  const tx = symbol.TransferTransaction.create(
    symbol.Deadline.create(EPOCH),
    symbol.Address.createFromRawAddress(addr),
    [
      new symbol.Mosaic(
        new symbol.MosaicId(XYM_ID),
        symbol.UInt64.fromUint(Number(amount))
      )
    ],
    symbol.PlainMessage.create(message),
    NET_TYPE,
    symbol.UInt64.fromUint(2000000)
  )

  window.SSS.setTransaction(tx)

  window.SSS.requestSign().then(signedTx => {
    console.log('signedTx', signedTx)
    transactionHttp.announce(signedTx)
  })
}
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <title>SSS My Wallet</title>
  <link rel="stylesheet" href="style.css" type="text/css">
</head>
<body>
  <h1>SSS My Wallet</h1>
  
  <div class="wrapper">
      <div class="left">
        <div class="account-card">
          <h2>Account Info</h2>
          <div class="container">
            <div>Address</div>
            <div id="wallet-addr"></div>
          </div>
          <div class="container">
            <div id="wallet-xym"></div>
          </div>
        </div>
        <div class="sender-card">
          <h2>SEND</h2>
          <div class="textfiled-wrapper">
            <div>TO: </div>
            <input type="text" id="form-addr" class="textfiled" />
          </div>
          <div class="textfiled-wrapper">
            <div>AMOUNT: </div>
            <input type="text" id="form-amount" class="textfiled" />
          </div>
          <div class="textfiled-wrapper">
            <div>MESSAGE: </div>
            <input type="text" id="form-message" class="textfiled" />
          </div>
          <input type="button" onclick="handleSSS();" value="SSSで署名"/>
        </div>
      </div>
      <div class="right">
        <div class="tx-card">
          <h2>Transactions</h2>
          <div id="wallet-transactions"></div>
        </div>
      </div>
  </div>

  <script type="text/javascript" src="https://xembook.github.io/nem2-browserify/symbol-sdk-pack-1.0.3.js"></script>
  <script type="text/javascript" src="script.js"></script>
</body>
</html>
style.css
body {
  background: rgb(242, 242, 242);
}

h1 {
  color:rgba(22, 22, 22, 0.4);
  font-size: 96px;
  text-align: right;
  margin-right: 80px;
}

.left {
  width: 800px;
}
.right {
  width: 100%;
}

.account-card {
  background: white;
  border-radius: 16px;
  margin: 16px;
  padding: 16px;
}

.sender-card {
  background: white;
  border-radius: 16px;
  margin: 16px;
  padding: 16px;
}

.tx-card {
  background: white;
  border-radius: 16px;
  padding: 32px;
  width: 1200px;
  margin: 16px;
}

.textfiled-wrapper {
  display: flex;
  justify-content: space-between;
  margin: 8px;
}

.textfiled {
  width: 400px;
}

#wallet-addr {
  font-size: 24px;
}

.container {
  font-size: 24px;
  margin: 16px;
}

.wrapper {
  display: flex;
  justify-content: space-between;
  margin: 80px;
}

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24