Skip to content
Draft
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
26 changes: 0 additions & 26 deletions api/background/handlers/ethereum/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import * as secp from "@noble/secp256k1";
import { bytesToHex } from "viem";
import { ApiResponseSchema } from "@/api/message";
import { kairos, mainnet } from "viem/chains";

Check warning on line 6 in api/background/handlers/ethereum/utils.ts

View workflow job for this annotation

GitHub Actions / submit

'kairos' is defined but never used

Check warning on line 6 in api/background/handlers/ethereum/utils.ts

View workflow job for this annotation

GitHub Actions / submit

'mainnet' is defined but never used

export const isMatchCurrentAddress = async (address: string) => {
const account = await ApiUtils.getCurrentAccount();
Expand All @@ -27,29 +27,3 @@
const result = ApiResponseSchema.safeParse(response);
return result.success && result.data.error === "User Denied";
};

export const TESTNET_SUPPORTED_EVM_L2_CHAINS = [
kairos,

// Kasplex Testnet
{
id: 167_012,
name: "Kasplex Network Testnet",
network: "kasplex-testnet",
nativeCurrency: {
decimals: 18,
name: "Bridged KAS",
symbol: "WKAS",
},
rpcUrls: {
default: { http: ["https://rpc.kasplextest.xyz"] },
},
blockExplorers: {
default: {
name: "Kasplex Testnet Explorer",
url: "https:/frontend.kasplextest.xyz",
},
},
testnet: true,
},
];
Binary file modified assets/kaspa_bg.wasm
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,49 +1,82 @@
import React, { useEffect, useState } from "react";
import useKeyring from "@/hooks/useKeyring";
import useWalletManager from "@/hooks/useWalletManager";
import { AccountFactory } from "@/lib/ethereum/wallet/account-factory";
import { IWallet } from "@/lib/ethereum/wallet/wallet-interface";
import { AccountFactory as EthAccountFactory } from "@/lib/ethereum/wallet/account-factory";
import { IWallet as EthWallet } from "@/lib/ethereum/wallet/wallet-interface";
import { IWallet as KasWallet } from "@/lib/wallet/wallet-interface";
import SendTransaction from "./SendTransaction";
import Splash from "@/components/screens/Splash";
import { useSettings } from "@/hooks/useSettings";
import { kasplexTestnet } from "@/lib/layer2";
import useRpcClient from "@/hooks/useRpcClientStateful";
import { AccountFactory as KasAccountFactory } from "@/lib/wallet/wallet-factory";
import SendKasplexL2Transaction from "./SendKasplexL2Transaction";

