@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 +21 -0
- package/README.md +185 -0
- package/dist/index.cjs +975 -0
- package/dist/index.js +952 -0
- package/package.json +59 -0
- package/src/adapters.ts +486 -0
- package/src/deliver.ts +45 -0
- package/src/errors.ts +55 -0
- package/src/index.ts +53 -0
- package/src/issue.ts +29 -0
- package/src/sponsored-relay.ts +242 -0
- package/src/sponsored-revocation.ts +120 -0
- package/src/sponsored.ts +63 -0
- package/src/submit.ts +111 -0
- package/src/types.ts +128 -0
- package/src/user-operation.ts +162 -0
- package/src/witness.ts +30 -0
- package/types/index.d.ts +550 -0
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.
|