@gatedpath/frontend-kit 0.2.1 → 0.2.3
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/README.md +62 -2
- package/dist/abi.d.ts +249 -0
- package/dist/abi.js +49 -0
- package/dist/checkRecipient.d.ts +73 -0
- package/dist/checkRecipient.js +118 -0
- package/dist/dev/DevStatePanel.d.ts +1 -1
- package/dist/dev/DevStatePanel.js +1 -1
- package/dist/dev/index.js +1 -1
- package/dist/dev/localDeployment.d.ts +2 -2
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/useRegister.d.ts +55 -0
- package/dist/useRegister.js +146 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,6 +17,10 @@ For agents
|
|
|
17
17
|
table (`@gatedpath/agent-rules/failures`): NotEligible (with the credential state when you pass
|
|
18
18
|
one), a wallet rejection, a wrong chain, not enough RBNT. Show `plainWords` and `fix.link`, never
|
|
19
19
|
`error.message` alone; `kind: "unknown"` carries the error's own words in `raw`.
|
|
20
|
+
- Before a send, `checkRecipient(client, { recipient, chainId })` (or `useRecipientCheck(recipient)`)
|
|
21
|
+
answers what card 13's `RecipientGuard` will answer on chain: `ok`, a `verdict`, and `reasons`
|
|
22
|
+
with a sentence each. Show the sentences; never build the transaction while `ok` is false. It
|
|
23
|
+
means an address someone verified holds, not the right person.
|
|
20
24
|
- Build: `npm run build`. Tests: `npm test` (Vitest, jsdom, a mocked transport; no network).
|
|
21
25
|
```
|
|
22
26
|
|
|
@@ -58,6 +62,23 @@ State derivation, in order: no address is `disconnected`; nothing cached and an
|
|
|
58
62
|
says; `isEligible` false is `ineligible`; `isEligible` true with an expiry inside the window is
|
|
59
63
|
`expiring`; otherwise `eligible`.
|
|
60
64
|
|
|
65
|
+
## The register hook
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
const r = useRegister(tokenAddress, address);
|
|
69
|
+
// r.state: 'disconnected' | 'checking' | 'not-a-register' | 'registered' | 'not-registered' | 'frozen' | 'paused' | 'error'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`useRegister(token, wallet)` reads an ERC-3643 register (a T-REX token, the shape of every real asset
|
|
73
|
+
on this network) and, when a wallet is given, whether that register's identity registry has verified
|
|
74
|
+
it. It is the third gate a holder meets, after `permission.isAllowed` and a dApp's recipe, which
|
|
75
|
+
`useEligibility` reads. The result carries the token's facts (name, symbol, decimals, supply, T-REX
|
|
76
|
+
version, paused), the registry's shape (trusted issuers, required claim topics, and `gate`:
|
|
77
|
+
`registration only` when no topics are required, `claims` otherwise) and the wallet's standing
|
|
78
|
+
(verified, registered, frozen, balance). Reads are batched through Multicall3, cached per chain and
|
|
79
|
+
refetched on a new block. Only the issuer can register a wallet, so `not-registered` is information
|
|
80
|
+
for the page, never a button. The ops kit's `readRegister` is the same read from a script.
|
|
81
|
+
|
|
61
82
|
## The components
|
|
62
83
|
|
|
63
84
|
| Component | State | Says |
|
|
@@ -116,12 +137,51 @@ The panel renders only on chain 31337. On 151 or 153 it renders nothing whatever
|
|
|
116
137
|
and the scaffold imports it only in the dev server behind a build-time constant, so a production
|
|
117
138
|
bundle carries none of it; the scaffolder's integration test builds one and checks.
|
|
118
139
|
|
|
140
|
+
## checkRecipient
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
import { useRecipientCheck } from '@gatedpath/frontend-kit';
|
|
144
|
+
|
|
145
|
+
const { check, isChecking, error } = useRecipientCheck(recipient, { allowContract });
|
|
146
|
+
// check: { ok, verdict, reasons: [{ kind, error, plainWords }], permission, chainId } | null
|
|
147
|
+
return (
|
|
148
|
+
<>
|
|
149
|
+
{isChecking && <p>Asking the network…</p>}
|
|
150
|
+
{check?.reasons.map((r) => <p key={r.kind}>{r.plainWords}</p>)}
|
|
151
|
+
<button disabled={!check?.ok} onClick={send}>Send through the guard</button>
|
|
152
|
+
</>
|
|
153
|
+
);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
A transfer that refuses to lose your money (card 13) goes through `RecipientGuard`, a contract
|
|
157
|
+
that asks the network whether the recipient has passed the identity gate on this chain and
|
|
158
|
+
refuses if not. `checkRecipient(client, { recipient, chainId, allowContract?, amount?, permission?,
|
|
159
|
+
registry? })` asks the same questions from the browser first, in the guard's order, so the page can
|
|
160
|
+
say no before it asks for a signature: the zero address (`ZeroRecipient`); code at the address,
|
|
161
|
+
which makes it a contract the guard refuses unless `allowContract` is true (`RecipientIsContract`;
|
|
162
|
+
a 23-byte EIP-7702 delegation is still a wallet); the bootstrap registry naming no permission
|
|
163
|
+
contract, or one with no code (`NoPermissionContract`); `permission.isAllowed` false
|
|
164
|
+
(`RecipientNotPermissioned`); and, when you pass `amount`, zero (`NothingToSend`). Each reason
|
|
165
|
+
carries the guard's error name and a sentence for the page. The permission contract is resolved
|
|
166
|
+
through the registry on every call, as the guard does, because its proxy admin has moved before;
|
|
167
|
+
pass `permission` to skip that when the page already holds it. `client` is a viem `PublicClient`
|
|
168
|
+
(`usePublicClient()` in wagmi); the call throws if the client is on another chain than `chainId`,
|
|
169
|
+
and an RPC failure is thrown, never turned into a verdict.
|
|
170
|
+
|
|
171
|
+
`useRecipientCheck(recipient, { chainId?, allowContract?, permission?, registry?, refreshInterval?,
|
|
172
|
+
enabled? })` runs it when a recipient is set, caches the answer under the chain id and the recipient
|
|
173
|
+
for 30 s, and exposes `refresh()`. The page's answer is a courtesy and the contract's is the rule:
|
|
174
|
+
`ok` means an address someone verified holds on this chain, or a contract the user said they mean.
|
|
175
|
+
A typo that lands on another verified address goes through, and the page should say so.
|
|
176
|
+
|
|
119
177
|
## Tests
|
|
120
178
|
|
|
121
|
-
`npm test`:
|
|
179
|
+
`npm test`: 122 Vitest tests in jsdom. `test/mock-chain.tsx` is a wagmi config over a `custom`
|
|
122
180
|
transport that answers `eth_chainId`, `eth_blockNumber`, `eth_call` and Multicall3 `aggregate3`
|
|
181
|
+
(and, since `checkRecipient`, `eth_getCode` and the bootstrap registry's `getContractAddress`)
|
|
123
182
|
from an in-memory state the tests mutate; nothing leaves the process and no address in it exists
|
|
124
|
-
anywhere.
|
|
183
|
+
anywhere. `test/checkRecipient.test.tsx` covers every refusal the guard makes, the ok path, the
|
|
184
|
+
EIP-7702 case, a given permission contract, the chain mismatch and the hook's cache and refresh. The hook tests cover every state, that reads happen once and then only on a new block,
|
|
125
185
|
manual refresh without a block, shared reads, the expiry window, errors and `enabled: false`. The
|
|
126
186
|
component tests render every state, the gate's selection and override, the links (Access dApp,
|
|
127
187
|
faucet, Vine) and the retry and refresh buttons. `test/dev-panel.test.tsx` runs the dev panel
|
package/dist/abi.d.ts
CHANGED
|
@@ -15,6 +15,24 @@ export declare const verifierAbi: readonly [{
|
|
|
15
15
|
readonly type: "bool";
|
|
16
16
|
}];
|
|
17
17
|
}];
|
|
18
|
+
/**
|
|
19
|
+
* The bootstrap registry, the one fixed address on both chains: `getContractAddress("permission")`
|
|
20
|
+
* names the permission contract. `checkRecipient` asks it on every call rather than remembering
|
|
21
|
+
* the answer, because the permission proxy's admin has moved before.
|
|
22
|
+
*/
|
|
23
|
+
export declare const bootstrapRegistryAbi: readonly [{
|
|
24
|
+
readonly type: "function";
|
|
25
|
+
readonly name: "getContractAddress";
|
|
26
|
+
readonly stateMutability: "view";
|
|
27
|
+
readonly inputs: readonly [{
|
|
28
|
+
readonly name: "name";
|
|
29
|
+
readonly type: "string";
|
|
30
|
+
}];
|
|
31
|
+
readonly outputs: readonly [{
|
|
32
|
+
readonly name: "";
|
|
33
|
+
readonly type: "address";
|
|
34
|
+
}];
|
|
35
|
+
}];
|
|
18
36
|
/** The network's permission contract: `isAllowed(address)` (RESEARCH.md question 7). */
|
|
19
37
|
export declare const permissionAbi: readonly [{
|
|
20
38
|
readonly type: "function";
|
|
@@ -96,3 +114,234 @@ export declare const receptorMockStatusAbi: readonly [{
|
|
|
96
114
|
readonly type: "uint8";
|
|
97
115
|
}];
|
|
98
116
|
}];
|
|
117
|
+
/**
|
|
118
|
+
* ERC-3643 (T-REX 4.1.x), the shape of every real register on this network (RESEARCH.md source log,
|
|
119
|
+
* 12 October 2026): the token, its identity registry, the trusted-issuers and claim-topics registries.
|
|
120
|
+
* Reads only; the interface was confirmed against a live mainnet register before it was written here.
|
|
121
|
+
*/
|
|
122
|
+
export declare const registerTokenAbi: readonly [{
|
|
123
|
+
readonly type: "function";
|
|
124
|
+
readonly name: "identityRegistry";
|
|
125
|
+
readonly stateMutability: "view";
|
|
126
|
+
readonly inputs: readonly [];
|
|
127
|
+
readonly outputs: readonly [{
|
|
128
|
+
readonly name: "";
|
|
129
|
+
readonly type: "address";
|
|
130
|
+
}];
|
|
131
|
+
}, {
|
|
132
|
+
readonly type: "function";
|
|
133
|
+
readonly name: "compliance";
|
|
134
|
+
readonly stateMutability: "view";
|
|
135
|
+
readonly inputs: readonly [];
|
|
136
|
+
readonly outputs: readonly [{
|
|
137
|
+
readonly name: "";
|
|
138
|
+
readonly type: "address";
|
|
139
|
+
}];
|
|
140
|
+
}, {
|
|
141
|
+
readonly type: "function";
|
|
142
|
+
readonly name: "onchainID";
|
|
143
|
+
readonly stateMutability: "view";
|
|
144
|
+
readonly inputs: readonly [];
|
|
145
|
+
readonly outputs: readonly [{
|
|
146
|
+
readonly name: "";
|
|
147
|
+
readonly type: "address";
|
|
148
|
+
}];
|
|
149
|
+
}, {
|
|
150
|
+
readonly type: "function";
|
|
151
|
+
readonly name: "name";
|
|
152
|
+
readonly stateMutability: "view";
|
|
153
|
+
readonly inputs: readonly [];
|
|
154
|
+
readonly outputs: readonly [{
|
|
155
|
+
readonly name: "";
|
|
156
|
+
readonly type: "string";
|
|
157
|
+
}];
|
|
158
|
+
}, {
|
|
159
|
+
readonly type: "function";
|
|
160
|
+
readonly name: "symbol";
|
|
161
|
+
readonly stateMutability: "view";
|
|
162
|
+
readonly inputs: readonly [];
|
|
163
|
+
readonly outputs: readonly [{
|
|
164
|
+
readonly name: "";
|
|
165
|
+
readonly type: "string";
|
|
166
|
+
}];
|
|
167
|
+
}, {
|
|
168
|
+
readonly type: "function";
|
|
169
|
+
readonly name: "decimals";
|
|
170
|
+
readonly stateMutability: "view";
|
|
171
|
+
readonly inputs: readonly [];
|
|
172
|
+
readonly outputs: readonly [{
|
|
173
|
+
readonly name: "";
|
|
174
|
+
readonly type: "uint8";
|
|
175
|
+
}];
|
|
176
|
+
}, {
|
|
177
|
+
readonly type: "function";
|
|
178
|
+
readonly name: "totalSupply";
|
|
179
|
+
readonly stateMutability: "view";
|
|
180
|
+
readonly inputs: readonly [];
|
|
181
|
+
readonly outputs: readonly [{
|
|
182
|
+
readonly name: "";
|
|
183
|
+
readonly type: "uint256";
|
|
184
|
+
}];
|
|
185
|
+
}, {
|
|
186
|
+
readonly type: "function";
|
|
187
|
+
readonly name: "version";
|
|
188
|
+
readonly stateMutability: "view";
|
|
189
|
+
readonly inputs: readonly [];
|
|
190
|
+
readonly outputs: readonly [{
|
|
191
|
+
readonly name: "";
|
|
192
|
+
readonly type: "string";
|
|
193
|
+
}];
|
|
194
|
+
}, {
|
|
195
|
+
readonly type: "function";
|
|
196
|
+
readonly name: "paused";
|
|
197
|
+
readonly stateMutability: "view";
|
|
198
|
+
readonly inputs: readonly [];
|
|
199
|
+
readonly outputs: readonly [{
|
|
200
|
+
readonly name: "";
|
|
201
|
+
readonly type: "bool";
|
|
202
|
+
}];
|
|
203
|
+
}, {
|
|
204
|
+
readonly type: "function";
|
|
205
|
+
readonly name: "isFrozen";
|
|
206
|
+
readonly stateMutability: "view";
|
|
207
|
+
readonly inputs: readonly [{
|
|
208
|
+
readonly name: "wallet";
|
|
209
|
+
readonly type: "address";
|
|
210
|
+
}];
|
|
211
|
+
readonly outputs: readonly [{
|
|
212
|
+
readonly name: "";
|
|
213
|
+
readonly type: "bool";
|
|
214
|
+
}];
|
|
215
|
+
}, {
|
|
216
|
+
readonly type: "function";
|
|
217
|
+
readonly name: "getFrozenTokens";
|
|
218
|
+
readonly stateMutability: "view";
|
|
219
|
+
readonly inputs: readonly [{
|
|
220
|
+
readonly name: "wallet";
|
|
221
|
+
readonly type: "address";
|
|
222
|
+
}];
|
|
223
|
+
readonly outputs: readonly [{
|
|
224
|
+
readonly name: "";
|
|
225
|
+
readonly type: "uint256";
|
|
226
|
+
}];
|
|
227
|
+
}, {
|
|
228
|
+
readonly type: "function";
|
|
229
|
+
readonly name: "balanceOf";
|
|
230
|
+
readonly stateMutability: "view";
|
|
231
|
+
readonly inputs: readonly [{
|
|
232
|
+
readonly name: "wallet";
|
|
233
|
+
readonly type: "address";
|
|
234
|
+
}];
|
|
235
|
+
readonly outputs: readonly [{
|
|
236
|
+
readonly name: "";
|
|
237
|
+
readonly type: "uint256";
|
|
238
|
+
}];
|
|
239
|
+
}];
|
|
240
|
+
export declare const identityRegistryAbi: readonly [{
|
|
241
|
+
readonly type: "function";
|
|
242
|
+
readonly name: "identityStorage";
|
|
243
|
+
readonly stateMutability: "view";
|
|
244
|
+
readonly inputs: readonly [];
|
|
245
|
+
readonly outputs: readonly [{
|
|
246
|
+
readonly name: "";
|
|
247
|
+
readonly type: "address";
|
|
248
|
+
}];
|
|
249
|
+
}, {
|
|
250
|
+
readonly type: "function";
|
|
251
|
+
readonly name: "issuersRegistry";
|
|
252
|
+
readonly stateMutability: "view";
|
|
253
|
+
readonly inputs: readonly [];
|
|
254
|
+
readonly outputs: readonly [{
|
|
255
|
+
readonly name: "";
|
|
256
|
+
readonly type: "address";
|
|
257
|
+
}];
|
|
258
|
+
}, {
|
|
259
|
+
readonly type: "function";
|
|
260
|
+
readonly name: "topicsRegistry";
|
|
261
|
+
readonly stateMutability: "view";
|
|
262
|
+
readonly inputs: readonly [];
|
|
263
|
+
readonly outputs: readonly [{
|
|
264
|
+
readonly name: "";
|
|
265
|
+
readonly type: "address";
|
|
266
|
+
}];
|
|
267
|
+
}, {
|
|
268
|
+
readonly type: "function";
|
|
269
|
+
readonly name: "isVerified";
|
|
270
|
+
readonly stateMutability: "view";
|
|
271
|
+
readonly inputs: readonly [{
|
|
272
|
+
readonly name: "wallet";
|
|
273
|
+
readonly type: "address";
|
|
274
|
+
}];
|
|
275
|
+
readonly outputs: readonly [{
|
|
276
|
+
readonly name: "";
|
|
277
|
+
readonly type: "bool";
|
|
278
|
+
}];
|
|
279
|
+
}, {
|
|
280
|
+
readonly type: "function";
|
|
281
|
+
readonly name: "identity";
|
|
282
|
+
readonly stateMutability: "view";
|
|
283
|
+
readonly inputs: readonly [{
|
|
284
|
+
readonly name: "wallet";
|
|
285
|
+
readonly type: "address";
|
|
286
|
+
}];
|
|
287
|
+
readonly outputs: readonly [{
|
|
288
|
+
readonly name: "";
|
|
289
|
+
readonly type: "address";
|
|
290
|
+
}];
|
|
291
|
+
}, {
|
|
292
|
+
readonly type: "function";
|
|
293
|
+
readonly name: "investorCountry";
|
|
294
|
+
readonly stateMutability: "view";
|
|
295
|
+
readonly inputs: readonly [{
|
|
296
|
+
readonly name: "wallet";
|
|
297
|
+
readonly type: "address";
|
|
298
|
+
}];
|
|
299
|
+
readonly outputs: readonly [{
|
|
300
|
+
readonly name: "";
|
|
301
|
+
readonly type: "uint16";
|
|
302
|
+
}];
|
|
303
|
+
}, {
|
|
304
|
+
readonly type: "function";
|
|
305
|
+
readonly name: "contains";
|
|
306
|
+
readonly stateMutability: "view";
|
|
307
|
+
readonly inputs: readonly [{
|
|
308
|
+
readonly name: "wallet";
|
|
309
|
+
readonly type: "address";
|
|
310
|
+
}];
|
|
311
|
+
readonly outputs: readonly [{
|
|
312
|
+
readonly name: "";
|
|
313
|
+
readonly type: "bool";
|
|
314
|
+
}];
|
|
315
|
+
}];
|
|
316
|
+
export declare const trustedIssuersRegistryAbi: readonly [{
|
|
317
|
+
readonly type: "function";
|
|
318
|
+
readonly name: "getTrustedIssuers";
|
|
319
|
+
readonly stateMutability: "view";
|
|
320
|
+
readonly inputs: readonly [];
|
|
321
|
+
readonly outputs: readonly [{
|
|
322
|
+
readonly name: "";
|
|
323
|
+
readonly type: "address[]";
|
|
324
|
+
}];
|
|
325
|
+
}, {
|
|
326
|
+
readonly type: "function";
|
|
327
|
+
readonly name: "getTrustedIssuerClaimTopics";
|
|
328
|
+
readonly stateMutability: "view";
|
|
329
|
+
readonly inputs: readonly [{
|
|
330
|
+
readonly name: "issuer";
|
|
331
|
+
readonly type: "address";
|
|
332
|
+
}];
|
|
333
|
+
readonly outputs: readonly [{
|
|
334
|
+
readonly name: "";
|
|
335
|
+
readonly type: "uint256[]";
|
|
336
|
+
}];
|
|
337
|
+
}];
|
|
338
|
+
export declare const claimTopicsRegistryAbi: readonly [{
|
|
339
|
+
readonly type: "function";
|
|
340
|
+
readonly name: "getClaimTopics";
|
|
341
|
+
readonly stateMutability: "view";
|
|
342
|
+
readonly inputs: readonly [];
|
|
343
|
+
readonly outputs: readonly [{
|
|
344
|
+
readonly name: "";
|
|
345
|
+
readonly type: "uint256[]";
|
|
346
|
+
}];
|
|
347
|
+
}];
|
package/dist/abi.js
CHANGED
|
@@ -12,6 +12,20 @@ export const verifierAbi = [
|
|
|
12
12
|
outputs: [{ name: '', type: 'bool' }],
|
|
13
13
|
},
|
|
14
14
|
];
|
|
15
|
+
/**
|
|
16
|
+
* The bootstrap registry, the one fixed address on both chains: `getContractAddress("permission")`
|
|
17
|
+
* names the permission contract. `checkRecipient` asks it on every call rather than remembering
|
|
18
|
+
* the answer, because the permission proxy's admin has moved before.
|
|
19
|
+
*/
|
|
20
|
+
export const bootstrapRegistryAbi = [
|
|
21
|
+
{
|
|
22
|
+
type: 'function',
|
|
23
|
+
name: 'getContractAddress',
|
|
24
|
+
stateMutability: 'view',
|
|
25
|
+
inputs: [{ name: 'name', type: 'string' }],
|
|
26
|
+
outputs: [{ name: '', type: 'address' }],
|
|
27
|
+
},
|
|
28
|
+
];
|
|
15
29
|
/** The network's permission contract: `isAllowed(address)` (RESEARCH.md question 7). */
|
|
16
30
|
export const permissionAbi = [
|
|
17
31
|
{
|
|
@@ -77,3 +91,38 @@ export const receptorMockStatusAbi = [
|
|
|
77
91
|
outputs: [{ name: '', type: 'uint8' }],
|
|
78
92
|
},
|
|
79
93
|
];
|
|
94
|
+
/**
|
|
95
|
+
* ERC-3643 (T-REX 4.1.x), the shape of every real register on this network (RESEARCH.md source log,
|
|
96
|
+
* 12 October 2026): the token, its identity registry, the trusted-issuers and claim-topics registries.
|
|
97
|
+
* Reads only; the interface was confirmed against a live mainnet register before it was written here.
|
|
98
|
+
*/
|
|
99
|
+
export const registerTokenAbi = [
|
|
100
|
+
{ type: 'function', name: 'identityRegistry', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'address' }] },
|
|
101
|
+
{ type: 'function', name: 'compliance', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'address' }] },
|
|
102
|
+
{ type: 'function', name: 'onchainID', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'address' }] },
|
|
103
|
+
{ type: 'function', name: 'name', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'string' }] },
|
|
104
|
+
{ type: 'function', name: 'symbol', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'string' }] },
|
|
105
|
+
{ type: 'function', name: 'decimals', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint8' }] },
|
|
106
|
+
{ type: 'function', name: 'totalSupply', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint256' }] },
|
|
107
|
+
{ type: 'function', name: 'version', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'string' }] },
|
|
108
|
+
{ type: 'function', name: 'paused', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'bool' }] },
|
|
109
|
+
{ type: 'function', name: 'isFrozen', stateMutability: 'view', inputs: [{ name: 'wallet', type: 'address' }], outputs: [{ name: '', type: 'bool' }] },
|
|
110
|
+
{ type: 'function', name: 'getFrozenTokens', stateMutability: 'view', inputs: [{ name: 'wallet', type: 'address' }], outputs: [{ name: '', type: 'uint256' }] },
|
|
111
|
+
{ type: 'function', name: 'balanceOf', stateMutability: 'view', inputs: [{ name: 'wallet', type: 'address' }], outputs: [{ name: '', type: 'uint256' }] },
|
|
112
|
+
];
|
|
113
|
+
export const identityRegistryAbi = [
|
|
114
|
+
{ type: 'function', name: 'identityStorage', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'address' }] },
|
|
115
|
+
{ type: 'function', name: 'issuersRegistry', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'address' }] },
|
|
116
|
+
{ type: 'function', name: 'topicsRegistry', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'address' }] },
|
|
117
|
+
{ type: 'function', name: 'isVerified', stateMutability: 'view', inputs: [{ name: 'wallet', type: 'address' }], outputs: [{ name: '', type: 'bool' }] },
|
|
118
|
+
{ type: 'function', name: 'identity', stateMutability: 'view', inputs: [{ name: 'wallet', type: 'address' }], outputs: [{ name: '', type: 'address' }] },
|
|
119
|
+
{ type: 'function', name: 'investorCountry', stateMutability: 'view', inputs: [{ name: 'wallet', type: 'address' }], outputs: [{ name: '', type: 'uint16' }] },
|
|
120
|
+
{ type: 'function', name: 'contains', stateMutability: 'view', inputs: [{ name: 'wallet', type: 'address' }], outputs: [{ name: '', type: 'bool' }] },
|
|
121
|
+
];
|
|
122
|
+
export const trustedIssuersRegistryAbi = [
|
|
123
|
+
{ type: 'function', name: 'getTrustedIssuers', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'address[]' }] },
|
|
124
|
+
{ type: 'function', name: 'getTrustedIssuerClaimTopics', stateMutability: 'view', inputs: [{ name: 'issuer', type: 'address' }], outputs: [{ name: '', type: 'uint256[]' }] },
|
|
125
|
+
];
|
|
126
|
+
export const claimTopicsRegistryAbi = [
|
|
127
|
+
{ type: 'function', name: 'getClaimTopics', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint256[]' }] },
|
|
128
|
+
];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { PublicClient } from 'viem';
|
|
2
|
+
import type { Address } from './useEligibility.js';
|
|
3
|
+
/** `RecipientGuard.Verdict`, plus the one case where the guard cannot reach a verdict at all. */
|
|
4
|
+
export type RecipientVerdict = 'permissioned' | 'not-permissioned' | 'contract' | 'zero' | 'no-permission-contract';
|
|
5
|
+
/** One per named revert in `RecipientGuard`, except `NativeTransferFailed`, which only the send itself can find. */
|
|
6
|
+
export type RecipientReasonKind = 'zero-recipient' | 'recipient-is-contract' | 'recipient-not-permissioned' | 'no-permission-contract' | 'nothing-to-send';
|
|
7
|
+
export interface RecipientReason {
|
|
8
|
+
kind: RecipientReasonKind;
|
|
9
|
+
/** The guard's error name for this refusal, as a block explorer or `explainError` would show it. */
|
|
10
|
+
error: 'ZeroRecipient' | 'RecipientIsContract' | 'RecipientNotPermissioned' | 'NoPermissionContract' | 'NothingToSend';
|
|
11
|
+
/** A sentence for the page. */
|
|
12
|
+
plainWords: string;
|
|
13
|
+
}
|
|
14
|
+
export interface CheckRecipientOptions {
|
|
15
|
+
recipient: Address;
|
|
16
|
+
/** The chain the client reads. Refused when the client says it is on a different one. */
|
|
17
|
+
chainId: number;
|
|
18
|
+
/** True when the user has said a contract is what they mean to pay (`send(to, allowContract)`). Default false. */
|
|
19
|
+
allowContract?: boolean;
|
|
20
|
+
/** The amount about to be sent, when known; zero is refused the way `send` refuses an empty call. */
|
|
21
|
+
amount?: bigint;
|
|
22
|
+
/** Skip the registry and read this permission contract. Default: resolve it live, as the guard does. */
|
|
23
|
+
permission?: Address;
|
|
24
|
+
/** The bootstrap registry. Default: the shared address from @gatedpath/chains. */
|
|
25
|
+
registry?: Address;
|
|
26
|
+
}
|
|
27
|
+
export interface RecipientCheck {
|
|
28
|
+
/** True when the guard would let the send through to this recipient. */
|
|
29
|
+
ok: boolean;
|
|
30
|
+
verdict: RecipientVerdict;
|
|
31
|
+
/** Empty when `ok`. Otherwise every refusal the guard would raise, in the order it raises them. */
|
|
32
|
+
reasons: RecipientReason[];
|
|
33
|
+
recipient: Address;
|
|
34
|
+
chainId: number;
|
|
35
|
+
/** The permission contract the answer came from, when one was read. */
|
|
36
|
+
permission: Address | null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Asks the network what `RecipientGuard.check(to)` would answer, and turns it into the refusals
|
|
40
|
+
* `send` would raise. Reads, in the guard's order: the recipient's code; then, for a wallet, the
|
|
41
|
+
* registry's code, the permission contract it names, that contract's code, and `isAllowed`. The
|
|
42
|
+
* permission contract is resolved on every call and never remembered, because its proxy admin has
|
|
43
|
+
* moved before (14 September 2026); pass `permission` to skip that when the page already knows it.
|
|
44
|
+
*
|
|
45
|
+
* What `ok` means: an address someone verified holds on this chain, or a contract the user has
|
|
46
|
+
* said they mean. It does not mean the right person: a typo that lands on another verified
|
|
47
|
+
* address goes through. Say that on the page.
|
|
48
|
+
*/
|
|
49
|
+
export declare function checkRecipient(client: PublicClient, options: CheckRecipientOptions): Promise<RecipientCheck>;
|
|
50
|
+
export interface UseRecipientCheckOptions {
|
|
51
|
+
/** Chain to read on. Defaults to wagmi's current chain. */
|
|
52
|
+
chainId?: number;
|
|
53
|
+
allowContract?: boolean;
|
|
54
|
+
permission?: Address;
|
|
55
|
+
registry?: Address;
|
|
56
|
+
/** How long an answer is kept before the next render reads again. Default 30 000 ms. */
|
|
57
|
+
refreshInterval?: number;
|
|
58
|
+
enabled?: boolean;
|
|
59
|
+
}
|
|
60
|
+
export interface RecipientCheckResult {
|
|
61
|
+
/** The last answer, or null before the first one. */
|
|
62
|
+
check: RecipientCheck | null;
|
|
63
|
+
isChecking: boolean;
|
|
64
|
+
error: Error | null;
|
|
65
|
+
/** Read again now. */
|
|
66
|
+
refresh: () => Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* `checkRecipient` as a hook: runs when `recipient` is set, caches the answer under the chain id
|
|
70
|
+
* and the recipient for `refreshInterval`, and reads again on `refresh()`. Mount it on the
|
|
71
|
+
* recipient field; show `check.reasons[*].plainWords` under it.
|
|
72
|
+
*/
|
|
73
|
+
export declare function useRecipientCheck(recipient: Address | undefined, options?: UseRecipientCheckOptions): RecipientCheckResult;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
// checkRecipient: the question card 13's RecipientGuard asks on chain, asked from the browser
|
|
3
|
+
// first, so a page can refuse before it asks for a signature. Same reads, same order, same
|
|
4
|
+
// answers as `RecipientGuard.check` and `_requirePasses`; the contract's answer is still the rule
|
|
5
|
+
// and this one is the courtesy. No cache here: `useRecipientCheck` caches per chain so an answer
|
|
6
|
+
// from 153 is never shown for 151.
|
|
7
|
+
import { useCallback, useMemo } from 'react';
|
|
8
|
+
import { useQuery } from '@tanstack/react-query';
|
|
9
|
+
import { useChainId, usePublicClient } from 'wagmi';
|
|
10
|
+
import { addresses, chainById } from '@gatedpath/chains';
|
|
11
|
+
import { bootstrapRegistryAbi, permissionAbi } from './abi.js';
|
|
12
|
+
const ZERO = '0x0000000000000000000000000000000000000000';
|
|
13
|
+
const WORDS = {
|
|
14
|
+
'zero-recipient': {
|
|
15
|
+
error: 'ZeroRecipient',
|
|
16
|
+
plainWords: 'That is the zero address. Nothing can be sent there, and the guard refuses it.',
|
|
17
|
+
},
|
|
18
|
+
'recipient-is-contract': {
|
|
19
|
+
error: 'RecipientIsContract',
|
|
20
|
+
plainWords: 'This is a contract, not a wallet, so it has never been through the identity gate. The guard refuses it unless you say a contract is what you mean to pay.',
|
|
21
|
+
},
|
|
22
|
+
'recipient-not-permissioned': {
|
|
23
|
+
error: 'RecipientNotPermissioned',
|
|
24
|
+
plainWords: 'No one verified holds this address on this chain. Money sent there would be gone, so the guard refuses.',
|
|
25
|
+
},
|
|
26
|
+
'no-permission-contract': {
|
|
27
|
+
error: 'NoPermissionContract',
|
|
28
|
+
plainWords: 'This chain has no permission contract behind its registry, so there is nothing to check a recipient against. The guard refuses every send here.',
|
|
29
|
+
},
|
|
30
|
+
'nothing-to-send': {
|
|
31
|
+
error: 'NothingToSend',
|
|
32
|
+
plainWords: 'The amount is zero. There is nothing to send.',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
function reason(kind) {
|
|
36
|
+
return { kind, ...WORDS[kind] };
|
|
37
|
+
}
|
|
38
|
+
/** `RecipientGuard._isContract`: code at the address, unless it is an EIP-7702 delegation designator, which is still a wallet. */
|
|
39
|
+
function isContractCode(code) {
|
|
40
|
+
if (!code || code === '0x')
|
|
41
|
+
return false;
|
|
42
|
+
const bytes = (code.length - 2) / 2;
|
|
43
|
+
return !(bytes === 23 && code.toLowerCase().startsWith('0xef0100'));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Asks the network what `RecipientGuard.check(to)` would answer, and turns it into the refusals
|
|
47
|
+
* `send` would raise. Reads, in the guard's order: the recipient's code; then, for a wallet, the
|
|
48
|
+
* registry's code, the permission contract it names, that contract's code, and `isAllowed`. The
|
|
49
|
+
* permission contract is resolved on every call and never remembered, because its proxy admin has
|
|
50
|
+
* moved before (14 September 2026); pass `permission` to skip that when the page already knows it.
|
|
51
|
+
*
|
|
52
|
+
* What `ok` means: an address someone verified holds on this chain, or a contract the user has
|
|
53
|
+
* said they mean. It does not mean the right person: a typo that lands on another verified
|
|
54
|
+
* address goes through. Say that on the page.
|
|
55
|
+
*/
|
|
56
|
+
export async function checkRecipient(client, options) {
|
|
57
|
+
const { recipient, chainId } = options;
|
|
58
|
+
if (client.chain && client.chain.id !== chainId) {
|
|
59
|
+
throw new Error(`checkRecipient: the client is on chain ${client.chain.id}, not ${chainId}; an answer from one chain is never shown for another`);
|
|
60
|
+
}
|
|
61
|
+
const reasons = [];
|
|
62
|
+
if (options.amount !== undefined && options.amount === 0n)
|
|
63
|
+
reasons.push(reason('nothing-to-send'));
|
|
64
|
+
const base = { recipient, chainId, permission: null };
|
|
65
|
+
if (recipient.toLowerCase() === ZERO) {
|
|
66
|
+
reasons.push(reason('zero-recipient'));
|
|
67
|
+
return { ...base, ok: false, verdict: 'zero', reasons };
|
|
68
|
+
}
|
|
69
|
+
const code = await client.getCode({ address: recipient });
|
|
70
|
+
if (isContractCode(code)) {
|
|
71
|
+
if (!options.allowContract)
|
|
72
|
+
reasons.push(reason('recipient-is-contract'));
|
|
73
|
+
return { ...base, ok: reasons.length === 0, verdict: 'contract', reasons };
|
|
74
|
+
}
|
|
75
|
+
let permission = options.permission ?? null;
|
|
76
|
+
if (!permission) {
|
|
77
|
+
const registry = options.registry ?? chainById(chainId)?.contracts['bootstrapRegistry']?.address ?? addresses.mainnet.bootstrapRegistry.address;
|
|
78
|
+
const registryCode = await client.getCode({ address: registry });
|
|
79
|
+
const named = registryCode && registryCode !== '0x'
|
|
80
|
+
? await client.readContract({ address: registry, abi: bootstrapRegistryAbi, functionName: 'getContractAddress', args: ['permission'] })
|
|
81
|
+
: ZERO;
|
|
82
|
+
const permissionCode = named.toLowerCase() === ZERO ? '0x' : await client.getCode({ address: named });
|
|
83
|
+
if (!permissionCode || permissionCode === '0x') {
|
|
84
|
+
reasons.push(reason('no-permission-contract'));
|
|
85
|
+
return { ...base, ok: false, verdict: 'no-permission-contract', reasons };
|
|
86
|
+
}
|
|
87
|
+
permission = named;
|
|
88
|
+
}
|
|
89
|
+
const allowed = await client.readContract({ address: permission, abi: permissionAbi, functionName: 'isAllowed', args: [recipient] });
|
|
90
|
+
if (!allowed)
|
|
91
|
+
reasons.push(reason('recipient-not-permissioned'));
|
|
92
|
+
return { ...base, permission, ok: reasons.length === 0, verdict: allowed ? 'permissioned' : 'not-permissioned', reasons };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* `checkRecipient` as a hook: runs when `recipient` is set, caches the answer under the chain id
|
|
96
|
+
* and the recipient for `refreshInterval`, and reads again on `refresh()`. Mount it on the
|
|
97
|
+
* recipient field; show `check.reasons[*].plainWords` under it.
|
|
98
|
+
*/
|
|
99
|
+
export function useRecipientCheck(recipient, options = {}) {
|
|
100
|
+
const currentChainId = useChainId();
|
|
101
|
+
const chainId = options.chainId ?? currentChainId;
|
|
102
|
+
const client = usePublicClient({ chainId });
|
|
103
|
+
const allowContract = options.allowContract ?? false;
|
|
104
|
+
const enabled = (options.enabled ?? true) && Boolean(client && recipient);
|
|
105
|
+
const { permission, registry } = options;
|
|
106
|
+
const query = useQuery({
|
|
107
|
+
queryKey: ['redbelly-frontend-kit', 'recipient', chainId, recipient?.toLowerCase(), allowContract, permission, registry],
|
|
108
|
+
queryFn: () => checkRecipient(client, { recipient: recipient, chainId, allowContract, permission, registry }),
|
|
109
|
+
enabled,
|
|
110
|
+
staleTime: options.refreshInterval ?? 30_000,
|
|
111
|
+
refetchOnWindowFocus: false,
|
|
112
|
+
});
|
|
113
|
+
const refetch = query.refetch;
|
|
114
|
+
const refresh = useCallback(async () => {
|
|
115
|
+
await refetch();
|
|
116
|
+
}, [refetch]);
|
|
117
|
+
return useMemo(() => ({ check: query.data ?? null, isChecking: enabled && query.isPending, error: query.error ?? null, refresh }), [query.data, query.isPending, query.error, enabled, refresh]);
|
|
118
|
+
}
|
|
@@ -5,7 +5,7 @@ export interface DevStatePanelProps {
|
|
|
5
5
|
className?: string;
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
|
-
* The dev-only state switcher (
|
|
8
|
+
* The dev-only state switcher (the local loop). Lists the five wallets `npm run dev` seeded, marks
|
|
9
9
|
* the connected one, and flips the connected wallet through the five credential states by calling
|
|
10
10
|
* `setStatus` on the `ReceptorMock` as the deployer, through Anvil's impersonation RPC. Every
|
|
11
11
|
* mounted `useEligibility` for that wallet refetches on the resulting signal, so the page moves
|
|
@@ -23,7 +23,7 @@ function rpc(client, method, params = []) {
|
|
|
23
23
|
return client.request({ method, params });
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
-
* The dev-only state switcher (
|
|
26
|
+
* The dev-only state switcher (the local loop). Lists the five wallets `npm run dev` seeded, marks
|
|
27
27
|
* the connected one, and flips the connected wallet through the five credential states by calling
|
|
28
28
|
* `setStatus` on the `ReceptorMock` as the deployer, through Anvil's impersonation RPC. Every
|
|
29
29
|
* mounted `useEligibility` for that wallet refetches on the resulting signal, so the page moves
|
package/dist/dev/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `@gatedpath/frontend-kit/dev`: the local-loop pieces (
|
|
1
|
+
// `@gatedpath/frontend-kit/dev`: the local-loop pieces (the local loop). Import this path only from
|
|
2
2
|
// code a dev server runs; the scaffold's `web/src/components/DevState.tsx` does, behind a build-time
|
|
3
3
|
// check that leaves nothing of it in a production bundle.
|
|
4
4
|
export { DevStatePanel } from './DevStatePanel.js';
|
|
@@ -12,9 +12,9 @@ export interface LocalWallet {
|
|
|
12
12
|
note: string;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
* `deployments/local.json` as `scripts/dev.mjs` writes it in a scaffold (
|
|
15
|
+
* `deployments/local.json` as `scripts/dev.mjs` writes it in a scaffold (the local loop). `deployer`
|
|
16
16
|
* is Anvil account 7, the account that deployed the mock and holds the contract's admin roles; the
|
|
17
|
-
* panel impersonates it to call `setStatus`. Accounts 0 and 1 are never used (
|
|
17
|
+
* panel impersonates it to call `setStatus`. Accounts 0 and 1 are never used (research question 30 on the builder site).
|
|
18
18
|
*/
|
|
19
19
|
export interface LocalDeployment {
|
|
20
20
|
chainId: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,11 @@ export { receptorMockExpiry } from './expiry.js';
|
|
|
4
4
|
export { notifyEligibilityChanged, onEligibilityChanged } from './events.js';
|
|
5
5
|
export { EligibilityGate, NotConnected, NotVerified, Ineligible, Eligible, ExpiringSoon, Checking, EligibilityError, LastChecked, GasWarning, } from './components.js';
|
|
6
6
|
export type { EligibilityGateProps, StateProps, NotVerifiedProps, IneligibleProps, ExpiringSoonProps, EligibilityErrorProps, LastCheckedProps, GasWarningProps, } from './components.js';
|
|
7
|
-
export {
|
|
7
|
+
export { checkRecipient, useRecipientCheck } from './checkRecipient.js';
|
|
8
|
+
export type { RecipientCheck, RecipientReason, RecipientReasonKind, RecipientVerdict, CheckRecipientOptions, UseRecipientCheckOptions, RecipientCheckResult } from './checkRecipient.js';
|
|
9
|
+
export { verifierAbi, permissionAbi, bootstrapRegistryAbi, receptorMockAbi, receptorMockStatusAbi, registerTokenAbi, identityRegistryAbi, trustedIssuersRegistryAbi, claimTopicsRegistryAbi } from './abi.js';
|
|
10
|
+
export { useRegister } from './useRegister.js';
|
|
11
|
+
export type { RegisterState, RegisterResult, UseRegisterOptions } from './useRegister.js';
|
|
8
12
|
export { ACCESS_URL, FAUCET_URL, VINE_INTERACTION_URL, VINE_FEES_URL } from './urls.js';
|
|
9
13
|
export { explainError } from './explainError.js';
|
|
10
14
|
export type { ExplainedError, ExplainErrorOptions } from './explainError.js';
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,8 @@ export { useEligibility } from './useEligibility.js';
|
|
|
2
2
|
export { receptorMockExpiry } from './expiry.js';
|
|
3
3
|
export { notifyEligibilityChanged, onEligibilityChanged } from './events.js';
|
|
4
4
|
export { EligibilityGate, NotConnected, NotVerified, Ineligible, Eligible, ExpiringSoon, Checking, EligibilityError, LastChecked, GasWarning, } from './components.js';
|
|
5
|
-
export {
|
|
5
|
+
export { checkRecipient, useRecipientCheck } from './checkRecipient.js';
|
|
6
|
+
export { verifierAbi, permissionAbi, bootstrapRegistryAbi, receptorMockAbi, receptorMockStatusAbi, registerTokenAbi, identityRegistryAbi, trustedIssuersRegistryAbi, claimTopicsRegistryAbi } from './abi.js';
|
|
7
|
+
export { useRegister } from './useRegister.js';
|
|
6
8
|
export { ACCESS_URL, FAUCET_URL, VINE_INTERACTION_URL, VINE_FEES_URL } from './urls.js';
|
|
7
9
|
export { explainError } from './explainError.js';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Address } from './useEligibility.js';
|
|
2
|
+
/**
|
|
3
|
+
* Where a wallet stands with one ERC-3643 register, in words a page can switch on.
|
|
4
|
+
* `not-a-register`: the address does not answer `identityRegistry()` with a contract.
|
|
5
|
+
* `registered`: the register's identity registry has verified this wallet.
|
|
6
|
+
* `not-registered`: it has not; only the issuer can change that.
|
|
7
|
+
* `frozen`: verified, but the issuer has frozen this wallet's tokens.
|
|
8
|
+
* `paused`: the issuer has paused every transfer of this register.
|
|
9
|
+
*/
|
|
10
|
+
export type RegisterState = 'disconnected' | 'checking' | 'not-a-register' | 'registered' | 'not-registered' | 'frozen' | 'paused' | 'error';
|
|
11
|
+
export interface UseRegisterOptions {
|
|
12
|
+
chainId?: number;
|
|
13
|
+
/** Refetch floor in milliseconds; the reads also refetch on every new block. Default 30 000. */
|
|
14
|
+
refreshInterval?: number;
|
|
15
|
+
watchBlocks?: boolean;
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface RegisterResult {
|
|
19
|
+
state: RegisterState;
|
|
20
|
+
/** The token's own facts, when it is a register. */
|
|
21
|
+
token: {
|
|
22
|
+
name: string | null;
|
|
23
|
+
symbol: string | null;
|
|
24
|
+
decimals: number | null;
|
|
25
|
+
totalSupply: bigint | null;
|
|
26
|
+
version: string | null;
|
|
27
|
+
paused: boolean | null;
|
|
28
|
+
identityRegistry: Address | null;
|
|
29
|
+
};
|
|
30
|
+
/** The registry's shape: which issuers it trusts and which claim topics it requires. */
|
|
31
|
+
registry: {
|
|
32
|
+
trustedIssuers: Address[];
|
|
33
|
+
claimTopics: bigint[];
|
|
34
|
+
gate: 'registration only' | 'claims' | null;
|
|
35
|
+
};
|
|
36
|
+
/** The wallet's standing, when a wallet was given. */
|
|
37
|
+
wallet: {
|
|
38
|
+
isVerified: boolean | null;
|
|
39
|
+
registered: boolean | null;
|
|
40
|
+
frozen: boolean | null;
|
|
41
|
+
balance: bigint | null;
|
|
42
|
+
} | null;
|
|
43
|
+
blockNumber: bigint | undefined;
|
|
44
|
+
isFetching: boolean;
|
|
45
|
+
error: Error | null;
|
|
46
|
+
refresh: () => Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Reads an ERC-3643 register (a T-REX token) and, if a wallet is given, whether that register's
|
|
50
|
+
* identity registry has verified it. The third of the three gates a holder meets on this network,
|
|
51
|
+
* after `permission.isAllowed` and a dApp's recipe (`useEligibility`). Read-only; batched through
|
|
52
|
+
* Multicall3 by wagmi; cached per chain and refetched on a new block. Only the issuer can register a
|
|
53
|
+
* wallet, so a `not-registered` answer is information for the page, never a button.
|
|
54
|
+
*/
|
|
55
|
+
export declare function useRegister(token: Address | undefined, wallet: Address | undefined, options?: UseRegisterOptions): RegisterResult;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
3
|
+
import { useBlockNumber, useChainId, useReadContracts } from 'wagmi';
|
|
4
|
+
import { registerTokenAbi, identityRegistryAbi, claimTopicsRegistryAbi, trustedIssuersRegistryAbi } from './abi.js';
|
|
5
|
+
const ZERO = '0x0000000000000000000000000000000000000000';
|
|
6
|
+
/**
|
|
7
|
+
* Reads an ERC-3643 register (a T-REX token) and, if a wallet is given, whether that register's
|
|
8
|
+
* identity registry has verified it. The third of the three gates a holder meets on this network,
|
|
9
|
+
* after `permission.isAllowed` and a dApp's recipe (`useEligibility`). Read-only; batched through
|
|
10
|
+
* Multicall3 by wagmi; cached per chain and refetched on a new block. Only the issuer can register a
|
|
11
|
+
* wallet, so a `not-registered` answer is information for the page, never a button.
|
|
12
|
+
*/
|
|
13
|
+
export function useRegister(token, wallet, options = {}) {
|
|
14
|
+
const currentChainId = useChainId();
|
|
15
|
+
const chainId = options.chainId ?? currentChainId;
|
|
16
|
+
const refreshInterval = options.refreshInterval ?? 30_000;
|
|
17
|
+
const watchBlocks = options.watchBlocks ?? true;
|
|
18
|
+
const enabled = (options.enabled ?? true) && Boolean(token);
|
|
19
|
+
const block = useBlockNumber({ chainId, watch: watchBlocks && enabled, query: { enabled: watchBlocks && enabled } });
|
|
20
|
+
const query = { enabled, staleTime: refreshInterval, refetchInterval: refreshInterval, refetchOnWindowFocus: false };
|
|
21
|
+
// Pass one: the token. `identityRegistry()` decides whether there is a register at all.
|
|
22
|
+
const tokenReads = useReadContracts({
|
|
23
|
+
allowFailure: true,
|
|
24
|
+
contracts: [
|
|
25
|
+
{ address: (token ?? ZERO), abi: registerTokenAbi, functionName: 'identityRegistry', chainId },
|
|
26
|
+
{ address: (token ?? ZERO), abi: registerTokenAbi, functionName: 'name', chainId },
|
|
27
|
+
{ address: (token ?? ZERO), abi: registerTokenAbi, functionName: 'symbol', chainId },
|
|
28
|
+
{ address: (token ?? ZERO), abi: registerTokenAbi, functionName: 'decimals', chainId },
|
|
29
|
+
{ address: (token ?? ZERO), abi: registerTokenAbi, functionName: 'totalSupply', chainId },
|
|
30
|
+
{ address: (token ?? ZERO), abi: registerTokenAbi, functionName: 'version', chainId },
|
|
31
|
+
{ address: (token ?? ZERO), abi: registerTokenAbi, functionName: 'paused', chainId },
|
|
32
|
+
],
|
|
33
|
+
query,
|
|
34
|
+
});
|
|
35
|
+
const t = tokenReads.data;
|
|
36
|
+
const registryAddress = (t?.[0]?.status === 'success' ? t[0].result : null) ?? null;
|
|
37
|
+
const isRegister = registryAddress !== null && registryAddress !== ZERO;
|
|
38
|
+
// Pass two: the registry's shape and the wallet's standing, once the registry is known.
|
|
39
|
+
// The wallet reads are always in the batch, against the zero address when no wallet is given, so the
|
|
40
|
+
// batch has one shape and the results can be read by position. Those answers are ignored without a wallet.
|
|
41
|
+
const who = (wallet ?? ZERO);
|
|
42
|
+
const registryFor = (registryAddress ?? ZERO);
|
|
43
|
+
const tokenFor = (token ?? ZERO);
|
|
44
|
+
const detailReads = useReadContracts({
|
|
45
|
+
allowFailure: true,
|
|
46
|
+
contracts: [
|
|
47
|
+
{ address: registryFor, abi: identityRegistryAbi, functionName: 'issuersRegistry', chainId },
|
|
48
|
+
{ address: registryFor, abi: identityRegistryAbi, functionName: 'topicsRegistry', chainId },
|
|
49
|
+
{ address: registryFor, abi: identityRegistryAbi, functionName: 'isVerified', args: [who], chainId },
|
|
50
|
+
{ address: registryFor, abi: identityRegistryAbi, functionName: 'identity', args: [who], chainId },
|
|
51
|
+
{ address: tokenFor, abi: registerTokenAbi, functionName: 'isFrozen', args: [who], chainId },
|
|
52
|
+
{ address: tokenFor, abi: registerTokenAbi, functionName: 'balanceOf', args: [who], chainId },
|
|
53
|
+
],
|
|
54
|
+
query: { ...query, enabled: enabled && isRegister },
|
|
55
|
+
});
|
|
56
|
+
const d = detailReads.data;
|
|
57
|
+
const issuersRegistry = d?.[0]?.status === 'success' ? d[0].result : null;
|
|
58
|
+
const topicsRegistry = d?.[1]?.status === 'success' ? d[1].result : null;
|
|
59
|
+
// Pass three: the issuers and the topics.
|
|
60
|
+
const shapeReads = useReadContracts({
|
|
61
|
+
allowFailure: true,
|
|
62
|
+
contracts: [
|
|
63
|
+
{ address: (issuersRegistry ?? ZERO), abi: trustedIssuersRegistryAbi, functionName: 'getTrustedIssuers', chainId },
|
|
64
|
+
{ address: (topicsRegistry ?? ZERO), abi: claimTopicsRegistryAbi, functionName: 'getClaimTopics', chainId },
|
|
65
|
+
],
|
|
66
|
+
query: { ...query, enabled: enabled && Boolean(issuersRegistry && topicsRegistry) },
|
|
67
|
+
});
|
|
68
|
+
const s = shapeReads.data;
|
|
69
|
+
const refresh = async () => {
|
|
70
|
+
await Promise.all([tokenReads.refetch(), detailReads.refetch(), shapeReads.refetch()]);
|
|
71
|
+
};
|
|
72
|
+
// Refetch on a new block, and only on a new block: wagmi polls the block number for us.
|
|
73
|
+
const lastBlock = useRef(undefined);
|
|
74
|
+
const refetchToken = tokenReads.refetch;
|
|
75
|
+
const refetchDetail = detailReads.refetch;
|
|
76
|
+
const refetchShape = shapeReads.refetch;
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!watchBlocks || !enabled || block.data === undefined)
|
|
79
|
+
return;
|
|
80
|
+
if (lastBlock.current === undefined) {
|
|
81
|
+
lastBlock.current = block.data;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (block.data !== lastBlock.current) {
|
|
85
|
+
lastBlock.current = block.data;
|
|
86
|
+
void refetchToken();
|
|
87
|
+
if (isRegister)
|
|
88
|
+
void refetchDetail();
|
|
89
|
+
if (issuersRegistry && topicsRegistry)
|
|
90
|
+
void refetchShape();
|
|
91
|
+
}
|
|
92
|
+
}, [block.data, watchBlocks, enabled, isRegister, issuersRegistry, topicsRegistry, refetchToken, refetchDetail, refetchShape]);
|
|
93
|
+
return useMemo(() => {
|
|
94
|
+
const ok = (r) => (r && r.status === 'success' ? r.result : null);
|
|
95
|
+
const error = (tokenReads.error ?? detailReads.error ?? shapeReads.error);
|
|
96
|
+
const trustedIssuers = (ok(s?.[0]) ?? []);
|
|
97
|
+
const claimTopics = (ok(s?.[1]) ?? []);
|
|
98
|
+
const paused = ok(t?.[6]);
|
|
99
|
+
const isVerified = wallet ? ok(d?.[2]) : null;
|
|
100
|
+
const identity = wallet ? ok(d?.[3]) : null;
|
|
101
|
+
const frozen = wallet ? ok(d?.[4]) : null;
|
|
102
|
+
const balance = wallet ? ok(d?.[5]) : null;
|
|
103
|
+
let state;
|
|
104
|
+
if (!token)
|
|
105
|
+
state = 'disconnected';
|
|
106
|
+
else if (error)
|
|
107
|
+
state = 'error';
|
|
108
|
+
else if (tokenReads.isPending)
|
|
109
|
+
state = 'checking';
|
|
110
|
+
else if (!isRegister)
|
|
111
|
+
state = 'not-a-register';
|
|
112
|
+
else if (wallet && detailReads.isPending)
|
|
113
|
+
state = 'checking';
|
|
114
|
+
else if (paused)
|
|
115
|
+
state = 'paused';
|
|
116
|
+
else if (wallet && frozen)
|
|
117
|
+
state = 'frozen';
|
|
118
|
+
else if (wallet)
|
|
119
|
+
state = isVerified ? 'registered' : 'not-registered';
|
|
120
|
+
else
|
|
121
|
+
state = 'registered';
|
|
122
|
+
return {
|
|
123
|
+
state,
|
|
124
|
+
token: {
|
|
125
|
+
name: ok(t?.[1]),
|
|
126
|
+
symbol: ok(t?.[2]),
|
|
127
|
+
decimals: ok(t?.[3]),
|
|
128
|
+
totalSupply: ok(t?.[4]),
|
|
129
|
+
version: ok(t?.[5]),
|
|
130
|
+
paused,
|
|
131
|
+
identityRegistry: isRegister ? registryAddress : null,
|
|
132
|
+
},
|
|
133
|
+
registry: {
|
|
134
|
+
trustedIssuers,
|
|
135
|
+
claimTopics,
|
|
136
|
+
gate: isRegister && s ? (claimTopics.length === 0 ? 'registration only' : 'claims') : null,
|
|
137
|
+
},
|
|
138
|
+
wallet: wallet && isRegister ? { isVerified, registered: identity !== null && identity !== ZERO, frozen, balance } : null,
|
|
139
|
+
blockNumber: block.data,
|
|
140
|
+
isFetching: tokenReads.isFetching || detailReads.isFetching || shapeReads.isFetching,
|
|
141
|
+
error,
|
|
142
|
+
refresh,
|
|
143
|
+
};
|
|
144
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
145
|
+
}, [token, wallet, t, d, s, isRegister, registryAddress, tokenReads.isPending, detailReads.isPending, tokenReads.isFetching, detailReads.isFetching, shapeReads.isFetching, tokenReads.error, detailReads.error, shapeReads.error, block.data]);
|
|
146
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gatedpath/frontend-kit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
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
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|