diff --git a/apps/web/src/app/[lang]/[asset]/_components/activity-tabs.tsx b/apps/web/src/app/[lang]/[asset]/_components/activity-tabs.tsx
index 14b1db8..8a0f182 100644
--- a/apps/web/src/app/[lang]/[asset]/_components/activity-tabs.tsx
+++ b/apps/web/src/app/[lang]/[asset]/_components/activity-tabs.tsx
@@ -39,6 +39,8 @@ import { createTradeHistoryReader, tradeFingerprint } from "@/lib/api/trade-hist
import { xcpOrderRemaining } from "@/lib/order-legs";
import { tradeRoleForAddress } from "@launchpad/xcp69/trades";
import {
+ BURNED_ROW,
+ withBurnedRow,
currentHolderCount,
includeFormerHolders,
poolLockStatus,
@@ -102,8 +104,10 @@ export function ActivityTabs({
poolXcpRaw,
poolTokensRaw,
lpAsset,
+ burnedQuantity = "0",
}: {
asset: string;
+ burnedQuantity?: Raw;
mints: Fairmint[];
divisible: boolean;
/** During the sale there is no market yet: no trades, no book. */
@@ -221,14 +225,14 @@ export function ActivityTabs({
// Counterparty's balance endpoint is a live snapshot, not holder history.
// Restore absent minters/traders at zero so the table can show who sold out,
// while the tab/header/card counts above remain strictly current balances.
- const holderHistory = includeFormerHolders(
+ const holderHistory = withBurnedRow(includeFormerHolders(
holders ?? [],
[
...mints.map((mint) => mint.source),
...(roomState?.trades ?? []).map((trade) => trade.address),
...(trades ?? []).map((trade) => trade.addr),
],
- );
+ ), BURN_ADDRESS, burnedQuantity);
// One row: the pool is a single concentration of supply, and how much of it
// is locked is a property OF that row, not a second holder.
const poolLock = poolLockStatus(
@@ -245,11 +249,11 @@ export function ActivityTabs({
: holderHistory;
const holderBalance = new Map(
holderRows
- .filter((row) => row.address !== POOL_ROW)
+ .filter((row) => row.address !== POOL_ROW && row.address !== BURNED_ROW)
.map((row) => [row.address, row.quantity]),
);
- // Shares are of circulating PLUS the locked pool, which is what makes them
- // read as shares of supply rather than of whatever is left over.
+ // Display shares include the pool and removed tokens once, so destruction
+ // doesn't make the remaining holders' shares of this distribution inflate.
const holderTotal = sumRaw(holderRows.map((h) => h.quantity));
/**
@@ -454,7 +458,7 @@ export function ActivityTabs({
tab === "minters"
? minters.slice(from, from + PER_PAGE).map((r) => r.source)
: tab === "holders"
- ? holderRows.slice(from, from + PER_PAGE).map((h) => h.address)
+ ? holderRows.slice(from, from + PER_PAGE).filter(h => h.address !== POOL_ROW && h.address !== BURNED_ROW).map((h) => h.address)
: [],
);
@@ -837,6 +841,7 @@ export function ActivityTabs({
{holderRows.slice(from, from + PER_PAGE).map((h, i) => {
const pct = ratio(h.quantity, holderTotal) * 100;
const isPool = h.address === POOL_ROW;
+ const isBurned = h.address === BURNED_ROW;
const isUtxo = h.address.startsWith("utxo:");
const soldOut = h.quantity === 0n;
const displayedQuantity = tokenQty(h.quantity, divisible);
@@ -858,7 +863,11 @@ export function ActivityTabs({
{from + i + 1}
- {isPool ? (
+ {isBurned ? (
+
+ 🔥 {t("Burned")}
+
+ ) : isPool ? (
<>
{/* The chip follows the LP, not the pool. Liquidity
whose LP is burned can never be withdrawn; whatever
@@ -923,11 +932,11 @@ export function ActivityTabs({
doesn't mean anything for a holder who bought in
on the market; only the issuer's own row is a
fact worth flagging post-graduation. */}
-
+ />}
{soldOut ? (
diff --git a/apps/web/src/app/[lang]/[asset]/_components/launch-view.tsx b/apps/web/src/app/[lang]/[asset]/_components/launch-view.tsx
index 7a442a7..301ef83 100644
--- a/apps/web/src/app/[lang]/[asset]/_components/launch-view.tsx
+++ b/apps/web/src/app/[lang]/[asset]/_components/launch-view.tsx
@@ -776,6 +776,7 @@ export function LaunchView({
row.address !== burnAddress && row.address !== BURNED_ROW);
+ const quantity = big(burnedQuantity);
+ if (quantity > 0n) result.push({ address: BURNED_ROW, quantity });
+ return result.sort((a, b) => compareRawDesc(a.quantity, b.quantity));
+}
+
/**
* One row per ownership location, including locations whose live balance is
* zero. Counterparty can return an address more than once (for example an
@@ -37,7 +48,7 @@ export function currentHolderCount(
rows: HolderRow[],
excludedAddresses: Iterable = [],
): number {
- const excluded = new Set(excludedAddresses);
+ const excluded = new Set([BURNED_ROW, ...excludedAddresses]);
return rows.filter((row) => row.quantity > 0n && !excluded.has(row.address)).length;
}
diff --git a/apps/web/src/locales/_source.json b/apps/web/src/locales/_source.json
index d1a38f8..2f57d72 100644
--- a/apps/web/src/locales/_source.json
+++ b/apps/web/src/locales/_source.json
@@ -1065,6 +1065,9 @@
"burn address": [
"app/[lang]/[asset]/_components/address-badges.tsx"
],
+ "Burned": [
+ "app/[lang]/[asset]/_components/activity-tabs.tsx"
+ ],
"Burner": [
"app/[lang]/activity/_components/activity-view.tsx"
],
@@ -1983,6 +1986,9 @@
"In XCP terms, exactly. In fiat terms, only if XCP's price held during the week-long window. Refunds return quantity, not value.": [
"app/[lang]/faq/page.tsx"
],
+ "Includes tokens sent to the burn address and explicitly destroyed tokens.": [
+ "app/[lang]/[asset]/_components/activity-tabs.tsx"
+ ],
"Inscribe the image on-chain.": [
"app/[lang]/create/page.tsx"
],
diff --git a/apps/web/src/locales/es.json b/apps/web/src/locales/es.json
index 524eb8e..514ebf1 100644
--- a/apps/web/src/locales/es.json
+++ b/apps/web/src/locales/es.json
@@ -1295,5 +1295,7 @@
"Finish launching {asset}": "Terminar de lanzar {asset}",
"The inscription commit for {asset} is broadcast, but its reveal is not. The launch exists only once the reveal is on-chain, and until then the commit's BTC waits in an output only that reveal can spend.": "El commit de la inscripción de {asset} está transmitido, pero su reveal no. El lanzamiento solo existe cuando el reveal está en la cadena, y hasta entonces el BTC del commit espera en una salida que solo ese reveal puede gastar.",
"The inscription commit for {asset} never made it on-chain, and a coin it spends has since been spent by another confirmed transaction, so it never will. None of its BTC is waiting on a reveal, and you can start a new launch.": "El commit de la inscripción de {asset} nunca llegó a la cadena, y una de las monedas que gasta ya la gastó otra transacción confirmada, así que nunca llegará. Nada de su BTC espera un reveal, y puedes empezar un lanzamiento nuevo.",
- "No expiry": "Sin vencimiento"
+ "No expiry": "Sin vencimiento",
+ "Burned": "Quemado",
+ "Includes tokens sent to the burn address and explicitly destroyed tokens.": "Incluye los tokens enviados a la dirección de quema y los tokens destruidos explícitamente."
}
diff --git a/apps/web/src/locales/fr.json b/apps/web/src/locales/fr.json
index f378b4c..a7c76d7 100644
--- a/apps/web/src/locales/fr.json
+++ b/apps/web/src/locales/fr.json
@@ -1295,5 +1295,7 @@
"Finish launching {asset}": "Terminer le lancement de {asset}",
"The inscription commit for {asset} is broadcast, but its reveal is not. The launch exists only once the reveal is on-chain, and until then the commit's BTC waits in an output only that reveal can spend.": "Le commit de l'inscription de {asset} est diffusé, mais pas son reveal. Le lancement n'existe qu'une fois le reveal on-chain ; d'ici là, les BTC du commit attendent dans une sortie que seul ce reveal peut dépenser.",
"The inscription commit for {asset} never made it on-chain, and a coin it spends has since been spent by another confirmed transaction, so it never will. None of its BTC is waiting on a reveal, and you can start a new launch.": "Le commit de l'inscription de {asset} n'est jamais arrivé on-chain, et une des pièces qu'il dépense a depuis été dépensée par une autre transaction confirmée : il n'y arrivera donc jamais. Aucun de ses BTC n'attend de reveal, et vous pouvez commencer un nouveau lancement.",
- "No expiry": "Sans expiration"
+ "No expiry": "Sans expiration",
+ "Burned": "Brûlés",
+ "Includes tokens sent to the burn address and explicitly destroyed tokens.": "Comprend les tokens envoyés à l’adresse de destruction et les tokens explicitement détruits."
}
diff --git a/apps/web/src/locales/ja.json b/apps/web/src/locales/ja.json
index 3e0ee06..8e20833 100644
--- a/apps/web/src/locales/ja.json
+++ b/apps/web/src/locales/ja.json
@@ -1295,5 +1295,7 @@
"Finish launching {asset}": "{asset} のローンチを完了",
"The inscription commit for {asset} is broadcast, but its reveal is not. The launch exists only once the reveal is on-chain, and until then the commit's BTC waits in an output only that reveal can spend.": "{asset} の刻印のコミットはブロードキャスト済みですが、リビールはまだです。ローンチはリビールがオンチェーンになって初めて成立し、それまでコミットの BTC は、そのリビールだけが使える出力で待機します。",
"The inscription commit for {asset} never made it on-chain, and a coin it spends has since been spent by another confirmed transaction, so it never will. None of its BTC is waiting on a reveal, and you can start a new launch.": "{asset} の刻印のコミットはオンチェーンに届かず、それが使うコインの一つはその後、別の承認済みトランザクションで使われたため、今後も届くことはありません。リビールを待っている BTC はなく、新しいローンチを始められます。",
- "No expiry": "有効期限なし"
+ "No expiry": "有効期限なし",
+ "Burned": "バーン済み",
+ "Includes tokens sent to the burn address and explicitly destroyed tokens.": "バーンアドレスに送信されたトークンと、明示的に破棄されたトークンを含みます。"
}
diff --git a/apps/web/src/locales/ko.json b/apps/web/src/locales/ko.json
index 8b79558..1aea158 100644
--- a/apps/web/src/locales/ko.json
+++ b/apps/web/src/locales/ko.json
@@ -1295,5 +1295,7 @@
"Finish launching {asset}": "{asset} 런칭 마치기",
"The inscription commit for {asset} is broadcast, but its reveal is not. The launch exists only once the reveal is on-chain, and until then the commit's BTC waits in an output only that reveal can spend.": "{asset} 인스크립션의 커밋은 전송되었지만 리빌은 아직입니다. 런칭은 리빌이 온체인에 올라가야 성립하며, 그때까지 커밋의 BTC는 그 리빌만 사용할 수 있는 출력에 머뭅니다.",
"The inscription commit for {asset} never made it on-chain, and a coin it spends has since been spent by another confirmed transaction, so it never will. None of its BTC is waiting on a reveal, and you can start a new launch.": "{asset} 인스크립션의 커밋은 온체인에 올라가지 못했고, 커밋이 사용하는 코인 하나가 이후 다른 확정된 트랜잭션에서 사용되어 앞으로도 올라갈 수 없습니다. 리빌을 기다리는 BTC는 없으며, 새 런칭을 시작할 수 있습니다.",
- "No expiry": "만료 없음"
+ "No expiry": "만료 없음",
+ "Burned": "소각됨",
+ "Includes tokens sent to the burn address and explicitly destroyed tokens.": "소각 주소로 전송된 토큰과 명시적으로 파기된 토큰을 포함합니다."
}
diff --git a/apps/web/src/locales/pt.json b/apps/web/src/locales/pt.json
index c6ed1b2..b35b240 100644
--- a/apps/web/src/locales/pt.json
+++ b/apps/web/src/locales/pt.json
@@ -1295,5 +1295,7 @@
"Finish launching {asset}": "Concluir o lançamento de {asset}",
"The inscription commit for {asset} is broadcast, but its reveal is not. The launch exists only once the reveal is on-chain, and until then the commit's BTC waits in an output only that reveal can spend.": "O commit da inscrição de {asset} foi transmitido, mas o reveal não. O lançamento só existe quando o reveal está on-chain e, até lá, o BTC do commit espera numa saída que só esse reveal pode gastar.",
"The inscription commit for {asset} never made it on-chain, and a coin it spends has since been spent by another confirmed transaction, so it never will. None of its BTC is waiting on a reveal, and you can start a new launch.": "O commit da inscrição de {asset} nunca chegou à chain, e uma das moedas que ele gasta já foi gasta por outra transação confirmada, então nunca vai chegar. Nenhum BTC dele espera um reveal, e você pode começar um novo lançamento.",
- "No expiry": "Sem expiração"
+ "No expiry": "Sem expiração",
+ "Burned": "Queimados",
+ "Includes tokens sent to the burn address and explicitly destroyed tokens.": "Inclui tokens enviados ao endereço de queima e tokens explicitamente destruídos."
}
diff --git a/apps/web/src/locales/ru.json b/apps/web/src/locales/ru.json
index d45f86b..44ad49a 100644
--- a/apps/web/src/locales/ru.json
+++ b/apps/web/src/locales/ru.json
@@ -1295,5 +1295,7 @@
"Finish launching {asset}": "Завершить запуск {asset}",
"The inscription commit for {asset} is broadcast, but its reveal is not. The launch exists only once the reveal is on-chain, and until then the commit's BTC waits in an output only that reveal can spend.": "Commit записи {asset} отправлен в сеть, а reveal — нет. Запуск появляется только когда reveal в блокчейне, а до тех пор BTC из commit ждут в выходе, который может потратить только этот reveal.",
"The inscription commit for {asset} never made it on-chain, and a coin it spends has since been spent by another confirmed transaction, so it never will. None of its BTC is waiting on a reveal, and you can start a new launch.": "Commit записи {asset} так и не попал в блокчейн, а одну из монет, которые он тратит, уже потратила другая подтверждённая транзакция, так что он туда никогда не попадёт. Никакие его BTC не ждут reveal, и можно начать новый запуск.",
- "No expiry": "Бессрочно"
+ "No expiry": "Бессрочно",
+ "Burned": "Сожжено",
+ "Includes tokens sent to the burn address and explicitly destroyed tokens.": "Включает токены, отправленные на адрес сжигания, и токены, уничтоженные явно."
}
diff --git a/apps/web/src/locales/uk.json b/apps/web/src/locales/uk.json
index b88ff58..6c42b83 100644
--- a/apps/web/src/locales/uk.json
+++ b/apps/web/src/locales/uk.json
@@ -1295,5 +1295,7 @@
"Finish launching {asset}": "Завершити запуск {asset}",
"The inscription commit for {asset} is broadcast, but its reveal is not. The launch exists only once the reveal is on-chain, and until then the commit's BTC waits in an output only that reveal can spend.": "Commit інскрипції {asset} надіслано в мережу, а reveal — ні. Запуск з’являється лише тоді, коли reveal у блокчейні, а доти BTC з commit чекають у виході, який може витратити лише цей reveal.",
"The inscription commit for {asset} never made it on-chain, and a coin it spends has since been spent by another confirmed transaction, so it never will. None of its BTC is waiting on a reveal, and you can start a new launch.": "Commit інскрипції {asset} так і не потрапив у блокчейн, а одну з монет, які він витрачає, уже витратила інша підтверджена транзакція, тож він туди ніколи не потрапить. Жодні його BTC не чекають на reveal, і можна почати новий запуск.",
- "No expiry": "Безстроково"
+ "No expiry": "Безстроково",
+ "Burned": "Спалено",
+ "Includes tokens sent to the burn address and explicitly destroyed tokens.": "Включає токени, надіслані на адресу спалювання, і токени, знищені явно."
}
diff --git a/apps/web/src/locales/zh-hk.json b/apps/web/src/locales/zh-hk.json
index b5ac282..11f6660 100644
--- a/apps/web/src/locales/zh-hk.json
+++ b/apps/web/src/locales/zh-hk.json
@@ -1295,5 +1295,7 @@
"Finish launching {asset}": "完成發射 {asset}",
"The inscription commit for {asset} is broadcast, but its reveal is not. The launch exists only once the reveal is on-chain, and until then the commit's BTC waits in an output only that reveal can spend.": "{asset} 銘刻的 commit 已廣播,但 reveal 尚未廣播。reveal 上鏈後發射才算成立;在此之前,commit 的 BTC 會停留在只有該 reveal 才能花費的輸出中。",
"The inscription commit for {asset} never made it on-chain, and a coin it spends has since been spent by another confirmed transaction, so it never will. None of its BTC is waiting on a reveal, and you can start a new launch.": "{asset} 銘刻的 commit 始終沒有上鏈,而它花費的一枚幣已被另一筆已確認的交易花掉,所以它永遠不會上鏈。它的 BTC 沒有在等待 reveal,你可以開始新的發射。",
- "No expiry": "永不過期"
+ "No expiry": "永不過期",
+ "Burned": "已銷毀",
+ "Includes tokens sent to the burn address and explicitly destroyed tokens.": "包括傳送到銷毀地址的代幣及透過銷毀交易銷毀的代幣。"
}
diff --git a/apps/web/src/locales/zh-tw.json b/apps/web/src/locales/zh-tw.json
index 6cd8a52..9d57c38 100644
--- a/apps/web/src/locales/zh-tw.json
+++ b/apps/web/src/locales/zh-tw.json
@@ -1295,5 +1295,7 @@
"Finish launching {asset}": "完成發射 {asset}",
"The inscription commit for {asset} is broadcast, but its reveal is not. The launch exists only once the reveal is on-chain, and until then the commit's BTC waits in an output only that reveal can spend.": "{asset} 銘刻的 commit 已廣播,但 reveal 尚未廣播。reveal 上鏈後發射才算成立;在此之前,commit 的 BTC 會停留在只有該 reveal 才能花費的輸出中。",
"The inscription commit for {asset} never made it on-chain, and a coin it spends has since been spent by another confirmed transaction, so it never will. None of its BTC is waiting on a reveal, and you can start a new launch.": "{asset} 銘刻的 commit 始終沒有上鏈,而它花費的一枚幣已被另一筆已確認的交易花掉,所以它永遠不會上鏈。它的 BTC 沒有在等待 reveal,你可以開始新的發射。",
- "No expiry": "永不過期"
+ "No expiry": "永不過期",
+ "Burned": "已銷毀",
+ "Includes tokens sent to the burn address and explicitly destroyed tokens.": "包括傳送到銷毀地址的代幣和透過銷毀交易銷毀的代幣。"
}
diff --git a/apps/web/src/locales/zh.json b/apps/web/src/locales/zh.json
index ebe25e2..c3a7c43 100644
--- a/apps/web/src/locales/zh.json
+++ b/apps/web/src/locales/zh.json
@@ -1295,5 +1295,7 @@
"Finish launching {asset}": "完成发射 {asset}",
"The inscription commit for {asset} is broadcast, but its reveal is not. The launch exists only once the reveal is on-chain, and until then the commit's BTC waits in an output only that reveal can spend.": "{asset} 铭刻的 commit 已广播,但 reveal 尚未广播。reveal 上链后发射才算成立;在此之前,commit 的 BTC 会停留在只有该 reveal 才能花费的输出中。",
"The inscription commit for {asset} never made it on-chain, and a coin it spends has since been spent by another confirmed transaction, so it never will. None of its BTC is waiting on a reveal, and you can start a new launch.": "{asset} 铭刻的 commit 始终没有上链,而它花费的一枚币已被另一笔已确认的交易花掉,所以它永远不会上链。它的 BTC 没有在等待 reveal,你可以开始新的发射。",
- "No expiry": "永不过期"
+ "No expiry": "永不过期",
+ "Burned": "已销毁",
+ "Includes tokens sent to the burn address and explicitly destroyed tokens.": "包括发送到销毁地址的代币和通过销毁交易销毁的代币。"
}
diff --git a/tests/holders.test.ts b/tests/holders.test.ts
index 7bd63b5..81b467c 100644
--- a/tests/holders.test.ts
+++ b/tests/holders.test.ts
@@ -1,11 +1,25 @@
import { describe, expect, it } from "vitest";
import {
+ BURNED_ROW,
+ withBurnedRow,
coalesceHolderBalances,
currentHolderCount,
includeFormerHolders,
} from "@/lib/holders";
describe("holder balances", () => {
+ it("combines burns and destructions without counting the burn balance twice", () => {
+ const rows = withBurnedRow([{address:"alice",quantity:70n},{address:"burn",quantity:10n}], "burn", "30");
+ expect(rows).toEqual([{address:"alice",quantity:70n},{address:BURNED_ROW,quantity:30n}]);
+ expect(currentHolderCount(rows)).toBe(1);
+ expect(rows.reduce((sum,row)=>sum+row.quantity,0n)).toBe(100n);
+ });
+ it("shows destruction-only totals even without a burn-address balance", () => {
+ expect(withBurnedRow([], "burn", "123456789012345678")).toEqual([{address:BURNED_ROW,quantity:123456789012345678n}]);
+ });
+ it("hides zero burns and does not mistake a reserved burn-address balance for a burn", () => {
+ expect(withBurnedRow([{address:"burn",quantity:10n}], "burn", "0")).toEqual([]);
+ });
it("counts positive owners while retaining sold-out rows", () => {
const rows = coalesceHolderBalances([
{ address: "alice", utxo: null, quantity: "50" },