Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions api/background/handlers/kaspa/signAndBroadcastTx.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Handler } from "@/api/background/utils";
import { ApiRequestWithHost } from "@/api/message";
import { ApiRequestWithHost, RPC_ERRORS } from "@/api/message";
import { ApiUtils } from "@/api/background/utils";
import { SignTxPayloadSchema } from "./utils";
import { SignTxPayloadSchema, isCovenantTxJson } from "./utils";

/** signAndBroadcastTx handler to serve BrowserMessageType.SIGN_AND_BROADCAST_TX message */
export const signAndBroadcastTxHandler: Handler = async (
Expand Down Expand Up @@ -44,6 +44,17 @@ export const signAndBroadcastTxHandler: Handler = async (
return;
}

if (isCovenantTxJson(result.data.txJson)) {
sendResponse(
ApiUtils.createApiResponse(
message.id,
null,
RPC_ERRORS.COVENANT_TX_UNSUPPORTED,
),
);
return;
}

const url = new URL(browser.runtime.getURL("/popup.html"));
url.hash = "/sign-and-broadcast-tx";
url.searchParams.set("requestId", message.id);
Expand Down
15 changes: 13 additions & 2 deletions api/background/handlers/kaspa/signTx.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Handler } from "@/api/background/utils";
import { ApiRequestWithHost } from "@/api/message";
import { ApiRequestWithHost, RPC_ERRORS } from "@/api/message";
import { ApiUtils } from "@/api/background/utils";
import { SignTxPayloadSchema } from "./utils";
import { SignTxPayloadSchema, isCovenantTxJson } from "./utils";

