@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/src/types.ts ADDED
@@ -0,0 +1,128 @@
1
+ import type { Hex32, SerializedZkCredential } from "@coordination-network/credential"
2
+ import type { IssuerMembershipWitness } from "@coordination-network/issuer-set"
3
+ import type { Address, Hex, buildAnnouncement } from "@coordination-network/identity"
4
+ import type {
5
+ IdentityUpdate,
6
+ NetworkMemberProof,
7
+ RegistryLocation,
8
+ RevocationUpdate
9
+ } from "@coordination-network/network-member-proof"
10
+ import type { Group } from "@semaphore-protocol/group"
11
+
12
+ export type { ApprovalResolver, CredentialSigner } from "@coordination-network/credential"
13
+ export type { ContractLeafReader, IssuerMembershipWitness } from "@coordination-network/issuer-set"
14
+ export type { ChainDomain, RegistrationRequest } from "@coordination-network/user-client"
15
+
16
+ /** issueVC's result: the signed claim-tree credential plus a fresh Merkle path into the approved-issuer set. */
17
+ export interface IssuedCredential {
18
+ credential: SerializedZkCredential
19
+ issuerSetRoot: bigint
20
+ issuerMembershipWitness: IssuerMembershipWitness
21
+ }
22
+
23
+ /**
24
+ * Reads `IdentityRegistry.identity(networkId)`. Empty bytes on chain (unregistered) resolves to
25
+ * null. The method name matches the UserClient's registry-reader port so one adapter serves both.
26
+ */
27
+ export interface IdentityReader {
28
+ /** The IdentityRegistry deployment this reader targets, when the reader knows one. submitRegistration checks it against `domain`. */
29
+ readonly registryAddress?: Address
30
+ getIdentity(networkId: Hex32): Promise<Hex | null>
31
+ }
32
+
33
+ /** `buildAnnouncement`'s return shape, derived rather than restated so it can't drift. */
34
+ export type Announcement = ReturnType<typeof buildAnnouncement>
35
+
36
+ /** Submits a built ERC-5564 announcement on chain. */
37
+ export interface AnnouncementSubmitter {
38
+ announce(announcement: Announcement): Promise<Hex>
39
+ }
40
+
41
+ /**
42
+ * deliverToStealth's result. txHash is the hash of the submitted announce transaction, not proof
43
+ * of inclusion: confirm it before treating the delivery as done.
44
+ */
45
+ export interface DeliveredToStealth {
46
+ stealthAddress: Address
47
+ announcement: Announcement
48
+ txHash: Hex
49
+ }
50
+
51
+ /** The approved-member group, rebuilt fresh on every call so a proof always proves against a live root. */
52
+ export interface GroupMirror {
53
+ currentGroup(): Promise<Group>
54
+ }
55
+
56
+ /** Produces the membership proof authorising one IdentityRegistry write. */
57
+ export interface RegistrationProver {
58
+ /** The IdentityRegistry deployment this proof is bound to. submitRegistration checks it against `domain`. */
59
+ readonly registry: RegistryLocation
60
+ prove(update: IdentityUpdate): Promise<NetworkMemberProof>
61
+ }
62
+
63
+ /** Produces the membership proof authorising one CredentialRevocationRegistry write. */
64
+ export interface RevocationProver {
65
+ /** The CredentialRevocationRegistry deployment this proof is bound to. */
66
+ readonly registry: RegistryLocation
67
+ prove(update: RevocationUpdate): Promise<NetworkMemberProof>
68
+ }
69
+
70
+ /**
71
+ * Facts about a sponsored registerIdentity call: what the disposable sender and the paymaster's
72
+ * deposit looked like before and after, so a caller can show what the paymaster actually paid.
73
+ */
74
+ export interface Sponsorship {
75
+ sender: Address
76
+ senderBalanceBefore: bigint
77
+ senderBalanceAfter: bigint
78
+ userOpHash: Hex
79
+ bundleTxHash: Hex
80
+ actualGasCost: bigint
81
+ paymaster: Address
82
+ paymasterDepositBefore: bigint
83
+ paymasterDepositAfter: bigint
84
+ }
85
+
86
+ /**
87
+ * registerIdentity's result. txHash is the hash of the write (or, when sponsored, the bundle)
88
+ * transaction, not proof of inclusion: confirm it before treating the registration as done.
89
+ */
90
+ export interface RegistrationWrite {
91
+ txHash: Hex
92
+ sponsorship?: Sponsorship
93
+ }
94
+
95
+ /** Submits a network-member-authorised write to `IdentityRegistry`. */
96
+ export interface RegistrationWriter {
97
+ /** The IdentityRegistry deployment this writer targets. submitRegistration checks it against `domain`. */
98
+ readonly registry: RegistryLocation
99
+ /** An `AlreadyRegistered` revert must surface as `RegistryRevertError`: submitRegistration's
100
+ * lost-race handling keys on that one specifically. Any other revert may surface as any error. */
101
+ registerIdentity(networkId: Hex32, metaStealthAddress: Hex, proof: NetworkMemberProof): Promise<RegistrationWrite>
102
+ }
103
+
104
+ /**
105
+ * revoke's result. txHash is the hash of the write (or, when sponsored, the bundle) transaction,
106
+ * not proof of inclusion: confirm it before treating the revocation as done.
107
+ */
108
+ export interface RevocationWrite {
109
+ txHash: Hex
110
+ sponsorship?: Sponsorship
111
+ }
112
+
113
+ /** Submits a network-member-authorised write to `CredentialRevocationRegistry`. */
114
+ export interface RevocationRegistryWriter {
115
+ /** The CredentialRevocationRegistry deployment this writer targets. */
116
+ readonly registry: RegistryLocation
117
+ /** A revert must surface as `RevocationRevertError`: a caller has to tell a stale
118
+ * `lowLeafIndex`, which it retries, from an already-revoked commitment, which it does not. */
119
+ revoke(commitment: bigint, lowLeafIndex: bigint, proof: NetworkMemberProof): Promise<RevocationWrite>
120
+ }
121
+
122
+ /**
123
+ * submitRegistration's result. txHash on `submitted` is the hash of the write transaction, not
124
+ * proof of inclusion: confirm it before treating the registration as done.
125
+ */
126
+ export type SubmitRegistrationResult =
127
+ | { status: "submitted"; networkId: Hex32; metaStealthAddress: Hex; txHash: Hex; sponsorship?: Sponsorship }
128
+ | { status: "alreadyRegistered"; networkId: Hex32; metaStealthAddress: Hex; matchesRequest: boolean }
@@ -0,0 +1,162 @@
1
+ import { encodeFunctionData, http, parseEventLogs } from "viem"
2
+ import type {
3
+ Account,
4
+ Address,
5
+ Chain,
6
+ Hex,
7
+ PublicClient,
8
+ SignedAuthorization,
9
+ Transport,
10
+ TransactionReceipt,
11
+ WalletClient
12
+ } from "viem"
13
+ import {
14
+ createBundlerClient,
15
+ entryPoint08Abi,
16
+ formatUserOperationRequest,
17
+ getUserOperationHash,
18
+ toPackedUserOperation
19
+ } from "viem/account-abstraction"
20
+
21
+ /**
22
+ * A fully assembled ERC-4337 v0.8 user operation for a 7702-delegated, paymaster-sponsored call.
23
+ * Every gas and paymaster field is required, never left to a bundler default: `factory` is always
24
+ * the 7702 marker and `authorization` is always present, since this track has no other kind of op.
25
+ */
26
+ export interface SponsoredUserOperation {
27
+ sender: Address
28
+ nonce: bigint
29
+ factory: "0x7702"
30
+ callData: Hex
31
+ callGasLimit: bigint
32
+ verificationGasLimit: bigint
33
+ preVerificationGas: bigint
34
+ maxFeePerGas: bigint
35
+ maxPriorityFeePerGas: bigint
36
+ paymaster: Address
37
+ paymasterVerificationGasLimit: bigint
38
+ paymasterPostOpGasLimit: bigint
39
+ paymasterData: Hex
40
+ authorization: SignedAuthorization
41
+ signature: Hex
42
+ }
43
+
44
+ /** One submitted user operation's outcome, parsed from the EntryPoint's own events so both submitters report the same shape. */
45
+ export interface UserOperationReceipt {
46
+ transactionHash: Hex
47
+ success: boolean
48
+ actualGasCost: bigint
49
+ actualGasUsed: bigint
50
+ /** Raw `UserOperationRevertReason` data, present only when `success` is false. */
51
+ revertReason?: Hex
52
+ }
53
+
54
+ /** Submits a signed user operation and waits for its outcome. `waitForReceipt` takes the hash `send` returns. */
55
+ export interface UserOperationSubmitter {
56
+ send(userOperation: SponsoredUserOperation): Promise<Hex>
57
+ waitForReceipt(userOpHash: Hex): Promise<UserOperationReceipt>
58
+ }
59
+
60
+ /** Decodes `UserOperationEvent`/`UserOperationRevertReason` for one userOpHash out of a mined transaction's logs. */
61
+ export function parseUserOperationReceipt(
62
+ transactionReceipt: TransactionReceipt,
63
+ entryPointAbi: typeof entryPoint08Abi,
64
+ userOpHash: Hex
65
+ ): UserOperationReceipt {
66
+ const operationEvent = parseEventLogs({
67
+ abi: entryPointAbi,
68
+ eventName: "UserOperationEvent",
69
+ logs: transactionReceipt.logs
70
+ }).find((event) => event.args.userOpHash === userOpHash)
71
+ if (!operationEvent) {
72
+ throw new Error(`no UserOperationEvent for ${userOpHash} in transaction ${transactionReceipt.transactionHash}`)
73
+ }
74
+
75
+ const revertEvent = parseEventLogs({
76
+ abi: entryPointAbi,
77
+ eventName: "UserOperationRevertReason",
78
+ logs: transactionReceipt.logs
79
+ }).find((event) => event.args.userOpHash === userOpHash)
80
+
81
+ return {
82
+ transactionHash: transactionReceipt.transactionHash,
83
+ success: operationEvent.args.success,
84
+ actualGasCost: operationEvent.args.actualGasCost,
85
+ actualGasUsed: operationEvent.args.actualGasUsed,
86
+ revertReason: revertEvent?.args.revertReason
87
+ }
88
+ }
89
+
90
+ /** Submits to a bundler's JSON-RPC (`eth_sendUserOperation`/`eth_getUserOperationReceipt`), for the member server. */
91
+ export class BundlerRpcSubmitter implements UserOperationSubmitter {
92
+ private readonly client: ReturnType<typeof createBundlerClient>
93
+
94
+ constructor(
95
+ url: string,
96
+ private readonly entryPoint: Address
97
+ ) {
98
+ this.client = createBundlerClient({ transport: http(url) })
99
+ }
100
+
101
+ async send(userOperation: SponsoredUserOperation): Promise<Hex> {
102
+ return this.client.request({
103
+ method: "eth_sendUserOperation",
104
+ params: [formatUserOperationRequest(userOperation), this.entryPoint]
105
+ })
106
+ }
107
+
108
+ async waitForReceipt(userOpHash: Hex): Promise<UserOperationReceipt> {
109
+ const receipt = await this.client.waitForUserOperationReceipt({ hash: userOpHash })
110
+ return parseUserOperationReceipt(receipt.receipt, entryPoint08Abi, userOpHash)
111
+ }
112
+ }
113
+
114
+ type EntryPointChainClient = Pick<WalletClient<Transport, Chain, Account>, "account" | "chain" | "sendTransaction"> &
115
+ Pick<PublicClient, "waitForTransactionReceipt">
116
+
117
+ /**
118
+ * Submits straight to the EntryPoint's `handleOps`, for the stack bundler and the integration
119
+ * test: no mempool, one operation per call, the beneficiary is the submitting account itself.
120
+ */
121
+ export class EntryPointSubmitter implements UserOperationSubmitter {
122
+ private readonly transactionHashes = new Map<Hex, Hex>()
123
+
124
+ constructor(
125
+ private readonly client: EntryPointChainClient,
126
+ private readonly entryPoint: Address
127
+ ) {}
128
+
129
+ async send(userOperation: SponsoredUserOperation): Promise<Hex> {
130
+ const userOpHash = getUserOperationHash({
131
+ chainId: this.client.chain.id,
132
+ entryPointAddress: this.entryPoint,
133
+ entryPointVersion: "0.8",
134
+ userOperation
135
+ })
136
+ const data = encodeFunctionData({
137
+ abi: entryPoint08Abi,
138
+ functionName: "handleOps",
139
+ args: [[toPackedUserOperation(userOperation)], this.client.account.address]
140
+ })
141
+ const transactionHash = await this.client.sendTransaction({
142
+ account: this.client.account,
143
+ chain: this.client.chain,
144
+ to: this.entryPoint,
145
+ data,
146
+ authorizationList: [userOperation.authorization]
147
+ })
148
+
149
+ // waitForReceipt only gets the userOpHash back, so the mapping to the transaction that
150
+ // carried it has to live here.
151
+ this.transactionHashes.set(userOpHash, transactionHash)
152
+ return userOpHash
153
+ }
154
+
155
+ async waitForReceipt(userOpHash: Hex): Promise<UserOperationReceipt> {
156
+ const transactionHash = this.transactionHashes.get(userOpHash)
157
+ if (transactionHash === undefined) throw new Error(`no transaction sent for user operation ${userOpHash}`)
158
+
159
+ const transactionReceipt = await this.client.waitForTransactionReceipt({ hash: transactionHash })
160
+ return parseUserOperationReceipt(transactionReceipt, entryPoint08Abi, userOpHash)
161
+ }
162
+ }
package/src/witness.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { ApprovedIssuerSet } from "@coordination-network/issuer-set"
2
+ import type { ContractLeafReader, IssuerMembershipWitness, IssuerPublicKey } from "@coordination-network/issuer-set"
3
+ import { NetworkMemberClientError } from "./errors"
4
+
5
+ export interface FetchIssuerMembershipWitnessParams {
6
+ issuerSetReader: ContractLeafReader
7
+ issuerPublicKey: IssuerPublicKey
8
+ }
9
+
10
+ export interface IssuerMembershipWitnessResult {
11
+ root: bigint
12
+ witness: IssuerMembershipWitness
13
+ }
14
+
15
+ /** Rebuilds the approved-issuer set from its current on-chain leaves and proves membership against the fresh root. */
16
+ export async function fetchIssuerMembershipWitness({
17
+ issuerSetReader,
18
+ issuerPublicKey
19
+ }: FetchIssuerMembershipWitnessParams): Promise<IssuerMembershipWitnessResult> {
20
+ const issuerSet = await ApprovedIssuerSet.fromContract(issuerSetReader)
21
+
22
+ if (!issuerSet.isApproved(issuerPublicKey)) {
23
+ throw new NetworkMemberClientError(
24
+ "ISSUER_KEY_NOT_PUBLISHED",
25
+ "issuer signing key is not published to the approved-issuer set"
26
+ )
27
+ }
28
+
29
+ return { root: issuerSet.root, witness: issuerSet.generateProof(issuerPublicKey) }
30
+ }