@coordination-network/network-member-governance 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 +250 -0
- package/dist/index.cjs +583 -0
- package/dist/index.js +555 -0
- package/package.json +54 -0
- package/src/constants.ts +19 -0
- package/src/governance-rule.ts +20 -0
- package/src/index.ts +43 -0
- package/src/network-member-calls.ts +321 -0
- package/src/safe-transaction.ts +361 -0
- package/src/types.ts +37 -0
- package/types/index.d.ts +472 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export { NO_REFUND, ZERO_ADDRESS } from "./constants"
|
|
2
|
+
export { governanceThreshold } from "./governance-rule"
|
|
3
|
+
export {
|
|
4
|
+
NETWORK_MEMBER_GOVERNANCE_ABI,
|
|
5
|
+
NETWORK_MEMBER_REGISTRY_ABI,
|
|
6
|
+
admissionCall,
|
|
7
|
+
buildAdmissionTransaction,
|
|
8
|
+
buildRemovalTransaction,
|
|
9
|
+
decodeJoinRequestLog,
|
|
10
|
+
encodeAdmitMember,
|
|
11
|
+
encodeExitNetwork,
|
|
12
|
+
encodeRemoveMember,
|
|
13
|
+
encodeRequestToJoin,
|
|
14
|
+
removalCall
|
|
15
|
+
} from "./network-member-calls"
|
|
16
|
+
export type {
|
|
17
|
+
GovernanceContracts,
|
|
18
|
+
JoinRequestLog,
|
|
19
|
+
NetworkMemberJoinRequest,
|
|
20
|
+
NetworkMemberRemoval
|
|
21
|
+
} from "./network-member-calls"
|
|
22
|
+
export {
|
|
23
|
+
buildSafeTransactionData,
|
|
24
|
+
checkQuorum,
|
|
25
|
+
encodeApproveHash,
|
|
26
|
+
encodeExecTransaction,
|
|
27
|
+
encodeSafeSignatures,
|
|
28
|
+
hashSafeTransaction,
|
|
29
|
+
onChainApproval,
|
|
30
|
+
recoverSafeSigner,
|
|
31
|
+
resolveMultiSend,
|
|
32
|
+
signSafeTransaction
|
|
33
|
+
} from "./safe-transaction"
|
|
34
|
+
export type { TypedDataSigner } from "./safe-transaction"
|
|
35
|
+
export { OperationType } from "./types"
|
|
36
|
+
export type {
|
|
37
|
+
MetaTransactionData,
|
|
38
|
+
QuorumCheck,
|
|
39
|
+
SafeContext,
|
|
40
|
+
SafeSignature,
|
|
41
|
+
SafeTransactionData,
|
|
42
|
+
SafeVersion
|
|
43
|
+
} from "./types"
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import type { MetaTransactionData, SafeTransactionData } from "@safe-global/types-kit"
|
|
2
|
+
import { decodeEventLog, encodeFunctionData, getAddress, isAddressEqual, type Address, type Hex } from "viem"
|
|
3
|
+
|
|
4
|
+
import { buildSafeTransactionData } from "./safe-transaction"
|
|
5
|
+
import type { SafeContext } from "./types"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The `NetworkMemberGovernance` surface: the governance operations, each one call.
|
|
9
|
+
*
|
|
10
|
+
* There is no threshold argument anywhere here. The contract computes it from the Safe's live owner
|
|
11
|
+
* count, and removal reads the member's commitment and signer from the registry, so no caller can
|
|
12
|
+
* carry a wrong threshold, point at another member's leaf, or take arbitrary calldata.
|
|
13
|
+
*
|
|
14
|
+
* `expectedMemberCount` is not a policy input either. It pins the member set a transaction was built
|
|
15
|
+
* against, so a transaction goes stale the moment that set changes. It has to exist because a
|
|
16
|
+
* voluntary exit changes the owner count and the threshold without advancing the Safe's nonce, which is
|
|
17
|
+
* what would otherwise invalidate transactions in flight.
|
|
18
|
+
*/
|
|
19
|
+
export const NETWORK_MEMBER_GOVERNANCE_ABI = [
|
|
20
|
+
{
|
|
21
|
+
type: "function",
|
|
22
|
+
name: "admitMember",
|
|
23
|
+
stateMutability: "nonpayable",
|
|
24
|
+
inputs: [
|
|
25
|
+
{ name: "memberNetworkId", type: "bytes32" },
|
|
26
|
+
{ name: "semaphoreCommitment", type: "uint256" },
|
|
27
|
+
{ name: "governanceSigner", type: "address" },
|
|
28
|
+
{ name: "expectedMemberCount", type: "uint256" }
|
|
29
|
+
],
|
|
30
|
+
outputs: []
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
type: "function",
|
|
34
|
+
name: "removeMember",
|
|
35
|
+
stateMutability: "nonpayable",
|
|
36
|
+
inputs: [
|
|
37
|
+
{ name: "memberNetworkId", type: "bytes32" },
|
|
38
|
+
{ name: "merkleProofSiblings", type: "uint256[]" },
|
|
39
|
+
{ name: "expectedMemberCount", type: "uint256" }
|
|
40
|
+
],
|
|
41
|
+
outputs: []
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: "function",
|
|
45
|
+
name: "exitNetwork",
|
|
46
|
+
stateMutability: "nonpayable",
|
|
47
|
+
inputs: [
|
|
48
|
+
{ name: "memberNetworkId", type: "bytes32" },
|
|
49
|
+
{ name: "merkleProofSiblings", type: "uint256[]" }
|
|
50
|
+
],
|
|
51
|
+
outputs: []
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: "function",
|
|
55
|
+
name: "governanceThreshold",
|
|
56
|
+
stateMutability: "pure",
|
|
57
|
+
inputs: [{ name: "memberCount", type: "uint256" }],
|
|
58
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
type: "function",
|
|
62
|
+
name: "thresholdAfterAdmission",
|
|
63
|
+
stateMutability: "view",
|
|
64
|
+
inputs: [],
|
|
65
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
66
|
+
}
|
|
67
|
+
] as const
|
|
68
|
+
|
|
69
|
+
/** The `NetworkMemberRegistry` surface a member server needs directly. */
|
|
70
|
+
export const NETWORK_MEMBER_REGISTRY_ABI = [
|
|
71
|
+
{
|
|
72
|
+
type: "function",
|
|
73
|
+
name: "requestToJoin",
|
|
74
|
+
stateMutability: "nonpayable",
|
|
75
|
+
inputs: [
|
|
76
|
+
{ name: "memberNetworkId", type: "bytes32" },
|
|
77
|
+
{ name: "semaphoreCommitment", type: "uint256" },
|
|
78
|
+
{ name: "governanceSigner", type: "address" },
|
|
79
|
+
{ name: "metadataURI", type: "string" }
|
|
80
|
+
],
|
|
81
|
+
outputs: []
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
type: "event",
|
|
85
|
+
name: "NetworkMemberJoinRequested",
|
|
86
|
+
inputs: [
|
|
87
|
+
{ name: "memberNetworkId", type: "bytes32", indexed: true },
|
|
88
|
+
{ name: "semaphoreCommitment", type: "uint256", indexed: false },
|
|
89
|
+
{ name: "governanceSigner", type: "address", indexed: false },
|
|
90
|
+
{ name: "metadataURI", type: "string", indexed: false }
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
] as const
|
|
94
|
+
|
|
95
|
+
/** Where the governance operations live. */
|
|
96
|
+
export interface GovernanceContracts {
|
|
97
|
+
/** The `NetworkMemberGovernance` orchestrator, enabled as a module on the Safe. */
|
|
98
|
+
governance: Address
|
|
99
|
+
/** The `NetworkMemberRegistry`, for reading records and for a candidate's own join request. */
|
|
100
|
+
registry: Address
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* What a candidate published in its `NetworkMemberJoinRequested` event, which is every argument its
|
|
105
|
+
* admission will carry.
|
|
106
|
+
*
|
|
107
|
+
* Approvers build this from the event rather than from anything another party sends them, so an
|
|
108
|
+
* admission is a function of the candidate's own announcement.
|
|
109
|
+
*/
|
|
110
|
+
export interface NetworkMemberJoinRequest {
|
|
111
|
+
/** The candidate's networkId. */
|
|
112
|
+
memberNetworkId: Hex
|
|
113
|
+
/** The Semaphore identity commitment it wants in the approved-member group. */
|
|
114
|
+
semaphoreCommitment: bigint
|
|
115
|
+
/** The address it wants added to the governance Safe, so it can vote on later candidates. */
|
|
116
|
+
governanceSigner: Address
|
|
117
|
+
/** Where the vetting material lives, or an empty string. */
|
|
118
|
+
metadataURI?: string
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** What removing a member needs. */
|
|
122
|
+
export interface NetworkMemberRemoval {
|
|
123
|
+
/** The member's networkId. Its commitment and signer are read on chain. */
|
|
124
|
+
memberNetworkId: Hex
|
|
125
|
+
/**
|
|
126
|
+
* The membership proof for its group leaf, computed off chain against the group state this
|
|
127
|
+
* transaction will execute in. If another group write lands first, recompute it and approve the
|
|
128
|
+
* rebuilt transaction.
|
|
129
|
+
*/
|
|
130
|
+
merkleProofSiblings: bigint[]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The shape of a log this package can read: enough of viem's `Log` to decode it. */
|
|
134
|
+
export interface JoinRequestLog {
|
|
135
|
+
/** The contract that emitted it. Checked against the registry when both are given. */
|
|
136
|
+
address?: Address
|
|
137
|
+
/** The log's topics, the first being the event signature. */
|
|
138
|
+
topics: readonly Hex[]
|
|
139
|
+
/** The log's non-indexed data. */
|
|
140
|
+
data: Hex
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Turns a `NetworkMemberJoinRequested` log into the request it announced.
|
|
145
|
+
*
|
|
146
|
+
* This is the only honest source of a {NetworkMemberJoinRequest}: an approver reads the candidate's own
|
|
147
|
+
* event from a node it trusts and decodes it here, so the transaction it approves is a function of what
|
|
148
|
+
* the chain reported. Building the object by hand from something another party sent gives up that
|
|
149
|
+
* property, and with it the meaning of the approval.
|
|
150
|
+
*
|
|
151
|
+
* It stays free of any RPC dependency: fetching the log is the caller's job, with `getLogs` against its
|
|
152
|
+
* own node. What this removes is the field mapping, and the chance of a silent mismatch in it.
|
|
153
|
+
*
|
|
154
|
+
* @param log The log, as viem returns it from `getLogs`.
|
|
155
|
+
* @param registry The registry the log must have come from. Checked when the log carries an address.
|
|
156
|
+
* @returns The request the candidate published.
|
|
157
|
+
*/
|
|
158
|
+
export function decodeJoinRequestLog(log: JoinRequestLog, registry?: Address): NetworkMemberJoinRequest {
|
|
159
|
+
if (log.address && registry && !isAddressEqual(log.address, getAddress(registry))) {
|
|
160
|
+
throw new Error(`Log came from ${log.address}, not from the registry at ${getAddress(registry)}`)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const { args } = decodeEventLog({
|
|
164
|
+
abi: NETWORK_MEMBER_REGISTRY_ABI,
|
|
165
|
+
eventName: "NetworkMemberJoinRequested",
|
|
166
|
+
data: log.data,
|
|
167
|
+
topics: log.topics as [signature: Hex, ...args: Hex[]]
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
memberNetworkId: args.memberNetworkId,
|
|
172
|
+
semaphoreCommitment: args.semaphoreCommitment,
|
|
173
|
+
governanceSigner: getAddress(args.governanceSigner),
|
|
174
|
+
metadataURI: args.metadataURI
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The single call that admits a network member: its record, its group leaf, and its seat at the Safe.
|
|
180
|
+
*
|
|
181
|
+
* @param admission The vetted join request being approved.
|
|
182
|
+
* @param memberCount The Safe's live owner count, from `getOwners().length`. Pinned into the call so the
|
|
183
|
+
* transaction goes stale if the member set changes before it executes.
|
|
184
|
+
* @returns Calldata for the orchestrator.
|
|
185
|
+
*/
|
|
186
|
+
export function encodeAdmitMember(admission: NetworkMemberJoinRequest, memberCount: number): Hex {
|
|
187
|
+
return encodeFunctionData({
|
|
188
|
+
abi: NETWORK_MEMBER_GOVERNANCE_ABI,
|
|
189
|
+
functionName: "admitMember",
|
|
190
|
+
args: [
|
|
191
|
+
admission.memberNetworkId,
|
|
192
|
+
admission.semaphoreCommitment,
|
|
193
|
+
getAddress(admission.governanceSigner),
|
|
194
|
+
BigInt(memberCount)
|
|
195
|
+
]
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The single call that removes a network member from all three places.
|
|
201
|
+
*
|
|
202
|
+
* @param removal The member to remove, and its membership proof.
|
|
203
|
+
* @param memberCount The Safe's live owner count, pinned for the reason given on {encodeAdmitMember}.
|
|
204
|
+
* @returns Calldata for the orchestrator.
|
|
205
|
+
*/
|
|
206
|
+
export function encodeRemoveMember(removal: NetworkMemberRemoval, memberCount: number): Hex {
|
|
207
|
+
return encodeFunctionData({
|
|
208
|
+
abi: NETWORK_MEMBER_GOVERNANCE_ABI,
|
|
209
|
+
functionName: "removeMember",
|
|
210
|
+
args: [removal.memberNetworkId, removal.merkleProofSiblings, BigInt(memberCount)]
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The one call an admission is, as a Safe meta-transaction. */
|
|
215
|
+
export function admissionCall(
|
|
216
|
+
contracts: GovernanceContracts,
|
|
217
|
+
admission: NetworkMemberJoinRequest,
|
|
218
|
+
memberCount: number
|
|
219
|
+
): MetaTransactionData {
|
|
220
|
+
return { to: getAddress(contracts.governance), value: "0", data: encodeAdmitMember(admission, memberCount) }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** The one call a removal is, as a Safe meta-transaction. */
|
|
224
|
+
export function removalCall(
|
|
225
|
+
contracts: GovernanceContracts,
|
|
226
|
+
removal: NetworkMemberRemoval,
|
|
227
|
+
memberCount: number
|
|
228
|
+
): MetaTransactionData {
|
|
229
|
+
return { to: getAddress(contracts.governance), value: "0", data: encodeRemoveMember(removal, memberCount) }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Builds the Safe transaction that admits a network member.
|
|
234
|
+
*
|
|
235
|
+
* One plain call to the orchestrator, so there is no MultiSend batch to decode and no threshold in the
|
|
236
|
+
* calldata to check. The result is a pure function of the on-chain request and the nonce, which is what
|
|
237
|
+
* makes building safe to do in parallel: two approvers who build from the same chain state produce
|
|
238
|
+
* byte-identical transactions and therefore the same `safeTxHash`, so their approvals accumulate
|
|
239
|
+
* against one hash instead of competing.
|
|
240
|
+
*
|
|
241
|
+
* @param contracts The governance contracts.
|
|
242
|
+
* @param admission The vetted join request being approved.
|
|
243
|
+
* @param context The Safe and chain the transaction is for.
|
|
244
|
+
* @param nonce The Safe's current `nonce()`.
|
|
245
|
+
* @param memberCount The Safe's live owner count, from `getOwners().length`.
|
|
246
|
+
* @returns The transaction data to sign.
|
|
247
|
+
*/
|
|
248
|
+
export function buildAdmissionTransaction(
|
|
249
|
+
contracts: GovernanceContracts,
|
|
250
|
+
admission: NetworkMemberJoinRequest,
|
|
251
|
+
context: SafeContext,
|
|
252
|
+
nonce: number,
|
|
253
|
+
memberCount: number
|
|
254
|
+
): SafeTransactionData {
|
|
255
|
+
return buildSafeTransactionData([admissionCall(contracts, admission, memberCount)], context, nonce)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Builds the Safe transaction that removes a network member.
|
|
260
|
+
*
|
|
261
|
+
* @param contracts The governance contracts.
|
|
262
|
+
* @param removal The member to remove, and its membership proof.
|
|
263
|
+
* @param context The Safe and chain the transaction is for.
|
|
264
|
+
* @param nonce The Safe's current `nonce()`.
|
|
265
|
+
* @param memberCount The Safe's live owner count, from `getOwners().length`.
|
|
266
|
+
* @returns The transaction data to sign.
|
|
267
|
+
*/
|
|
268
|
+
export function buildRemovalTransaction(
|
|
269
|
+
contracts: GovernanceContracts,
|
|
270
|
+
removal: NetworkMemberRemoval,
|
|
271
|
+
context: SafeContext,
|
|
272
|
+
nonce: number,
|
|
273
|
+
memberCount: number
|
|
274
|
+
): SafeTransactionData {
|
|
275
|
+
return buildSafeTransactionData([removalCall(contracts, removal, memberCount)], context, nonce)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Calldata for a member leaving the network of its own accord: its record, its group leaf and its seat
|
|
280
|
+
* at the Safe, in one call.
|
|
281
|
+
*
|
|
282
|
+
* Not a Safe transaction. The member's own governance signer sends this directly to the orchestrator,
|
|
283
|
+
* because departure does not need the consent of the members being left, so there is no transaction
|
|
284
|
+
* for anyone else to approve. It is also how a member that is winding down avoids leaving a seat behind
|
|
285
|
+
* that nobody can sign for, which would still count toward everyone else's threshold.
|
|
286
|
+
*
|
|
287
|
+
* Rejoining means a new request and a new place in the group, and every credential the member issued
|
|
288
|
+
* stops verifying once its record is gone.
|
|
289
|
+
*
|
|
290
|
+
* @param memberNetworkId The member leaving. The caller must be its governance signer.
|
|
291
|
+
* @param merkleProofSiblings The membership proof for its group leaf, computed off chain against the
|
|
292
|
+
* group state this transaction will execute in.
|
|
293
|
+
* @returns Calldata to send to the orchestrator.
|
|
294
|
+
*/
|
|
295
|
+
export function encodeExitNetwork(memberNetworkId: Hex, merkleProofSiblings: bigint[]): Hex {
|
|
296
|
+
return encodeFunctionData({
|
|
297
|
+
abi: NETWORK_MEMBER_GOVERNANCE_ABI,
|
|
298
|
+
functionName: "exitNetwork",
|
|
299
|
+
args: [memberNetworkId, merkleProofSiblings]
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Calldata for a candidate's own join request, the one permissionless call in this flow. It must be
|
|
305
|
+
* sent from the auth address the networkId encodes, and `governanceSigner` must be a different key.
|
|
306
|
+
*
|
|
307
|
+
* @param request The request to announce.
|
|
308
|
+
* @returns Calldata for the registry.
|
|
309
|
+
*/
|
|
310
|
+
export function encodeRequestToJoin(request: NetworkMemberJoinRequest): Hex {
|
|
311
|
+
return encodeFunctionData({
|
|
312
|
+
abi: NETWORK_MEMBER_REGISTRY_ABI,
|
|
313
|
+
functionName: "requestToJoin",
|
|
314
|
+
args: [
|
|
315
|
+
request.memberNetworkId,
|
|
316
|
+
request.semaphoreCommitment,
|
|
317
|
+
getAddress(request.governanceSigner),
|
|
318
|
+
request.metadataURI ?? ""
|
|
319
|
+
]
|
|
320
|
+
})
|
|
321
|
+
}
|