【solidity初心者】ブログに仮想通貨(ETH)の投げ銭機能を追加

最近、ブロックチェーンを用いたスマートコントラクトに興味があり「Solidity」言語の学習を始めました。

簡単なチュートリアルを終えたので、スマートコントラクトとWeb3.jsを使ってブログに仮想通貨(ETH)の投げ銭機能を追加してみました。

Solidityの学習方法

Solidityはイーサリアム上で動作するスマートコントラクトを実装する為のプログラミング言語です。

Solidity学習サイト「cryptozombies」

まず基礎を学ぶために「cryptozombies」を利用しました。

cryptozombies.io

Dapp(分散型アプリケーション)開発をゲーム作成を通じて学ぶコースで日本語にも対応しています。

プログラミング初心者でも分かるくらいに基礎から丁寧に解説しているため分かりやすいです。唯一の欠点は古いバージョンを使った解説なので、最新のコンパイラでは動作しない事が多い点です。そのため流し読みして概要を把握する程度の使い方が良いかと思います。

Solidityの特徴

「Beginner to Intermediate Smart Contracts」のコースに一通り目を通しました。

Solidityは他のプログラミング言語と大差が無いですが、ブロックチェーンの書き込みをなるべく少なくする(ブロックチェーン書き込み時に手数料がかかるため)等注意が必要です。

また一度リリースするとプログラムの修正が難しいため、拡張性を持たせた設計にする必要もあります。

仮想通貨の投げ銭機能

チュートリアルを終えたので、スマートコントラクトを使ったブログ投げ銭機能を実装します。

概要は以下の通り

投げ銭(ブログ作者にETH送信)

送金額(ETH):

  • 送金額(ETH)を入力し送金ボタンを押すと、コントラクトへ送金する(MetaMaskを利用)
  • 送金した人には、お礼として秘密のURLを伝える
  • オーナーはコントラクト内に貯まったETHを自分のアドレスに引き出せる

コントラクトの実装

まずは「cryptozombies」で学んだ(コンパイラのバージョン「0.4.19」)記述方法でコードを作成します。

// SPDX-License-Identifier: MIT
pragma solidity ^0.4.19;

import "./Ownable.sol";

contract SocialTipping is Ownable {

    mapping (address => uint) public tipAmount;
    string private _secretURI;

    function tip() external payable {
        require(msg.value > 0);
        tipAmount[msg.sender] += msg.value;
    }
    function withdraw() external onlyOwner {
        require(this.balance > 0);
        owner.transfer(this.balance);
    }
    function getBalance() external view onlyOwner returns(uint) {
        return this.balance;
    }
    function setSecretURI(string uri) external onlyOwner {
        _secretURI = uri;
    }
    function getSecretURI() external view returns(string) {
        require(tipAmount[msg.sender] > 0);
        return _secretURI;
    }
}

最新のコンパイラで動作するよう下記の通り修正しました。またSolidityのコミュニティ使用されている、natspecフォーマットのコメントを追加しました。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;

import "@openzeppelin/contracts@4.4.1/access/Ownable.sol";

/// @title ソーシャル投げ銭
/// @author denim012
/// @notice 投げ銭(ETH)を受け取る。投げ銭者はお礼として秘密のURLにアクセスできる
contract SocialTipping is Ownable {

    // アドレス毎の投げ銭額(累計)
    mapping (address => uint) public tipAmount;
    // 投げ銭者に表示するURI
    string private _secretURI;

    /// @notice 投げ銭を受け取る
    function tip() external payable {
        require(msg.value > 0);
        tipAmount[msg.sender] += msg.value;
    }
    /// @notice 投げ銭を引き出す
    /// @dev スマートコントラクト内のETHをオーナーに送信
    function withdraw() external onlyOwner {
        require(address(this).balance > 0);
        payable(msg.sender).transfer(address(this).balance);
    }
    /// @notice 投げ銭の残高照会
    /// @return EHT残高
    /// @dev スマートコントラクト内のETH額(Wei)表示
    function getBalance() external view onlyOwner returns(uint) {
        return address(this).balance;
    }
    /// @notice 秘密のURIを設定する
    /// @param uri URI
    function setSecretURI(string memory uri) external onlyOwner {
        _secretURI = uri;
    }
    /// @notice 秘密のURIを取得する
    /// @return URI
    /// @dev 送金累計額がゼロより大きいアドレスのみ値を返却
    function getSecretURI() external view returns(string memory) {
        require(tipAmount[msg.sender] > 0);
        return _secretURI;
    }
}