export default function HotWalletSendTransaction() {
const { getWalletSecret } = useKeyring();
const { wallet: walletInfo, account } = useWalletManager();
const [walletSigner, setWalletSigner] = useState<IWallet | null>(null);
const [ethSigner, setEthSigner] = useState<EthWallet | null>(null);
const { rpcClient, networkId } = useRpcClient();
const [kasSigner, setKasSigner] = useState<KasWallet | null>(null);
const [settings] = useSettings();
const callOnce = React.useRef(false);

useEffect(() => {
const init = async () => {
if (!walletInfo || !account) {
return;
}
if (!walletInfo || !account || !networkId || !rpcClient) {
return;
}

const init = async () => {
const { walletSecret } = await getWalletSecret({
walletId: walletInfo.id,
});

const kasAccountFactory = new KasAccountFactory(rpcClient, networkId);

switch (walletInfo.type) {
case "mnemonic":
setWalletSigner(
AccountFactory.createFromMnemonic(
setEthSigner(
EthAccountFactory.createFromMnemonic(
walletSecret.value,
account.index,
),
);
setKasSigner(
kasAccountFactory.createFromMnemonic(
walletSecret.value,
account.index,
),
);
break;
case "privateKey":
setWalletSigner(
AccountFactory.createFromPrivateKey(walletSecret.value),
setEthSigner(
EthAccountFactory.createFromPrivateKey(walletSecret.value),
);
setKasSigner(
kasAccountFactory.createFromPrivateKey(walletSecret.value),
);
break;
}
};

if (callOnce.current) {
return;
}
init();
callOnce.current = true;
}, [getWalletSecret]);

const isLoading = !walletSigner;
const isKasplexLayer2 =
settings?.evmL2ChainId?.[settings.networkId] === kasplexTestnet.id;

const isLoading = !ethSigner || !kasSigner;
return (
<>
{isLoading && <Splash />}
{!isLoading && <SendTransaction walletSigner={walletSigner} />}
{!isLoading && !isKasplexLayer2 && <SendTransaction signer={ethSigner} />}
{!isLoading && isKasplexLayer2 && (
<SendKasplexL2Transaction ethSigner={ethSigner} kasSigner={kasSigner} />
)}
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { IWallet as EthSigner } from "@/lib/ethereum/wallet/wallet-interface";
import useWalletManager from "@/hooks/useWalletManager";
import ledgerSignImage from "@/assets/images/ledger-on-sign.svg";
import signImage from "@/assets/images/sign.png";
import Header from "@/components/GeneralHeader";
import { useBoolean } from "usehooks-ts";
import { ApiExtensionUtils } from "@/api/extension";
import { ApiUtils } from "@/api/background/utils";
import { RPC_ERRORS } from "@/api/message";
import {
TransactionSerializable,
hexToBigInt,
createPublicClient,
http,
hexToNumber,
} from "viem";
import { estimateFeesPerGas } from "viem/actions";
import { ethereumTransactionRequestSchema } from "@/api/background/handlers/ethereum/sendTransaction";
import { TESTNET_SUPPORTED_EVM_L2_CHAINS } from "@/lib/layer2";
import { IWallet as KasWallet } from "@/lib/wallet/wallet-interface";
import { sendKasplexTransaction } from "@/lib/kasplex";

type SendTransactionProps = {
ethSigner: EthSigner;
kasSigner: KasWallet;
};

export default function SendKasplexL2Transaction({
ethSigner,
kasSigner,
}: SendTransactionProps) {
const [settings] = useSettings();
const { wallet } = useWalletManager();

const { value: isSigning, toggle: toggleIsSigning } = useBoolean(false);

const requestId =
new URLSearchParams(window.location.search).get("requestId") ?? "";
const encodedPayload = new URLSearchParams(window.location.search).get(
"payload",
);

const payload = encodedPayload
? JSON.parse(decodeURIComponent(encodedPayload))
: null;

const onConfirm = async () => {
if (isSigning || !settings || !kasSigner) {
return;
}

const result = ethereumTransactionRequestSchema.safeParse(payload);
if (!result.success) {
await ApiExtensionUtils.sendMessage(
requestId,
ApiUtils.createApiResponse(requestId, null, RPC_ERRORS.INVALID_PARAMS),
);
window.close();
return;
}
const parsedRequest = result.data;

const supportedChains =
settings.networkId === "mainnet" ? [] : TESTNET_SUPPORTED_EVM_L2_CHAINS;

const txChainId = parsedRequest.chainId
? hexToNumber(parsedRequest.chainId)
: settings.evmL2ChainId?.[settings.networkId];

if (!txChainId) {
await ApiExtensionUtils.sendMessage(
requestId,
ApiUtils.createApiResponse(requestId, null, RPC_ERRORS.INVALID_PARAMS),
);
window.close();
return;
}

const network = supportedChains.find((chain) => chain.id === txChainId);
if (!network) {
await ApiExtensionUtils.sendMessage(
requestId,
ApiUtils.createApiResponse(
requestId,
null,
RPC_ERRORS.UNSUPPORTED_CHAIN,
),
);
window.close();
return;
}

toggleIsSigning();
try {
const ethClient = createPublicClient({
chain: network,
transport: http(),
});

const nonce = await ethClient.getTransactionCount({
address: (await ethSigner.getAddress()) as `0x${string}`,
});

const estimatedGas = await estimateFeesPerGas(ethClient);
const gasLimit = await ethClient.estimateGas({
account: parsedRequest.from,
to: parsedRequest.to,
value: parsedRequest.value && hexToBigInt(parsedRequest.value),
data: parsedRequest.data,
});

// Build eip1559 transaction
const transaction: TransactionSerializable = {
to: parsedRequest.to,
value: parsedRequest.value && hexToBigInt(parsedRequest.value),
data: parsedRequest.data,

gas: gasLimit,
maxFeePerGas: parsedRequest.maxFeePerGas
? hexToBigInt(parsedRequest.maxFeePerGas)
: estimatedGas.maxFeePerGas,
maxPriorityFeePerGas: parsedRequest.maxPriorityFeePerGas
? hexToBigInt(parsedRequest.maxPriorityFeePerGas)
: estimatedGas.maxPriorityFeePerGas,
chainId: txChainId,
type: "eip1559",
nonce,
};

// Sign the message
const [ethTxId] = await sendKasplexTransaction(
transaction,
ethSigner,
kasSigner,
);
await ApiExtensionUtils.sendMessage(
requestId,
ApiUtils.createApiResponse(requestId, ethTxId),
);
toggleIsSigning();
} catch (err) {
await ApiExtensionUtils.sendMessage(
requestId,
ApiUtils.createApiResponse(requestId, null, RPC_ERRORS.INTERNAL_ERROR),
);
} finally {

Check failure on line 146 in components/screens/browser-api/ethereum/send-transaction/SendKasplexL2Transaction.tsx

View workflow job for this annotation

GitHub Actions / submit

Empty block statement
}
};

const cancel = async () => {
await ApiExtensionUtils.sendMessage(
requestId,
ApiUtils.createApiResponse(
requestId,
null,
RPC_ERRORS.USER_REJECTED_REQUEST,
),
);
window.close();
};

return (
<div className="flex h-full flex-col justify-between">
<div>
<Header showPrevious={false} showClose={false} title="Confirm" />
<div className="relative">
{wallet?.type !== "ledger" && (
<img src={signImage} alt="Sign" className="mx-auto" />
)}
{wallet?.type === "ledger" && (
<img src={ledgerSignImage} alt="Sign" className="mx-auto" />
)}
</div>

{/* Confirm Content */}
<div className="text-center">
<h2 className="mt-4 text-2xl font-semibold">Send Transaction</h2>
<p className="mt-2 text-base text-daintree-400">
Please confirm the transaction you are signing
</p>
<div className="mt-4 rounded-md bg-daintree-700 p-4">
<p className="overflow-auto whitespace-pre text-start text-sm">
{JSON.stringify(payload, null, 2)}
</p>
</div>
</div>
</div>

{/* Buttons */}
<div className="flex gap-2 text-base font-semibold">
<button className="rounded-full p-5 text-[#7B9AAA]" onClick={cancel}>
Cancel
</button>
<button
className="flex flex-auto items-center justify-center rounded-full bg-icy-blue-400 py-5 font-semibold hover:bg-icy-blue-600"
onClick={onConfirm}
>
{isSigning ? (
<div className="flex gap-2">
<div
className="inline-block size-5 animate-spin self-center rounded-full border-[3px] border-current border-t-[#A2F5FF] text-icy-blue-600"
role="status"
aria-label="loading"
/>
{wallet?.type === "ledger" && (
<span className="text-sm">Please approve on Ledger</span>
)}
</div>
) : (
`Confirm`
)}
</button>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ import {
import { estimateFeesPerGas } from "viem/actions";
import { ethereumTransactionRequestSchema } from "@/api/background/handlers/ethereum/sendTransaction";
import { TESTNET_SUPPORTED_EVM_L2_CHAINS } from "@/lib/layer2";
type SignTransactionProps = {
walletSigner: IWallet;

type SendTransactionProps = {
signer: IWallet;
};

export default function SendTransaction({
walletSigner,
}: SignTransactionProps) {
signer: walletSigner,
}: SendTransactionProps) {
const [settings] = useSettings();
const { wallet } = useWalletManager();
const { value: isSigning, toggle: toggleIsSigning } = useBoolean(false);
Expand Down
1 change: 1 addition & 0 deletions components/send/ConfirmStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const ConfirmStep = ({
txIds: await signer.send(
kaspaToSompi(amount) ?? BigInt(0),
address,
"",
priorityFee,
),
};
Expand Down
2 changes: 2 additions & 0 deletions docs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ document
priorityFee: kaspaWasm.kaspaToSompi("0.1"),
changeAddress: address,
networkId: network,
payload:
"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
});

const transaction = pending.transactions[0];
Expand Down
Loading
Loading