Skip to content
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
9 changes: 9 additions & 0 deletions apps/demo-identity-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { VerifyButton } from "./components/VerifyButton"
import { IdentityCard } from "./components/IdentityCard"
import { SigningModal } from "./components/SigningModal"
import { ClaimButton } from "./components/ClaimButton"
import { WalletLinkWidget } from "./components/WalletLinkWidget"

const tamaguiConfig = createTamagui(config)

Expand Down Expand Up @@ -263,6 +264,14 @@ const App: React.FC = () => {
)}
</YStack>

{/* --- NEW WALLET LINK WIDGET SECTION --- */}
{isConnected && (
<YStack width="100%" maxWidth={600} marginTop={24}>
<WalletLinkWidget />
</YStack>
)}
{/* -------------------------------------- */}

<SigningModal
open={isSigningModalOpen}
onClose={() => setIsSigningModalOpen(false)}
Expand Down
121 changes: 121 additions & 0 deletions apps/demo-identity-app/src/components/WalletLinkWidget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { useState } from "react";
import { useWalletLink } from "@goodsdks/react-hooks";
import { Address, isAddress } from "viem";

export const WalletLinkWidget = () => {
const [targetAddress, setTargetAddress] = useState<string>("");
const { connectAccount, disconnectAccount, connectedStatus } = useWalletLink("development", targetAddress as Address);

const handleConnect = async () => {
if (!targetAddress) return;
if (!isAddress(targetAddress)) {
alert("Invalid Ethereum Address format!");
return;
}
try {
await connectAccount.connect(targetAddress as Address);
connectedStatus.refetch();
} catch (err) {
console.error("Connect failed", err);
}
};

const handleDisconnect = async () => {
if (!targetAddress) return;
if (!isAddress(targetAddress)) {
alert("Invalid Ethereum Address format!");
return;
}
try {
await disconnectAccount.disconnect(targetAddress as Address);
connectedStatus.refetch();
} catch (err) {
console.error("Disconnect failed", err);
}
};

const handleCheckStatus = () => {
if (!targetAddress) return;
if (!isAddress(targetAddress)) {
alert("Invalid Ethereum Address format!");
return;
}
connectedStatus.refetch();
};

if (connectAccount.pendingSecurityConfirm || disconnectAccount.pendingSecurityConfirm) {
const pending = connectAccount.pendingSecurityConfirm || disconnectAccount.pendingSecurityConfirm;
const confirmFn = connectAccount.pendingSecurityConfirm
? connectAccount.confirmSecurity
: disconnectAccount.confirmSecurity;

return (
<div style={{ border: "2px solid red", padding: "1rem", margin: "1rem 0", borderRadius: "8px" }}>
<h3>⚠️ Security Notice</h3>
<pre style={{ whiteSpace: "pre-wrap", fontSize: "12px" }}>{pending?.message}</pre>
<div style={{ display: "flex", gap: "10px", marginTop: "10px" }}>
<button onClick={() => confirmFn(true)} style={{ background: "red", color: "white", padding: "8px" }}>
I Understand, Proceed
</button>
<button onClick={() => confirmFn(false)} style={{ padding: "8px" }}>Cancel</button>
</div>
</div>
);
}

return (
<div style={{ border: "1px solid #ccc", padding: "1rem", borderRadius: "8px", marginTop: "2rem" }}>
<h2>🔗 Wallet Link (Citizen SDK)</h2>

<div style={{ marginBottom: "1rem" }}>
<input
type="text"
placeholder="0xSecondaryWalletAddress..."
value={targetAddress}
onChange={(e) => setTargetAddress(e.target.value)}
style={{ width: "100%", padding: "8px", marginBottom: "10px" }}
/>
<div style={{ display: "flex", gap: "10px" }}>
<button onClick={handleConnect} disabled={connectAccount.loading || !targetAddress}>
{connectAccount.loading ? "Connecting..." : "Connect Wallet"}
</button>
<button onClick={handleDisconnect} disabled={disconnectAccount.loading || !targetAddress}>
{disconnectAccount.loading ? "Disconnecting..." : "Disconnect Wallet"}
</button>
<button onClick={handleCheckStatus}>Check Status</button>
</div>
</div>

{(connectAccount.error || disconnectAccount.error) && (
<p style={{ color: "red" }}>Error: {connectAccount.error || disconnectAccount.error}</p>
)}

{(connectAccount.txHash || disconnectAccount.txHash) && (
<p style={{ color: "green" }}>Tx Hash: {connectAccount.txHash || disconnectAccount.txHash}</p>
)}

<div style={{ background: "#f5f5f5", padding: "10px", marginTop: "1rem", fontSize: "14px" }}>
<h4>Status for {targetAddress || "..."}</h4>
{connectedStatus.loading ? (
<p>Loading status...</p>
) : (
<>
<p><strong>Connected (Current Chain):</strong> {connectedStatus.status?.isConnected ? "✅ Yes" : "❌ No"}</p>
<p><strong>Root Identity:</strong> {connectedStatus.status?.root || "None"}</p>

<details style={{ marginTop: "10px" }}>
<summary>Multi-Chain Statuses</summary>
<ul>
{connectedStatus.allChainStatuses.map(chain => (
<li key={chain.chainId}>
{chain.chainName}: {chain.isConnected ? `✅ (Root: ${chain.root})` : "❌"}
</li>
))}
</ul>
</details>
</>
)}
</div>
</div>
);
};
2 changes: 1 addition & 1 deletion apps/demo-identity-app/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { AppKitProvider } from "@/config";
import { AppKitProvider } from "./config";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import "./index.css";
Expand Down
48 changes: 47 additions & 1 deletion packages/citizen-sdk/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,18 @@ export const chainConfigs: Record<SupportedChains, ChainConfig> = {
label: "XDC Network",
shortName: "XDC",
explorer: makeExplorer("https://xdcscan.com"),
// rpcUrls: ["https://rpc.xdc.network", "https://rpc.ankr.com/xdc"],
rpcUrls: ["https://rpc.ankr.com/xdc"],
defaultGasPrice: BigInt(12.5e9),
claimGasBuffer: 150000n,
fvDefaultChain: SupportedChains.XDC,
contracts: {
// Production identity contract sourced from reference-assets connect-a-wallet example
Copy link
Collaborator

Choose a reason for hiding this comment

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

If unsure, the expected way to contribute is to ask questions, you can always put a PR in draft and communicate with maintainers and reviewers.

We have deployed contracts for XDC: https://github.com/GoodDollar/GoodProtocol/blob/259ca3702afd2601ef4e908963c270282b8aa08e/releases/deployment.json#L671

production: {
identityContract: "0x27a4a02C9ed591E1a86e2e5D05870292c34622C9",
ubiContract: "0x0000000000000000000000000000000000000000", // TODO: add when deployed
faucetContract: "0x0000000000000000000000000000000000000000", // TODO: add when deployed
g$Contract: "0x0000000000000000000000000000000000000000", // TODO: add when deployed
},
development: {
identityContract: "0xa6632e9551A340E8582cc797017fbA645695E29f",
ubiContract: "0xA2619D468EfE2f6D8b3D915B999327C8fE13aE2c",
Expand Down Expand Up @@ -189,12 +195,29 @@ export const createRpcUrlIterator = (chainId: SupportedChains) => {
}
}

/**
* Core identity ABI — includes both read/write identity methods
* and IdentityV4 wallet-link methods (connectAccount, disconnectAccount, connectedAccounts).
*/
export const identityV2ABI = parseAbi([
// Whitelist management (IdentityV2)
"function addWhitelisted(address account)",
"function removeWhitelisted(address account)",
"function getWhitelistedRoot(address account) view returns (address)",
"function lastAuthenticated(address account) view returns (uint256)",
"function authenticationPeriod() view returns (uint256)",
// Wallet-link methods (IdentityV4)
"function connectAccount(address account) external",
"function disconnectAccount(address account) external",
"function connectedAccounts(address account) view returns (address)",
])

/** Alias exported for clarity when consumers only need wallet-link methods. */
export const walletLinkABI = parseAbi([
"function connectAccount(address account) external",
"function disconnectAccount(address account) external",
"function connectedAccounts(address account) view returns (address)",
"function getWhitelistedRoot(address account) view returns (address)",
])

// ABI for the UBISchemeV2 contract for essential functions and events
Expand All @@ -217,3 +240,26 @@ export const faucetABI = parseAbi([
export const g$ABI = parseAbi([
"function balanceOf(address account) view returns (uint256)",
])

/**
* Security messages shown to end-users before wallet-link actions.
* Wallet integrators can opt out by passing `skipSecurityMessage: true` in WalletLinkOptions.
*/
export const WALLET_LINK_SECURITY_MESSAGES = {
connect: [
"SECURITY NOTICE — Connect Wallet",
"You are about to link a secondary wallet to your GoodDollar identity.",
"• Only connect wallets you own and control.",
"• All connected wallets share a single daily UBI claim.",
"• The whitelisted root identity is the only account that can connect a wallet.",
"Do not proceed if you did not initiate this action.",
].join("\n"),

disconnect: [
"SECURITY NOTICE — Disconnect Wallet",
"You are about to remove a wallet from your GoodDollar identity.",
"• Once disconnected the wallet no longer shares your UBI claim.",
"• Either the root identity or the connected account itself can disconnect.",
"Do not proceed if you did not initiate this action.",
].join("\n"),
} as const
10 changes: 10 additions & 0 deletions packages/citizen-sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
export * from "./sdks"
export * from "./constants"
export * from "./utils/triggerFaucet"


export type {
IdentityExpiry,
IdentityExpiryData,
IdentityContract,
WalletLinkOptions,
ConnectedAccountStatus,
ChainConnectedStatus
} from "./types"
Loading
Loading