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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Common tasks:
- `apps/demo-identity-app` – React demo covering identity + claim flows
- `apps/engagement-app` – engagement rewards showcase
- `apps/demo-webcomponents` – Vite playground for Lit components
- `apps/demo-savings-widget` – Vite demo to test the savings widget with an injected wallet

Each app documents its development server and environment requirements inside its own README.

Expand Down
50 changes: 50 additions & 0 deletions apps/demo-savings-widget/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GoodDollar Savings Widget Demo</title>
</head>
<body
style="
margin: 0;
background: #f3f4f6;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
"
>
<main
style="
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
padding: 24px;
"
>
<div style="display: flex; flex-direction: column; gap: 8px; align-items: center">
<h1 style="margin: 0; color: #111827">Savings Widget Demo</h1>
<p id="walletStatus" style="margin: 0; color: #4b5563">Wallet not connected</p>
<button
id="connectWalletButton"
style="
border: none;
border-radius: 10px;
padding: 10px 14px;
color: #fff;
background: #00b0ff;
cursor: pointer;
font-size: 15px;
font-weight: 600;
"
>
Connect Wallet
</button>
</div>

<gooddollar-savings-widget id="savingsWidget"></gooddollar-savings-widget>
</main>
<script type="module" src="/src/index.js"></script>
</body>
</html>
17 changes: 17 additions & 0 deletions apps/demo-savings-widget/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "demo-savings-widget",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --port 3001",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@goodsdks/savings-widget": "workspace:*"
},
"devDependencies": {
"vite": "6.3.5"
}
}
58 changes: 58 additions & 0 deletions apps/demo-savings-widget/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import "@goodsdks/savings-widget"

const savingsWidget = document.getElementById("savingsWidget")
const walletStatus = document.getElementById("walletStatus")
const connectWalletButton = document.getElementById("connectWalletButton")

const getProvider = () => window.ethereum ?? null

const updateWalletStatus = async () => {
const provider = getProvider()
if (!provider?.request) {
walletStatus.textContent = "No injected wallet found (install a browser wallet like MetaMask)"
connectWalletButton.disabled = true
connectWalletButton.style.opacity = "0.6"
return
}

const accounts = await provider.request({ method: "eth_accounts" })
if (accounts?.length) {
const [account] = accounts
walletStatus.textContent = `Connected: ${account.slice(0, 6)}...${account.slice(-4)}`
savingsWidget.web3Provider = provider
} else {
walletStatus.textContent = "Wallet not connected"
savingsWidget.web3Provider = null
}
Comment on lines +18 to +26

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: eth_accounts call in the demo is unguarded and can throw, which would leave the UI in an inconsistent state.

In updateWalletStatus, if provider.request({ method: "eth_accounts" }) throws (e.g. provider error), the exception will bubble up and prevent the status text from updating. Since this is example code for integrators, consider wrapping the call in try/catch and setting a clear error message so the demo stays in a consistent state even when the injected wallet misbehaves.

Suggested change
const accounts = await provider.request({ method: "eth_accounts" })
if (accounts?.length) {
const [account] = accounts
walletStatus.textContent = `Connected: ${account.slice(0, 6)}...${account.slice(-4)}`
savingsWidget.web3Provider = provider
} else {
walletStatus.textContent = "Wallet not connected"
savingsWidget.web3Provider = null
}
try {
const accounts = await provider.request({ method: "eth_accounts" })
if (accounts?.length) {
const [account] = accounts
walletStatus.textContent = `Connected: ${account.slice(0, 6)}...${account.slice(-4)}`
savingsWidget.web3Provider = provider
} else {
walletStatus.textContent = "Wallet not connected"
savingsWidget.web3Provider = null
}
} catch (error) {
console.error("Failed to read accounts from injected wallet", error)
walletStatus.textContent = "Error reading accounts from wallet. Please check your browser wallet and try again."
savingsWidget.web3Provider = null
}

}

const connectWallet = async () => {
const provider = getProvider()
if (!provider?.request) {
walletStatus.textContent = "No injected wallet found (install a browser wallet like MetaMask)"
return
}

try {
await provider.request({ method: "eth_requestAccounts" })
await updateWalletStatus()
} catch (error) {
console.error("Wallet connection failed:", error)
walletStatus.textContent = "Wallet connection was rejected"
}
}

