@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/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
|
+
}
|