@gatedpath/frontend-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/dist/abi.d.ts +98 -0
- package/dist/abi.js +79 -0
- package/dist/components.d.ts +70 -0
- package/dist/components.js +85 -0
- package/dist/dev/DevStatePanel.d.ts +19 -0
- package/dist/dev/DevStatePanel.js +123 -0
- package/dist/dev/anvilConnector.d.ts +25 -0
- package/dist/dev/anvilConnector.js +78 -0
- package/dist/dev/index.d.ts +6 -0
- package/dist/dev/index.js +6 -0
- package/dist/dev/localDeployment.d.ts +37 -0
- package/dist/dev/localDeployment.js +41 -0
- package/dist/dev.css +37 -0
- package/dist/events.d.ts +7 -0
- package/dist/events.js +13 -0
- package/dist/expiry.d.ts +12 -0
- package/dist/expiry.js +19 -0
- package/dist/explainError.d.ts +34 -0
- package/dist/explainError.js +113 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +7 -0
- package/dist/styles.css +100 -0
- package/dist/urls.d.ts +8 -0
- package/dist/urls.js +10 -0
- package/dist/useEligibility.d.ts +58 -0
- package/dist/useEligibility.js +143 -0
- package/docs/wallet-gas-warning.md +44 -0
- package/package.json +67 -0
package/dist/styles.css
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/* @gatedpath/frontend-kit default style.
|
|
2
|
+
Every colour, radius and duration comes from the site's design tokens (site/design/tokens.css,
|
|
3
|
+
12 September 2026) through var() with that token's light value as the fallback, so the kit
|
|
4
|
+
looks right on its own and picks up the site's dark theme when tokens.css is loaded. Load this
|
|
5
|
+
file or don't: the components only carry class names. */
|
|
6
|
+
|
|
7
|
+
.rb-elig {
|
|
8
|
+
font-family: var(--sl-font, 'Readex Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
|
9
|
+
color: var(--rb-color-text, #283A46);
|
|
10
|
+
background: var(--rb-color-surface, #EFF3F6);
|
|
11
|
+
border: 1px solid var(--rb-color-hairline, #DFE7EC);
|
|
12
|
+
border-left: 4px solid var(--rb-color-hairline-strong, #848B91);
|
|
13
|
+
border-radius: var(--rb-radius-lg, 10px);
|
|
14
|
+
padding: var(--rb-space-4, 1rem) var(--rb-space-5, 1.25rem);
|
|
15
|
+
margin: var(--rb-space-4, 1rem) 0;
|
|
16
|
+
line-height: var(--sl-line-height, 1.6);
|
|
17
|
+
transition: border-color var(--rb-duration-base, 250ms) var(--rb-ease-standard, ease);
|
|
18
|
+
}
|
|
19
|
+
.rb-elig-title {
|
|
20
|
+
margin: 0 0 var(--rb-space-2, 0.5rem);
|
|
21
|
+
font-weight: var(--rb-weight-strong, 600);
|
|
22
|
+
color: var(--rb-color-heading, #283A46);
|
|
23
|
+
}
|
|
24
|
+
.rb-elig-body { margin: 0 0 var(--rb-space-2, 0.5rem); }
|
|
25
|
+
.rb-elig-muted, .rb-elig-last-checked, .rb-elig-gas {
|
|
26
|
+
color: var(--rb-color-text-muted, #4C6B80);
|
|
27
|
+
font-size: var(--sl-text-sm, 0.875rem);
|
|
28
|
+
margin: var(--rb-space-2, 0.5rem) 0 0;
|
|
29
|
+
}
|
|
30
|
+
.rb-elig-steps { margin: 0 0 var(--rb-space-2, 0.5rem); padding-left: 1.25rem; }
|
|
31
|
+
.rb-elig-steps li { margin: var(--rb-space-1, 0.25rem) 0; }
|
|
32
|
+
.rb-elig a { color: var(--rb-color-accent, #0F766E); }
|
|
33
|
+
.rb-elig a:hover { color: var(--rb-color-accent-high, #115E59); }
|
|
34
|
+
.rb-elig code {
|
|
35
|
+
font-family: var(--sl-font-mono, 'JetBrains Mono', ui-monospace, Menlo, monospace);
|
|
36
|
+
font-size: var(--sl-text-code, 0.875rem);
|
|
37
|
+
background: var(--rb-color-code-bg, #F4F4F4);
|
|
38
|
+
color: var(--rb-color-code-text, #283A46);
|
|
39
|
+
padding: 0 0.3em;
|
|
40
|
+
border-radius: var(--rb-radius-sm, 3px);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* One accent per state, all from the token file's status colours. */
|
|
44
|
+
.rb-elig-disconnected { border-left-color: var(--rb-color-hairline-strong, #848B91); }
|
|
45
|
+
.rb-elig-checking { border-left-color: var(--rb-color-hairline-strong, #848B91); opacity: 0.85; }
|
|
46
|
+
.rb-elig-unverified {
|
|
47
|
+
border-left-color: var(--rb-color-caution-border, #B45309);
|
|
48
|
+
background: var(--rb-color-caution-bg, #FEF3C7);
|
|
49
|
+
color: var(--rb-color-caution-text, #78350F);
|
|
50
|
+
}
|
|
51
|
+
.rb-elig-unverified .rb-elig-title, .rb-elig-unverified .rb-elig-muted { color: inherit; }
|
|
52
|
+
.rb-elig-ineligible {
|
|
53
|
+
border-left-color: var(--rb-color-danger-border, #B91C1C);
|
|
54
|
+
background: var(--rb-color-danger-bg, #FEE2E2);
|
|
55
|
+
color: var(--rb-color-danger-text, #991B1B);
|
|
56
|
+
}
|
|
57
|
+
.rb-elig-ineligible .rb-elig-title, .rb-elig-ineligible .rb-elig-muted { color: inherit; }
|
|
58
|
+
.rb-elig-eligible {
|
|
59
|
+
border-left-color: var(--rb-color-tip-border, #0F766E);
|
|
60
|
+
background: var(--rb-color-tip-bg, #CCFBF1);
|
|
61
|
+
color: var(--rb-color-tip-text, #134E4A);
|
|
62
|
+
}
|
|
63
|
+
.rb-elig-eligible .rb-elig-title { color: inherit; }
|
|
64
|
+
.rb-elig-expiring {
|
|
65
|
+
border-left-color: var(--rb-color-caution-border, #B45309);
|
|
66
|
+
background: var(--rb-color-caution-bg, #FEF3C7);
|
|
67
|
+
color: var(--rb-color-caution-text, #78350F);
|
|
68
|
+
}
|
|
69
|
+
.rb-elig-expiring .rb-elig-title { color: inherit; }
|
|
70
|
+
.rb-elig-error {
|
|
71
|
+
border-left-color: var(--rb-color-danger-border, #B91C1C);
|
|
72
|
+
background: var(--rb-color-danger-bg, #FEE2E2);
|
|
73
|
+
color: var(--rb-color-danger-text, #991B1B);
|
|
74
|
+
}
|
|
75
|
+
.rb-elig-error .rb-elig-title { color: inherit; }
|
|
76
|
+
|
|
77
|
+
.rb-elig-button {
|
|
78
|
+
font: inherit;
|
|
79
|
+
font-size: var(--sl-text-sm, 0.875rem);
|
|
80
|
+
font-weight: var(--rb-weight-button, 600);
|
|
81
|
+
min-height: var(--rb-target-min, 44px);
|
|
82
|
+
padding: 0 var(--rb-space-4, 1rem);
|
|
83
|
+
border-radius: var(--rb-radius-sm, 3px);
|
|
84
|
+
border: 1px solid var(--rb-color-accent, #0F766E);
|
|
85
|
+
background: var(--rb-color-accent, #0F766E);
|
|
86
|
+
color: var(--rb-color-accent-text-on, #FFFFFF);
|
|
87
|
+
cursor: pointer;
|
|
88
|
+
transition: background var(--rb-duration-fast, 150ms) var(--rb-ease-standard, ease);
|
|
89
|
+
}
|
|
90
|
+
.rb-elig-button:hover { background: var(--rb-color-accent-high, #115E59); border-color: var(--rb-color-accent-high, #115E59); }
|
|
91
|
+
.rb-elig-button:focus-visible {
|
|
92
|
+
outline: var(--rb-focus-width, 2px) solid var(--rb-color-focus, #0F766E);
|
|
93
|
+
outline-offset: var(--rb-focus-offset, 2px);
|
|
94
|
+
}
|
|
95
|
+
.rb-elig-button:disabled { opacity: 0.5; cursor: default; }
|
|
96
|
+
.rb-elig-button-small { min-height: 32px; margin-left: var(--rb-space-2, 0.5rem); padding: 0 var(--rb-space-3, 0.75rem); }
|
|
97
|
+
|
|
98
|
+
@media (prefers-reduced-motion: reduce) {
|
|
99
|
+
.rb-elig, .rb-elig-button { transition: none; }
|
|
100
|
+
}
|
package/dist/urls.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Where a wallet gets verified. */
|
|
2
|
+
export declare const ACCESS_URL = "https://access.redbelly.network";
|
|
3
|
+
/** Testnet RBNT, with a Discord login (RESEARCH.md question 12). */
|
|
4
|
+
export declare const FAUCET_URL = "https://redbelly.faucetme.pro/";
|
|
5
|
+
/** Vine's page with the wallet gas warning, quoted in docs/wallet-gas-warning.md. */
|
|
6
|
+
export declare const VINE_INTERACTION_URL = "https://vine.redbelly.network/smart-contracts/interaction/";
|
|
7
|
+
/** Vine's page on how gas is priced in US dollars. */
|
|
8
|
+
export declare const VINE_FEES_URL = "https://vine.redbelly.network/network-fees/";
|
package/dist/urls.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Redbelly's own pages. The kit links to them and never restates what they say.
|
|
2
|
+
// Every URL was read on 12 September 2026 (RESEARCH.md source log).
|
|
3
|
+
/** Where a wallet gets verified. */
|
|
4
|
+
export const ACCESS_URL = 'https://access.redbelly.network';
|
|
5
|
+
/** Testnet RBNT, with a Discord login (RESEARCH.md question 12). */
|
|
6
|
+
export const FAUCET_URL = 'https://redbelly.faucetme.pro/';
|
|
7
|
+
/** Vine's page with the wallet gas warning, quoted in docs/wallet-gas-warning.md. */
|
|
8
|
+
export const VINE_INTERACTION_URL = 'https://vine.redbelly.network/smart-contracts/interaction/';
|
|
9
|
+
/** Vine's page on how gas is priced in US dollars. */
|
|
10
|
+
export const VINE_FEES_URL = 'https://vine.redbelly.network/network-fees/';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type Address = `0x${string}`;
|
|
2
|
+
/** The five UI states, plus the two every real page also has. */
|
|
3
|
+
export type EligibilityState = 'disconnected' | 'checking' | 'unverified' | 'ineligible' | 'eligible' | 'expiring' | 'error';
|
|
4
|
+
export interface UseEligibilityOptions {
|
|
5
|
+
/** Chain to read on. Defaults to wagmi's current chain. */
|
|
6
|
+
chainId?: number;
|
|
7
|
+
/** The network's permission contract. Defaults to the address in @gatedpath/chains for `chainId`. */
|
|
8
|
+
permission?: Address;
|
|
9
|
+
/** Refetch floor in milliseconds; the reads also refetch on every new block. Default 30 000. */
|
|
10
|
+
refreshInterval?: number;
|
|
11
|
+
/** Watch `eth_blockNumber` and refetch when it changes. Default true. */
|
|
12
|
+
watchBlocks?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* When the verifier can tell: a function that returns the credential's expiry as a Unix time in
|
|
15
|
+
* seconds, or null when unknown. `ReceptorMock` can; adapters over real verifiers cannot.
|
|
16
|
+
* See `receptorMockExpiry`.
|
|
17
|
+
*/
|
|
18
|
+
expiry?: (args: {
|
|
19
|
+
address: Address;
|
|
20
|
+
requestId: bigint;
|
|
21
|
+
}) => Promise<number | bigint | null>;
|
|
22
|
+
/** How long before expiry the state turns to `expiring`. Default seven days. */
|
|
23
|
+
expiringWindowSeconds?: number;
|
|
24
|
+
/** Turn every read off (for example while a modal is closed). Default true. */
|
|
25
|
+
enabled?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface EligibilityResult {
|
|
28
|
+
state: EligibilityState;
|
|
29
|
+
/** `permission.isAllowed(address)`: the wallet has been through access.redbelly.network. */
|
|
30
|
+
isAllowed: boolean | undefined;
|
|
31
|
+
/** `verifier.isEligible(address, requestId)`: the wallet passes this action's gate. */
|
|
32
|
+
isEligible: boolean | undefined;
|
|
33
|
+
/** Unix seconds when the credential expires, when the verifier can tell. */
|
|
34
|
+
expiresAt: number | null;
|
|
35
|
+
/** Seconds until expiry, when known. */
|
|
36
|
+
secondsToExpiry: number | null;
|
|
37
|
+
/** When the eligibility read last returned, in milliseconds since the epoch. */
|
|
38
|
+
lastCheckedAt: number | null;
|
|
39
|
+
/** The block the last refetch was triggered by, when block watching is on. */
|
|
40
|
+
blockNumber: bigint | undefined;
|
|
41
|
+
isFetching: boolean;
|
|
42
|
+
error: Error | null;
|
|
43
|
+
/** Read both again now. */
|
|
44
|
+
refresh: () => Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Reads the two things a Redbelly UI has to know about a wallet before it offers a button: is
|
|
48
|
+
* the wallet allowed to transact at all (the network's gate), and does it pass this action's
|
|
49
|
+
* eligibility check (the dApp's verifier). Both are cached by wagmi's query client and shared by
|
|
50
|
+
* every component that asks for the same wallet, verifier and request id; they refetch once per
|
|
51
|
+
* new block, or every `refreshInterval` milliseconds if no block arrives (blocks on Redbelly are
|
|
52
|
+
* produced on demand, so a quiet chain has no blocks to watch), and on `refresh()`.
|
|
53
|
+
*
|
|
54
|
+
* @param address The connected wallet, or undefined when nothing is connected.
|
|
55
|
+
* @param verifier The dApp's verifier behind `IRedbellyVerifier`.
|
|
56
|
+
* @param requestId The request id the gated contract is bound to (`Gated.requestId()`).
|
|
57
|
+
*/
|
|
58
|
+
export declare function useEligibility(address: Address | undefined, verifier: Address | undefined, requestId: bigint | number | undefined, options?: UseEligibilityOptions): EligibilityResult;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
3
|
+
import { useQuery } from '@tanstack/react-query';
|
|
4
|
+
import { useBlockNumber, useChainId, useReadContract } from 'wagmi';
|
|
5
|
+
import { chainById } from '@gatedpath/chains';
|
|
6
|
+
import { permissionAbi, verifierAbi } from './abi.js';
|
|
7
|
+
import { onEligibilityChanged } from './events.js';
|
|
8
|
+
const SEVEN_DAYS = 7 * 24 * 60 * 60;
|
|
9
|
+
/**
|
|
10
|
+
* Reads the two things a Redbelly UI has to know about a wallet before it offers a button: is
|
|
11
|
+
* the wallet allowed to transact at all (the network's gate), and does it pass this action's
|
|
12
|
+
* eligibility check (the dApp's verifier). Both are cached by wagmi's query client and shared by
|
|
13
|
+
* every component that asks for the same wallet, verifier and request id; they refetch once per
|
|
14
|
+
* new block, or every `refreshInterval` milliseconds if no block arrives (blocks on Redbelly are
|
|
15
|
+
* produced on demand, so a quiet chain has no blocks to watch), and on `refresh()`.
|
|
16
|
+
*
|
|
17
|
+
* @param address The connected wallet, or undefined when nothing is connected.
|
|
18
|
+
* @param verifier The dApp's verifier behind `IRedbellyVerifier`.
|
|
19
|
+
* @param requestId The request id the gated contract is bound to (`Gated.requestId()`).
|
|
20
|
+
*/
|
|
21
|
+
export function useEligibility(address, verifier, requestId, options = {}) {
|
|
22
|
+
const currentChainId = useChainId();
|
|
23
|
+
const chainId = options.chainId ?? currentChainId;
|
|
24
|
+
const refreshInterval = options.refreshInterval ?? 30_000;
|
|
25
|
+
const enabled = (options.enabled ?? true) && Boolean(address);
|
|
26
|
+
const watchBlocks = options.watchBlocks ?? true;
|
|
27
|
+
const requestIdBig = requestId === undefined ? undefined : BigInt(requestId);
|
|
28
|
+
const permission = options.permission ?? chainById(chainId)?.contracts['permission']?.address;
|
|
29
|
+
const block = useBlockNumber({
|
|
30
|
+
chainId,
|
|
31
|
+
watch: watchBlocks && enabled,
|
|
32
|
+
query: { enabled: watchBlocks && enabled },
|
|
33
|
+
});
|
|
34
|
+
const allowed = useReadContract({
|
|
35
|
+
address: permission,
|
|
36
|
+
abi: permissionAbi,
|
|
37
|
+
functionName: 'isAllowed',
|
|
38
|
+
args: address ? [address] : undefined,
|
|
39
|
+
chainId,
|
|
40
|
+
query: {
|
|
41
|
+
enabled: enabled && Boolean(permission),
|
|
42
|
+
staleTime: refreshInterval,
|
|
43
|
+
refetchInterval: refreshInterval,
|
|
44
|
+
refetchOnWindowFocus: false,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
const eligible = useReadContract({
|
|
48
|
+
address: verifier,
|
|
49
|
+
abi: verifierAbi,
|
|
50
|
+
functionName: 'isEligible',
|
|
51
|
+
args: address && requestIdBig !== undefined ? [address, requestIdBig] : undefined,
|
|
52
|
+
chainId,
|
|
53
|
+
query: {
|
|
54
|
+
enabled: enabled && Boolean(verifier) && requestIdBig !== undefined,
|
|
55
|
+
staleTime: refreshInterval,
|
|
56
|
+
refetchInterval: refreshInterval,
|
|
57
|
+
refetchOnWindowFocus: false,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
const expiryFn = options.expiry;
|
|
61
|
+
const expiry = useQuery({
|
|
62
|
+
queryKey: ['redbelly-frontend-kit', 'expiry', chainId, address, verifier, requestIdBig?.toString()],
|
|
63
|
+
queryFn: async () => {
|
|
64
|
+
if (!expiryFn || !address || requestIdBig === undefined)
|
|
65
|
+
return null;
|
|
66
|
+
const value = await expiryFn({ address, requestId: requestIdBig });
|
|
67
|
+
return value === null || value === undefined ? null : Number(value);
|
|
68
|
+
},
|
|
69
|
+
enabled: enabled && Boolean(expiryFn) && eligible.data === true,
|
|
70
|
+
staleTime: refreshInterval,
|
|
71
|
+
refetchInterval: refreshInterval,
|
|
72
|
+
refetchOnWindowFocus: false,
|
|
73
|
+
});
|
|
74
|
+
// Refetch on a new block, and only on a new block: the block number itself is polled by wagmi.
|
|
75
|
+
const lastBlock = useRef(undefined);
|
|
76
|
+
const refetchAllowed = allowed.refetch;
|
|
77
|
+
const refetchEligible = eligible.refetch;
|
|
78
|
+
const refetchExpiry = expiry.refetch;
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!watchBlocks || !enabled || block.data === undefined)
|
|
81
|
+
return;
|
|
82
|
+
if (lastBlock.current === undefined) {
|
|
83
|
+
lastBlock.current = block.data;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (block.data !== lastBlock.current) {
|
|
87
|
+
lastBlock.current = block.data;
|
|
88
|
+
void refetchAllowed();
|
|
89
|
+
void refetchEligible();
|
|
90
|
+
if (expiryFn)
|
|
91
|
+
void refetchExpiry();
|
|
92
|
+
}
|
|
93
|
+
}, [block.data, watchBlocks, enabled, refetchAllowed, refetchEligible, refetchExpiry, expiryFn]);
|
|
94
|
+
const refresh = useCallback(async () => {
|
|
95
|
+
await Promise.all([refetchAllowed(), refetchEligible(), expiryFn ? refetchExpiry() : Promise.resolve()]);
|
|
96
|
+
}, [refetchAllowed, refetchEligible, refetchExpiry, expiryFn]);
|
|
97
|
+
// Something in this tab changed a wallet's eligibility (the dev state panel after `setStatus`
|
|
98
|
+
// on a local Anvil, or the app after its own transaction): read again now, for this wallet.
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!enabled || !address)
|
|
101
|
+
return;
|
|
102
|
+
return onEligibilityChanged((changed) => {
|
|
103
|
+
if (changed === undefined || changed.toLowerCase() === address.toLowerCase())
|
|
104
|
+
void refresh();
|
|
105
|
+
});
|
|
106
|
+
}, [enabled, address, refresh]);
|
|
107
|
+
const expiresAt = expiry.data ?? null;
|
|
108
|
+
const window = options.expiringWindowSeconds ?? SEVEN_DAYS;
|
|
109
|
+
return useMemo(() => {
|
|
110
|
+
const error = (allowed.error ?? eligible.error ?? expiry.error);
|
|
111
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
112
|
+
const secondsToExpiry = expiresAt === null || expiresAt === 0 ? null : expiresAt - nowSeconds;
|
|
113
|
+
let state;
|
|
114
|
+
if (!address)
|
|
115
|
+
state = 'disconnected';
|
|
116
|
+
else if (error && allowed.data === undefined && eligible.data === undefined)
|
|
117
|
+
state = 'error';
|
|
118
|
+
else if (allowed.data === undefined && eligible.data === undefined)
|
|
119
|
+
state = 'checking';
|
|
120
|
+
else if (allowed.data === false)
|
|
121
|
+
state = 'unverified';
|
|
122
|
+
else if (eligible.data === false)
|
|
123
|
+
state = 'ineligible';
|
|
124
|
+
else if (eligible.data === true && secondsToExpiry !== null && secondsToExpiry <= window)
|
|
125
|
+
state = 'expiring';
|
|
126
|
+
else if (eligible.data === true)
|
|
127
|
+
state = 'eligible';
|
|
128
|
+
else
|
|
129
|
+
state = 'checking';
|
|
130
|
+
return {
|
|
131
|
+
state,
|
|
132
|
+
isAllowed: allowed.data,
|
|
133
|
+
isEligible: eligible.data,
|
|
134
|
+
expiresAt: expiresAt === 0 ? null : expiresAt,
|
|
135
|
+
secondsToExpiry,
|
|
136
|
+
lastCheckedAt: eligible.dataUpdatedAt || null,
|
|
137
|
+
blockNumber: block.data,
|
|
138
|
+
isFetching: allowed.isFetching || eligible.isFetching,
|
|
139
|
+
error,
|
|
140
|
+
refresh,
|
|
141
|
+
};
|
|
142
|
+
}, [address, allowed.data, allowed.error, allowed.isFetching, eligible.data, eligible.error, eligible.isFetching, eligible.dataUpdatedAt, expiry.error, expiresAt, window, block.data, refresh]);
|
|
143
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# The wallet gas warning
|
|
2
|
+
|
|
3
|
+
Vine's page on interacting with smart contracts carries a warning that every Redbelly dApp
|
|
4
|
+
should repeat near its first transaction. Read on 12 September 2026 at
|
|
5
|
+
https://vine.redbelly.network/smart-contracts/interaction/ (RESEARCH.md question 13):
|
|
6
|
+
|
|
7
|
+
> for most wallets, gas related information may not be handled accurately
|
|
8
|
+
|
|
9
|
+
The same page steers dApps to WalletConnect. Here is why the warning exists and what the kit does
|
|
10
|
+
about it.
|
|
11
|
+
|
|
12
|
+
## Why wallets get it wrong here
|
|
13
|
+
|
|
14
|
+
Gas on Redbelly is priced in US dollars, not in a fee market. A native transfer costs US$0.01 at
|
|
15
|
+
21,000 gas; an on-chain price oracle publishes the USD/RBNT rate and nodes read it at block
|
|
16
|
+
execution, so the base fee in wei moves with the RBNT price and `eth_maxPriorityFeePerGas` is
|
|
17
|
+
zero (https://vine.redbelly.network/network-fees/, and measured, RESEARCH.md question 6). Wallets
|
|
18
|
+
built for Ethereum assume a fee market: they estimate a priority fee, show "low, medium, high"
|
|
19
|
+
tiers that mean nothing here, sometimes display the fee in a fiat conversion that uses their own
|
|
20
|
+
price source rather than the network's oracle, and may warn about "high gas" when the number is
|
|
21
|
+
just the RBNT price doing its job.
|
|
22
|
+
|
|
23
|
+
The consequences for a dApp:
|
|
24
|
+
|
|
25
|
+
- The fee your app shows should come from the chain, not the wallet. `gasCostUsd` in
|
|
26
|
+
`@gatedpath/chains` reads the base fee and the feed and returns RBNT and USD for a gas
|
|
27
|
+
amount. `<GasWarning transferUsd={...}>` takes that figure.
|
|
28
|
+
- Never set fee fields in the transaction request. Leave `maxFeePerGas` and
|
|
29
|
+
`maxPriorityFeePerGas` unset and let the client estimate; a hard-coded value copied from
|
|
30
|
+
Ethereum will be wrong in one direction or the other. The scaffolder's wagmi config leaves them
|
|
31
|
+
unset on purpose.
|
|
32
|
+
- There is nothing to bump. With no priority fee there is no "speed up" that does anything; a
|
|
33
|
+
transaction that is not in the next block produced is not coming (RESEARCH.md question 4, blocks
|
|
34
|
+
on demand). Tell the user to re-send rather than re-price.
|
|
35
|
+
- Show the warning once, near the first action, and link to Vine rather than restating the fee
|
|
36
|
+
model. `<GasWarning>` links to the interaction page and the network-fees page.
|
|
37
|
+
|
|
38
|
+
## What the component says
|
|
39
|
+
|
|
40
|
+
"Gas on Redbelly is priced in US dollars and paid in RBNT; a plain transfer costs about US$X right
|
|
41
|
+
now. Redbelly warns that most wallets show gas information wrongly on this network, so trust the
|
|
42
|
+
figure here over the wallet's", followed by the two links. The X is whatever you pass in
|
|
43
|
+
`transferUsd`; the scaffolder's page fills it from `gasCostUsd({ gasUsed: 21_000 })`. Without a
|
|
44
|
+
figure the sentence drops the number rather than printing a stale one.
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gatedpath/frontend-kit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React components and one hook for the five eligibility states every Redbelly dApp has: not connected, connected but not verified on the network, verified but ineligible for this action, eligible, credential expiring soon; plus explainError, the plain words for a viem or wagmi error from the shared failure table. wagmi + viem, headless with a small default style.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/tshirtdropper/redbelly-development-tool.git",
|
|
9
|
+
"directory": "packages/frontend-kit"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./dev": {
|
|
26
|
+
"types": "./dist/dev/index.d.ts",
|
|
27
|
+
"import": "./dist/dev/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./styles.css": "./dist/styles.css",
|
|
30
|
+
"./dev.css": "./dist/dev.css"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"docs"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc -p tsconfig.json && node -e \"require('fs').copyFileSync('src/styles.css','dist/styles.css');require('fs').copyFileSync('src/dev.css','dist/dev.css')\"",
|
|
39
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test:watch": "vitest",
|
|
42
|
+
"prepack": "npm run build"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@gatedpath/chains": "*",
|
|
46
|
+
"@tanstack/react-query": ">=5.0.0",
|
|
47
|
+
"react": ">=18.0.0",
|
|
48
|
+
"viem": ">=2.0.0",
|
|
49
|
+
"wagmi": ">=2.0.0",
|
|
50
|
+
"@gatedpath/agent-rules": "*"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@gatedpath/agent-rules": "file:../agent-rules",
|
|
54
|
+
"@gatedpath/chains": "file:../chain-definitions",
|
|
55
|
+
"@tanstack/react-query": "5.102.8",
|
|
56
|
+
"@testing-library/react": "16.3.3",
|
|
57
|
+
"@types/react": "19.2.3",
|
|
58
|
+
"@types/react-dom": "19.2.3",
|
|
59
|
+
"jsdom": "30.0.1",
|
|
60
|
+
"react": "19.3.0",
|
|
61
|
+
"react-dom": "19.3.0",
|
|
62
|
+
"typescript": "5.9.3",
|
|
63
|
+
"viem": "2.56.3",
|
|
64
|
+
"vitest": "5.0.0",
|
|
65
|
+
"wagmi": "3.7.7"
|
|
66
|
+
}
|
|
67
|
+
}
|