概要
下記記事でTONコインの取引履歴をJSONファイルに保存する方法を紹介しました。
7rikazhexde-techlog.hatenablog.com
TONはPoSネットワークでステーキングに対応しています。本記事ではTONコインのステーキングについて、提供サービスの一つである、Ton Whalesでのステーキング方法について紹介します。合わせてAPIを使用したステーキング報酬のデータの作成方法ついて検討した内容を紹介します。
注意事項
- 本記事は2024/07/31時点の情報です。
- 本記事に記載された内容やコードによって生じたいかなる損害についても責任を負いません。使用する際は自己責任でお願いします。
- コードで使用するTonhub API / ton-api-v4の仕様やコード内で使用する暗号資産の損益計算サービスのCryptactが指定するデータ形式は変更される可能性があるため、最新の情報を確認してください。
- コードは特定のTONアドレスに関連するトランザクションデータを取得できることは確認していますが、データの正確性は保証しません。
- トランザクション履歴はトランザクションの状態に依存します。実際にトランザクションの内容を確認して、取得したデータが正確でエラーがないことを確認してください。
- 本記事で紹介するステーキング報酬データの作成対象はTon Whalesでstake可能なpoolが対象です。
- tonstakerなど他サービスには適用できません。これは仕様が異なるためです。
- また、現時点で稼働中の各pool全てに当てはまる保証はありません。これは未検証であるためです。
TONコインについて
前回の記事でも紹介しましたが、公式ページやステーキング関連の情報を追記します。
TON公式ドキュメント
https://ton.org/mining
Toncoinの採掘は終了しました。 2020年6月、利用可能なToncoinトークンのすべて(総供給量の98.55%)が採掘可能になりました。 トークンは特別なGiverスマートコントラクトに配置され、2022年6月28日まで誰でもマイニングに参加できるようになりました。 ユーザーは毎日約20万TONを採掘しました 2年間の採掘の後、最後のToncoinが採掘され、TONの最初の配布フェーズが完了しました。
https://ton.org/validators
TONはProof-of-Stakeネットワークであり、そのセキュリティと安定性はValidatorによって維持されています。
バリデーターになる TONはプルーフ・オブ・ステーク(PoS)コンセンサスモデルを採用しています。 つまり、ネットワークのセキュリティと安定性は、一連のネットワークバリデータによって維持されます。 誰でもバリデーターになることができ、ネットワークのセキュリティに貢献しながら、報酬としてTONインを受け取ることができます。
https://ton.org/stake
バリデーターは大量のToncoinを持っている必要がありますが、ノミネータープールに参加することで、少額のトンコインでもネットワークの安全確保に貢献することができます。
Catchain Consensus Protocol
Catchain Consensus ProtocolはTONブロックチェーンのブロック生成と検証のために特別に作られたビザンチン障害耐性(BFT)プロトコルです。
- https://docs.ton.org/catchain.pdf
- https://medium.com/thedarkside/catchain-the-consensus-algorithm-of-ton-blockchain-224b85d9055c
以下はClaudeによるCatchain Consensusプロトコルの回答です。
1. これは真のビザンチン障害耐性(BFT)プロトコルです。マリシャスな参加者(バリデータ)が全体の1/3未満である限り、最終的に有効な次のブロックについてコンセンサスを達成します。 2. 1/3以上の参加者がマリシャスな場合、BFTコンセンサスの達成は理論的に不可能であることが知られています。したがって、Catchain Consensusプロトコルはこの点で理論的に可能な限り優れています。 3. 2018年12月に初めて実装されたとき、世界中に分散した最大300ノードでテストされました: - 300ノードで6秒でコンセンサスを達成 - 100ノードで4-5秒 - 10ノードで3秒 これは、一部のノードが参加しないか不正な動作を示す場合でも達成されました。 4. TONブロックチェーンのタスクグループは100バリデータ以下で構成されることが想定されているため、これは4-5秒ごとに新しいブロックを生成できることを意味します。 5. この性能は、2019年3月のTONブロックチェーンのテストネットワークの立ち上げでさらにテストされ、確認されました。 プロトコルの構造と特徴: 1. Catchainプロトコルは、プロセスグループ内でのメッセージブロードキャストのための「強化された」システムとして機能します。 2. 各ラウンドは複数の試行(アテンプト)に分かれており、最初のY回の試行は「高速試行」、残りは「低速試行」として扱われます。 3. ブロック候補の提案、検証、投票、コミットの流れでコンセンサスを形成します。 4. フォーク(同じ高さで異なるブロックを提案すること)の検出と処理のメカニズムが組み込まれています。 5. 内部状態の効率的な管理と更新のための最適化技術が使用されています。 6. プロトコルの正当性と終了の証明が提供されています。 このプロトコルは、大規模なバリデータグループでの高速なコンセンサス達成を可能にする高度に最適化されたBFTプロトコルであり、TONブロックチェーンの効率的な運用に重要な役割を果たしています。
以上の情報から、TONコインのコンセンサスアルゴリズムは初期段階(ローンチから2022年6月まで)はPoWとして開発、および、稼働し、最初のコイン配布後に終了し、その後、2022年6月以降はPoSに変更されてバリデーターを追加しブロック生成および、ネットワーク機能が変更されたという状況のようです。
ValidatorとNominatorについての詳細は割愛しますが、TONの公式ドキュメントに関連アプリのサイト情報が公開されています。
公式のTON app / Stakingには他のステーキングサービスが記載されていますが、ここでは一部を取り上げます。
Validator Pool (https://tonvalidators.org/)
- バリデーターのためのサイト。
- バリデーターは、TONネットワークのセキュリティと運用を維持するノードを運営を担う。
- このサイトでは、バリデーターになるための情報や、現在のバリデーターのリスト、パフォーマンス統計などが提供されている可能性が高いです。
- バリデーターになるには通常、高いテクニカルスキルと大量のTONコインのステーキングが必要です。
- 未確認ですが、Tonscanよりステーキング(Add / Deposit)する。
- Ultra TON blades #1では最小ステーク数は10001TON
Nominator Pool (https://tonwhales.com/)
- ノミネーター(または委任者)のためのサイト。
- ノミネーターは、自分のTONコインをバリデーターに委任してステーキング報酬を得る一般のユーザーです。
- このサイトでは、ユーザーが少量のTONコインでもステーキングに参加できるよう、プールを形成している。
- 技術的な知識や大量のコインを持っていなくても、ステーキングに参加可能
- Whales Nominators Queue2では最小ステーク数は50TON。
Ton Whalesについて
上記リンクにも記載されていましたが、元々Ton WhalesはPoWでのマイニングサービスを提供していたようです。
現在はNominator Poolによるステーキングサービスを含む、Whales Staking, mobile TON wallet Tonhub, blockchain explorer & The Whales Club提供しているようです。
補足として、私はWalletとしてTonhubとTonkeeperを使用しています。また、APIも公開されており、Tonhub API / ton-api-v4の開発も行われているようです。前回の記事でも紹介したTonkeeperはTON api / pytonapiを開発しているようです。
Ton Whalesのステーキング仕様
ステーキングサイクルの詳細
34時間のバリデーション期間: この期間中、コインはElectorに送られ、validationに使用される。
2時間の遅延期間: 34時間後、コインは2時間の間ウォレットに戻ります。この期間中は、簡略化された期間(Simplified Period)がアクティブで、即座に引き出しやデポジットが可能。
簡略化された期間(Simplified Period)
- 即時処理: 2時間のSimplified Period中は、サイクルの終了を待たずに即座に引き出しやデポジットができる。
- 手数料削減: 引き出しに2回のトランザクションが不要になり、手数料が半減する。
- 資金移動: 簡略化された期間中にプール間で資金を移動させることができ、サイクルの利益を失うことはない。
Poolへのステーキング方法
Webブラウザ向け
- https://tonwhales.com/でWallet接続し、各Poolページ(例:Warles Nominators)にアクセスし、StakeボタンでStakeする数量を指定する。
- 認証はWallet経由で行われる。

スマホ向け
- Ton Walletを起動し、Products->StakingStakingでStaking poolsからpoolを選択する。
- 例としてWarles NominatorsのNominator1を選択し、Top upする。


ステーキング報酬の取得方法について
前置きが長くなりましたが、ここからが本題です。 PoolにStakeすることで報酬は付与されますが、TONの取引履歴、つまり、トランザクションには、ステーク元のアドレスに対してステーキング報酬データを記録するスキーマがありません。トランザクションではステーキング期間中に発生した報酬量の合計とステーク済みの総量の和が送金(返金)されるのみです。
つまり、前回紹介した取引履歴の取得方法で作成したJSONファイルを解析しても報酬毎のデータは取得できません。この辺りはDOTのステーキング仕様とは異なります。
一方で、Ton WhalesのProfit情報が公開されているStaking Statsというページでは、Total/Month/Weeklyで期間毎の報酬量を確認できます。また、グラフ(Dynamics)には総量が時系列で表示されており、報酬が付与されていることを確認できます。 しかし、このグラフが圧倒的に見づらく、直近5日程度のデータしか表示されず、期間も遡れないないため報酬値管理には不向きでした。
ただ、グラフ表示されていることから、何らかの方法でデータを取得していることは予想できたため、その方法がわかればデータ取得は可能だと思いました。
そこで、実際に開発者モード(F12/Networkタブ)で通信内容を確認したところ、Ton WahrlesではTonhub API / ton-api-v4を使用してGETリクエスト送信し、データを取得していることがわかりました。
これらの調査結果から、tonhub API(/block/:seqno/:address/run/:command/:args?)を使用して、実際のステーキング報酬量を日毎の総量をベースに増加量として算出することを考えました。
補足:API仕様
:command:get_memberメソッド:args?:get_memberメソッドで取得するアドレス1
関連コード
- https://github.com/ton-community/ton-api-v4/blob/main/src/api/startApi.ts
- https://github.com/ton-community/ton-api-v4/blob/main/src/api/handlers/handleAccountRun.ts#L133
算出方法の詳細は下記の通りです。
- unixtime指定でブロック情報を取得し、ステーキング先とステーキング元の情報からステーキング元のアドレスの残高を取得。
- 指定区間内(1,2,...,N-1,N)で取得した各残高に対して、ブロック1とブロック2の総量を取得し、その差を計算する。その値が閾値以上であれば、報酬が付与されたとみなし、報酬量として記録する。
- これを総件数Nまで行います。基本的には報酬分は毎日加算されていますが、どれだけ増えているかは具体的に計算できないため、各残高の差と閾値を比較することで決定します。私の環境では閾値は5に設定することにしました。(ただし、閾値はステーク量に依存するため、総量を元に決定する必要があります。)
- 注意点として、例えば他のWalletから送金を受けた場合はその分が加算され、閾値以上になることがあります。その場合はその分を除く等して考慮する必要があります。
ソースコードについて
実際のコードはGitHubにコミットしています。
対象コードはton_whales_staking_dashboard.pyです。
詳細はREADME_ja.mdを確認してください。
ポイントになる処理は下記コードです。
コードでは算出方法の詳細に基づき、
ブロック情報からseqnoとtimestampを取得し、seqno, timestamp, poolのアドレス, get_memberメソッドで取得するアドレスを指定して、get_memberメソッドでGETリクエストを送信します。
そして、指定したタイムスタンプで取得したレスポンスから総量を取得します。
これらをdatetime型の日付範囲で繰り返して取得します。
上記の一連の処理はPlotly Dashで作成するコールバック関数経由で実行し、最終的にグラフ化します。
async def get_staking_history( start_date: datetime, end_date: datetime, hour: int, pool_address: str, get_member_user_address: str, ) -> List[Dict[str, Any]]: """ Fetch staking history for a given date range. Args: start_date (datetime): The start date of the range to query. end_date (datetime): The end date of the range to query. hour (int): The hour of the day to query for each date. pool_address (str): The address of the staking pool. get_member_user_address (str): The address of the user to query. Returns: List[Dict[str, Any]]: A list of dictionaries containing staking information for each day in the range. """ async with aiohttp.ClientSession() as session: tasks = [] current_date = start_date.replace(hour=hour, minute=0, second=0, microsecond=0) while current_date <= end_date: tasks.append( get_block_and_staking_info( session, current_date, pool_address, get_member_user_address ) ) current_date += timedelta(days=1) results = await asyncio.gather(*tasks) return [result for result in results if result is not None] async def get_block_and_staking_info( session: aiohttp.ClientSession, target_time: datetime, pool_address: str, get_member_user_address: str, ) -> Optional[Dict[str, Any]]: """ Fetch block and staking information for a specific time. Args: session (aiohttp.ClientSession): An active aiohttp client session. target_time (datetime): The time to query. pool_address (str): The address of the staking pool. get_member_user_address (str): The address of the user to query. Returns: Optional[Dict[str, Any]]: A dictionary containing staking information, or None if the information couldn't be retrieved. """ seqno, actual_time = await get_block_by_unix_time( session, int(target_time.timestamp()) ) if seqno and actual_time: return await get_staking_info( session, seqno, actual_time, pool_address, get_member_user_address ) return None async def get_block_by_unix_time( session: aiohttp.ClientSession, unix_time: int ) -> Tuple[Optional[int], Optional[datetime]]: """ Fetch block information for a specific Unix timestamp. Args: session (aiohttp.ClientSession): An active aiohttp client session. unix_time (int): The Unix timestamp to query. Returns: Tuple[Optional[int], Optional[datetime]]: The sequence number and timestamp of the block, or (None, None) if the block doesn't exist. """ async with session.get( f"https://mainnet-v4.tonhubapi.com/block/utime/{int(unix_time)}" ) as response: data = await response.json() if data["exist"]: shard_data = data["block"]["shards"][0] seqno = shard_data["seqno"] timestamp = shard_data.get("timestamp", int(unix_time)) return seqno, datetime.fromtimestamp(timestamp, tz=timezone.utc) else: return None, None async def get_staking_info( session: aiohttp.ClientSession, seqno: int, timestamp: datetime, pool_address: str, get_member_user_address: str, ) -> Optional[Dict[str, Any]]: """ Fetch staking information for a specific block and pool address. Args: session (aiohttp.ClientSession): An active aiohttp client session. seqno (int): The sequence number of the block to query. timestamp (datetime): The timestamp of the block. pool_address (str): The address of the staking pool. get_member_user_address (str): The address of the user to query. Returns: Optional[Dict[str, Any]]: A dictionary containing staking information, or None if the information couldn't be retrieved. """ url = f"https://mainnet-v4.tonhubapi.com/block/{seqno}/{pool_address}/run/get_member/{get_member_user_address}" async with session.get(url) as response: data = await response.json() if "result" in data and len(data["result"]) >= 4: return { "Timestamp": timestamp.astimezone(TZ), "Seqno": seqno, "Staked Amount": int(data["result"][0]["value"]) / 1e9, "Pending Deposit": int(data["result"][1]["value"]) / 1e9, "Pending Withdraw": int(data["result"][2]["value"]) / 1e9, "Withdraw Available": int(data["result"][3]["value"]) / 1e9, } else: return None
その後、取得した総量に対して、N-1,Nの差分値計算と閾値比較をして増加量を算出します。データはDataFrameオブジェクトで作成し、グラフ表示とCSV保存に使用します。
def calculate_staking_rewards(df: pd.DataFrame, adjust_val: float) -> pd.DataFrame: """ Calculate staking rewards based on the difference in staked amounts. Args: df (pd.DataFrame): A DataFrame containing staking history. adjust_val (float): The threshold value for considering a difference as a reward. Returns: pd.DataFrame: A DataFrame containing calculated staking rewards. """ results = [] for i in range(1, len(df)): current_amount = df.iloc[i]["Staked Amount"] previous_amount = df.iloc[i - 1]["Staked Amount"] difference = current_amount - previous_amount if 0 < difference <= adjust_val: timestamp = pd.to_datetime(df.iloc[i]["Original_Timestamp"]) results.append( { "Timestamp": f"'{timestamp.strftime('%Y/%m/%d %H:%M:%S')}", "Action": "STAKING", "Source": "TON_WALLET", "Base": "TON", "Volume": difference, "Price": "", "Counter": DEFAULT_COUNTER_VAL, "Fee": 0, "FeeCcy": "TON", "Comment": f"Seqno Segment:{df.iloc[i-1]['Seqno']} - {df.iloc[i]['Seqno']}", } ) return pd.DataFrame(results)
以上でブロック毎の総量に基づく報酬量の算出と取引履歴の作成を実現しました。
その他
他のステーキングアプリについて
Ton AppのStakingページで提供されているアプリの情報が公開されています。
以下確認した一部の情報を記載します。手数料などの差異もあると思いますが、詳細は割愛します。(気が向けば追記します。)
https://tonstakers.com/
- 報酬は18時間ごとでtsTONで払われる。
- アンステーク時にtsTONバーンされ、TONがウォレットに送金される。
https://app.hipo.finance/#/
- 報酬はhTonで支払われる。
- 報酬サイクルは不明。
- 報酬はクールダウン期間を待つか、分散型取引所(DEX)で即座にトークンをスワップするかを決める。
- hTONのスワップは現在DeDustとSTON.fiで可能。
- トランザクションウォレットで取引を確認すると、ステイクされていないTONと発生した報酬を受け取る。
https://tonstake.com/#/
- TonWarlesと比較するとアカウント登録が必要。
- 最小ステーキング額は1TON(1 TON未満は「Dust」扱い)
- ステーキングとアンステーキングそれぞれに最大36時間、合計で最大72時間。
- TON AppのTonStake.comのスクリーンショットを確認すると日毎のRewardを確認するページが存在するが、APIによるデータ取得方法は不明。
- アンステーキング完了後、自動的に検証済みウォレットに送金される。
上記を内容から、tonStakeが管理しやすそうな印象があります。おそらくAPIでrewardsも取得できるのではないかと思います。気が向けばこちらも確認したいと思います。
まとめ
本記事ではTONコインのステーキングについて、提供サービスの一つである、Ton WhalesでのPoolへのステーキング方法とAPIを使用してブロック情報を取得し、ステーキングの報酬データの取引履歴を作成する方法について紹介しました。
クリプトサービスはプロトコルに従いアプリを開発、提供されますが、TONではAPIやSDKなどアプリ開発者側で提供する傾向が強いように思えます。
TONのステーキングについて、報酬履歴のスキーマーが存在せず、一手間必要で扱いずらさを感じましたが、一つの方法としてAPI経由でブロック情報から取引履歴を取得して作成する方法を検討しました。(Txnに記録されると楽なのですが。)
今回はTon Whalesでのステーキングについて紹介しましたが、他にもサービスがあるので気が向けが使用してみようかと思います。
以上です。