/** signTxHandler to serve BrowserMessageType.SIGN_TX message */
export const signTxHandler: Handler = async (
Expand Down Expand Up @@ -37,6 +37,17 @@ export const signTxHandler: Handler = async (
return;
}

if (isCovenantTxJson(result.data.txJson)) {
sendResponse(
ApiUtils.createApiResponse(
message.id,
null,
RPC_ERRORS.COVENANT_TX_UNSUPPORTED,
),
);
return;
}

const url = new URL(browser.runtime.getURL("/popup.html"));
url.hash = "/sign-tx";
url.searchParams.set("requestId", message.id);
Expand Down
16 changes: 16 additions & 0 deletions api/background/handlers/kaspa/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,19 @@ export const SignTxPayloadSchema = z.object({
});

export type SignTxPayload = z.infer<typeof SignTxPayloadSchema>;

/**
* M2: a covenant (version >= 1) tx can spend the user's KCC-20 pieces — the
* covenant only checks that a co-present input is the owner's P2PK — and the
* dApp confirm screen shows raw JSON, so dApp sign requests refuse them until
* a KCC-20-aware decoder exists. The wallet's own KCC-20 send/swap signs via
* wallet.signTx directly and never passes through here.
* Fail closed: anything that isn't a parseable version-0 tx counts as covenant.
*/
export function isCovenantTxJson(txJson: string): boolean {
try {
return JSON.parse(txJson).version !== 0;
} catch {
return true;
}
}
5 changes: 5 additions & 0 deletions api/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ export const RPC_ERRORS = {
// (compounding transactions first, the payment last). sendKaspa and
// compoundUtxos can sign exactly one transaction and return one id, so they
// refuse the batch rather than broadcast only its first, non-paying element.
COVENANT_TX_UNSUPPORTED: RpcErrorSchema.parse({
code: RpcErrorCode.METHOD_NOT_SUPPORTED,
message:
"Kastle does not sign covenant (version 1+) transactions requested by dApps yet: they can move KCC-20 tokens, which the confirm screen cannot show.",
}),
BATCH_REQUIRED: RpcErrorSchema.parse({
code: RpcErrorCode.BATCH_REQUIRED,
message:
Expand Down
17 changes: 16 additions & 1 deletion components/dashboard/Assets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import {
formatTokenPrice,
symbolForCurrencyCode,
} from "@/lib/utils.ts";
import TokenListItem from "@/components/dashboard/TokenListItem.tsx";
import TokenListItem, {
Kcc20TokenListItem,
} from "@/components/dashboard/TokenListItem.tsx";
import useKcc20Tokens from "@/lib/kcc20/useKcc20Tokens";
import { applyDecimal } from "@/lib/krc20.ts";
import useCurrencyValue from "@/hooks/useCurrencyValue.ts";
import { useTokenListByAddress } from "@/hooks/kasplex/useTokenListByAddress";
Expand All @@ -20,6 +23,7 @@ import { numberToHex } from "viem";
import useKaspaPrice from "@/hooks/useKaspaPrice";
import useWalletManager from "@/hooks/wallet/useWalletManager";
import useKaspaBalance from "@/hooks/wallet/useKaspaBalance";
import { useFeatureFlags } from "@/hooks/useFeatureFlags.ts";

export default function Assets() {
const navigate = useNavigate();
Expand Down Expand Up @@ -49,6 +53,12 @@ export default function Assets() {
);
});

// Kill switch off: no indexer polling, so the address never leaves the wallet.
const { isKcc20Enabled } = useFeatureFlags();
const { data: kcc20Tokens } = useKcc20Tokens(
isKcc20Enabled ? address : undefined,
);

const isAssetListLoading = balance === undefined;

const supportEvmL2s =
Expand Down Expand Up @@ -97,6 +107,11 @@ export default function Assets() {
{/*KRC20 tokens*/}
{tokens?.map((token) => <TokenListItem key={token.id} token={token} />)}

{/*KCC20 tokens*/}
{kcc20Tokens?.map((token) => (
<Kcc20TokenListItem key={token.covenantId} token={token} />
))}

{/* ERC20 tokens */}
<Erc20Assets />
</div>
Expand Down
41 changes: 41 additions & 0 deletions components/dashboard/TokenListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
formatToken,
formatTokenPrice,
symbolForCurrencyCode,
textEllipsis,
} from "@/lib/utils.ts";
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
Expand All @@ -14,6 +15,10 @@ import { useTokenInfo } from "@/hooks/kasplex/useTokenInfo";
import Layer2AssetImage from "../Layer2AssetImage";
import { useKrc20Prices } from "@/hooks/kasplex/useKrc20Prices";
import useKrc20Logo from "@/hooks/kasplex/useKrc20Logo";
import { formatUnits } from "viem";
import { Kcc20Token } from "@/lib/kcc20";
import { useFeatureFlags } from "@/hooks/useFeatureFlags.ts";
import useWalletManager from "@/hooks/wallet/useWalletManager";

type TokenListItemProps = {
token: TokenItem;
Expand Down Expand Up @@ -78,3 +83,39 @@ export default function TokenListItem({ token }: TokenListItemProps) {
</div>
);
}

export function Kcc20TokenListItem({ token }: { token: Kcc20Token }) {
const navigate = useNavigate();
const [settings] = useSettings();
const { isKcc20Enabled } = useFeatureFlags();
const { meta } = token;
const { wallet } = useWalletManager();
// Only verified tokens are sendable, and never from a Ledger wallet.
const canSend = isKcc20Enabled && !!meta && wallet?.type !== "ledger";
// Without verified decimals, show base units rather than guess a scale.
const balance = meta
? formatUnits(token.amount, meta.decimals)
: token.amount.toString();

return (
<button
type="button"
className={`flex w-full items-center gap-3 rounded-xl border border-daintree-700 bg-daintree-800 p-3 text-left ${canSend ? "cursor-pointer" : "cursor-default"}`}
disabled={!canSend}
onClick={() => navigate(`/kcc20/send/${token.covenantId}`)}
>
<Layer2AssetImage tokenImage={meta?.logoURI} chainImage={kasIcon} />
<div className="flex min-w-0 flex-grow flex-col gap-1">
<div className="flex items-start justify-between gap-2 text-base text-white">
<span>{meta?.symbol ?? textEllipsis(token.covenantId)}</span>
<span className="break-all text-right">
{settings?.hideBalances ? "*****" : balance}
</span>
</div>
<div className="text-sm text-daintree-400">
{meta?.name ?? "Unverified KCC-20 (base units)"}
</div>
</div>
</button>
);
}
Loading
Loading