@coordination-network/network-member-client 0.0.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ethereum Foundation
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,185 @@
1
+ # @coordination-network/network-member-client
2
+
3
+ The two things a network-member app does: issue a UserApproval credential a wallet can present, and
4
+ deliver value to a recipient's stealth address by announcing it on chain. Both gate on the caller
5
+ being currently approved, checked against the chain rather than assumed from configuration.
6
+
7
+ Built on packages this repo already ships — `issueZkCredential` and the registry client
8
+ (`packages/credential`), `ApprovedIssuerSet` (`packages/issuer-set`), and the stealth-address and
9
+ announcement builders (`packages/identity`). This package composes them; it adds no new
10
+ cryptography of its own.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ yarn add @coordination-network/network-member-client
16
+ ```
17
+
18
+ ## issueVC
19
+
20
+ Issues a UserApproval credential and, alongside it, the issuer's fresh Merkle path into the
21
+ approved-issuer set: the witness a holder's wallet needs to prove the issuer was approved when it
22
+ signed, without revealing which issuer.
23
+
24
+ ```ts
25
+ import { issueVC } from "@coordination-network/network-member-client"
26
+
27
+ const { credential, issuerSetRoot, issuerMembershipWitness } = await issueVC(params, {
28
+ signer,
29
+ approval,
30
+ issuerSetReader
31
+ })
32
+ ```
33
+
34
+ `signer` and `approval` are `packages/credential`'s ports (`CredentialSigner`, `ApprovalResolver`);
35
+ `issuerSetReader` is `packages/issuer-set`'s `ContractLeafReader`. `params` is
36
+ `ZkCredentialIssueParams` from `packages/credential`, unchanged.
37
+
38
+ The witness goes stale the moment the approved-issuer set changes: a key published or retired
39
+ after issuance moves the root. `fetchIssuerMembershipWitness` is the same lookup on its own, for a
40
+ member server to re-serve a fresh witness to a wallet whose credential has outlived the root it
41
+ was issued against. `ApprovedIssuerSetClient` pins all reads of one rebuild to a single block and
42
+ must not serve two rebuilds at once, so a server handling concurrent requests should construct a
43
+ fresh reader per request.
44
+
45
+ ```ts
46
+ import { fetchIssuerMembershipWitness } from "@coordination-network/network-member-client"
47
+
48
+ const { root, witness } = await fetchIssuerMembershipWitness({ issuerSetReader, issuerPublicKey })
49
+ ```
50
+
51
+ An approved issuer whose signing key was never published to the set fails at `issueVC` with
52
+ `ISSUER_KEY_NOT_PUBLISHED` — a misconfiguration caught at issuance, not at the holder's first
53
+ presentation.
54
+
55
+ ## deliverToStealth
56
+
57
+ Delivers value to a network member's recipient by resolving their registered identity to a
58
+ meta-stealth address and announcing a fresh stealth address for it, ERC-5564 style.
59
+
60
+ ```ts
61
+ import { deliverToStealth } from "@coordination-network/network-member-client"
62
+
63
+ const { stealthAddress, announcement, txHash } = await deliverToStealth(
64
+ { memberNetworkId, recipientNetworkId },
65
+ { approval, identityReader, submitter }
66
+ )
67
+ ```
68
+
69
+ `ERC5564Announcer.announce` is permissionless — anyone can call it — so "an unapproved member is
70
+ rejected" is not a rule the chain enforces on its own. This package enforces it client-side, and
71
+ first: `approval.isApprovedNetworkMember(memberNetworkId)` runs before any other port is touched,
72
+ so an unapproved caller never even reads the recipient's identity.
73
+
74
+ - `MEMBER_NOT_APPROVED` — the member is not currently approved.
75
+ - `RECIPIENT_NOT_REGISTERED` — the recipient's networkId has no identity on `IdentityRegistry`.
76
+ - `INVALID_META_STEALTH_ADDRESS` — the resolved identity doesn't decode to a valid meta-stealth
77
+ address.
78
+
79
+ `txHash` is returned when the announce transaction is submitted, not when it lands. A dropped or
80
+ reverted transaction leaves no `Announcement` event for the recipient's indexer to find, so
81
+ confirm the transaction (viem's `waitForTransactionReceipt`, for example) before recording the
82
+ delivery as done.
83
+
84
+ `stealthAddress` and `announcement` are fresh on every call: `buildAnnouncement` draws a new
85
+ ephemeral key each time, so the same recipient gets a different, unlinkable address for every
86
+ delivery.
87
+
88
+ ## submitRegistration
89
+
90
+ Submits a user's signed registration consent (`@coordination-network/user-client`'s
91
+ `RegistrationRequest`) to `IdentityRegistry`, as an approved network member. The user's EIP-712
92
+ signature is off-chain consent; the chain itself only checks the member's anonymous Semaphore
93
+ membership proof, so this is the check that stands between an arbitrary request and a spent proof.
94
+
95
+ ```ts
96
+ import { submitRegistration } from "@coordination-network/network-member-client"
97
+
98
+ const result = await submitRegistration(request, { domain, identityReader, prover, writer })
99
+ ```
100
+
101
+ `domain` and `identityReader` are the same `ChainDomain` and `IdentityReader` used elsewhere in
102
+ this package; `prover` and `writer` are new ports (`RegistrationProver`, `RegistrationWriter`), each
103
+ carrying a `registry` (`RegistryLocation`) naming the `IdentityRegistry` deployment it's bound to.
104
+ `identityReader` can optionally report the same thing through `registryAddress`.
105
+ `submitRegistration` checks all three against `domain` before anything else, including before
106
+ `request` is verified: this is a configuration guard over what the ports report about
107
+ themselves, not a check on the request, so a mismatch throws a plain `Error` rather than a coded
108
+ one — a member app that wires a port at the wrong registry fails loudly instead of proving or
109
+ writing against the wrong deployment. `request` itself is then verified against `domain` — it is
110
+ `domain`, not any port, that the user's own signature is pinned to, so a forged or mistargeted
111
+ request never reaches the registry.
112
+
113
+ - If the registry already holds an entry for the request's networkId, `submitRegistration` returns
114
+ `{ status: "alreadyRegistered", networkId, metaStealthAddress, matchesRequest }` without
115
+ generating a proof. `matchesRequest` is whether the stored entry equals the request's own
116
+ `metaStealthAddress` — false means someone else's registration is occupying this networkId.
117
+ - Otherwise it proves membership and writes, returning `{ status: "submitted", networkId,
118
+ metaStealthAddress, txHash }`. As with `deliverToStealth`, `txHash` is the hash of the submitted
119
+ transaction, not proof it landed — confirm it before treating the registration as done.
120
+ - Losing the race between the read above and the write (someone else registers the same networkId
121
+ in between) also resolves to `alreadyRegistered`, re-read from the registry rather than trusted
122
+ from the revert.
123
+
124
+ Error codes, beyond the two already listed under `deliverToStealth`:
125
+
126
+ - `INVALID_REGISTRATION_REQUEST` — the request failed `verifyRegistrationRequest` against `domain`.
127
+ - `REGISTRY_UNAVAILABLE` — a registry read or write failed for a reason other than a named revert.
128
+ - `GROUP_MIRROR_UNAVAILABLE` — the approved-member group could not be rebuilt, or its rebuilt root
129
+ did not match the chain's.
130
+ - `MEMBER_NOT_IN_GROUP` — the member's own identity is not (or no longer) a member of the group.
131
+ - `REGISTRATION_REJECTED` — `IdentityRegistry` or Semaphore reverted for any other named reason; the
132
+ thrown `RegistryRevertError`'s `errorName` carries which one.
133
+
134
+ The approved-member group is re-mirrored on every call, never cached: a stale mirror proves
135
+ membership against a root the chain may have moved past, and a proof against a root outside
136
+ Semaphore's recent-root window is rejected on submission anyway.
137
+
138
+ ## Adapters
139
+
140
+ `adapters.ts` wires the ports above to viem, so a member app has one way to run this against a real
141
+ chain:
142
+
143
+ - Approval comes from `@coordination-network/credential`'s `NetworkMemberRegistryClient`, used
144
+ as-is for both issuance and delivery.
145
+ - `IdentityRegistryClient` — `IdentityReader` over `IdentityRegistry.identity`.
146
+ - `ApprovedIssuerSetClient` — `ContractLeafReader` over `ApprovedIssuerSet`'s paging getters.
147
+ - `ERC5564AnnouncerClient` — `AnnouncementSubmitter` over `ERC5564Announcer.announce`, given a
148
+ `WalletClient` already carrying its account and chain.
149
+ - `SemaphoreGroupClient` — `GroupMirror` over Semaphore's group events, replayed into a
150
+ `@semaphore-protocol/core` `Group` and checked against `getMerkleTreeRoot` on every call.
151
+ `fromBlock` should be the Semaphore deployment's own block. Each call makes one
152
+ topic-filtered `eth_getLogs` per event type over `[fromBlock, toBlock]`, where `toBlock` is the
153
+ block pinned at the start of that call. Range caps on hosted RPCs are the scaling limit here — a
154
+ chunked or incremental mirror is a follow-up, not something this client does today.
155
+ - `SemaphoreRegistrationProver` — `RegistrationProver` that mirrors the group and generates the
156
+ membership proof, given the member's own Semaphore `Identity` and where the registry lives.
157
+ - `IdentityRegistryWriter` — `RegistrationWriter` over `IdentityRegistry.registerIdentity`, given a
158
+ `WalletClient` and the `RegistryLocation` it targets. Maps a named revert (`AlreadyRegistered`, a
159
+ `Semaphore__*` error, ...) to `RegistryRevertError` rather than letting viem's raw error through.
160
+ `submitRegistration`'s lost-race handling depends on that revert surfacing at the write call, which
161
+ in turn depends on viem's own gas estimation — that only runs for a local account. A `WalletClient`
162
+ backed by a JSON-RPC account (a browser wallet, for example) skips it, so `AlreadyRegistered`
163
+ surfaces only once the transaction is mined, not at submission.
164
+
165
+ None of it is required. Every port is a plain interface, so an app can supply its own
166
+ implementation — a different chain library, a queued submitter — without touching `issueVC` or
167
+ `deliverToStealth`.
168
+
169
+ ## What this package does not do
170
+
171
+ - **It holds no keys.** `signer`, `approval`, `identityReader`, `submitter`, `prover` and `writer`
172
+ are ports the caller supplies. Issuing a credential, announcing a stealth address, or proving and
173
+ submitting a registration happens with keys and clients this package never sees — including the
174
+ member's own Semaphore identity, which lives inside whatever `RegistrationProver` the caller
175
+ constructs.
176
+ - **It owns no RPC.** The functions take ports, not a client or an RPC URL. `adapters.ts` is one
177
+ convenient wiring, not the only one; nothing here calls `createPublicClient` on its own.
178
+ - **It does not revoke.** `packages/credential`'s revocation registry and commitments are
179
+ unrelated to this package's two flows.
180
+ - **It does not move assets.** `deliverToStealth` announces a stealth address; it transfers
181
+ nothing. The ERC-1155 scoped-token machinery in
182
+ [`specs/05-stealth-addresses.md`](../../specs/05-stealth-addresses.md) doesn't exist yet, and
183
+ wiring it in is a separate change.
184
+ - **It is not `apps/issuer-service`.** That app still runs its own HTTP layer over pieces this
185
+ package composes. Refactoring it onto `issueVC` is a follow-up issue, not part of this package.