Skip to content
Merged
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
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,8 @@ jobs:
- name: Run linter
run: npm run lint

- name: Run unit tests
run: npm run test:unit

- name: Build project
run: npm run build
run: npm run build
71 changes: 71 additions & 0 deletions lib/utils/stellar-address.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, it, expect } from "vitest";
import * as StellarSdk from "@stellar/stellar-sdk";
import { isValidStellarAddress, getStellarAddressError } from "./stellar-address";

// A real valid Stellar public key (from a random keypair)
const VALID_ADDRESS = StellarSdk.Keypair.random().publicKey();

describe("isValidStellarAddress", () => {
it("accepts a real Stellar public key", () => {
expect(isValidStellarAddress(VALID_ADDRESS)).toBe(true);
});

it("rejects an address with wrong length", () => {
expect(isValidStellarAddress("GABC123")).toBe(false);
});

it("rejects an address that does not start with G", () => {
// Replace leading 'G' with 'A'
const bad = "A" + VALID_ADDRESS.slice(1);
expect(isValidStellarAddress(bad)).toBe(false);
});

it("rejects an address with a bad checksum", () => {
// Flip the last character
const last = VALID_ADDRESS[VALID_ADDRESS.length - 1];
const flipped = last === "A" ? "B" : "A";
expect(isValidStellarAddress(VALID_ADDRESS.slice(0, -1) + flipped)).toBe(false);
});

it("rejects empty string", () => {
expect(isValidStellarAddress("")).toBe(false);
});

it("rejects non-string values", () => {
expect(isValidStellarAddress(null)).toBe(false);
expect(isValidStellarAddress(undefined)).toBe(false);
expect(isValidStellarAddress(123)).toBe(false);
expect(isValidStellarAddress({})).toBe(false);
});

it("rejects whitespace-only string", () => {
expect(isValidStellarAddress(" ")).toBe(false);
});

it("rejects an address with invalid base32 characters", () => {
expect(isValidStellarAddress("G" + "0".repeat(55))).toBe(false);
});
});

describe("getStellarAddressError", () => {
it("returns null for a valid address", () => {
expect(getStellarAddressError(VALID_ADDRESS)).toBeNull();
});

it("returns required message for empty string", () => {
expect(getStellarAddressError("")).toBe("Stellar address is required");
});

it("returns required message for non-string", () => {
expect(getStellarAddressError(null)).toBe("Stellar address is required");
expect(getStellarAddressError(undefined)).toBe("Stellar address is required");
});

it("returns invalid message for a malformed address", () => {
expect(getStellarAddressError("GABC123")).toBe("Invalid Stellar address");
});

it("returns invalid message for wrong-length address starting with G", () => {
expect(getStellarAddressError("G" + "A".repeat(54))).toBe("Invalid Stellar address");
});
});
31 changes: 31 additions & 0 deletions lib/utils/stellar-address.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Stellar address validation utility using the Stellar SDK.
*
* Uses StrKey.isValidEd25519PublicKey which decodes the Base32-encoded
* address and validates the checksum — far more robust than a simple
* length + prefix check.
*/
import { StrKey } from "@stellar/stellar-sdk";

/**
* Returns true if `address` is a valid Stellar Ed25519 public key (G…).
*/
export function isValidStellarAddress(address: unknown): boolean {
if (typeof address !== "string" || address.trim() === "") return false;
try {
return StrKey.isValidEd25519PublicKey(address);
} catch {
return false;
}
}

/**
* Returns a descriptive error message when `address` is invalid,
* or null when it is valid.
*/
export function getStellarAddressError(address: unknown): string | null {
if (typeof address !== "string" || address.trim() === "") {
return "Stellar address is required";
}
return isValidStellarAddress(address) ? null : "Invalid Stellar address";
}
39 changes: 0 additions & 39 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
"test:sensitive-actions": "node scripts/test-sensitive-actions-api.mjs",
"test:vote-remove": "node scripts/test-vote-remove-api.mjs",
"test:ephemeral": "node scripts/test-ephemeral-cleanup.mjs",
"test:unit": "vitest",
"start": "next start",
"cleanup:worker": "node scripts/ephemeral-cleanup-worker.js",
"cleanup:trigger": "node scripts/trigger-cleanup.mjs"
Expand Down
1 change: 1 addition & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default defineConfig({
environment: "happy-dom",
include: [
"lib/blockchain/group-verification.test.ts",
"lib/utils/stellar-address.test.ts",
"components/GroupVerificationBadge.test.tsx",
],
globals: false,
Expand Down
Loading