Skip to content
This repository was archived by the owner on Mar 9, 2026. It is now read-only.
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
111 changes: 3 additions & 108 deletions dapp/bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions dapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"react-dom": "^19.2.0",
"react18-json-view": "^0.2.9",
"recharts": "^3.2.1",
"sshsig": "^0.3.1",
"toml": "^3.0.0",
"typescript": "^5.9.2"
},
Expand Down
2 changes: 2 additions & 0 deletions dapp/packages/tansu/src/index.ts

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I am not sure we need all that to be in the contract itself vs the meta file-if even needed. I would keep the handle and key only. And in meta nothing. What do you think?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry i don't get you clearly

you mean i should remove this git_identity?: string; git_pubkey?: Buffer; msg?: string; sig?: Buffer; signed_at?: u64;

?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we just need the identity and key to be stored in the contract. When I will call the smart contract function add_member I will pass everything though as to validate on-chain the data.

Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export interface Config {
export interface Member {
meta: string;
projects: Array<ProjectBadges>;
git_identity?: string;
git_pubkey?: Buffer;
}

export type DataKey =
Expand Down
3 changes: 3 additions & 0 deletions dapp/public/icons/check-circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions dapp/public/icons/x-circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions dapp/src/components/page/dashboard/JoinCommunityModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { useState, type FC, useEffect } from "react";
import Input from "components/utils/Input";
import Button from "components/utils/Button";
import FlowProgressModal from "components/utils/FlowProgressModal";
import GitVerification from "components/utils/GitVerification";
import { loadedPublicKey } from "@service/walletService";
import { toast } from "utils/utils";
import { validateStellarAddress, validateUrl } from "utils/validations";
import SimpleMarkdownEditor from "components/utils/SimpleMarkdownEditor";
import type { GitVerificationData } from "utils/gitVerification";

interface ProfileImageFile {
localUrl: string;
Expand Down Expand Up @@ -33,6 +35,9 @@ const JoinCommunityModal: FC<{
const [updateSuccessful, setUpdateSuccessful] = useState(false);
const [step, setStep] = useState<number>(1);
const [error, setError] = useState<string | null>(null);


const [gitVerificationData, setGitVerificationData] = useState<GitVerificationData | null>(null);

// Validation errors
const [addressError, setAddressError] = useState<string | null>(null);
Expand Down Expand Up @@ -155,6 +160,7 @@ const JoinCommunityModal: FC<{
await joinCommunityFlow({
memberAddress: address,
profileFiles: [],
gitVerificationData,
onProgress: setStep,
});

Expand Down Expand Up @@ -201,6 +207,7 @@ const JoinCommunityModal: FC<{
await joinCommunityFlow({
memberAddress: address,
profileFiles: files,
gitVerificationData,
onProgress: setStep,
});

Expand Down Expand Up @@ -364,6 +371,13 @@ const JoinCommunityModal: FC<{
</div>
</div>

<GitVerification
onVerificationComplete={setGitVerificationData}
networkPassphrase={import.meta.env.PUBLIC_SOROBAN_NETWORK_PASSPHRASE}
signingAccount={address}
contractId={import.meta.env.PUBLIC_TANSU_CONTRACT_ID}
/>

<div className="flex justify-end gap-[18px]">
<Button type="secondary" onClick={onClose}>
Cancel
Expand Down
24 changes: 24 additions & 0 deletions dapp/src/components/page/dashboard/MemberProfileModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,30 @@ const MemberProfileModal: FC<Props> = ({ onClose, member, address }) => {
</a>
)}

{member?.git_identity ? (
<div className="flex items-center justify-center gap-2 mt-2 p-2 bg-green-50 rounded-lg">
<img
src="/icons/check-circle.svg"
alt="Verified"
className="w-4 h-4 text-green-600"
/>
<span className="text-sm text-green-700 font-medium">
Git: {member?.git_identity} ✓
</span>
</div>
) : (
<div className="flex items-center justify-center gap-2 mt-2 p-2 bg-gray-50 rounded-lg">
<img
src="/icons/x-circle.svg"
alt="Not linked"
className="w-4 h-4 text-gray-500"
/>
<span className="text-sm text-gray-600">
No Git handle linked
</span>
</div>
)}

{/* IPFS metadata link */}
{member?.meta && hasValidMetadata && (
<a
Expand Down
281 changes: 281 additions & 0 deletions dapp/src/components/utils/GitVerification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
import { useState, type FC } from "react";
import Input from "./Input";
import Button from "./Button";
import {
parseGitHandle,
fetchGitPublicKeys,
extractEd25519PublicKey,
createSEP53Envelope,
generateSSHSignCommand,
generateGPGSignCommand,
parseSSHSignature,
validateGitVerification,
type GitVerificationData
} from "../../utils/gitVerification";
import CopyButton from "./CopyButton";
import { toast } from "utils/utils";

interface GitVerificationProps {
onVerificationComplete: (data: GitVerificationData | null) => void;
networkPassphrase: string;
signingAccount: string;
contractId: string;
}

const GitVerification: FC<GitVerificationProps> = ({
onVerificationComplete,
networkPassphrase,
signingAccount,
contractId
}) => {

const [wantGitLink, setWantGitLink] = useState<boolean | null>(null);
const [gitHandle, setGitHandle] = useState("");
const [selectedKey, setSelectedKey] = useState("");
const [availableKeys, setAvailableKeys] = useState<string[]>([]);
const [envelope, setEnvelope] = useState("");
const [signature, setSignature] = useState("");
const [isLoadingKeys, setIsLoadingKeys] = useState(false);
const [isVerifying, setIsVerifying] = useState(false);
const [step, setStep] = useState<"choice" | "handle" | "key" | "sign" | "verify">("choice");

const handleGitHandleSubmit = async () => {
if (!gitHandle.trim()) {
toast.error("Error", "Please enter a Git handle");
return;
}

const parsed = parseGitHandle(gitHandle.trim());
if (!parsed) {
toast.error("Error", "Invalid Git handle format. Use provider:username (e.g., github:alice)");
return;
}

setIsLoadingKeys(true);
try {
const keys = await fetchGitPublicKeys(gitHandle.trim());
setAvailableKeys(keys);
setStep("key");
} catch (error) {
toast.error("Error", error instanceof Error ? error.message : "Failed to fetch Git keys");
} finally {
setIsLoadingKeys(false);
}
};

const handleKeySelection = () => {
if (!selectedKey) {
toast.error("Error", "Please select a public key");
return;
}

// if (!contractId) {
// toast.error("Error", "Contract ID is not available. Please try again later.");
// return;
// }

const generatedEnvelope = createSEP53Envelope(
networkPassphrase,
signingAccount,
contractId,
gitHandle.trim()
);
setEnvelope(generatedEnvelope);
setStep("sign");
};

const handleVerifySignature = async () => {
if (!signature.trim()) {
toast.error("Error", "Please paste your signature");
return;
}

setIsVerifying(true);
try {
const publicKey = extractEd25519PublicKey(selectedKey);
const parsedSignature = parseSSHSignature(signature.trim());

const verificationData: GitVerificationData = {
gitHandle: gitHandle.trim(),
publicKey,
envelope,
signature: parsedSignature
};

const validation = await validateGitVerification(
verificationData,
networkPassphrase,
signingAccount,
contractId
);

console.log("Git verification result:", validation);

if (validation.valid) {
toast.success("Success", "Git verification completed successfully!");
onVerificationComplete(verificationData);
} else {
toast.error("Verification Failed", validation.error || "Unknown error");
}
} catch (error) {
toast.error("Error", error instanceof Error ? error.message : "Verification failed");
} finally {
setIsVerifying(false);
}
};

const handleSkip = () => {
setWantGitLink(false);
onVerificationComplete(null);
};

if (wantGitLink === false) {
return null;
}

return (
<div className="border-t pt-4 mt-4">
<h3 className="text-lg font-semibold text-primary mb-4">Git Handle Verification</h3>

{step === "choice" && (
<div className="space-y-4">
<p className="text-sm text-secondary">
Would you like to link a Git handle to your account for verification?
</p>
<div className="flex gap-3">
<Button type="primary" onClick={() => { setWantGitLink(true); setStep("handle"); }}>
Yes, Link Git Handle
</Button>
<Button type="secondary" onClick={handleSkip}>
Skip
</Button>
</div>
</div>
)}

{step === "handle" && (
<div className="space-y-4">
<Input
label="Git Handle"
placeholder="github:alice or gitlab:bob"
value={gitHandle}
onChange={(e) => setGitHandle(e.target.value)}
helpText="Format: provider:username (supports GitHub and GitLab)"
/>
<div className="flex gap-3">
<Button
type="primary"
onClick={handleGitHandleSubmit}
isLoading={isLoadingKeys}
>
Fetch Public Keys
</Button>
<Button type="secondary" onClick={() => setStep("choice")}>
Back
</Button>
</div>
</div>
)}

{step === "key" && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-primary mb-2">
Select Ed25519 Public Key
</label>
<div className="space-y-2 max-h-40 overflow-y-auto">
{availableKeys.map((key, index) => (
<label key={index} className="flex items-start gap-2 p-2 border rounded cursor-pointer hover:bg-gray-50">
<input
type="radio"
name="publicKey"
value={key}
checked={selectedKey === key}
onChange={(e) => setSelectedKey(e.target.value)}
className="mt-1"
/>
<span className="text-xs font-mono break-all">{key}</span>
</label>
))}
</div>
</div>
<div className="flex gap-3">
<Button type="primary" onClick={handleKeySelection}>
Continue
</Button>
<Button type="secondary" onClick={() => setStep("handle")}>
Back
</Button>
</div>
</div>
)}

{step === "sign" && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-primary mb-2">
SEP-53 Envelope to Sign
</label>
<textarea
value={envelope}
readOnly
className="w-full p-3 border rounded font-mono text-xs bg-gray-50"
rows={6}
/>
</div>

<div className="bg-blue-50 p-4 rounded">
<h4 className="font-medium text-blue-900 mb-2">Sign this envelope with your Git key:</h4>
<div className="space-y-2">
<div className="flex items-start gap-2">
<div className="flex-1">
<p className="text-sm text-blue-800 mb-1">SSH Command:</p>
<code className="block p-2 bg-blue-100 rounded text-xs break-all">
{generateSSHSignCommand(envelope).replace(/\s*\/dev\/stdin\s*/g, "")}
</code>
</div>
<CopyButton textToCopy={generateSSHSignCommand(envelope).replace(/\s*\/dev\/stdin\s*/g, "")} size="sm" />
</div>
{/* Only show GPG command if not using SSH key (future extensibility) */}
{/*
<div className="flex items-start gap-2">
<div className="flex-1">
<p className="text-sm text-blue-800 mb-1">GPG Command:</p>
<code className="block p-2 bg-blue-100 rounded text-xs break-all">
{generateGPGSignCommand(envelope)}
</code>
</div>
<CopyButton textToCopy={generateGPGSignCommand(envelope)} size="sm" />
</div>
*/}
</div>
</div>

<Input
label="Paste Signature"
placeholder="Paste the signature output here..."
value={signature}
onChange={(e) => setSignature(e.target.value)}
multiline
rows={6}
/>

<div className="flex gap-3">
<Button
type="primary"
onClick={handleVerifySignature}
isLoading={isVerifying}
>
Verify Signature
</Button>
<Button type="secondary" onClick={() => setStep("key")}>
Back
</Button>
</div>
</div>
)}
</div>
);
};

export default GitVerification;
Loading