remix上でコンパイルし、Ropstenテストネットワークにデプロイします。

コントラクトのアドレスとABIはクライアントから呼び出す際に必要なのでコピーします。

▼ABI

f:id:denim012:20220113162658p:plain

▼コントラクトのアドレス

f:id:denim012:20220113162709p:plain

クライアントの実装

クライアント側は下記の通り実装しました。

今使用している「はてなブログ」ではHTML編集は可能ですが、一部属性は自動で削除されてしまいます(onClick属性など)。はてなブログで動作する形で記述し、下記をブログ記事に貼り付けます。

// コントラクトアドレス
const address = "0x65e532cEc0cDcf7D9Aa3c90Ef8cCEF2d9bC326a2";
// コントラクトABI
const abi = [
	{
		"anonymous": false,
		"inputs": [
			{
				"indexed": true,
				"internalType": "address",
				"name": "previousOwner",
				"type": "address"
			},
			{
				"indexed": true,
				"internalType": "address",
				"name": "newOwner",
				"type": "address"
			}
		],
		"name": "OwnershipTransferred",
		"type": "event"
	},
	{
		"inputs": [],
		"name": "renounceOwnership",
		"outputs": [],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "string",
				"name": "uri",
				"type": "string"
			}
		],
		"name": "setSecretURI",
		"outputs": [],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"inputs": [],
		"name": "tip",
		"outputs": [],
		"stateMutability": "payable",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "address",
				"name": "newOwner",
				"type": "address"
			}
		],
		"name": "transferOwnership",
		"outputs": [],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"inputs": [],
		"name": "withdraw",
		"outputs": [],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"inputs": [],
		"name": "getBalance",
		"outputs": [
			{
				"internalType": "uint256",
				"name": "",
				"type": "uint256"
			}
		],
		"stateMutability": "view",
		"type": "function"
	},
	{
		"inputs": [],
		"name": "getSecretURI",
		"outputs": [
			{
				"internalType": "string",
				"name": "",
				"type": "string"
			}
		],
		"stateMutability": "view",
		"type": "function"
	},
	{
		"inputs": [],
		"name": "owner",
		"outputs": [
			{
				"internalType": "address",
				"name": "",
				"type": "address"
			}
		],
		"stateMutability": "view",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "address",
				"name": "",
				"type": "address"
			}
		],
		"name": "tipAmount",
		"outputs": [
			{
				"internalType": "uint256",
				"name": "",
				"type": "uint256"
			}
		],
		"stateMutability": "view",
		"type": "function"
	}
];

var btn = document.querySelector("#tip");
btn.addEventListener("click", tip);
function tip() {
	// Metamaskのインストールチェック
	if (!window.ethereum) {
		alert("MetaMaskをインストールしてください");
		return;
	}
	const web3 = new Web3(window.ethereum);
	// コントラクトのインスタンス化
	const contract = new web3.eth.Contract(
		abi,
		address
	);
	const amt = document.querySelector("#amount").value;
	//ユーザのアドレスを取得
	ethereum.request({ method: "eth_requestAccounts" })
	.then((accounts) => {
		const userAccount = accounts[0];
		document.querySelector("#secretURI").innerText = "送金中・・・";
		contract.methods.tip().send({ from: userAccount, value: web3.utils.toWei(amt, "ether") })
		// 秘密のURL表示処理
		.then(() => {
			contract.methods.getSecretURI().call({ from: userAccount })
			.then((result) => {
				document.querySelector("#secretURI").innerText = result;
			})
		})
		.catch(() => {
			document.querySelector("#secretURI").innerText = "送金失敗";
		})
	});
}

動作確認

送金額を入力し、送金ボタンを押します。

すると、MetaMaskが起動し接続確認ダイアログが表示されます。

f:id:denim012:20220113162729p:plain

接続後、コントラクトへの送金ダイアログが表示されました。投げ銭金額の他に手数料がかかります。

