-
Stellar Verification
-
- Welcome to the Stellar verification page!
+
+
- This page is for verifying your Discord membership with the Stellar
- network.
-
-
-
About Stellar
-
- Stellar is an open-source, decentralized protocol for digital currency
- to fiat money transfers, enabling cross-border transactions between
- any currencies.
-
-
- Learn more about Stellar
-
-
-
-
Note: Wallet connection and verification are not yet
- available for Stellar. This page is for informational purposes only.
+
+
{
+ disconnect();
+ }}
+ />
+
+
+ {verifiedSignature && (
+
+ {typeof window !== "undefined" && (
+
+ )}
+
+ Identity: verified
+
+
YOU'RE ALL SET FREN
+ you shall close this tab
+
+ )}
+ {!verifiedSignature && stellarWalletDiv}
+ {process.env.NEXT_PUBLIC_STARKY_OFFICIAL &&
}
);
};
-export default VerifyStellar;
+const VerifyStellarPage = (props: Props) => {
+ return (
+
+
+
+ );
+};
+
+export async function getServerSideProps({ res, query }: any) {
+ await setupDb();
+ let discordServerName = null;
+ let discordServerIcon = null;
+ const { discordServerId, discordMemberId, customLink } = query;
+ const discordMember = await DiscordMemberRepository.findOne({
+ where: {
+ customLink,
+ discordServerId,
+ discordMemberId,
+ },
+ relations: ["discordServer"],
+ });
+ if (!discordMember || discordMember.customLink !== customLink) {
+ res.setHeader("location", "/");
+ res.statusCode = 302;
+ res.end();
+ return { props: {} };
+ }
+ try {
+ const serverInfo = await getDiscordServerInfo(`${query.discordServerId}`);
+ discordServerName = serverInfo.name;
+ discordServerIcon = serverInfo.icon
+ ? `https://cdn.discordapp.com/icons/${query.discordServerId}/${
+ serverInfo.icon
+ }${serverInfo.icon.startsWith("a_") ? ".gif" : ".png"}`
+ : null;
+ } catch (e: any) {
+ WatchTowerLogger.error(e.message, e);
+ }
+ return {
+ props: {
+ discordServerName,
+ discordServerIcon,
+ network: discordMember.starknetNetwork, // This might need to be updated to support Stellar networks
+ },
+ };
+}
+
+export default VerifyStellarPage;
diff --git a/pages/verify/[discordServerId]/[discordMemberId]/[customLink].tsx b/pages/verify/[discordServerId]/[discordMemberId]/[customLink].tsx
index 214a7a8..2f9bc60 100644
--- a/pages/verify/[discordServerId]/[discordMemberId]/[customLink].tsx
+++ b/pages/verify/[discordServerId]/[discordMemberId]/[customLink].tsx
@@ -10,11 +10,13 @@ import SocialLinks from "../../../../components/SocialLinks";
import chainAliasByNetwork from "../../../../configs/chainAliasByNetwork.json";
import { DiscordMemberRepository, setupDb } from "../../../../db";
import { getDiscordServerInfo } from "../../../../discord/utils";
-import { StarknetNetworkName } from "../../../../types/networks";
+import { StarknetNetworkName, NetworkName } from "../../../../types/networks";
import messageToSign from "../../../../utils/starknet/message";
import WatchTowerLogger from "../../../../watchTower";
import DiscordServerInfo from "../../../../components/verification/DiscordServerInfo";
import WalletInfo from "../../../../components/verification/WalletInfo";
+import DynamicVerificationRouter from "../../../../components/verification/DynamicVerificationRouter";
+import { getChainType } from "../../../../utils/networkDetection";
import styles from "../../../../styles/Verify.module.scss";
@@ -22,6 +24,7 @@ type Props = {
discordServerName: string;
discordServerIcon?: string | null;
starknetNetwork: StarknetNetworkName;
+ network: NetworkName;
};
const getSignatureErrorMessage = (
@@ -60,6 +63,7 @@ const VerifyPage = ({
discordServerName,
discordServerIcon,
starknetNetwork,
+ network,
}: Props) => {
const router = useRouter();
const { discordServerId, discordMemberId, customLink } = router.query;
@@ -232,6 +236,19 @@ const VerifyPage = ({
}
}, [account, verifySignature, chainId]);
+ // Check if this is not a Starknet network and route to appropriate page
+ const chainType = getChainType(network);
+ if (chainType && chainType !== "starknet") {
+ return (
+
+ );
+ }
+
let starknetWalletDiv = (
{!account && (
@@ -409,6 +426,7 @@ export async function getServerSideProps({ res, query }: any) {
discordServerName,
discordServerIcon,
starknetNetwork: discordMember.starknetNetwork,
+ network: discordMember.starknetNetwork, // For backward compatibility
},
};
}
diff --git a/public/assets/stellar-icon1.png b/public/assets/stellar-icon1.png
new file mode 100644
index 0000000..ab49d6e
Binary files /dev/null and b/public/assets/stellar-icon1.png differ
diff --git a/public/assets/stellar-xlm-icon.png b/public/assets/stellar-xlm-icon.png
new file mode 100644
index 0000000..6e79a21
Binary files /dev/null and b/public/assets/stellar-xlm-icon.png differ
diff --git a/types/freighter.d.ts b/types/freighter.d.ts
new file mode 100644
index 0000000..5d914cf
--- /dev/null
+++ b/types/freighter.d.ts
@@ -0,0 +1,12 @@
+declare global {
+ interface Window {
+ freighter?: {
+ isConnected: () => Promise
;
+ getPublicKey: () => Promise;
+ signTransaction: (xdr: string, network?: string) => Promise;
+ getNetwork: () => Promise;
+ };
+ }
+}
+
+export {};
diff --git a/types/networks.ts b/types/networks.ts
index f3e2a33..33a1454 100644
--- a/types/networks.ts
+++ b/types/networks.ts
@@ -1,6 +1,7 @@
export type StarknetNetworkName = "mainnet" | "goerli" | "sepolia";
+export type StellarNetworkName = "stellar-mainnet" | "stellar-testnet";
export type NetworkName =
| StarknetNetworkName
| "ethereum-mainnet"
- | "stellar-mainnet"
- | "stellar-testnet";
+ | StellarNetworkName;
+export type ChainType = "starknet" | "ethereum" | "stellar";
diff --git a/utils/ethereum/context/WalletConnect.tsx b/utils/ethereum/context/WalletConnect.tsx
index 07ba6f2..4da2d5b 100644
--- a/utils/ethereum/context/WalletConnect.tsx
+++ b/utils/ethereum/context/WalletConnect.tsx
@@ -9,6 +9,7 @@ import {
disconnect as starknetDisconnect,
} from "starknetkit";
import { INFURA_PROJECT_ID, ETHEREUM_ENABLED } from "../ethereumEnv";
+import { ChainType } from "../../../types/networks";
type WalletContextType = {
connect: (networkName: string) => Promise;
@@ -16,7 +17,7 @@ type WalletContextType = {
account: string | null;
provider: ethers.BrowserProvider | null;
chainId: number | string | null;
- networkType: "ethereum" | "starknet" | null;
+ networkType: ChainType | null;
signMessage: (message: string) => Promise;
balance: string | null;
};
@@ -27,9 +28,7 @@ export function WalletProvider({ children }: { children: React.ReactNode }) {
const [account, setAccount] = useState(null);
const [provider, setProvider] = useState(null);
const [chainId, setChainId] = useState(null);
- const [networkType, setNetworkType] = useState<
- "ethereum" | "starknet" | null
- >(null);
+ const [networkType, setNetworkType] = useState(null);
const [wcProvider, setWcProvider] = useState(null);
const [balance, setBalance] = useState(null);
diff --git a/utils/networkDetection.ts b/utils/networkDetection.ts
new file mode 100644
index 0000000..86cf864
--- /dev/null
+++ b/utils/networkDetection.ts
@@ -0,0 +1,61 @@
+import networks from "../configs/networks.json";
+import { NetworkName, ChainType } from "../types/networks";
+
+export interface NetworkConfig {
+ label: string;
+ name: string;
+ url: string;
+ chain: ChainType;
+ verifyPage: string;
+ chainId?: number;
+ explorerUrl?: string;
+ currency?: string;
+ indexer: boolean;
+ description: string;
+}
+
+export const getNetworkConfig = (
+ networkName: NetworkName
+): NetworkConfig | null => {
+ return (
+ (networks.find(
+ (network) => network.name === networkName
+ ) as NetworkConfig) || null
+ );
+};
+
+export const getChainType = (networkName: NetworkName): ChainType | null => {
+ const network = getNetworkConfig(networkName);
+ return network?.chain || null;
+};
+
+export const getVerifyPageForNetwork = (networkName: NetworkName): string => {
+ const network = getNetworkConfig(networkName);
+ if (!network) return "verify"; // Default fallback
+
+ // Return the verification page based on chain type
+ switch (network.chain) {
+ case "starknet":
+ return "verify";
+ case "ethereum":
+ return "verify-eth";
+ case "stellar":
+ return "verify-stellar";
+ default:
+ return "verify";
+ }
+};
+
+export const getAllNetworksByChain = (
+ chainType: ChainType
+): NetworkConfig[] => {
+ return networks.filter(
+ (network) => (network as NetworkConfig).chain === chainType
+ ) as NetworkConfig[];
+};
+
+export const getNetworksByChain = {
+ starknet: () => getAllNetworksByChain("starknet"),
+ ethereum: () => getAllNetworksByChain("ethereum"),
+ stellar: () => getAllNetworksByChain("stellar"),
+};
diff --git a/utils/stellar/context/StellarWalletConnect.tsx b/utils/stellar/context/StellarWalletConnect.tsx
new file mode 100644
index 0000000..8d1cbed
--- /dev/null
+++ b/utils/stellar/context/StellarWalletConnect.tsx
@@ -0,0 +1,183 @@
+"use client";
+
+import { createContext, useContext, useState } from "react";
+import {
+ isConnected,
+ getAddress,
+ requestAccess,
+ signTransaction,
+} from "@stellar/freighter-api";
+import * as StellarSdk from "stellar-sdk";
+import networks from "../../../configs/networks.json";
+import {
+ getStellarHorizonUrl,
+ getStellarNetworkPassphrase,
+} from "../stellarEnv";
+import WatchTowerLogger from "../../../watchTower";
+
+type StellarWalletContextType = {
+ connect: (networkName: string) => Promise;
+ disconnect: () => void;
+ account: string | null;
+ networkType: "stellar-mainnet" | "stellar-testnet" | null;
+ signMessage: (message: string) => Promise;
+ balance: string | null;
+ server: StellarSdk.Horizon.Server | null;
+};
+
+const StellarWalletContext = createContext(
+ {} as StellarWalletContextType
+);
+
+export function StellarWalletProvider({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const [account, setAccount] = useState(null);
+ const [networkType, setNetworkType] = useState<
+ "stellar-mainnet" | "stellar-testnet" | null
+ >(null);
+ const [balance, setBalance] = useState(null);
+ const [server, setServer] = useState(null);
+
+ const connect = async (networkName: string) => {
+ console.log("Attempting to connect to Stellar network:", networkName);
+
+ const network = networks.find((n) => n.name === networkName);
+ if (!network || network.chain !== "stellar") {
+ console.error("Invalid network:", network);
+ throw new Error("Invalid Stellar network");
+ }
+
+ try {
+ console.log("Checking if Freighter is available...");
+
+ // Request access to Freighter - this handles both checking if it's installed and getting permission
+ console.log("Requesting Freighter access...");
+ const accessResult = await requestAccess();
+ console.log("Access result:", accessResult);
+
+ if (accessResult.error) {
+ throw new Error(`Freighter error: ${accessResult.error}`);
+ }
+
+ // Get the address (public key)
+ console.log("Getting address...");
+ const addressResult = await getAddress();
+ console.log("Address result:", addressResult);
+
+ if (addressResult.error) {
+ throw new Error(`Failed to get address: ${addressResult.error}`);
+ }
+
+ const publicKey = addressResult.address;
+
+ if (!publicKey) {
+ throw new Error("Failed to get public key from Freighter wallet");
+ }
+
+ // Initialize Stellar server
+ const horizonUrl = getStellarHorizonUrl(
+ networkName as "stellar-mainnet" | "stellar-testnet"
+ );
+ const stellarServer = new StellarSdk.Horizon.Server(horizonUrl);
+
+ try {
+ // Get account balance
+ const accountInfo = await stellarServer.loadAccount(publicKey);
+ const nativeBalance = accountInfo.balances.find(
+ (balance) => balance.asset_type === "native"
+ );
+ const xlmBalance = nativeBalance
+ ? parseFloat(nativeBalance.balance).toFixed(7)
+ : "0";
+
+ setBalance(xlmBalance);
+ } catch (error) {
+ WatchTowerLogger.warn("Could not load account balance:", {
+ error: error as Error,
+ });
+ setBalance("0");
+ }
+
+ setAccount(publicKey);
+ setNetworkType(networkName as "stellar-mainnet" | "stellar-testnet");
+ setServer(stellarServer);
+ } catch (error) {
+ console.error("Stellar connection error:", error);
+ throw error;
+ }
+ };
+
+ const disconnect = () => {
+ setAccount(null);
+ setNetworkType(null);
+ setBalance(null);
+ setServer(null);
+ };
+
+ const signMessage = async (message: string) => {
+ if (!account || !server) {
+ throw new Error("Wallet not connected");
+ }
+
+ try {
+ console.log("Signing message with Stellar account:", account);
+
+ // Create a transaction with the message as memo for signing
+ const sourceAccount = await server.loadAccount(account);
+
+ const transaction = new StellarSdk.TransactionBuilder(sourceAccount, {
+ fee: StellarSdk.BASE_FEE,
+ networkPassphrase: getStellarNetworkPassphrase(networkType!),
+ })
+ .addMemo(StellarSdk.Memo.text(message.substring(0, 28))) // Stellar memo limit is 28 chars
+ .addOperation(
+ StellarSdk.Operation.manageData({
+ name: "verify",
+ value: message.substring(0, 64), // Store part of message in account data
+ })
+ )
+ .setTimeout(0)
+ .build();
+
+ const xdr = transaction.toXDR();
+ console.log("Transaction XDR:", xdr);
+
+ // Sign the transaction using Freighter
+ const signedTxResult = await signTransaction(xdr, {
+ networkPassphrase: getStellarNetworkPassphrase(networkType!),
+ address: account,
+ });
+
+ if (signedTxResult.error) {
+ throw new Error(`Signing failed: ${signedTxResult.error}`);
+ }
+
+ console.log("Signed transaction XDR:", signedTxResult.signedTxXdr);
+ return signedTxResult.signedTxXdr;
+ } catch (error) {
+ console.error("Message signing error:", error);
+ throw error;
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export const useStellarWallet = () => useContext(StellarWalletContext);
diff --git a/utils/stellar/message.ts b/utils/stellar/message.ts
new file mode 100644
index 0000000..dba1319
--- /dev/null
+++ b/utils/stellar/message.ts
@@ -0,0 +1,30 @@
+const messageToSign = {
+ types: {
+ EIP712Domain: [
+ { name: "name", type: "string" },
+ { name: "version", type: "string" },
+ { name: "chainId", type: "string" },
+ { name: "verifyingContract", type: "string" },
+ ],
+ Message: [
+ { name: "contents", type: "string" },
+ { name: "from", type: "string" },
+ { name: "to", type: "string" },
+ ],
+ },
+ primaryType: "Message",
+ domain: {
+ name: "Starky",
+ version: "1",
+ chainId: "stellar",
+ verifyingContract: "starky.wtf",
+ },
+ message: {
+ contents:
+ "Sign this message to verify that you are the owner of this account. This will not trigger a blockchain transaction or cost any gas fees.",
+ from: "starky.wtf",
+ to: "Discord server",
+ },
+};
+
+export default messageToSign;
diff --git a/utils/stellar/stellarEnv.ts b/utils/stellar/stellarEnv.ts
new file mode 100644
index 0000000..a432109
--- /dev/null
+++ b/utils/stellar/stellarEnv.ts
@@ -0,0 +1,25 @@
+export const getStellarHorizonUrl = (
+ network: "stellar-mainnet" | "stellar-testnet"
+) => {
+ switch (network) {
+ case "stellar-mainnet":
+ return "https://horizon.stellar.org";
+ case "stellar-testnet":
+ return "https://horizon-testnet.stellar.org";
+ default:
+ return "https://horizon-testnet.stellar.org";
+ }
+};
+
+export const getStellarNetworkPassphrase = (
+ network: "stellar-mainnet" | "stellar-testnet"
+) => {
+ switch (network) {
+ case "stellar-mainnet":
+ return "Public Global Stellar Network ; September 2015";
+ case "stellar-testnet":
+ return "Test SDF Network ; September 2015";
+ default:
+ return "Test SDF Network ; September 2015";
+ }
+};
diff --git a/utils/stellar/verifySignature.ts b/utils/stellar/verifySignature.ts
new file mode 100644
index 0000000..8099017
--- /dev/null
+++ b/utils/stellar/verifySignature.ts
@@ -0,0 +1,124 @@
+import * as StellarSdk from "stellar-sdk";
+import {
+ getStellarHorizonUrl,
+ getStellarNetworkPassphrase,
+} from "./stellarEnv";
+
+export const verifySignature = async (
+ publicKey: string,
+ message: string,
+ signedTxXdr: string,
+ network: string
+): Promise<{ signatureValid: boolean; error?: string }> => {
+ try {
+ console.log("Verifying Stellar signature for:", publicKey);
+ console.log("Message:", message);
+ console.log("Signed XDR:", signedTxXdr);
+
+ // Basic validation - check if public key is valid Stellar address
+ if (!publicKey || publicKey.length !== 56 || !publicKey.startsWith("G")) {
+ return {
+ signatureValid: false,
+ error: "Invalid Stellar public key format",
+ };
+ }
+
+ // Check if signature (XDR) is provided
+ if (!signedTxXdr || signedTxXdr.length === 0) {
+ return {
+ signatureValid: false,
+ error: "No signed transaction provided",
+ };
+ }
+
+ // Check if the account exists on the Stellar network
+ const horizonUrl = getStellarHorizonUrl(
+ network as "stellar-mainnet" | "stellar-testnet"
+ );
+ const server = new StellarSdk.Horizon.Server(horizonUrl);
+
+ try {
+ await server.loadAccount(publicKey);
+ } catch (error) {
+ return {
+ signatureValid: false,
+ error: "Stellar account not found on network",
+ };
+ }
+
+ try {
+ // Parse the signed transaction XDR
+ const networkPassphrase = getStellarNetworkPassphrase(
+ network as "stellar-mainnet" | "stellar-testnet"
+ );
+ const transaction = StellarSdk.TransactionBuilder.fromXDR(
+ signedTxXdr,
+ networkPassphrase
+ );
+
+ console.log("Parsed transaction:", transaction);
+
+ // Verify the transaction is signed by the correct account
+ let sourceAccount: string;
+ if ("source" in transaction) {
+ sourceAccount = transaction.source;
+ } else {
+ // For FeeBumpTransaction, use the inner transaction's source
+ sourceAccount = transaction.innerTransaction.source;
+ }
+
+ if (sourceAccount !== publicKey) {
+ return {
+ signatureValid: false,
+ error: "Transaction not signed by the expected account",
+ };
+ }
+
+ // Check if the transaction has signatures
+ if (transaction.signatures.length === 0) {
+ return {
+ signatureValid: false,
+ error: "No signatures found in transaction",
+ };
+ }
+
+ // Verify signature using Stellar SDK
+ const keypair = StellarSdk.Keypair.fromPublicKey(publicKey);
+ const txHash = transaction.hash();
+
+ // Check if any signature is valid for this keypair
+ let validSignature = false;
+ for (const signature of transaction.signatures) {
+ try {
+ const isValid = keypair.verify(txHash, signature.signature());
+ if (isValid) {
+ validSignature = true;
+ break;
+ }
+ } catch (error) {
+ console.log("Signature verification failed:", error);
+ }
+ }
+
+ if (validSignature) {
+ return { signatureValid: true };
+ } else {
+ return {
+ signatureValid: false,
+ error: "Invalid signature",
+ };
+ }
+ } catch (error: any) {
+ console.error("XDR parsing error:", error);
+ return {
+ signatureValid: false,
+ error: `Failed to parse signed transaction: ${error.message}`,
+ };
+ }
+ } catch (error: any) {
+ return {
+ signatureValid: false,
+ error: `Verification failed: ${error.message}`,
+ };
+ }
+};