diff --git a/src/adapters/index.ts b/src/adapters/index.ts index d5f18f67..89ad58fb 100755 --- a/src/adapters/index.ts +++ b/src/adapters/index.ts @@ -97,6 +97,7 @@ import teleswap from "./teleswap"; import agglayer from "./agglayer"; import fxrp from "./flare/fxrp"; import snowbridge from "./snowbridge"; +import kaspabridge from "./kaspabridge"; import starkgate from "./starkgate"; export default { @@ -198,6 +199,7 @@ export default { agglayer, fxrp, snowbridge, + kaspabridge, starkgate, } as { [bridge: string]: BridgeAdapter | AsyncBridgeAdapter; diff --git a/src/adapters/kaspabridge/index.ts b/src/adapters/kaspabridge/index.ts new file mode 100644 index 00000000..4d71c09c --- /dev/null +++ b/src/adapters/kaspabridge/index.ts @@ -0,0 +1,130 @@ +import { BridgeAdapter } from "../../helpers/bridgeAdapter.type"; +import { Contract, providers, utils, BigNumber } from "ethers"; + +// ---------- CONFIG ---------- + +const CHAIN_RPC: Record = { + kasplex: "https://evmrpc.kasplex.org", + bsc: "https://bsc-rpc.publicnode.com", +}; + +const MAILBOX: Record = { + kasplex: "0x01e6A1a37942b51b08B047DdfDf345507A818d4d", + bsc: "0x5f297B3A1e8154c4D58702F7a880b7631bBf5340", +}; + +// Minimal ABI for Dispatch / Process events +const MAILBOX_ABI = [ + "event Dispatch(bytes32 indexed id, uint32 indexed destinationDomain, bytes32 recipientAddress, bytes messageBody)", + "event Process(bytes32 indexed id, bool success)", +]; + +// ---------- TYPES ---------- + +export interface KasplexEvent { + txHash: string; + blockNumber: number; + from: string; + to: string; + token: string; + isDeposit: boolean; + amount: BigNumber; +} + +// ---------- HELPERS ---------- + +function decodeMessageBody(messageBody: string) { + try { + const abiCoder = new utils.AbiCoder(); + const decoded = abiCoder.decode(["address", "address", "address", "uint256"], messageBody); + return { + from: decoded[0] as string, + to: decoded[1] as string, + token: decoded[2] as string, + amount: BigNumber.from(decoded[3] as BigNumber), + }; + } catch (e) { + console.error("Failed to decode messageBody:", e); + return { + from: "0x0000000000000000000000000000000000000000", + to: "0x0000000000000000000000000000000000000000", + token: "0x0000000000000000000000000000000000000000", + amount: BigNumber.from(0), + }; + } +} + +// ---------- MAIN ADAPTER FUNCTION ---------- + +const BLOCK_CHUNK = 2_000; + +export const getKaspaBridgeEvents = + (chain: string) => + async (fromBlock: number, toBlock: number): Promise => { + const rpcUrl = CHAIN_RPC[chain]; + const mailboxAddress = MAILBOX[chain]; + if (!rpcUrl || !mailboxAddress) { + throw new Error(`Missing kaspabridge config for chain ${chain}`); + } + + const provider = new providers.JsonRpcProvider(rpcUrl); + const mailbox = new Contract(mailboxAddress, MAILBOX_ABI, provider); + const events: KasplexEvent[] = []; + + for (let start = fromBlock; start <= toBlock; start += BLOCK_CHUNK) { + const end = Math.min(start + BLOCK_CHUNK - 1, toBlock); + + const [dispatchLogs, processLogs] = await Promise.all([ + provider.getLogs({ ...mailbox.filters.Dispatch(), fromBlock: start, toBlock: end }), + provider.getLogs({ ...mailbox.filters.Process(), fromBlock: start, toBlock: end }), + ]); + + for (const log of dispatchLogs) { + const parsed = mailbox.interface.parseLog(log); + const decoded = decodeMessageBody(parsed.args["messageBody"] as string); + + events.push({ + txHash: log.transactionHash, + blockNumber: log.blockNumber, + from: decoded.from, + to: decoded.to, + token: decoded.token, + isDeposit: true, + amount: decoded.amount, + }); + } + + for (const log of processLogs) { + events.push({ + txHash: log.transactionHash, + blockNumber: log.blockNumber, + from: "0x0000000000000000000000000000000000000000", + to: "0x0000000000000000000000000000000000000000", + token: "0x0000000000000000000000000000000000000000", + isDeposit: false, + amount: BigNumber.from(0), + }); + } + } + + return events; + }; + +// ---------- BRIDGE ADAPTER ---------- + +export async function setUp(): Promise { + return Object.keys(CHAIN_RPC); +} + +export async function build(): Promise { + const adapter: BridgeAdapter = {}; + const chains = await setUp(); + + for (const chain of chains) { + adapter[chain] = getKaspaBridgeEvents(chain); + } + + return adapter; +} + +export default { isAsync: true, build }; diff --git a/src/data/bridgeNetworkData.ts b/src/data/bridgeNetworkData.ts index 282af782..dfab5bc3 100644 --- a/src/data/bridgeNetworkData.ts +++ b/src/data/bridgeNetworkData.ts @@ -2696,4 +2696,20 @@ export default [ chains: ["Ethereum", "Starknet"], destinationChain: "Starknet", }, + { + id: 103, + displayName: "Kaspa Bridge", + bridgeDbName: "kaspabridge", + slug: "kaspabridge", + iconLink: "icons:kaspabridge", + largeTxThreshold: 10000, + url: "https://www.kaspabridge.com/", + chains: [ + "kasplex", + "bsc", + ], + chainMapping: { + avalanche: "avax", + }, + }, ] as BridgeNetwork[];