f:id:denim012:20220113162742p:plain

送金後、10秒ほど経った後にURLが表示されました。

f:id:denim012:20220113162829p:plain

Etherscanで確認すると、コントラクト内にETHが保存されていることが確認できます。

f:id:denim012:20220113162847p:plain

各処理の手数料(ガス代)

今回のコントラクト実行時の手数料(ガス代)は下記の通りです。

  • デプロイ:0.00265ETH (=1,012円)
  • setSecretURI():0.000286ETH (=109円)
  • tip():0.000211ETH (=81円)
  • withdraw():0.000106ETH (=40円)

現在のイーサリアムメインネットワークでは1ETH=381,940円です。円換算すると手数料が馬鹿にならないことが分かります。

まとめ

SolidityとWeb3.jsを使ってブログに仮想通貨の投げ銭機能を追加しました。

Solidityは一般的なプログラミング言語と大差が無く、基礎を学べばある程度の事は作成できそうです。ただ、バージョンアップが頻繁に行われていて、1年前の記述が全然動作しないと言ったことも発生しています。

後、投げ銭で受け取った仮想通貨収入に対しては税金がかかるので、真似して作成する場合はどのタイミングで(コントラクトに送金があったタイミング or コントラクトから自分のウォレットに引き出したタイミング)、どのような税金がかかるか理解する必要があります。

【上海】高級ホテルのサウナ・プールをビジター利用(インターコンチネンタル・ロンジモント)

何度か過去のブログで紹介していますが、私はサウナ・水風呂が好きで上海の数か所のスーパー銭湯に通っています。ただ、冬になってから利用客が増え、料金も高くなっています。

高級ホテルのジム・サウナの方が料金が安いので、最近はビジター利用できるホテルを巡っています。最近利用したホテルをまとめました。

インターコンチネンタル上海虹橋「国展洲际酒店」

2022/01/16(日) インターコンチネンタル上海虹橋のプール・サウナをビジター利用しました。

サウナ・プールのビジター利用

インターコンチネンタル上海虹橋の住所は「上海市诸光路1700号」

f:id:denim012:20220116185746j:plain

地下鉄2号線の西の終点「徐泾东」の6号出口から500m弱の距離です。

プールはホテルの5階で営業時間は7時~22時。

利用券は美団で購入しました。

f:id:denim012:20220116185830j:plain

料金は99.2元でした。(美団の「玩乐卡」の割引後)

プールへ移動

13:00 ホテルに到着。

f:id:denim012:20220116185948j:plain

入場時に健康QRコードのチェックがあります。

ビジター利用者はエレベータを使って5階に上がることが出来ません。(カードキーが無いとエレベータが使えない)

f:id:denim012:20220116190007j:plain

ロビーの係員にプールを利用したいと伝えます。すると担当者がロビーまで迎えに来てくれました。

美団で購入したクーポン券を見せて、氏名・電話番号を台帳に記帳します。

プール

ロッカーキーを受け取り水着に着替えます。

プールは25m長で5レーン。結構広いです。

利用者は10人ほど。水も綺麗で比較的快適に泳げます。受付の担当者が言うには今日は利用者が少ないそうです。最近上海でコロナ感染者が出た影響かもしれません。

1時間2kmほど泳ぎました。14時過ぎから人が減り利用者は計7人程度となりました。

サウナ

サウナ室の温度は60度とかなり低いです。ボタン一つで加湿可能ですが、温度が低いので全然汗をかきません。機器を調整すれば温度を上げることは出来そうでしたが、操作方法が分かりませんでした。

また水風呂が無く、ジャグジーはプールの奥でかなり遠い場所にあります。

ジム

ジムは狭く有酸素系の機器はそれなりにありますが、ウエイトトレーニングの機器は少ないです。

f:id:denim012:20220116190050j:plain

f:id:denim012:20220116190059j:plain

f:id:denim012:20220116190103j:plain

ザロンジモントホテル上海「龙之梦大酒店」

01/11(火) ザロンジモントホテル上海のサウナ・プールをビジター利用しました。

サウナ・プールのビジター利用

ホテルの住所は「上海市长宁区延安西路1116号」

f:id:denim012:20220116190129j:plain

