-
Staking Statistics
+
Staking Statistics (${activeChainName})
Total G$ Staked
@@ -420,8 +569,12 @@ export class GooddollarSavingsWidget extends LitElement {
- Your Weekly Rewards
- ${this.isLoading ? 'Loading...' : this.formatBigInt(this.userWeeklyRewards)}
+ Your Daily Rewards
+ ${this.isLoading ? 'Loading...' : this.formatBigInt(this.userDailyRewards)}
+
+
+ Your Total Rewards So Far
+ ${this.isLoading ? 'Loading...' : this.formatBigInt(this.streamedRewards)}
`
: ""
@@ -437,32 +590,142 @@ export class GooddollarSavingsWidget extends LitElement {
`;
}
+ private resolveActiveChainId(): number {
+ const supported = this.getSupportedChainsSafe();
+ if (
+ this.walletChainId !== null &&
+ supported.includes(this.walletChainId)
+ ) {
+ return this.walletChainId;
+ }
+ if (supported.includes(this.defaultChainId)) {
+ return this.defaultChainId;
+ }
+ return supported[0] ?? DEFAULT_CHAIN_ID;
+ }
+
+ private getSupportedChainsSafe(): number[] {
+ const list = (this.supportedChains ?? []).filter((id) =>
+ isSupportedChainId(Number(id)),
+ );
+ return list.length > 0 ? list.map(Number) : DEFAULT_SUPPORTED_CHAIN_IDS;
+ }
+
+ private getPublicClient(chainId: number): PublicClient {
+ const cached = this.publicClients.get(chainId);
+ if (cached) return cached;
+ const config = getSavingsChainConfig(chainId);
+ if (!config) {
+ throw new Error(`Unsupported chain id ${chainId}`);
+ }
+ const client = createPublicClient({
+ chain: config.chain,
+ transport: http(),
+ }) as unknown as PublicClient;
+ this.publicClients.set(chainId, client);
+ return client;
+ }
+
+ private getChainName(chainId: number): string {
+ return getSavingsChainConfig(chainId)?.label ?? `Chain ${chainId}`;
+ }
+
+ private buildWrongNetworkMessage(): string {
+ const supported = this.getSupportedChainsSafe();
+ if (
+ this.walletChainId !== null &&
+ !supported.includes(this.walletChainId)
+ ) {
+ const supportedNames = supported.map((id) => this.getChainName(id)).join(' or ');
+ return `Your wallet is on an unsupported network. Please switch to ${supportedNames}.`;
+ }
+ return `Your wallet network does not match the selected network (${this.getChainName(this.activeChainId)}).`;
+ }
+
private async refreshData() {
- if (!this.publicClient) {
- this.publicClient = createPublicClient({
- chain: celo,
- transport: http()
- }) as unknown as PublicClient;
+ const supported = this.getSupportedChainsSafe();
+ let walletChainId: number | null = null;
+ if (this.web3Provider?.request) {
+ try {
+ const chainIdHex = await this.web3Provider.request({ method: 'eth_chainId' });
+ walletChainId = parseInt(chainIdHex, 16);
+ } catch (error) {
+ console.error('Failed to read wallet chain id:', error);
+ walletChainId = null;
+ }
}
+ this.walletChainId = walletChainId;
+
+ const previousActive = this.activeChainId;
+ let nextActive: number;
+ if (walletChainId !== null && supported.includes(walletChainId)) {
+ nextActive = walletChainId;
+ } else if (supported.includes(this.defaultChainId)) {
+ nextActive = this.defaultChainId;
+ } else {
+ nextActive = supported[0] ?? DEFAULT_CHAIN_ID;
+ }
+
+ if (nextActive !== previousActive) {
+ this.activeChainId = nextActive;
+ this.sdk = null;
+ this.resetUserStats();
+ }
+
+ const activeConfig = getSavingsChainConfig(this.activeChainId);
+ if (!activeConfig) {
+ console.error(`No viem chain config for chain id ${this.activeChainId}`);
+ return;
+ }
+ const activeChain = activeConfig.chain;
+
+ const publicClient = this.getPublicClient(this.activeChainId);
+
+ const walletOnActiveChain =
+ !!this.web3Provider &&
+ this.web3Provider.isConnected &&
+ walletChainId === this.activeChainId;
- if (this.web3Provider && this.web3Provider.isConnected) {
+ if (walletOnActiveChain) {
this.walletClient = createWalletClient({
- chain: celo,
- transport: custom(this.web3Provider)
+ chain: activeChain,
+ transport: custom(this.web3Provider),
});
- this.sdk = new GooddollarSavingsSDK(this.publicClient!, this.walletClient);
- await this.loadStats();
-
- const accounts = await this.web3Provider.request({ method: 'eth_accounts' });
- if (accounts.length > 0) {
- this.userAddress = accounts[0];
- await this.loadUserStats();
- } else {
- this.resetUserStats();
+ try {
+ this.sdk = new GooddollarSavingsSDK(publicClient, this.walletClient);
+ } catch (error) {
+ console.error('Failed to initialize SDK with wallet:', error);
+ this.sdk = new GooddollarSavingsSDK(publicClient);
}
+
+ try {
+ const accounts = await this.web3Provider.request({ method: 'eth_accounts' });
+ if (accounts.length > 0) {
+ this.userAddress = accounts[0];
+ } else {
+ this.userAddress = null;
+ }
+ } catch (error) {
+ console.error('Failed to read accounts:', error);
+ this.userAddress = null;
+ }
+ } else {
+ this.walletClient = null;
+ this.userAddress = null;
+ try {
+ this.sdk = new GooddollarSavingsSDK(publicClient);
+ } catch (error) {
+ console.error('Failed to initialize SDK:', error);
+ this.sdk = null;
+ return;
+ }
+ }
+
+ await this.loadStats();
+ if (this.userAddress) {
+ await this.loadUserStats();
} else {
- this.sdk = new GooddollarSavingsSDK(this.publicClient);
- await this.loadStats();
+ this.resetUserStats();
}
}
@@ -472,6 +735,7 @@ export class GooddollarSavingsWidget extends LitElement {
const globalStats = await this.sdk.getGlobalStats();
this.totalStaked = globalStats.totalStaked;
this.annualAPR = globalStats.annualAPR;
+ this.isStreaming = globalStats.isStreaming;
} catch (error) {
console.error('Error loading global stats:', error);
}
@@ -483,8 +747,9 @@ export class GooddollarSavingsWidget extends LitElement {
const userStats = await this.sdk.getUserStats()
this.walletBalance = userStats.walletBalance;
this.currentStake = userStats.currentStake;
- this.unclaimedRewards = userStats.unclaimedRewards;
- this.userWeeklyRewards = userStats.userWeeklyRewards;
+ this.unclaimedRewards = userStats.unclaimedRewards ?? 0n;
+ this.streamedRewards = userStats.streamedRewards ?? 0n;
+ this.userDailyRewards = userStats.userDailyRewards;
} catch (error) {
console.error('Error loading user stats:', error);
}
@@ -493,7 +758,46 @@ export class GooddollarSavingsWidget extends LitElement {
this.walletBalance = 0n;
this.currentStake = 0n;
this.unclaimedRewards = 0n;
- this.userWeeklyRewards = 0n;
+ this.streamedRewards = 0n;
+ this.userDailyRewards = 0n;
+ }
+
+ private attachProviderListeners() {
+ if (!this.web3Provider?.on) return;
+ this.chainChangedHandler = (chainIdHex: string) => {
+ try {
+ this.walletChainId = parseInt(chainIdHex, 16);
+ } catch {
+ this.walletChainId = null;
+ }
+ this.refreshData().catch(console.error);
+ };
+ this.accountsChangedHandler = (accounts: string[]) => {
+ this.userAddress = accounts && accounts[0] ? accounts[0] : null;
+ this.refreshData().catch(console.error);
+ };
+ this.web3Provider.on('chainChanged', this.chainChangedHandler);
+ this.web3Provider.on('accountsChanged', this.accountsChangedHandler);
+ this.providerListenersAttachedTo = this.web3Provider;
+ }
+
+ private detachProviderListeners() {
+ const target = this.providerListenersAttachedTo;
+ if (!target?.removeListener) {
+ this.providerListenersAttachedTo = null;
+ this.chainChangedHandler = null;
+ this.accountsChangedHandler = null;
+ return;
+ }
+ if (this.chainChangedHandler) {
+ target.removeListener('chainChanged', this.chainChangedHandler);
+ }
+ if (this.accountsChangedHandler) {
+ target.removeListener('accountsChanged', this.accountsChangedHandler);
+ }
+ this.providerListenersAttachedTo = null;
+ this.chainChangedHandler = null;
+ this.accountsChangedHandler = null;
}
formatBigInt(num: bigint) {
@@ -533,7 +837,6 @@ export class GooddollarSavingsWidget extends LitElement {
return;
}
- // Check for invalid characters (only numbers and decimal point allowed)
const validInputRegex = /^[0-9]*\.?[0-9]*$/;
if (!validInputRegex.test(this.inputAmount)) {
this.inputError = 'Invalid value';
@@ -581,6 +884,9 @@ export class GooddollarSavingsWidget extends LitElement {
}
try {
+ const onActive = await this.ensureActiveNetwork();
+ if (!onActive) return;
+
this.txLoading = true;
this.transactionError = '';
const amount = parseEther(this.inputAmount);
@@ -593,7 +899,7 @@ export class GooddollarSavingsWidget extends LitElement {
}
} catch (error: any) {
console.error('Staking error:', error);
- this.transactionError = error.message || 'Staking failed';
+ this.transactionError = this.toUserErrorMessage(error, 'Staking failed');
} finally {
this.txLoading = false;
}
@@ -607,6 +913,9 @@ export class GooddollarSavingsWidget extends LitElement {
}
try {
+ const onActive = await this.ensureActiveNetwork();
+ if (!onActive) return;
+
this.txLoading = true;
this.transactionError = '';
const amount = parseEther(this.inputAmount);
@@ -619,7 +928,7 @@ export class GooddollarSavingsWidget extends LitElement {
}
} catch (error: any) {
console.error('Unstaking error:', error);
- this.transactionError = error.message || 'Unstaking failed';
+ this.transactionError = this.toUserErrorMessage(error, 'Unstaking failed');
} finally {
this.txLoading = false;
}
@@ -629,6 +938,9 @@ export class GooddollarSavingsWidget extends LitElement {
if (!this.sdk || !this.userAddress) return;
try {
+ const onActive = await this.ensureActiveNetwork();
+ if (!onActive) return;
+
this.isClaiming = true;
this.transactionError = '';
const receipt = await this.sdk.claimReward();
@@ -638,9 +950,81 @@ export class GooddollarSavingsWidget extends LitElement {
}
} catch (error: any) {
console.error('Claim error:', error);
- this.transactionError = error.message || 'Claim failed';
+ this.transactionError = this.toUserErrorMessage(error, 'Claim failed');
} finally {
this.isClaiming = false;
}
}
+
+ private async handleSwitchNetwork() {
+ const switched = await this.ensureActiveNetwork();
+ if (switched) {
+ await this.refreshData();
+ }
+ }
+
+ private async ensureActiveNetwork(): Promise
{
+ if (!this.web3Provider?.request) return true;
+
+ const chainIdHex = await this.web3Provider.request({ method: 'eth_chainId' });
+ const currentChainId = parseInt(chainIdHex, 16);
+ if (currentChainId === this.activeChainId) return true;
+
+ const targetHex = `0x${this.activeChainId.toString(16)}`;
+ try {
+ await this.web3Provider.request({
+ method: 'wallet_switchEthereumChain',
+ params: [{ chainId: targetHex }],
+ });
+ this.transactionError = '';
+ this.walletChainId = this.activeChainId;
+ return true;
+ } catch (error: any) {
+ this.transactionError = this.toUserErrorMessage(
+ error,
+ `Please switch your wallet to ${this.getChainName(this.activeChainId)}.`,
+ );
+ return false;
+ }
+ }
+
+ private toUserErrorMessage(error: unknown, fallback: string = 'Transaction failed') {
+ if (!error) return fallback;
+
+ const maybeError = error as {
+ shortMessage?: string;
+ message?: string;
+ cause?: { shortMessage?: string };
+ };
+ const shortMessage = maybeError.shortMessage || maybeError.cause?.shortMessage;
+ const message = shortMessage || maybeError.message || String(error);
+ const lines = message
+ .split('\n')
+ .map((line: string) => line.trim())
+ .filter(Boolean);
+ const firstLine = lines[0] || fallback;
+ const cleaned = firstLine
+ .split('Contract Call:')[0]
+ .split('Docs:')[0]
+ .split('Details:')[0]
+ .trim();
+
+ const lower = cleaned.toLowerCase();
+ if (lower.includes('user rejected') || lower.includes('rejected the request')) {
+ return 'Transaction rejected in wallet.';
+ }
+ if (lower.includes('insufficient funds')) {
+ return 'Insufficient funds to pay for gas.';
+ }
+ if (
+ lower.includes('wrong network') ||
+ lower.includes('chain mismatch') ||
+ lower.includes('switch your wallet') ||
+ lower.includes('unsupported chain')
+ ) {
+ return `Please switch to ${this.getChainName(this.activeChainId)} to continue.`;
+ }
+
+ return cleaned || fallback;
+ }
}
diff --git a/yarn.lock b/yarn.lock
index 4bbf83b..44de2eb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2228,7 +2228,7 @@ __metadata:
languageName: unknown
linkType: soft
-"@goodsdks/savings-widget@workspace:packages/savings-widget":
+"@goodsdks/savings-widget@workspace:*, @goodsdks/savings-widget@workspace:packages/savings-widget":
version: 0.0.0-use.local
resolution: "@goodsdks/savings-widget@workspace:packages/savings-widget"
dependencies:
@@ -10611,6 +10611,15 @@ __metadata:
languageName: unknown
linkType: soft
+"demo-savings-widget@workspace:apps/demo-savings-widget":
+ version: 0.0.0-use.local
+ resolution: "demo-savings-widget@workspace:apps/demo-savings-widget"
+ dependencies:
+ "@goodsdks/savings-widget": "workspace:*"
+ vite: "npm:6.3.5"
+ languageName: unknown
+ linkType: soft
+
"depd@npm:2.0.0":
version: 2.0.0
resolution: "depd@npm:2.0.0"