@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 gatedpath and contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# @gatedpath/frontend-kit
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
For agents
|
|
5
|
+
- One hook: `useEligibility(address, verifier, requestId, options?)`. It reads the network gate
|
|
6
|
+
(`permission.isAllowed`) and the dApp's gate (`verifier.isEligible`), caches both, refetches on
|
|
7
|
+
a new block or every 30 s, and exposes `refresh()`. Don't poll the RPC yourself around it.
|
|
8
|
+
- Five states, one component each, or `<EligibilityGate>` to pick for you: `disconnected`,
|
|
9
|
+
`unverified`, `ineligible`, `eligible`, `expiring` (plus `checking` and `error`). The gate is the
|
|
10
|
+
contract; these components decide what a button says, never whether a transaction may run.
|
|
11
|
+
- `expiring` only shows when the verifier can tell. `ReceptorMock` can (`receptorMockExpiry`);
|
|
12
|
+
real verifiers can't, so leave `expiry` unset on mainnet.
|
|
13
|
+
- Never suggest anvil's default accounts anywhere: they pass `isAllowed` on the real networks
|
|
14
|
+
(RESEARCH.md question 30). `test/hygiene.test.ts` fails the build if one appears.
|
|
15
|
+
- Show `<GasWarning>` near the first transaction; docs/wallet-gas-warning.md says why.
|
|
16
|
+
- `explainError(error)` turns a viem or wagmi error into the plain words from the shared failure
|
|
17
|
+
table (`@gatedpath/agent-rules/failures`): NotEligible (with the credential state when you pass
|
|
18
|
+
one), a wallet rejection, a wrong chain, not enough RBNT. Show `plainWords` and `fix.link`, never
|
|
19
|
+
`error.message` alone; `kind: "unknown"` carries the error's own words in `raw`.
|
|
20
|
+
- Build: `npm run build`. Tests: `npm test` (Vitest, jsdom, a mocked transport; no network).
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The frontend kit from `PLAN.md` section 5.6: React components for the states every Redbelly dApp
|
|
24
|
+
has and most will get wrong, and a hook that reads eligibility once and caches it so the UI does
|
|
25
|
+
not hammer the RPC. wagmi 3 and viem 2 as peers, headless with a small default style that takes
|
|
26
|
+
its colours, radii and type from the site's design tokens.
|
|
27
|
+
|
|
28
|
+
## The hook
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { useEligibility } from '@gatedpath/frontend-kit';
|
|
32
|
+
|
|
33
|
+
const eligibility = useEligibility(address, verifier, requestId, {
|
|
34
|
+
refreshInterval: 30_000, // floor; every new block also refetches
|
|
35
|
+
watchBlocks: true, // eth_blockNumber polled by wagmi, reads refetched only when it changes
|
|
36
|
+
expiry: receptorMockExpiry(publicClient, verifier), // testnet and forks only
|
|
37
|
+
expiringWindowSeconds: 7 * 86_400,
|
|
38
|
+
});
|
|
39
|
+
// eligibility.state, .isAllowed, .isEligible, .expiresAt, .secondsToExpiry,
|
|
40
|
+
// .lastCheckedAt, .blockNumber, .isFetching, .error, .refresh()
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Two reads, both `view`: the network's permission contract (address from
|
|
44
|
+
`@gatedpath/chains` for the current chain, or `options.permission`) and the dApp's verifier
|
|
45
|
+
behind `IRedbellyVerifier.isEligible(wallet, requestId)` from receptor-mock. wagmi batches them
|
|
46
|
+
through Multicall3, which is deployed on both networks, so one `eth_call` per refetch. Results are
|
|
47
|
+
cached by wagmi's query client under the wallet, verifier, request id and chain, so every
|
|
48
|
+
component asking the same question shares one read.
|
|
49
|
+
|
|
50
|
+
Why block-or-30-seconds: blocks on Redbelly are produced on demand (RESEARCH.md question 4). A
|
|
51
|
+
busy chain gives you a block every few seconds and the reads follow it; a quiet chain gives you
|
|
52
|
+
none, and then a credential that was revoked off-chain would never show until the next block, so
|
|
53
|
+
the interval is the floor. `refresh()` is for the moment after the user comes back from
|
|
54
|
+
access.redbelly.network.
|
|
55
|
+
|
|
56
|
+
State derivation, in order: no address is `disconnected`; nothing cached and an RPC error is
|
|
57
|
+
`error`; nothing cached yet is `checking`; `isAllowed` false is `unverified` whatever the verifier
|
|
58
|
+
says; `isEligible` false is `ineligible`; `isEligible` true with an expiry inside the window is
|
|
59
|
+
`expiring`; otherwise `eligible`.
|
|
60
|
+
|
|
61
|
+
## The components
|
|
62
|
+
|
|
63
|
+
| Component | State | Says |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `NotConnected` | `disconnected` | Connect a wallet; identity is per wallet here |
|
|
66
|
+
| `NotVerified` | `unverified` | The wallet is not allowed to transact; link to access.redbelly.network, the faucet on testnet, don't switch wallets |
|
|
67
|
+
| `Ineligible` | `ineligible` | Verified on the network but no accepted proof for this action; the answer is a plain yes or no |
|
|
68
|
+
| `Eligible` | `eligible` | Wraps your action |
|
|
69
|
+
| `ExpiringSoon` | `expiring` | Wraps your action and says when the credential expires |
|
|
70
|
+
| `Checking`, `EligibilityError` | `checking`, `error` | In flight; the RPC failed and nothing is cached, with a retry |
|
|
71
|
+
| `EligibilityGate` | any | Picks one of the above from the hook result; `render` overrides any state |
|
|
72
|
+
| `LastChecked` | | "Last checked at block N" with a refresh button |
|
|
73
|
+
| `GasWarning` | | Vine's wallet gas warning, with a live USD figure if you pass one |
|
|
74
|
+
|
|
75
|
+
Each renders plain elements with `rb-elig-*` class names. Import
|
|
76
|
+
`@gatedpath/frontend-kit/styles.css` for the default look, which reads the site's
|
|
77
|
+
`--rb-*` and `--sl-*` tokens with their light values as fallbacks, so it follows the site's dark
|
|
78
|
+
theme when `tokens.css` is loaded and looks the same on its own. Or skip the stylesheet and style
|
|
79
|
+
the class names.
|
|
80
|
+
|
|
81
|
+
## A page
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
const { address } = useConnection();
|
|
85
|
+
const eligibility = useEligibility(address, verifier, requestId);
|
|
86
|
+
return (
|
|
87
|
+
<>
|
|
88
|
+
<EligibilityGate eligibility={eligibility} address={address} chainName={chain.name} testnet={chain.testnet}>
|
|
89
|
+
<button onClick={subscribe}>Subscribe</button>
|
|
90
|
+
</EligibilityGate>
|
|
91
|
+
<LastChecked eligibility={eligibility} />
|
|
92
|
+
<GasWarning transferUsd={usd} />
|
|
93
|
+
</>
|
|
94
|
+
);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The scaffolder's `web/` app has this as a working page at `/eligibility`
|
|
98
|
+
(`templates/web/src/app/eligibility/page.tsx`): it reads the template contract's `verifier()` and
|
|
99
|
+
`requestId()`, uses `receptorMockExpiry` on testnet, and prices the gas figure from the on-chain
|
|
100
|
+
feed with `gasCostUsd` from `@gatedpath/chains`.
|
|
101
|
+
|
|
102
|
+
## The dev entry: `@gatedpath/frontend-kit/dev`
|
|
103
|
+
|
|
104
|
+
The local loop from PLAN.md 18.1. `DevStatePanel` lists the five wallets `npm run dev` seeded
|
|
105
|
+
in a scaffold, marks the connected one, and flips the connected wallet through NeverIssued,
|
|
106
|
+
Valid, Expired, Revoked and WrongJurisdiction by calling `setStatus` on the `ReceptorMock` as the
|
|
107
|
+
deployer through Anvil's impersonation RPC (`anvil_impersonateAccount`, then stop). It fires
|
|
108
|
+
`notifyEligibilityChanged(wallet)`, which every mounted `useEligibility` for that wallet listens
|
|
109
|
+
to, so the page moves into the new state without a reload or a new block. `anvilAccountConnector`
|
|
110
|
+
is a wagmi connector for one of Anvil's unlocked accounts: the node signs, the browser never holds
|
|
111
|
+
a key. Its style is `@gatedpath/frontend-kit/dev.css`, kept apart from `styles.css` so a
|
|
112
|
+
production bundle that loads the main stylesheet carries nothing of the panel. `parseLocalDeployment` reads `deployments/local.json` as `scripts/dev.mjs` writes it and
|
|
113
|
+
refuses anything that isn't chain 31337 with five wallets on accounts 2 or later.
|
|
114
|
+
|
|
115
|
+
The panel renders only on chain 31337. On 151 or 153 it renders nothing whatever it is given,
|
|
116
|
+
and the scaffold imports it only in the dev server behind a build-time constant, so a production
|
|
117
|
+
bundle carries none of it; the scaffolder's integration test builds one and checks.
|
|
118
|
+
|
|
119
|
+
## Tests
|
|
120
|
+
|
|
121
|
+
`npm test`: 80 Vitest tests in jsdom. `test/mock-chain.tsx` is a wagmi config over a `custom`
|
|
122
|
+
transport that answers `eth_chainId`, `eth_blockNumber`, `eth_call` and Multicall3 `aggregate3`
|
|
123
|
+
from an in-memory state the tests mutate; nothing leaves the process and no address in it exists
|
|
124
|
+
anywhere. The hook tests cover every state, that reads happen once and then only on a new block,
|
|
125
|
+
manual refresh without a block, shared reads, the expiry window, errors and `enabled: false`. The
|
|
126
|
+
component tests render every state, the gate's selection and override, the links (Access dApp,
|
|
127
|
+
faucet, Vine) and the retry and refresh buttons. `test/dev-panel.test.tsx` runs the dev panel
|
|
128
|
+
against a fake chain 31337 (and proves it renders nothing on 153 or 151), including the
|
|
129
|
+
impersonate, send, stop sequence and the refetch without a new block. `test/hygiene.test.ts` walks every file in the
|
|
130
|
+
package and fails on any of anvil's ten default accounts or any 64-hex value.
|
|
131
|
+
|
|
132
|
+
Not tested: a real wallet. Nothing here signs; the kit reads and renders.
|
|
133
|
+
|
|
134
|
+
## Not in v1
|
|
135
|
+
|
|
136
|
+
Storybook (optional in the brief, skipped). A connect button (wagmi's own connectors and the
|
|
137
|
+
scaffolder's `Connect` component do that). Anything that calls the Eligibility SDK: the kit tells
|
|
138
|
+
the user they are ineligible, and the SDK is the thing that can tell them why; the scaffolder's
|
|
139
|
+
`web/src/eligibility/` has the steps for adding it.
|
|
140
|
+
|
|
141
|
+
## explainError
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
import { explainError } from '@gatedpath/frontend-kit';
|
|
145
|
+
|
|
146
|
+
const write = useWriteContract();
|
|
147
|
+
// …
|
|
148
|
+
{write.error && <p>{explainError(write.error).plainWords}</p>}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`explainError(error, { eligibilityStatus? })` walks the error's `cause` chain and answers
|
|
152
|
+
`{ kind, plainWords, cause, fix: { command?, link? }, title?, docsPage?, notEligible?, selector?, raw }`
|
|
153
|
+
from the failure table in `@gatedpath/agent-rules/failures`, the same table the MCP server's
|
|
154
|
+
`explain_failure` and the site's `/errors` page read. It decodes a `ContractFunctionRevertedError`
|
|
155
|
+
by its revert data (a `NotEligible(wallet, requestId)` selector, `ZeroVerifier`, or an
|
|
156
|
+
`Error(string)` reason from the deploy script) or by the error name viem decoded, and recognises
|
|
157
|
+
`UserRejectedRequestError` (or an EIP-1193 code 4001), `ChainMismatchError`, `SwitchChainError` and
|
|
158
|
+
`InsufficientFundsError` by class wherever they sit in the chain. Pass `eligibilityStatus` when the
|
|
159
|
+
page already knows the state behind a `NotEligible` (a `ReceptorMock` can say; a real verifier
|
|
160
|
+
can't) and the entry for that state comes back. Anything else is `kind: "unknown"` with the
|
|
161
|
+
error's own message in `raw`, never a guess. Pure: no network, no React. The scaffold's web app
|
|
162
|
+
renders it in `web/src/components/FailureWords.tsx` under the gated button and the eligibility page.
|
package/dist/abi.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** `IRedbellyVerifier.isEligible(address, uint64)` from @gatedpath/receptor-mock. */
|
|
2
|
+
export declare const verifierAbi: readonly [{
|
|
3
|
+
readonly type: "function";
|
|
4
|
+
readonly name: "isEligible";
|
|
5
|
+
readonly stateMutability: "view";
|
|
6
|
+
readonly inputs: readonly [{
|
|
7
|
+
readonly name: "wallet";
|
|
8
|
+
readonly type: "address";
|
|
9
|
+
}, {
|
|
10
|
+
readonly name: "requestId";
|
|
11
|
+
readonly type: "uint64";
|
|
12
|
+
}];
|
|
13
|
+
readonly outputs: readonly [{
|
|
14
|
+
readonly name: "";
|
|
15
|
+
readonly type: "bool";
|
|
16
|
+
}];
|
|
17
|
+
}];
|
|
18
|
+
/** The network's permission contract: `isAllowed(address)` (RESEARCH.md question 7). */
|
|
19
|
+
export declare const permissionAbi: readonly [{
|
|
20
|
+
readonly type: "function";
|
|
21
|
+
readonly name: "isAllowed";
|
|
22
|
+
readonly stateMutability: "view";
|
|
23
|
+
readonly inputs: readonly [{
|
|
24
|
+
readonly name: "wallet";
|
|
25
|
+
readonly type: "address";
|
|
26
|
+
}];
|
|
27
|
+
readonly outputs: readonly [{
|
|
28
|
+
readonly name: "";
|
|
29
|
+
readonly type: "bool";
|
|
30
|
+
}];
|
|
31
|
+
}];
|
|
32
|
+
/**
|
|
33
|
+
* `ReceptorMock.recordFor(address, uint64)` from @gatedpath/receptor-mock. The mock is the
|
|
34
|
+
* only verifier in the kit that can say when a credential expires; real verifiers answer a plain
|
|
35
|
+
* boolean. Used by `receptorMockExpiry` for testnet and fork demos only.
|
|
36
|
+
*/
|
|
37
|
+
export declare const receptorMockAbi: readonly [{
|
|
38
|
+
readonly type: "function";
|
|
39
|
+
readonly name: "recordFor";
|
|
40
|
+
readonly stateMutability: "view";
|
|
41
|
+
readonly inputs: readonly [{
|
|
42
|
+
readonly name: "wallet";
|
|
43
|
+
readonly type: "address";
|
|
44
|
+
}, {
|
|
45
|
+
readonly name: "requestId";
|
|
46
|
+
readonly type: "uint64";
|
|
47
|
+
}];
|
|
48
|
+
readonly outputs: readonly [{
|
|
49
|
+
readonly name: "";
|
|
50
|
+
readonly type: "tuple";
|
|
51
|
+
readonly components: readonly [{
|
|
52
|
+
readonly name: "isSet";
|
|
53
|
+
readonly type: "bool";
|
|
54
|
+
}, {
|
|
55
|
+
readonly name: "status";
|
|
56
|
+
readonly type: "uint8";
|
|
57
|
+
}, {
|
|
58
|
+
readonly name: "expiresAt";
|
|
59
|
+
readonly type: "uint64";
|
|
60
|
+
}];
|
|
61
|
+
}];
|
|
62
|
+
}];
|
|
63
|
+
/**
|
|
64
|
+
* The two `ReceptorMock` functions the dev state panel uses on a local Anvil (chain 31337):
|
|
65
|
+
* `setStatus(wallet, requestId, status)` writes a state, `eligibilityStatus(wallet, requestId)`
|
|
66
|
+
* reads it back. Never called against 151 or 153; the panel refuses to render there.
|
|
67
|
+
*/
|
|
68
|
+
export declare const receptorMockStatusAbi: readonly [{
|
|
69
|
+
readonly type: "function";
|
|
70
|
+
readonly name: "setStatus";
|
|
71
|
+
readonly stateMutability: "nonpayable";
|
|
72
|
+
readonly inputs: readonly [{
|
|
73
|
+
readonly name: "wallet";
|
|
74
|
+
readonly type: "address";
|
|
75
|
+
}, {
|
|
76
|
+
readonly name: "requestId";
|
|
77
|
+
readonly type: "uint64";
|
|
78
|
+
}, {
|
|
79
|
+
readonly name: "status";
|
|
80
|
+
readonly type: "uint8";
|
|
81
|
+
}];
|
|
82
|
+
readonly outputs: readonly [];
|
|
83
|
+
}, {
|
|
84
|
+
readonly type: "function";
|
|
85
|
+
readonly name: "eligibilityStatus";
|
|
86
|
+
readonly stateMutability: "view";
|
|
87
|
+
readonly inputs: readonly [{
|
|
88
|
+
readonly name: "wallet";
|
|
89
|
+
readonly type: "address";
|
|
90
|
+
}, {
|
|
91
|
+
readonly name: "requestId";
|
|
92
|
+
readonly type: "uint64";
|
|
93
|
+
}];
|
|
94
|
+
readonly outputs: readonly [{
|
|
95
|
+
readonly name: "";
|
|
96
|
+
readonly type: "uint8";
|
|
97
|
+
}];
|
|
98
|
+
}];
|
package/dist/abi.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// The two reads the kit makes. Nothing else is called.
|
|
2
|
+
/** `IRedbellyVerifier.isEligible(address, uint64)` from @gatedpath/receptor-mock. */
|
|
3
|
+
export const verifierAbi = [
|
|
4
|
+
{
|
|
5
|
+
type: 'function',
|
|
6
|
+
name: 'isEligible',
|
|
7
|
+
stateMutability: 'view',
|
|
8
|
+
inputs: [
|
|
9
|
+
{ name: 'wallet', type: 'address' },
|
|
10
|
+
{ name: 'requestId', type: 'uint64' },
|
|
11
|
+
],
|
|
12
|
+
outputs: [{ name: '', type: 'bool' }],
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
/** The network's permission contract: `isAllowed(address)` (RESEARCH.md question 7). */
|
|
16
|
+
export const permissionAbi = [
|
|
17
|
+
{
|
|
18
|
+
type: 'function',
|
|
19
|
+
name: 'isAllowed',
|
|
20
|
+
stateMutability: 'view',
|
|
21
|
+
inputs: [{ name: 'wallet', type: 'address' }],
|
|
22
|
+
outputs: [{ name: '', type: 'bool' }],
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* `ReceptorMock.recordFor(address, uint64)` from @gatedpath/receptor-mock. The mock is the
|
|
27
|
+
* only verifier in the kit that can say when a credential expires; real verifiers answer a plain
|
|
28
|
+
* boolean. Used by `receptorMockExpiry` for testnet and fork demos only.
|
|
29
|
+
*/
|
|
30
|
+
export const receptorMockAbi = [
|
|
31
|
+
{
|
|
32
|
+
type: 'function',
|
|
33
|
+
name: 'recordFor',
|
|
34
|
+
stateMutability: 'view',
|
|
35
|
+
inputs: [
|
|
36
|
+
{ name: 'wallet', type: 'address' },
|
|
37
|
+
{ name: 'requestId', type: 'uint64' },
|
|
38
|
+
],
|
|
39
|
+
outputs: [
|
|
40
|
+
{
|
|
41
|
+
name: '',
|
|
42
|
+
type: 'tuple',
|
|
43
|
+
components: [
|
|
44
|
+
{ name: 'isSet', type: 'bool' },
|
|
45
|
+
{ name: 'status', type: 'uint8' },
|
|
46
|
+
{ name: 'expiresAt', type: 'uint64' },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
/**
|
|
53
|
+
* The two `ReceptorMock` functions the dev state panel uses on a local Anvil (chain 31337):
|
|
54
|
+
* `setStatus(wallet, requestId, status)` writes a state, `eligibilityStatus(wallet, requestId)`
|
|
55
|
+
* reads it back. Never called against 151 or 153; the panel refuses to render there.
|
|
56
|
+
*/
|
|
57
|
+
export const receptorMockStatusAbi = [
|
|
58
|
+
{
|
|
59
|
+
type: 'function',
|
|
60
|
+
name: 'setStatus',
|
|
61
|
+
stateMutability: 'nonpayable',
|
|
62
|
+
inputs: [
|
|
63
|
+
{ name: 'wallet', type: 'address' },
|
|
64
|
+
{ name: 'requestId', type: 'uint64' },
|
|
65
|
+
{ name: 'status', type: 'uint8' },
|
|
66
|
+
],
|
|
67
|
+
outputs: [],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
type: 'function',
|
|
71
|
+
name: 'eligibilityStatus',
|
|
72
|
+
stateMutability: 'view',
|
|
73
|
+
inputs: [
|
|
74
|
+
{ name: 'wallet', type: 'address' },
|
|
75
|
+
{ name: 'requestId', type: 'uint64' },
|
|
76
|
+
],
|
|
77
|
+
outputs: [{ name: '', type: 'uint8' }],
|
|
78
|
+
},
|
|
79
|
+
];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { Address, EligibilityResult, EligibilityState } from './useEligibility.js';
|
|
3
|
+
export interface StateProps {
|
|
4
|
+
className?: string;
|
|
5
|
+
children?: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
/** State 1: nothing is connected. */
|
|
8
|
+
export declare function NotConnected({ className, children }: StateProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export interface NotVerifiedProps extends StateProps {
|
|
10
|
+
address: Address;
|
|
11
|
+
/** Human name of the chain, for the sentence. */
|
|
12
|
+
chainName?: string;
|
|
13
|
+
/** True on chain 153: shows the faucet link. */
|
|
14
|
+
testnet?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** State 2: connected, but `permission.isAllowed` is false. The wallet cannot transact at all. */
|
|
17
|
+
export declare function NotVerified({ address, chainName, testnet, className, children }: NotVerifiedProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export interface IneligibleProps extends StateProps {
|
|
19
|
+
/** What the action needs, in plain words: "an Australian proof of address", "an over-18 credential". */
|
|
20
|
+
requirement?: string;
|
|
21
|
+
}
|
|
22
|
+
/** State 3: verified on the network, but the verifier says no for this action. */
|
|
23
|
+
export declare function Ineligible({ requirement, className, children }: IneligibleProps): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
/** State 4: eligible. Usually wraps the action button. */
|
|
25
|
+
export declare function Eligible({ className, children }: StateProps): import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
export interface ExpiringSoonProps extends StateProps {
|
|
27
|
+
expiresAt: number;
|
|
28
|
+
secondsToExpiry: number;
|
|
29
|
+
}
|
|
30
|
+
/** State 5: eligible, and the verifier says the credential expires soon. Wraps the action too. */
|
|
31
|
+
export declare function ExpiringSoon({ expiresAt, secondsToExpiry, className, children }: ExpiringSoonProps): import("react/jsx-runtime").JSX.Element;
|
|
32
|
+
/** Shown while both reads are in flight for the first time. */
|
|
33
|
+
export declare function Checking({ className, children }: StateProps): import("react/jsx-runtime").JSX.Element;
|
|
34
|
+
export interface EligibilityErrorProps extends StateProps {
|
|
35
|
+
error: Error | null;
|
|
36
|
+
onRetry?: () => void;
|
|
37
|
+
}
|
|
38
|
+
/** An RPC or contract read failed and nothing is cached. Says so; does not guess. */
|
|
39
|
+
export declare function EligibilityError({ error, onRetry, className, children }: EligibilityErrorProps): import("react/jsx-runtime").JSX.Element;
|
|
40
|
+
export interface EligibilityGateProps {
|
|
41
|
+
/** The result of `useEligibility`. */
|
|
42
|
+
eligibility: EligibilityResult;
|
|
43
|
+
address: Address | undefined;
|
|
44
|
+
chainName?: string;
|
|
45
|
+
testnet?: boolean;
|
|
46
|
+
requirement?: string;
|
|
47
|
+
className?: string;
|
|
48
|
+
/** Rendered inside the eligible and expiring states: the action itself. */
|
|
49
|
+
children?: ReactNode;
|
|
50
|
+
/** Override any state with your own markup. A function receives the hook result. */
|
|
51
|
+
render?: Partial<Record<EligibilityState, ReactNode | ((r: EligibilityResult) => ReactNode)>>;
|
|
52
|
+
}
|
|
53
|
+
/** One component that picks the state component for you. Bring your own action as `children`. */
|
|
54
|
+
export declare function EligibilityGate({ eligibility, address, chainName, testnet, requirement, className, children, render }: EligibilityGateProps): import("react/jsx-runtime").JSX.Element;
|
|
55
|
+
export interface LastCheckedProps {
|
|
56
|
+
eligibility: EligibilityResult;
|
|
57
|
+
className?: string;
|
|
58
|
+
}
|
|
59
|
+
/** "Last checked" line with a refresh button. Put it under any gate. */
|
|
60
|
+
export declare function LastChecked({ eligibility, className }: LastCheckedProps): import("react/jsx-runtime").JSX.Element;
|
|
61
|
+
export interface GasWarningProps {
|
|
62
|
+
/** A live USD figure for a plain transfer, from `gasCostUsd` in @gatedpath/chains, if you have one. */
|
|
63
|
+
transferUsd?: string | null;
|
|
64
|
+
className?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The wallet gas warning from Vine, which every Redbelly dApp should show near its first
|
|
68
|
+
* transaction. See docs/wallet-gas-warning.md for the source and the reasoning.
|
|
69
|
+
*/
|
|
70
|
+
export declare function GasWarning({ transferUsd, className }: GasWarningProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { ACCESS_URL, FAUCET_URL, VINE_FEES_URL, VINE_INTERACTION_URL } from './urls.js';
|
|
4
|
+
function cx(...parts) {
|
|
5
|
+
return parts.filter(Boolean).join(' ');
|
|
6
|
+
}
|
|
7
|
+
function short(address) {
|
|
8
|
+
return `${address.slice(0, 6)}…${address.slice(-4)}`;
|
|
9
|
+
}
|
|
10
|
+
/** State 1: nothing is connected. */
|
|
11
|
+
export function NotConnected({ className, children }) {
|
|
12
|
+
return (_jsxs("div", { className: cx('rb-elig', 'rb-elig-disconnected', className), role: "status", children: [_jsx("p", { className: "rb-elig-title", children: "Connect a wallet" }), _jsx("p", { className: "rb-elig-body", children: children ?? 'Redbelly checks identity per wallet, so the app needs a wallet before it can tell you anything.' })] }));
|
|
13
|
+
}
|
|
14
|
+
/** State 2: connected, but `permission.isAllowed` is false. The wallet cannot transact at all. */
|
|
15
|
+
export function NotVerified({ address, chainName = 'this network', testnet = false, className, children }) {
|
|
16
|
+
return (_jsxs("div", { className: cx('rb-elig', 'rb-elig-unverified', className), role: "status", children: [_jsx("p", { className: "rb-elig-title", children: "Verify your wallet" }), _jsxs("p", { className: "rb-elig-body", children: [_jsx("code", { children: short(address) }), " is not yet allowed to transact on ", chainName, ". Redbelly gates every wallet behind a one-time identity check; this app never sees your documents, only a true or false from the network."] }), _jsxs("ol", { className: "rb-elig-steps", children: [_jsxs("li", { children: ["Open", ' ', _jsx("a", { href: ACCESS_URL, target: "_blank", rel: "noreferrer", children: ACCESS_URL }), ' ', "with this same wallet and complete verification."] }), testnet && (_jsxs("li", { children: ["Get testnet RBNT from", ' ', _jsx("a", { href: FAUCET_URL, target: "_blank", rel: "noreferrer", children: "FAUCETME" }), ' ', "so you can pay gas."] })), _jsx("li", { children: "Come back here. This page re-checks on every new block and moves on by itself." })] }), _jsx("p", { className: "rb-elig-muted", children: children ?? "Don't switch to another wallet to get past this: the credential belongs to the wallet that was verified." })] }));
|
|
17
|
+
}
|
|
18
|
+
/** State 3: verified on the network, but the verifier says no for this action. */
|
|
19
|
+
export function Ineligible({ requirement, className, children }) {
|
|
20
|
+
return (_jsxs("div", { className: cx('rb-elig', 'rb-elig-ineligible', className), role: "status", children: [_jsx("p", { className: "rb-elig-title", children: "Not eligible for this action" }), _jsx("p", { className: "rb-elig-body", children: children ??
|
|
21
|
+
`Your wallet is verified on the network, but it does not hold an accepted proof for this action${requirement ? ` (${requirement})` : ''}. The contract would refuse the transaction, so the button stays off.` }), _jsx("p", { className: "rb-elig-muted", children: "The check is on-chain and the answer is a plain yes or no; the app cannot tell whether a credential is missing, expired or revoked. Your identity wallet can." })] }));
|
|
22
|
+
}
|
|
23
|
+
/** State 4: eligible. Usually wraps the action button. */
|
|
24
|
+
export function Eligible({ className, children }) {
|
|
25
|
+
return (_jsxs("div", { className: cx('rb-elig', 'rb-elig-eligible', className), role: "status", children: [_jsx("p", { className: "rb-elig-title", children: "Eligible" }), children] }));
|
|
26
|
+
}
|
|
27
|
+
function humanDuration(seconds) {
|
|
28
|
+
if (seconds <= 0)
|
|
29
|
+
return 'now';
|
|
30
|
+
const days = Math.floor(seconds / 86_400);
|
|
31
|
+
if (days >= 2)
|
|
32
|
+
return `${days} days`;
|
|
33
|
+
const hours = Math.floor(seconds / 3600);
|
|
34
|
+
if (hours >= 2)
|
|
35
|
+
return `${hours} hours`;
|
|
36
|
+
const minutes = Math.max(1, Math.floor(seconds / 60));
|
|
37
|
+
return `${minutes} minutes`;
|
|
38
|
+
}
|
|
39
|
+
/** State 5: eligible, and the verifier says the credential expires soon. Wraps the action too. */
|
|
40
|
+
export function ExpiringSoon({ expiresAt, secondsToExpiry, className, children }) {
|
|
41
|
+
const when = new Date(expiresAt * 1000);
|
|
42
|
+
return (_jsxs("div", { className: cx('rb-elig', 'rb-elig-expiring', className), role: "status", children: [_jsxs("p", { className: "rb-elig-title", children: ["Eligible, credential expires in ", humanDuration(secondsToExpiry)] }), _jsxs("p", { className: "rb-elig-body", children: ["After ", _jsx("time", { dateTime: when.toISOString(), children: when.toUTCString() }), " this wallet will be refused until the credential is renewed. Renew it in your identity wallet before then; a transaction sent after expiry reverts."] }), children] }));
|
|
43
|
+
}
|
|
44
|
+
/** Shown while both reads are in flight for the first time. */
|
|
45
|
+
export function Checking({ className, children }) {
|
|
46
|
+
return (_jsx("div", { className: cx('rb-elig', 'rb-elig-checking', className), role: "status", "aria-busy": "true", children: _jsx("p", { className: "rb-elig-body", children: children ?? 'Checking the network and the verifier…' }) }));
|
|
47
|
+
}
|
|
48
|
+
/** An RPC or contract read failed and nothing is cached. Says so; does not guess. */
|
|
49
|
+
export function EligibilityError({ error, onRetry, className, children }) {
|
|
50
|
+
return (_jsxs("div", { className: cx('rb-elig', 'rb-elig-error', className), role: "alert", children: [_jsx("p", { className: "rb-elig-title", children: "Could not check eligibility" }), _jsx("p", { className: "rb-elig-body", children: children ?? (error?.message ?? 'The RPC did not answer.') }), onRetry && (_jsx("button", { type: "button", className: "rb-elig-button", onClick: onRetry, children: "Try again" }))] }));
|
|
51
|
+
}
|
|
52
|
+
/** One component that picks the state component for you. Bring your own action as `children`. */
|
|
53
|
+
export function EligibilityGate({ eligibility, address, chainName, testnet, requirement, className, children, render }) {
|
|
54
|
+
const custom = render?.[eligibility.state];
|
|
55
|
+
if (custom !== undefined)
|
|
56
|
+
return _jsx(_Fragment, { children: typeof custom === 'function' ? custom(eligibility) : custom });
|
|
57
|
+
switch (eligibility.state) {
|
|
58
|
+
case 'disconnected':
|
|
59
|
+
return _jsx(NotConnected, { className: className });
|
|
60
|
+
case 'checking':
|
|
61
|
+
return _jsx(Checking, { className: className });
|
|
62
|
+
case 'unverified':
|
|
63
|
+
return _jsx(NotVerified, { address: address, chainName: chainName, testnet: testnet, className: className });
|
|
64
|
+
case 'ineligible':
|
|
65
|
+
return _jsx(Ineligible, { requirement: requirement, className: className });
|
|
66
|
+
case 'expiring':
|
|
67
|
+
return (_jsx(ExpiringSoon, { expiresAt: eligibility.expiresAt, secondsToExpiry: eligibility.secondsToExpiry, className: className, children: children }));
|
|
68
|
+
case 'eligible':
|
|
69
|
+
return _jsx(Eligible, { className: className, children: children });
|
|
70
|
+
case 'error':
|
|
71
|
+
return _jsx(EligibilityError, { error: eligibility.error, onRetry: () => void eligibility.refresh(), className: className });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** "Last checked" line with a refresh button. Put it under any gate. */
|
|
75
|
+
export function LastChecked({ eligibility, className }) {
|
|
76
|
+
const at = eligibility.lastCheckedAt;
|
|
77
|
+
return (_jsxs("p", { className: cx('rb-elig-last-checked', className), children: [at ? (_jsxs(_Fragment, { children: ["Last checked ", _jsx("time", { dateTime: new Date(at).toISOString(), children: new Date(at).toLocaleTimeString() }), eligibility.blockNumber !== undefined ? ` at block ${eligibility.blockNumber.toString()}` : ''] })) : ('Not checked yet'), _jsx("button", { type: "button", className: "rb-elig-button rb-elig-button-small", onClick: () => void eligibility.refresh(), disabled: eligibility.isFetching, children: eligibility.isFetching ? 'Checking…' : 'Refresh' })] }));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The wallet gas warning from Vine, which every Redbelly dApp should show near its first
|
|
81
|
+
* transaction. See docs/wallet-gas-warning.md for the source and the reasoning.
|
|
82
|
+
*/
|
|
83
|
+
export function GasWarning({ transferUsd, className }) {
|
|
84
|
+
return (_jsxs("p", { className: cx('rb-elig-gas', className), children: ["Gas on Redbelly is priced in US dollars and paid in RBNT", transferUsd ? `; a plain transfer costs about US$${transferUsd} right now` : '', ". Redbelly warns that most wallets show gas information wrongly on this network, so trust the figure here over the wallet's, and read", ' ', _jsx("a", { href: VINE_INTERACTION_URL, target: "_blank", rel: "noreferrer", children: "Vine's note" }), ' ', "and", ' ', _jsx("a", { href: VINE_FEES_URL, target: "_blank", rel: "noreferrer", children: "how fees work" }), "."] }));
|
|
85
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type LocalDeployment } from './localDeployment.js';
|
|
2
|
+
export interface DevStatePanelProps {
|
|
3
|
+
/** The parsed `deployments/local.json`; see `parseLocalDeployment`. */
|
|
4
|
+
deployment: LocalDeployment;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The dev-only state switcher (PLAN.md 18.1). Lists the five wallets `npm run dev` seeded, marks
|
|
9
|
+
* the connected one, and flips the connected wallet through the five credential states by calling
|
|
10
|
+
* `setStatus` on the `ReceptorMock` as the deployer, through Anvil's impersonation RPC. Every
|
|
11
|
+
* mounted `useEligibility` for that wallet refetches on the resulting signal, so the page moves
|
|
12
|
+
* into the new state without a reload.
|
|
13
|
+
*
|
|
14
|
+
* It renders only when the wallet (or, unconnected, the app) is on chain 31337 and the deployment
|
|
15
|
+
* it was given says 31337 too. On 151 or 153 it renders nothing, whatever it was given. In a
|
|
16
|
+
* scaffold it is imported only by the dev server and is absent from production bundles; the
|
|
17
|
+
* scaffolder's test asserts both.
|
|
18
|
+
*/
|
|
19
|
+
export declare function DevStatePanel({ deployment, className }: DevStatePanelProps): import("react/jsx-runtime").JSX.Element | null;
|