1. Symbol とは?
ここしばらくブログの更新をさぼって暗号資産関連の技術研究を続けている筆者なのですが、 最近Symbolというブロックチェーンで決済するアプリ (*) を作ってみたらものすごく簡単だったので、 調子に乗ってブログに記したいと思います。
Symbolとは、それ以前から有るNEMというブロックチェーンに大幅な改良を加えて立ち上げられた全く新しい物で、 2021年の3月にローンチしたばかりです。
新しすぎて余り取引所の対応が進んでないのですが、 幸運にして日本の一部取引所(Zaif さん)では早々に上場されて取引できるようになっています。 つまりビットコインやイーサ等、メジャーなコインと同様に、円建てで買ったり売ったりすることが出来ます。 取引所にユーザー登録して本人確認する手間はありますが、最低限決済利用できる環境が整っていると言っていいでしょう。 と言ってもこの手間はどの暗号資産でも同じですが・・・。
最初に断っておくと、この記事ではSymbolを含め暗号資産が投資や投機対象としてどうか、という事については一切触れません。 あくまでも決済手段として言及しますので、悪しからずご了承ください。
さて、Symbolについてもうちょっと詳しく説明しましょう。 Symbol はコンセンサスアルゴリズムとして PoS+ を採用したブロックチェーンで、 ビットコインやイーサリアム等の PoW(Proof of Work)を採用しているブロックチェーンよりも効率よくトランザクションを処理できます。 PoS+ は、Proof of Stake Plus の略であり、PoW ではノードがブロックを生成するために複雑な数学的問題をいち早く解く必要があるのに対して、 一定の計算式でノードの「インポータンス」が決定され、それに基づいて確率的にブロックを生成するノードが選定されます。 Proof of Stake は、イーサリアム2.0でも採用が予定されているコンセンサスアルゴリズムですが、Symbol では更に改良が加えられています。 PoS および PoS+ はどちらもアカウントが保有している通貨の量に応じて重要度が増すようになっていますが、 PoS+ では更に手数料の支払い(≒取引の量)やノードの実行等が加味されます。
PoWではマイニングと呼ばれたブロック生成作業が、PoS+では「ハーベスティング」と呼ばれます。 ハーベスティングはノードの所有者だけでなく、インポータンスが基準を満たすアカウント、具体的には10,000 XYM 以上保有しているアカウントなら誰でも行え、 ノードを所有していなくてもノード所有者に「委譲」して行うことが出来るという特徴があります。 逆にノードを所有していても前述の基準を満たさないとハーベスティングは行えません。
Symbol が用いるネイティブ通貨は XYM(ジム)と呼ばれ、トランザクション手数料の支払いやハーべスティング報酬に使用されます。 XYM の発行総量は約90億に設定されており、徐々に新規発行される量が減っていきます。新規発行が無くなるまでは100年程度かかるとされています。 XYM の価格は執筆時点で大体 1 XYM = 20円くらいです。
Symbol には、イーサリアムと違ってオンチェーンにチューリング完全なスマートコントラクトは実装されていません。 その代わり、オンチェーンに取引やdAppsの中核をなすであろうマイクロサービス群を用意する方法を取っています。 従って、スマートコントラクトは有りませんが、独自トークンやNFTを発行する事も可能です。
ブロックチェーン上で流通する通貨は「モザイク」と呼ばれるトークンで表現され、XYM 自体もモザイクの1種です。
更に、「アグリゲートトランザクション」という特殊なトランザクションタイプが、 スマートコントラクトで行う様なアトミックかつトラストレスな処理を実現するために使用されます。 アグリゲートトランザクションとは複数のトランザクションが1つにまとめられたトランザクションで、 トランザクション関係者全員の署名が揃うとアトミックにそれらが実行されるという機能です。 従ってエスクロー等の取引をトラストレスで実行することが出来ます。
また、「マルチシグ」機能も実装されています。マルチシグはトランザクションを実行するために連署名が必要なアカウントで、 例え一部のプライベートキーが漏洩したとしてもアカウントの資産を保護することが出来ます。 マルチシグは他のブロックチェーンでも実装されているため目新しいものではないですが、 Symbolでは多段レイヤーからなる壮大な承認ツリーを構築でき、メンバーの過半数承認を得たグループが、全グループ内の過半数揃ったら実行、みたいなことも出来ます。
その他アカウントに制限を掛けたり、任意のメタデータを付与したりと言ったことも可能です。 メタデータはイーサリアムでいうところのストレージとして使えますね。
Symbol では、上記の様なマイクロサービス群をオフチェーンのプログラムで駆動して複雑な取引を実行する事になります。
ところで「オンチェーンのスマートコントラクトは一度デプロイしてしまうとバグが見つかっても修正が利かない、 オフチェーンなら万が一バグが有っても修正できるので安全」 という説明が良くなされますが一長一短有ります。つまり用途次第だと思います。
(*) どんなアプリなのかは近日発表します。
2. 準備
先程説明した通り、Symbol はブロックチェーン上にスマートコントラクトをデプロイするといったことはないので、 アプリを構成する物はオフチェーンでSDKを用いてノードと通信を行うコードだけになります。
開発環境は TypeScript + React とし、ブラウザで動作するものとします。 その辺の環境準備は完了しているものとして、省略します!
2.1. Symbol SDK + 必要なライブラリの追加
早速、プロジェクトにSDKを追加しましょう。
$ yarn add symbol-sdk
また、ブロックチェーンを使うプログラムでは、桁の大きな数字を良く扱うので、安全のため long を使います。 標準の number 型では直ぐにオーバーフローしてしまいます。 SDK には UInt64 という型が用意されているのですが、SDK の内部でも long を使ってますし、long の方が演算用のメソッドが豊富です。
$ yarn add long
$ yarn add -D @types/long
以上です。
2.2. ノードを選ぶ
ブロックチェーンとインタラクトするためには、ネットワークに参加しているノードが必要になります。 ノードは公式が立てているスーパーノードの他、有志が立てているノードが世界中に存在します。もちろん日本にも存在します。
テスト用として用意されている Testnet と、本番用の Mainnet は別のブロックチェーンですから、それぞれに繋がった個別のノードが必要です。
Testnet は こちら 、 Mainnetは こちら からノードを探すことが出来ます。 「Peer Api Node」と書かれたノードが、APIで通信可能なノードです。 また、自分が居住している場所から近いノードを選ぶのが良いでしょう。
もちろん、自分でノードを立てることもできます。 ノードの立ち上げはそこそこ強いスペックのサーバーが必要(グラボは必要ありません)になり、データ同期のため時間もかかりますから注意してください。
ところで、ノードの分布図をよく見てみると、日本のノードは世界的に見ても数が豊富なようです。 NEMおよびSymbolは「日本人に特に人気」という話もあり、そのことはノードの数からも伺えますね。
ノードを選んだら http://ngl-dual-601.testnet.symboldev.network:3000
と言ったURLを控えておいてください。
このURLがRest APIにアクセスするためのエンドポイントとなります。
Symbol のノードは原則 http
困ったことに Symbol のノードは大抵 http で運営されています。
しかし、Web では https が標準になりつつある昨今、ブラウザからブロックチェーンにアクセスしようとすると、 Mixed content のセキュリティの警告が出てブロックされてしまいます。
これを回避したい場合は https で受けられるリバースプロキシをノードの手前に置く必要があります。 こちらの記事 に詳しいやり方が載っています。 https に対応済みのノードも幾らか有るようです。
3. ブロックチェーンと通信を行うサービスコード
今回作るサンプルアプリでは「ブログに寄付ボタンを設置して寄付を受け付ける」という機能を想定します。 ただ寄付を振り込むだけだとすぐ終わってしまうので、おまけで寄付を受けた金額を表示する機能もつけたいと思います。
3.1. ブロックチェーンの状態を取得する
トランザクションを作るには、まずノードからブロックチェーンの状態を取得する必要があります。
環境変数 REACT_APP_NODE_URL
に先程メモったノードのエンドポイントURLを渡してください。
const getNetwork = async () => {
assert(process.env.REACT_APP_NODE_URL);
const nodeUrl = process.env.REACT_APP_NODE_URL;
// ブロックチェーン上の各種データにアクセスするための「リポジトリ」のファクトリ
const repositoryFactory = new RepositoryFactoryHttp(nodeUrl, {
// ブラウザ環境の WebSocket API を注入
websocketInjected: WebSocket,
// ノードURL から WebSocket URL を生成
websocketUrl: nodeUrl.replace('http', 'ws') + '/ws',
});
// ネットワークタイプ(Mainnet なのか Testnet なのか)
const networkType = await repositoryFactory.getNetworkType().toPromise();
// ブロックチェーンの初期ブロックが生成されたときの時間(Unixtime秒)。トランザクションの期限を設定するときに使用する。
const epochAdjustment = await repositoryFactory.getEpochAdjustment().toPromise();
// ブロックチェーンの初期ブロックのハッシュ値。トランザクションに署名するときに使用する。
const networkGenerationHash = await repositoryFactory.getGenerationHash().toPromise();
// ネットワーク通貨のモザイクIDを取得(XYMのモザイクID)
const networkCurrencyMosaicId = (await repositoryFactory.getCurrencies().toPromise()).currency.mosaicId;
assert(networkCurrencyMosaicId);
// 各種ネットワークパラメータを取得できるリポジトリ
const networkHttp = repositoryFactory.createNetworkRepository();
// 現在のトランザクション手数料の係数を取得(ノードによって、ネットワークの状況によって変化する参考値)
const transactionFees = await networkHttp.getTransactionFees().toPromise();
// TODO: 物によっては呼び出す度にノードへのアクセスが発生するため、以下の戻り値はキャッシュしたほうがいいでしょう。
return {
repositoryFactory,
epochAdjustment,
networkGenerationHash,
networkCurrencyMosaicId,
transactionFees,
networkType,
};
}
3.2. トランザクションを作成
送金トランザクションは TransferTransaction というクラスで表現されます。 トランザクションを作成するためには、色々とノードの情報を渡してやる必要があります(勝手にノードから取って来てくれたりはしません)
export const createTransferTx = async (recipientAddr: string, amount: Long, message: string) => {
const { networkType, epochAdjustment, networkCurrencyMosaicId, transactionFees } = await getNetwork();
return TransferTransaction.create(
// トランザクションの有効期限。期限までにブロックチェーンで承認されなければ破棄される(=失敗する)。
// ここでは何も時間を指定してないので、デフォルトの2時間。
Deadline.create(epochAdjustment),
// 受取人のアドレス
Address.createFromRawAddress(recipientAddr),
// 送金するモザイクと金額(ここではネイティブ通貨の XYM を送ります)
[new Mosaic(networkCurrencyMosaicId, UInt64.fromNumericString(amount.toString()))],
// トランザクションに好きなメッセージを添付できます。
PlainMessage.create(message),
// Mainnet なのか Testnet なのか
networkType)
// 手数料率の「最大値」を設定。ここではノードから提示される平均的な処理速度の手数料率を使用する。
// 手数料は `手数料率xトランザクションサイズ` で決まりますが、トランザクションを実行するまで厳密には決まりません。
// ここで設定するのは支払いを許容する最大の手数料で、最終的に支払う手数料は必ずこれ以下になります。
.setMaxFee(transactionFees.averageFeeMultiplier);
}
3.3. トランザクションに署名
トランザクションが出来たら署名者(=支払元)のプライベートキーで署名します。 とりあえず、プライベートキーがユーザーから取得できたという前提で署名する部分だけを作ります。
尚、署名したアカウントがトランザクション手数料を負担することになります。
署名を行うと SignedTransaction が返り、トランザクションハッシュが決まります。
export const getAccount = async (signerPrivateKey: string) => {
const { networkType } = await getNetwork();
return Account.createFromPrivateKey(signerPrivateKey, networkType);
}
export const signTx = async (tx: Transaction, signer: Account) => {
const { networkGenerationHash } = await getNetwork();
// networkGenerationHash を指定する事で、そのネットワークでのみ通用するトランザクションになります。
// つまり間違ったネットワークでアナウンスしておかしなことにならない様にできます。
return signer.sign(tx, networkGenerationHash);
}
3.4. 署名済トランザクションをアナウンス
署名しただけでは、トランザクションが実行されたことにはなりません。 署名済トランザクションをブロックチェーン上に流す(アナウンスする)ことで、初めて送金が処理されます。
署名済トランザクションは署名した本人でなくても誰でもアナウンスできます。
つまり、USBメモリか何かに署名済みトランザクションペイロードとそれをアナウンスするコードを保存しておき、 「私に何かあったらこのプログラムを実行してほしい」なんて伝えておいて、 実行したらトランザクションがアナウンスされて秘密のメッセージと「遺産」が届く、なんてこともできます。
export const announceTx = async (signedTx: SignedTransaction) => {
const { repositoryFactory } = await getNetwork();
// アナウンスしたら直ぐに Promise が解決されます。トランザクションの承認までは待ちません。
return repositoryFactory
.createTransactionRepository()
.announce(signedTx)
.toPromise();
}
3.5. トランザクションが承認されるまで待つ
一度トランザクションがアナウンスされると、設定した手数料とネットワークの込み具合にもよりますが、大体数分以内に承認されます。
プログラム上でトランザクションの承認を待ちたい場合は、以下の様にブロックチェーンを「リッスン」します。 Promise でラップするので、承認されたトランザクションを検知するまで await できます。 リッスンする前に承認済みだった場合は無限に戻ってこなくなるので、一度ブロックチェーンに問い合わせて回避します。
const getConfirmedTx = async (txHash: string) => {
const { repositoryFactory } = await getNetwork();
return repositoryFactory.createTransactionRepository()
.getTransaction(txHash, TransactionGroup.Confirmed)
.toPromise()
.catch((e) => undefined);
}
export const waitForConfirmTx = async (signer: Account, signedTx: SignedTransaction) => {
const { repositoryFactory } = await getNetwork();
const listener = repositoryFactory.createListener();
return listener.open().then(() => {
return new Promise((resolve, reject) => {
// 一定時間通信がないとタイムアウトするので、定期発生するブロック生成イベントを受信しておく。
// それが必要なければ削除。
listener.newBlock();
// 指定のトランザクションが承認されるのを待ちます。
listener.confirmed(signer.address, signedTx.hash)
.subscribe((tx) => {
resolve(tx);
},
(e) => {
console.error(e);
reject(e);
});
// リッスンする以前に承認済みだった場合をケア
const tx = getConfirmedTx(signedTx.hash);
if (tx) {
return resolve(tx);
}
}).finally(() => {
listener.close();
});
});
}
3.6. 受取アカウントのバランス(残高)を取得
着金するアカウントが「寄付受付専用アカウントである」という想定で、アカウントのバランスをブログに表示するためのコードです。
export const getAccountBalance = async (address: string) => {
const { repositoryFactory, networkCurrencyMosaicId } = await getNetwork();
return repositoryFactory.createAccountRepository()
.getAccountInfo(Address.createFromRawAddress(address))
.toPromise()
.then((accountInfo) => {
// XYM だけを合計する(配列にはモザイクがユニークで現れるが、一応 reduce で合計)
return accountInfo.mosaics
.filter((mosaic) => mosaic.id.equals(networkCurrencyMosaicId))
.reduce((acc, curr) => acc.add(Long.fromString(curr.amount.toString())), Long.ZERO);
});
}
戻り値は Micro XYM(XYM の最小単位)なので、下記の様に単位を変換してください。
// Micro XYM → XYM へ変換
const toXYM = (microXYM: Long) => {
const decimal = ('000000' + microXYM.mod(1000000).toString()).slice(-6)
.replace(/0+$/g, '');
const integer = microXYM.div(1000000).toString();
return `${integer}${decimal && '.' + decimal}`;
}
フロントエンドに組み込めば、下図の様に表示出来ます。
4. フロントエンド
フロントエンドは特別なものはなく、先程作成したサービスコードと、ボタン類をリンクするコードを書くだけです。
とりあえず、その都度プライベートキーの入力を受け付ける形にしました。
フォーム入力は XYM 単位なので、fromXYM
で Micro XYM に単位変換してやります。
※HTMLコードは省略
// XYM → Micro XYM へ変換
const fromXYM = (xym: string) => {
const [integer, decimal] = xym.split('.');
return Long.fromString(integer).mul(1000000).add(
Long.fromString(decimal ? (decimal + '000000').slice(0, 6) : '0')
);
}
interface FormData {
amount: string,
privateKey: string,
message: string,
}
// ~~~中略~~~
const onSubmit = useCallback(async (values: FormData) => {
try {
const tx = await createTransferTx(recipientAddr, fromXYM(values.amount), values.message);
const signer = await getAccount(values.privateKey);
const signedTx = await signTx(tx, signer);
await announceTx(signedTx);
addToast('トランザクションをアナウンスしました!完了までお待ちください。',
{appearance: 'info', autoDismiss: true});
await waitForConfirmTx(signer, signedTx);
addToast('送金完了しました!',
{appearance: 'success', autoDismiss: true});
reset();
updateBalance();
updateTxs();
} catch (e) {
console.error(e);
addToast('エラーが発生しました。', {appearance: 'error', autoDismiss: true});
}
}, [addToast, updateBalance, reset, updateTxs]);
フォームは以下の様になりました。
UX とセキュリティはトレードオフ
プライベートキーの扱いについては、UXとセキュリティがトレードオフの関係にあります。
一番セキュリティが高い方法は「フロントエンドではプライベートキーを入力させない」です。 その代わりにフロントエンドはトランザクション・ペイロードをURIやQRコードの形式で提供し、 ユーザーがデスクトップウォレットにインポートして、トランザクションに署名&アナウンスします。
対して一番UXが良い(かつセキュリティは必要十分な程度の)方法は、フロントエンド上にウォレットアプリを実装することで、 具体的には、暗号化してパスワード保護したプライベートキーをブラウザストレージに保存し、以降はパスワード入力だけで署名&アナウンスする実装です。 イーサリアムでは「Metamask」というブラウザプラグインが、全く同様の事を担当しています。
サービス提供側がリスクを負っても構わない状況であれば、Symbolには「アグリゲートポンデッドトランザクション」という物があり、 支払いトランザクションをブロックチェーンを通して顧客に「提案」することが出来ます。 アグリゲートポンデッドトランザクションは、スパムしない保証金として 10 XYM をロックすることで発行が出来、 顧客が署名してトランザクションがファイナライズされると 10 XYM が返却されます。ファイナライズされずタイムアウトするとハーベスタに分配されてしまいます。 アグリゲートポンデッドトランザクションを使うとブロックチェーンを介して直接デスクトップウォレットに請求書を送るようなことが出来るわけです。
5. 実際に使ってみる
ソースコードはGitHubリポジトリにアップされています。
README に従ってこちらを実行してみましょう。
まず、送金元のアカウントに XYM 残高がないと送金できません。 Mainnet の場合は、Zaif 等で購入して出金を行っておいてください(お金がかかります)
Testnet の場合は フォーセット で入手することが出来ます(無料です)
また、ブラウザから送金する場合は、プライベートキー(秘密鍵)が必要です。プライベートキーはデスクトップウォレットで確認できます。
実行する前に、環境変数 REACT_APP_NODE_URL
にノードのURLを、
REACT_APP_RECIPIENT_ADDR
に寄付を受けるアドレスをそれぞれセットしてください。
.env
に書いておけば React が勝手に読み取ってくれます。
金額とメッセージ、そしてプライベートキーを入力し、「送金」ボタンをクリックしましょう。 しばらく待って、「送金完了しました!」と表示されれば、成功です。
デスクトップウォレットでトランザクションが承認されたことが確認できるかと思います。
また、寄付受付アカウントで残高が増えているかも確認してください。
6. 改良する
6.1. セキュリティを高める
寄付は基本的に単発の利用が多いと思いますので、今回のような実装でも構わないですが、 とは言え、毎度プライベートキーを入力するのは不安なものです。
そんな方のために、トランザクションを URI や QRコードでエクスポートして、デスクトップウォレットでお支払いしてもらう機能を実装しましょう。
まず、URI と QRコードエクスポートをサポートするライブラリが公式で紹介されていますので、それを導入しましょう。
$ yarn add symbol-qr-library symbol-uri-scheme
次に、このライブラリを使って URI と QRコードを取得するコードを作ります。
export const createTxUri = async (tx: Transaction) => {
const { networkGenerationHash } = await getNetwork();
return new TransactionURI(tx.serialize(), TransactionMapping.createFromPayload, networkGenerationHash).build();
}
export const createTxQRCode = async (tx: Transaction) => {
const { networkGenerationHash, networkType } = await getNetwork();
return QRCodeGenerator.createTransactionRequest(tx, networkType, networkGenerationHash).toBase64().toPromise();
}
QRコードは Base64 で得られるので、img タグの href に突っ込めば画像として表示されます。
以下の様にダイアログで表示する様にしました。
利用者は、これらの URI または QRコードを、以下の様にデスクトップウォレットでインポートして、支払いを行います (*)。
URI のインポート
QRコードのインポート
(*) URI をクリックするとデスクトップウォレットを開くように出来たらいいのですが、残念ながら未だそうなってないのでご注意ください。
6.2. 寄付一覧とメッセージを表示する
余りにも楽勝で出来すぎて余力があるので、寄付された金額だけでなく、寄付の一覧と添付されたメッセージを表示する様にしてみましょう。 全てブロックチェーンの情報だけで表示します。
寄付受付アドレスに向けたトランザクション一覧をブロックチェーンから取得するコードを作ります。
export const searchConfirmedTx = async (recipientAddr: string, limit: number, page: number) => {
const { repositoryFactory } = await getNetwork();
const txHttp = repositoryFactory.createTransactionRepository();
const criteria: TransactionSearchCriteria = {
// 承認済トランザクションを対象
group: TransactionGroup.Confirmed,
// 受取人アドレスで限定
recipientAddress: Address.createFromRawAddress(recipientAddr),
// 指定ページを取得
pageNumber: page,
// 1ページの件数
pageSize: limit,
// ソート順:ブロック高降順
order: Order.Desc,
// 送金トランザクションに限定
type: [ TransactionType.TRANSFER ],
};
return txHttp.search(criteria)
.toPromise()
.then((page) => page.data);
}
取得した承認済みトランザクションをリスト表示するフロントエンドコードです。
txs
に searchConfirmedTx
の戻り値が入ってきます。
<h3 className="title is-5">寄付一覧(最新100件)</h3>
{ txs ? <table className="table">
<tbody>
{ txs.map((tx, index) => <tr key={index}>
<td>{tx.signer?.address.plain()}</td>
<td>{toXYM(Long.fromString(tx.mosaics[0].amount.toString()))} XYM</td>
<td>{tx.message.payload}</td>
</tr> )}
</tbody>
</table> : <div className="notification is-warning">寄付はまだ1件もありません。</div>}
下図の様に表示されます。
6.3. 手数料をユーザーが設定できるようにする
Symbol はトランザクション手数料をユーザーが決めることが出来ます。 正確には「最大」手数料を決められます。
デスクトップウォレットではプライオリティが「早い」「平均」「遅い」「最遅」の4つから手数料を選べます (「無し」はトランザクションが通らない可能性が高い。手数料無料の特別なノードでは使えます) 一般にプライオリティが高いほど手数料も高くなりますが、より優先して承認されます。
もし同じようなUIで選択させたい場合は以下のコードが役に立つかなと思います (サンプルアプリでは実装してません)
export const estimateTxFeeMultipliers = async () => {
const { transactionFees: tf } = await getNetwork();
return {
fast: tf.minFeeMultiplier > tf.averageFeeMultiplier ? tf.minFeeMultiplier : tf.averageFeeMultiplier,
average: tf.minFeeMultiplier + tf.averageFeeMultiplier * 0.65
slow: tf.minFeeMultiplier + tf.averageFeeMultiplier * 0.35,
slowest: tf.minFeeMultiplier,
}
}
export const estimateTxMaxFee = (tx: Transaction, feeMultiplier: number) => {
return Long.fromNumber(tx.size * feeMultiplier);
}
手数料の計算方法は、デスクトップウォレットのソースコードから拝借しています。
手数料係数は minFeeMultiplier と averageFeeMultiplier の間で、適当な割合を掛けて各プライオリティの値を出している様です。 0.65 とか 0.35 の割合に根拠があるわけではなく、たぶん大人のさじ加減でなんとなくだと思います。
最終的な手数料は トランザクションサイズ
x 手数料係数
で決まります。
選択された手数料係数をトランザクションに設定するには、
estimateTxFeeMultipliers
で得られた手数料係数の1つを TransferTransaction
の setMaxFee()
に渡してください。
尚、署名するときは setMaxFee()
が返したインスタンスを使用してください( setMaxFee()
は自身を書き換えない)
7. まとめ
という事で、寄付を募るだけの dApps でしたが、Symbol ならこんなに簡単にできてしまうんですね。
これを dApps と呼んでいいのか?という疑問が聞こえて来そうですが、実際にアーキテクチャはイーサリアムの時とほとんど変わりません。 ブロックチェーンと、ブロックチェーンをインタラクトするフロントエンドコードのみから構成されており、中央集権的ななにがしも関与していません。 以前作った DeJunkeng も、Symbol のマイクロサービスを駆使すれば作成が可能と考えます(ただし、UXは多少劣るかもしれません)。
暗号資産とブロックチェーンを決済に導入したいと考えている方は本記事を参考にして頂けると幸いです。 また、Symbol は日本語ドキュメントもある程度揃っていて、取引手数料も安いので皆さんにオススメして行きたいと思います。
それではまた!
8. ソースコードは GitHub で公開中
MITライセンスで公開中です。 ご利用は自己責任でどうぞ。
バグなど見つけたら Issue までどうぞ。