はじめに
この記事では、暗号資産の数量を扱うときに string、decimal、最小単位の整数をどう使い分けるべきかを整理します。
この記事でいう decimal は、浮動小数点ではなく任意精度の decimal 型や decimal ライブラリを指します。
DB の DECIMAL 型は保存設計の話として後半で扱います。
暗号資産の実装では、見た目は単なる数値でも、実際には次の違いを意識する必要があります。
- 表示用の単位か
- 計算用の最小単位か
- 外部APIで受け渡す値か
- DBに保存する値か
ここを曖昧にすると、次のような問題が起きます。
- 小数が消える
- 丸め誤差が出る
- JavaScript の
numberで桁が壊れる - トークンごとの decimal 桁数違いで計算を誤る
特に暗号資産では、1 ETH をそのまま持つのではなく、1000000000000000000 wei のような最小単位で扱う場面が多くなります。
そのため、単に数値として扱うだけでは足りません。どの場面で、どの表現を使うのかを分けて設計する必要があります。
先に結論
実務では、次の分担にすると壊れにくくなります。
- 数量の入力と出力:
string - 小数を含む計算:
decimal - 内部数量: 最小単位の整数
つまり、1つの型で全部やろうとしないことが重要です。
なぜ暗号資産は普通の数値より難しいのか
暗号資産では、銘柄ごとに最小単位が違います。
- BTC:
1 BTC = 100000000 satoshi - ETH:
1 ETH = 1000000000000000000 wei - USDC:
1 USDC = 10^6の最小単位
ここで重要なのは、1.23 という見た目の値だけでは意味が確定しないことです。
-
1.23 BTCなのか -
1.23 ETHなのか - USDC の
1.23なのか - それは表示用なのか最小単位なのか
数量だけを裸の数値で持つと、この文脈が消えます。
最小単位を基準にする
暗号資産の数量管理では、内部表現を最小単位に寄せるのが基本です。
例えば 1.5 ETH は内部では次のように持ちます。
1.5 ETH
= 1500000000000000000 wei
この形にすると、少なくとも数量の加算・減算では小数誤差を避けられます。
Mermaid で書くと、流れは次のようになります。
最小単位の整数で持つ
暗号資産の内部数量は、最小単位の整数で持つ方が壊れにくいです。
例えば wei や satoshi は、すでに整数として意味が確定しています。
次の用途では、この表現が向いています。
- オンチェーン数量
- 残高
- 送金額
- 手数料の最小単位
- コントラクト呼び出し前後の整数計算
TypeScript では bigint を使うことが多いです。
const amountWei = 1500000000000000000n
const feeWei = 21000000000000n
const totalWei = amountWei + feeWei
この形なら、浮動小数点の丸め誤差は入りません。
ただし、最小単位の整数表現にも注意点があります。
- 小数は持てない
- JSON へそのままは載せられない
- 銘柄ごとの decimal 桁数を別で持たないと表示できない
つまり、内部数量には向いていますが、API や画面の表現にはそのまま使いにくいです。
decimal の役割
decimal は、小数を壊さず扱いたい計算で使います。
例えば次のような場面です。
- 暗号資産と法定通貨の換算
- レート計算
- 手数料率の計算
- 約定価格と数量からの金額算出
例えば ETH価格 × ETH数量 = USD金額 のような計算は、最小単位の整数だけでは扱いにくくなります。
価格: 3521.1284 USD
数量: 0.015 ETH
金額: 52.816926 USD
この種の計算を float64 や JavaScript の number で雑に扱うと、丸め誤差が混ざります。
そのため、レート計算系は decimal ライブラリを使う方が安全です。
ただし decimal も、暗号資産の内部残高表現そのものに使うとは限りません。
実務では次のように分ける方が管理しやすいです。
- 残高・送金額: 最小単位の整数
- 価格・換算・表示用の小数計算:
decimal
DB保存で気をつけること
DBに保存する場合も、型は慎重に決める必要があります。
最小単位の整数で持つ方針でも、単純に BIGINT へ入れれば十分とは限りません。
例えば ETH の wei やトークン数量は桁が大きくなりやすく、ユースケースによっては BIGINT の範囲に収まらないことがあります。
そのため、保存方法は次のどちらかで検討することが多いです。
-
DECIMAL(65,0)のような整数用 decimal カラムで保存する - 文字列として保存する
どちらを選ぶにしても、重要なのは「表示用の小数値」をそのまま曖昧に保存しないことです。
保存時には、表示単位なのか最小単位なのかを明確にしておく必要があります。
string の役割
string は雑に見えますが、境界では非常に重要です。
特に次の場面では、境界データを string のまま扱った方が安全です。
- API リクエスト
- API レスポンス
- フォーム入力
- CSV 取り込み
- 外部システム連携
理由は単純で、文字列なら情報が落ちないからです。ドメイン層では、責務に応じて最小単位の整数や decimal へ早めに変換します。
例えばフロントエンドで数量入力を受けるときに、すぐ number に変換すると危険です。
const raw = "0.000000000000000001"
const parsed = Number(raw)
この時点では見た目上問題ないように見えても、別の値や別の計算経路で精度を失う可能性があります。
また、JavaScript の number は整数でも 2^53 - 1 を超えると安全に表現できません。
そのため、フロントからバックエンドへの数量受け渡しは、次の形が扱いやすいです。
{
"asset": "ETH",
"amount": "0.015"
}
decimals をクライアントから送らせると、改ざんや不整合の原因になります。
decimal 桁数は、基本的にサーバー側が資産マスタから解決する方が安全です。
あるいは最小単位で統一するなら、次のように送る方法もあります。
{
"asset": "ETH",
"amountBaseUnit": "15000000000000000"
}
どちらにしても、JSON では数値ではなく文字列で送る方が安全です。
やってはいけない扱い
暗号資産の数量で壊れやすいパターンはだいたい決まっています。
JavaScript の number にすぐ変換する
これは最も典型的です。
- 大きい整数で壊れる
- 小さい小数で丸め誤差が出る
- 途中の四則演算で意図しない値になる
表示専用なら許容できる場面もありますが、残高計算や送金額計算に使ってはいけません。
decimal 桁数を固定だと思い込む
EVM 系トークンを触っていると、18 decimals を前提にしがちです。
しかし実際には次のようにばらつきます。
- ETH: 18
- USDC: 6
- USDT: 6
- WBTC: 8
さらに、同名資産でもチェーンが変わると decimal が異なる場合があります。
parseUnits(value, 18) を固定で書くと、USDC のようなトークンで数量が壊れます。
数量には必ず次の文脈を持たせる必要があります。
- 銘柄
- decimal 桁数
- その値が表示単位か最小単位か
最小単位の整数をそのまま画面やAPIに流す
内部では正しくても、境界で扱いにくくなります。
- JSON 化で失敗する
- フロント側で扱いづらい
- 人間に読めない
最小単位の整数は内部表現として使い、境界では string に変換する方が安全です。
string のまま計算し続ける
文字列は情報保持には向いていますが、計算器ではありません。
入力境界で string を受け取り、必要に応じて次へ変換します。
- 最小単位計算なら整数型
- 小数計算なら
decimal
ここを曖昧にすると、実装のあちこちで変換が始まり、責務が散らばります。
実務で決めておくべきこと
暗号資産の数量設計では、型そのものより先にルールを決める必要があります。
最低限、次の点は固定した方がよいです。
- 内部の正規形は表示単位か最小単位か
- API では数量を
stringで返すか - DB には最小単位整数で保存するか
- decimal 桁数をどこで管理するか
- 価格計算にどの decimal ライブラリを使うか
特に重要なのは、「数量」と「価格」を同じ型で雑に扱わないことです。
例えば次の分離は有効です。
- 数量:
AssetAmount - 価格:
Price - 法定通貨金額:
Money
型名や構造体を分けるだけでも、誤用はかなり減ります。
1つの現実的な設計例
迷ったら、次の運用から始めると安定しやすいです。
- 画面やAPIでは数量を
stringで受け取る - 銘柄の decimal 桁数を見て最小単位の整数へ変換する
- 残高や送金額は最小単位の整数のまま保持・保存する
- レート計算や法定通貨換算は
decimalで行う - 画面表示時だけ最小単位の整数を decimal 文字列へ整形する
この流れなら、入力、内部計算、外部出力の責務を分離しやすくなります。
まとめ
暗号資産の数量で重要なのは、数値を1種類の型で統一することではありません。
重要なのは、値の意味ごとに型を分けることです。
- 数量の境界入力は
string - 小数計算は
decimal - 内部数量は最小単位の整数
そして、どの値にも次の文脈を持たせる必要があります。
- どの銘柄か
- decimal は何桁か
- 表示単位か最小単位か
暗号資産の実装は、数値が壊れてもコンパイルでは気づきにくいです。
だからこそ、「どの型を使うか」よりも先に、「どの意味の値を、どの型で持つか」を決めておくことが重要です。