日本円ハッカソン。JPYCとTwitterで支援型クラウドファンディングできる「勝手に応援」の技術解説
どうも、日本円ハッカソンに参加して、発表の前日うまく動かなすぎて頭総白髪になるかと思ったデミオです。
こちらの日本円ハッカソン第二回に、國光DAO有志チームとして、「勝手に応援」というサービスの発表をしました。
「勝手に応援」というのは、
・応援してあげたい方のTwitterID
・応援のためにお金を出す方のMetamaskウォレット(JPYCが入っている)
があれば、
「勝手に(許可なく)応援して上げたい他人のためのクラウドファンディングを始められる」というサービスです。
この記事では、「勝手に応援」での技術解説として、solidityとjavascriptのコード解説をしようと思います。
ソースコードは下記に公開しています。
デモしたときのコードは Apr 24, 2022のこのリビジョンです。
ぜひ最後まで読んでください(^^)
日本円ハッカソン「勝手に応援」 デモ画面
すみません。当日デモしたときのデモサイトは今は動きません💦
UI担当が別の方の担当だったのと、プロトタイプなのでまだサーバーがないものです。
動作イメージ確認したい方、お手数ですがYoutubeのデモを見てください。
シーケンスイメージ
シーケンス図作ったのでこちらを御覧ください
日本円ハッカソン「勝手に応援」 Javascript編
当日のデモは動きませんが、僕が練習用に使っていたGitHubPageはあるので
そのページとそのページ内でのJavaScriptとSolidityについて説明します。
下記が練習に使っていたGitHubPageですが、いきなりウォレット接続して送金し始めるなかなかのウィルス的なサイトになっています。
「必ず」予めテストネット(Rinkebey)にメタマスクを接続してからアクセスしてください。
動作させるにはテストネットのJPYCの入手も必要です。
・テストネットのEthを取得できるリンク
・そのEthをもとにテストネットのJPYCを取得できるリンク
から取得お願いします。[参考資料]にも載せておきます
こちらでは下記の日本円ハッカソン入門ラボ第6~8回の内容をJavaScriptに落とし込んでいます。
説明が抜けているところは、お手数ですが上記と合わせて読めばだいたいわかると思います。
https://qiita.com/NandemoToken/items/0cf89c877134bf7193bd
ethers.jsや自分のjavascriptコードを呼び出す
まずは、ethers.jsをindex.htmlで呼び出します。
下記をindex.htmlの<head>タグの中に書きます
<script src="https://cdn.ethers.io/lib/ethers-5.2.umd.min.js" type="application/javascript"></script>
次にethers.jsを使って色々書くjavascriptを呼び出します。
下記をindex.htmlの<body>タグの中に書きます
<script src="./index2.js"></script>
たったこれだけの記載ですが、
閉じタグを忘れたり
「ethers.js」が「etherers.js」になってたりして
パッと見上記が原因とはわからないエラーが、consoleに出たのでお気をつけください
ウォレットの接続やJPYCスマートコントラクトの呼び出し
//JPYCのコントラクトアドレス。テストネット。Rinkebey
//JPYC Test Net address
const JPYCAddress = "0xbD9c419003A36F187DAf1273FCe184e1341362C0";
RinkebeyのJPYCのやりとり(送金や承認)をするために、そのコントラクトアドレスを入力します。
このアドレスを実際のアドレスにすると本物のJPYCのやり取りができます。
コントラクトアドレスとは、JPYCのプログラムが書いてあるアドレスで、このアドレスを知っていれば、そのアドレスで用意している機能を使えます。
プログラムの機能とアドレスが公開されていて、そのアドレスを呼び出せば全機能使えるのがブロックチェーンのスゴイところだなぁと思います。
Solidity内の関数をJavaScriptで呼び出せるようにするおまじない
//これはこの中に描いた関数を外から呼び出せるようになるおまじない。
// The ERC-20 Contract ABI, which is a common contract interface
// for tokens (this is the Human-Readable ABI format)
const JPYCAbi = [
// Some details about the token
"function name() view returns (string)",
"function symbol() view returns (string)",
// Get the account balance
"function balanceOf(address) view returns (uint)",
// Send some of your tokens to someone else
"function transfer(address to, uint amount)",
// FromからToへtransferFrom.
"function transferFrom(address from, address to, uint amount)",
// approveできるようにDaiに定義。
"function approve(address spender, uint amount)",
// allowance確認できるようにDaiに定義。
"function allowance(address owner, address spender)",
//これだけ関数じゃなくイベント。
// An event triggered whenever anyone transfers to someone else
"event Transfer(address indexed from, address indexed to, uint amount)"
];
const JpycSupportAbi = [
// Some details about the token
"function name() view returns (string)",
"function symbol() view returns (string)",
// Get the account balance
"function balanceOf(address) view returns (uint)",
// Send some of your tokens to someone else
"function transfer(address to, uint amount)",
// FromからToへtransferFrom.
"function transferFrom(address from, address to, uint amount)",
// approveできるように定義。
//"function approve(address spender, uint amount) returns(bool)",
// allowance確認できるように定義。
//"function allowance(address owner, address spender) view returns(bool)",
//オリジナルの関数をAbiに定義
"function createProject(string argtoTwID, string argfromTwID, address argfromAddress, uint argamount) payable",
//指定したTwIDの現在の支援総額を返す関数
"function projectAllowance(string argtoTwID) view returns (uint256)",
//期限になったときに呼んでほしい関数
"function projectFinish(string argtoTwID, uint256 targetAmount) payable returns (uint256 )",
//募集終了して実際に獲得した金額
"function finishedProjectAllowance(string argtoTwID) view returns (uint256)",
//現在スマートコントラクトが所持している金額を表示
"function jpycAmount() view returns (uint)",
//応援者の全データを取得
//"function getAllProject() view returns (string[])",
//これだけ関数じゃなくイベント。用途はよくわからない
// An event triggered whenever anyone transfers to someone else
"event Transfer(address indexed from, address indexed to, uint amount)"
];
JpycSupport.sol内や、JPYCのコントラクトアドレスで定義されているSolidityで書かれた関数の宣言をこのAbiの中ですることで、JavaScriptからブロックチェーンの関数を呼び出すことができます。
詳細は日本円ハッカソン入門ラボを参照ください。
ウォレットからお金を送金するような処理の関数の場合はpayableを書いてやる必要があったりしました。
index2.jsでやっていること
window.onload = async function(){
//myFunction();
//myFunction2();
await myFunctionJPYC(); //Providerとかの設定
await CreateProject( 10, "toTwId1", "fromTwId1"); //応援ボタン押したとみなす
//スマートコントラクトから情報を取得
const JpycSupportContract2 = await new ethers.Contract(testSmartContract, JpycSupportAbi, providerSC);
let tx = await JpycSupportContract2.jpycAmount();
console.log("jpycAmount is");
//console.log( ethers.utils.formatUnits(tx, 18));
let decimalTotal =ethers.utils.formatUnits(tx, 18);
console.log(decimalTotal );
await projectAllowance("toTwId1"); //toTwId1の募集中の金額を表示
//await projectFinish("toTwId1", 0); //応援ボタン押したとみなす
//await projectAllowance("toTwId1"); //toTwId1の募集中の金額を表示。0になるはず
//await finishedProjectAllowance("toTwId1"); //toTwId1の成功した募集中の金額を表示
}
index2.jsをindex.htmlの中で呼び出したら、この
window.onload = async function()
がよばれます。
ブロックチェーンのプログラムを呼び出す上で大事なことはとにかく関数呼び出すまえに
await
をつけること。
あとは呼び出される関数側には
async
をつけること。
こうしないと、ブロックチェーン側の処理結果が帰ってくる前にどんどんJavaScript側が先に進んでしまうため。
この辺も日本円ハッカソン入門ラボに書いてあります。
myFunctionJPYCの中
myFunctionJPCYの中では、ブロックチェーンとJavaScriptを関連付けるための初期設定をしています。
JPYCのコントラクトアドレスと、勝手に応援プロジェクトで作成したブロックチェーンプログラムのコントラクトアドレスをJavaScript側のプログラムと紐づけています。
CreateProject
CreateProjectは、新たに応援プロジェクトを作るときに呼び出す関数です。
本当は目標金額の設定もこの関数でやるべきなのですが、デモのタイミングでは間に合わなかったため、目標金額は固定値として、誰かから誰かに支援をするときの関数として使用しています。
引数
inputToTwID:応援される人のTwitterID
inputFromTwID:応援する人(お金を出す人)のTwitterID
inputYen:応援する人が今回募金する金額
//☆此処から先を応援するボタンを押したら実行したい。
async function CreateProject(inputYen, inputToTwId, inputFromTwId){
console.log("Create Projectstart");
const jpyc1 = ethers.utils.parseUnits(String(inputYen), 18);
console.log(String(inputYen));
//スマートコントラクトのアドレスにJPYCをアプルーブ
let tx = await JPYCWithSigner.approve( testSmartContract, jpyc1);
console.log("approve JPYC by JPYCWithSigner to testSmartContract");
//スマートコントラクトのCreateProject関数を実行
//tx = await JpycSupportWithSinger.createProject( inputToTwId, inputFromTwId, addressesSC[0], jpyc1);
await JpycSupportWithSinger.createProject( inputToTwId, inputFromTwId, addressesSC[0], jpyc1);
console.log("Create Project!");
}
関数の中では、まずparseUnits()を使って、入力された金額をブロックチェーンプログラムで扱える単位に変換します。
その後、勝手に応援のスマートコントラクトに対して、入力された金額をアプルーブします。
アプルーブ(approve)とは、自分のウォレットのうちの指定金額を指定したアドレスに対して自由に使って良いとお墨付きを与えることです。
次にスマートコントラクト内のCreateProject関数を実行して、
誰から誰にいくらの支援があったのか?
をスマートコントラクトの台帳に記入します。
台帳に記入したあとに指定した金額をスマートコントラクトに対して送金します。
記入するため、ガス代が発生します。このガス代は応援する人(募金する人)から支払われます。
function projectAllowance
ProjectAllowanceでは、現在募金活動中のプロジェクトに集まっている金額を返す関数です。同名の関数をスマートコントラクト内で呼び出します。
参照するだけなのでガス代は発生しません。
応援される人のTwitterIDがそのままプロジェクト名になるのでそれを入力してやると、現在の募金総額が返ってきます。
function projectFinish
function projectFinishは、プロジェクトを終了するときに呼び出す関数です。同名の関数をスマートコントラクト内で呼び出します。参照するだけなのでガス代は発生しません。
本来は期限を設定してその期限がやってきたときにサーバー側で実行されますが、デモ時は間に合わなかったため、Consoleから手打ちで実行したりしていました。
目標金額に達しているかを確認して、達していたらスマートコントラクトと紐づいたTwitterアカウントからTipJPYCを使って、応援される人のTwitterIDへ集まったJPYCを送金します。
達していなかった場合は、台帳をもとにすべて返金します。返金する場合のガス代はスマートコントラクト側持ちなので、多少余裕を持たせないとまずいのが課題です。
スマートコントラクトプログラム JpycSupport.sol
作成したブロックチェーンプログラムです。
Projectという構造体を持ちます。
//構造体で支援先、支援者の情報をまとめる
struct Project {
string toTwID; //支援される人。お金受け取る人
string fromTwID; //支援する人。お金送る人
address fromAddress; //支援する人のウォレットアドレス
uint256 amount; //支援額。allowanceで取れるので、要らないかも?と思ったけど送金してもらうことになったので必要
bool isFinish; //プロジェクトの終了。trueなら終了
}
Project[] public allProjects;
これまでの支援金額をすべてこのProject型のallProjectsという構造体配列に書き込むことで、全データを保持しています。
createProject
スマートコントラクト側のCreateProjectという関数です。
誰かから誰かに募金するときに呼び出します。
//引数をプッシュする。ガス発生
function createProject(
string memory argtoTwID,
string memory argfromTwID,
address argfromAddress,
uint256 argamount
) external payable {
uint256 thisAllowance = jpycInterface.allowance(
argfromAddress,
address(this)
);
for (uint256 i = 0; i < allProjects.length; i++) {
//toTwIDとfromAddressの同じ組み合わせで未終了があるか探す
if (
keccak256(abi.encodePacked(allProjects[i].toTwID)) ==
keccak256(abi.encodePacked(argtoTwID)) &&
allProjects[i].fromAddress == argfromAddress &&
!allProjects[i].isFinish
) {
emit DebugLogEvent("CreateProject 1");
//fromTwIDが違ったら後優先で上書き
if (
keccak256(abi.encodePacked(allProjects[i].fromTwID)) ==
keccak256(abi.encodePacked(argfromTwID))
) {
allProjects[i].fromTwID = argfromTwID;
}
if (allProjects[i].amount == thisAllowance) {
//何もしない
emit DebugLogEvent("CreateProject 2");
} else if (allProjects[i].amount < thisAllowance) {
//差額を貰う。
jpycInterface.transferFrom(
argfromAddress,
address(this),
thisAllowance - allProjects[i].amount
);
allProjects[i].amount = argamount;
emit DebugLogEvent("CreateProject 3");
} else {
//差額を返す
jpycInterface.transfer(
allProjects[i].fromAddress,
allProjects[i].amount - thisAllowance
);
allProjects[i].amount = argamount;
emit DebugLogEvent("CreateProject 4");
}
return;
}
}
//ここまで来ているということは同じ組み合わせが無いので配列へ書き込み
emit DebugLogEvent("CreateProject 5");
jpycInterface.transferFrom(
argfromAddress,
address(this),
thisAllowance
);
emit DebugLogEvent("CreateProject 6");
allProjects.push(
Project(
argtoTwID,
argfromTwID,
argfromAddress,
thisAllowance,
false
)
);
emit DebugLogEvent("CreateProject 7");
//approveは接続したメタマスクから呼び出すのが良さそうなのでコメントアウト
//jpycInterface.approve(msg.sender, argamount);
return;
}
ダラダラ書いていますが、入力された情報をもとにすでに同じ応援プロジェクトが動いているかどうかを探して、動いていたらそこに募金を追加するという処理をしています。
ここでは台帳にデータを書き込むところまで。
アプルーブ(指定金額の活用許可)と実際の募金はJavaScript側で呼んでいます。
ガス代が発生するからか、
external payable
を関数宣言のところに書いてあげないとうまく動きませんでした。
projectFinish
プロジェクトの期限が到来したら呼ばれます。台帳allProjectsを総当りして、支援金額が目標金額(デモでは固定値)を超えているかを確認。
超えてたら台帳のisFinishをTrueに変えてます。
集まったお金はスマートコントラクト内に溜まっているので、スマートコントラクトと紐づけたTwitterアカウントからTipJPYCを使って応援される人に送金してやることでお金を集めて送金することができます。TipJPYCを使った送金部分はデモでは手動で実行しました。(スマートコントラクト側では無理だったのでサーバー側でやる必要がある)
//プロジェクトの期限が到来したら呼ばれる関数。本当は期限の確認もしないといけないが割愛。期限がきたことは呼ぶ側が確認。
//支援先を探す⇒JPYCのインターフェイスでallowanceの合計金額計算と都度transferFrom⇒allowanceの合計金額をreturn
//function projectFinish(string memory argtoTwID, uint256 targetAmount) public view returns (uint256) {
function projectFinish(string memory argtoTwID, uint256 targetAmount)
external
payable
returns (uint256)
{
uint256 totalAllowance = 0;
totalAllowance = projectAllowance(argtoTwID);
//支援額が超えているか確認。
//支援総額が目標金額を超えていた。
//無駄だけど関数内でProjectの配列作れなかったのでもう一回回してtransferFromを繰り返す
for (uint256 i = 0; i < allProjects.length; i++) {
//引数の支援先を探す
if (
keccak256(abi.encodePacked(allProjects[i].toTwID)) ==
keccak256(abi.encodePacked(argtoTwID)) &&
!allProjects[i].isFinish
) {
//送金しててもしてなくてもプロジェクトから削除。配列の削除ができないので、isFinishを更新
allProjects[i].isFinish = true;
//目標金額に達しているか確認
if (totalAllowance >= targetAmount) {
//Create時に送金してもらっているので目標金額達成している場合は何もしない
//jpycInterface.transferFrom( tempProject.fromAddress , address(this), jpycInterface.allowance( tempProject.fromAddress , address(this)));
} else {
//目標金額に達しなかったので預かったお金を返す
jpycInterface.transfer(
allProjects[i].fromAddress,
allProjects[i].amount
);
allProjects[i].amount = 0;
}
}
}
if (totalAllowance >= targetAmount) {
return totalAllowance / 10**18; //目標金額超えたので総額をreturn。
}
return 0; //目標金額未満なので0をReturn
}
目標金額に達していない場合は、今回応援される人に集まったお金を、募金してくれた人にひとつひとつ返す処理をして、その支援金額を0に書き換えています。
projectAllowance
今動いている応援プロジェクトの募金総額がいくらかを返す関数です。
この関数はただ金額を返すだけで台帳の書き換えが発生しないので、ガス代が発生しないようにしないといけません。
allProjectsはPublic型の変数でスマートコントラクト内にずっと存在しています。で、ここから値を参照して別の構造体にコピーして使おうとしたところ、どうやってもガス代が発生することがわかりました。
どうやら関数内で構造体を作るとガス代が発生するっぽいです。なので、allProjectを関数内で呼び出すことで対応しました。(すくなくともこの書き方だとガス代が発生しなかった)
まとめ
いくつか手動で実行したり、固定値で割愛したりしていますが、こんな感じで
多人数からお金を集めて
指定の人(指定のTwitterID)にお金を送金する
勝手に応援プロジェクト
を作りました。
もうすこしフロントエンド側の知識があれば作成も楽でしたが、JavaScriptの知識がすこしあればGithubPageでここまでブロックチェーンプログラムが作れるのはすごいな、と思います。
難しい説明部分は「日本円ハッカソン入門ラボを参照」で片付けてしまいすみません。ハッカソンにて質問して答えてくれた方が教えてくれたページなども参考資料として記載しましたので、参考にしてください。
参考資料
日本円ハッカソン入門ラボ
solidity の使い方。DBっぽいものの作成系
https://solidity-by-example.org/function
https://solidity-by-example.org/data-locations
solidityの構造体structsの説明
メマタスクの説明
RemixでPolygonで貯金箱作成がうまくできた人のページ
0.1EthずつテストネットのEthがもらえる
JPYC(テストネットRinkebey)のアドレスやテストネットJPYCゲットの方法が書いてある
JPYCのコントラクトコード。JPYCに実装されているsolidityのcodeが読める
https://etherscan.io/address/0x2370f9d504c7a6e775bf6e14b3f12846b594cd53#code
GitHub