customElements.whenDefined("gooddollar-savings-widget").then(async () => {
savingsWidget.connectWallet = connectWallet
connectWalletButton.addEventListener("click", connectWallet)

const provider = getProvider()
provider?.on?.("accountsChanged", () => {
updateWalletStatus().catch(console.error)
})
provider?.on?.("chainChanged", () => {
updateWalletStatus().catch(console.error)
})

await updateWalletStatus()
})
100 changes: 100 additions & 0 deletions packages/savings-sdk/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { celo, xdc, type Chain } from "viem/chains"

/**
* Chains where the GoodDollar Savings (G$ Staking) flow is supported.
*
* The widget and SDK validate that both the public and wallet clients are
* connected to one of these chains. Anything else is treated as a "wrong
* network" error so callers can prompt the user to switch.
*/
export enum SupportedChainId {
CELO = 42220,
XDC = 50,
}

export const SUPPORTED_CHAIN_IDS: SupportedChainId[] = [
SupportedChainId.CELO,
SupportedChainId.XDC,
]

export const isSupportedChainId = (
chainId: number | undefined,
): chainId is SupportedChainId =>
typeof chainId === "number" &&
SUPPORTED_CHAIN_IDS.includes(chainId as SupportedChainId)

export interface SavingsContracts {
/** GoodDollarStaking proxy used to stake G$ and claim rewards. */
staking: `0x${string}`
/** G$ ERC-20 (or SuperGoodDollar) token contract. */
gdollar: `0x${string}`
/**
* Superfluid Host. Required on streaming chains; the SDK bundles
* approve / pool connect / stake calls through `host.batchCall`.
*/
superfluidHost?: `0x${string}`
/**
* Superfluid GDA Forwarder. Required on streaming chains to connect
* the user to the GDA pool that distributes streaming rewards.
*/
gdaForwarder?: `0x${string}`
}

export interface SavingsChainConfig {
id: SupportedChainId
label: string
chain: Chain
isStreaming: boolean // whether this chain uses Superfluid streaming rewards
contracts: SavingsContracts
}

// Default contract addresses per supported chain.
export const SAVINGS_CHAIN_CONFIG: Record<
SupportedChainId,
SavingsChainConfig
> = {
[SupportedChainId.CELO]: {
id: SupportedChainId.CELO,
label: "Celo",
chain: celo,
isStreaming: true,
contracts: {
staking: "0x059ee811414230d1Fb157878D2b491240F4D8d3B",
gdollar: "0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A",
superfluidHost: "0xA4Ff07cF81C02CFD356184879D953970cA957585",
gdaForwarder: "0x308b7405272d11494716e30C6E972DbF6fb89555",
},
},
[SupportedChainId.XDC]: {
id: SupportedChainId.XDC,
label: "XDC",
chain: xdc,
isStreaming: false,
contracts: {
staking: "0x61a1Da2a81FbaE6b1B3A45D94355A6A5c5973A52",
gdollar: "0xEC2136843a983885AebF2feB3931F73A8eBEe50c",
},
},
}

export function getSavingsChainConfig(chainId: number): SavingsChainConfig | undefined {
if (!isSupportedChainId(chainId)) {
return undefined
}
return SAVINGS_CHAIN_CONFIG[chainId]
}

/**
* Error thrown when the SDK is initialised with a client connected to a chain
* that is not in {@link SUPPORTED_CHAIN_IDS}.
*/
export class UnsupportedChainError extends Error {
readonly chainId: number | undefined
constructor(chainId: number | undefined) {
super(
`Unsupported chain ${chainId ?? "<unknown>"}.`,
)
this.name = "UnsupportedChainError"
this.chainId = chainId
}
}
6 changes: 3 additions & 3 deletions packages/savings-sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from "./viem-sdk";
export { useGooddollarSavings } from "./wagmi-sdk";
export type { GlobalStats, UserStats } from "./viem-sdk";
export * from "./viem-sdk"
export * from "./constants"
export { useGooddollarSavings } from "./wagmi-sdk"
Loading