71路バス停のすぐ近くです。ホテルの入り口は建物の東側です。

ビジター利用料金は118元。営業時間は06:00~22:30。

プールへ移動

18:10 ホテル到着

f:id:denim012:20220116190243j:plain

入口正面のエレベータでジムのある26階に移動します。(カードキーが不要なので直接移動)

ジムの受付で利用券を購入したことを伝えます。更衣室は階段を降りた場所です。

プール

プールは3レーンのみ

f:id:denim012:20220116190301j:plain

利用開始の18:15時点で私を含めて4人の利用。

その後、人が増え19時までに最大8人になりました。3レーンしかないのでかなり窮屈です。

ただ、その後は人が減り19:15には1人になり、21時まで人があまり増えずに3人位までの利用でした。

サウナ

サウナは2か所。

30度のスチームサウナと58度の普通のサウナがあります。温度設定の機械には鍵がかかっていて変更不可です。

温度が低すぎて全然温まりません。ジャグジーは大きめで水風呂もあります。

ジム

ジムは結構広く利用者が少ないです。

上半身用機器のエリア

f:id:denim012:20220116190401j:plain

有酸素機器

f:id:denim012:20220116190421j:plain

奥には下半身用機器とフリーウエイトエリアがありましたが、常連客と思われる人がたむろしていたので写真は撮っていません。

まとめ

最近、高級ホテルのサウナ・プールをビジター利用しています。上海のホテルのビジター料金は意外と安く100元前後で利用可能な場所が結構あります。

手ごろな値段で高級ホテルを見て回れるのでお勧めです。

▼参考:先月以降利用したホテルのサウナ・プール(宿泊時利用)

ai-china.hatenablog.com

ai-china.hatenablog.com

ai-china.hatenablog.com

【上海娄山关路】「世医堂中医推拿」~新規オープン駅直結の中医マッサージ

f:id:denim012:20220110113258j:plain

2022年1月9日(日)娄山关路駅直結の商業施設に新しくオープンした「世医堂中医推拿(天山店)に行きました。

この店は上海に5店舗ある中医マッサージチェーン店で、2週間前にオープンしたばかりです。オープン記念のためか料金がかなり安くなっています。

住所・営業時間

「世医堂中医推拿(天山店)」の住所は上海市天山路762号巴黎春天6楼方糖小镇617室。

f:id:denim012:20220110094230j:plain

地下鉄2号線「娄山关路」駅直結のショッピングモール内です。

営業時間は10:00~22:00。

美団で予約

マッサージのコースは下図の通り。

f:id:denim012:20220109093758j:plain

料金は68元~で全体的にかなり安いです。

今回は80分128元の「颈肩专项深层调理」コースを予約しました。

店の様子

09:50 ショッピングモール「巴黎春天」に到着

f:id:denim012:20220110112835j:plain

6階の「方糖小镇」に上がるには、東区と西区の中間の西区側のエレベータを利用します。

f:id:denim012:20220110112844j:plain

日系のジム「JOYFIT24」の東側に店はあります。

f:id:denim012:20220110112953j:plain

受付で美団で予約したことを伝えます。

f:id:denim012:20220110112910j:plain

マッサージ内容

部屋でマッサージ着に着替えてマッサージを受けます。

f:id:denim012:20220110112920j:plain

今回のコースは背中と脊髄がメインのコースです。まずうつ伏せになり、20分程マッサージを受けます。

その後仰向けになり、ツボを押しながら腕を内側や外側に動かしたり、胸の上部をゴリゴリ押したりと続きます。

更に横向きになり、腕を思いっきり後ろに引っ張ったり、脇の下を強く押したりと今まで受けたことが無い形のマッサージでした。

60分間技師にマッサージを受けた後は、機械による電気マッサージです。

f:id:denim012:20220110112941j:plain

肩に電極を付けて、20分間電気を流します。

揉んだり、叩いたりと自動的に強度や振動が切り替わります。

まとめ

「世医堂中医推拿(天山店)」に行ってきました。本格的な中医マッサージがかなり安く体験できるのでお勧めです。

今度はお灸のコースを申し込んでみようと思います。部屋の天井にお灸用の機器が設置されていました。

f:id:denim012:20220109113810j:plain