@coordination-network/network-member-governance 0.0.1 → 0.1.0
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/README.md +22 -14
- package/dist/index.cjs +50 -15
- package/dist/index.js +50 -17
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/network-member-calls.ts +82 -17
- package/types/index.d.ts +70 -7
package/README.md
CHANGED
|
@@ -10,9 +10,9 @@ ours. What this package adds is the network member layer: turning an on-chain jo
|
|
|
10
10
|
exact call that admits it.
|
|
11
11
|
|
|
12
12
|
Admission is not a call any single party can make. The
|
|
13
|
-
[`NetworkMemberRegistry`](
|
|
14
|
-
[`NetworkMemberGroup`](
|
|
15
|
-
[`NetworkMemberGovernance`](
|
|
13
|
+
[`NetworkMemberRegistry`](https://github.com/zk-kit/coordination-network/blob/main/packages/contracts/src/NetworkMemberRegistry.sol) and
|
|
14
|
+
[`NetworkMemberGroup`](https://github.com/zk-kit/coordination-network/blob/main/packages/contracts/src/NetworkMemberGroup.sol) are owned by the governance Safe, and
|
|
15
|
+
[`NetworkMemberGovernance`](https://github.com/zk-kit/coordination-network/blob/main/packages/contracts/src/NetworkMemberGovernance.sol) turns each governance
|
|
16
16
|
operation into a single call only that Safe can make, so a member joins only when a majority of the
|
|
17
17
|
already-approved members has approved the same transaction. This package is what a member server runs to
|
|
18
18
|
take part: it never sends anything and holds no state, it turns a join request into the exact
|
|
@@ -35,8 +35,9 @@ One and two members are bootstrap states, not operating ones. Majority rule tole
|
|
|
35
35
|
network permanently. Two is the worse of the two: if one of the pair goes dark, admitting a third needs
|
|
36
36
|
its signature and so does removing it, so nothing can change again. The only way out is for the live member to depart, which hands the network to the key
|
|
37
37
|
that already stopped working. Three is the first size that survives a lost key. The
|
|
38
|
-
[runbook](
|
|
39
|
-
operation takes a threshold argument, so
|
|
38
|
+
[runbook](https://github.com/zk-kit/coordination-network/blob/main/packages/contracts/docs/configuration/network-member-onboarding.md) has the full trade-offs. The rule is computed on chain by `NetworkMemberGovernance.governanceThreshold`, and no
|
|
39
|
+
operation takes a threshold argument, so no module call can move it. Direct Safe calls can; the
|
|
40
|
+
[runbook](https://github.com/zk-kit/coordination-network/blob/main/packages/contracts/docs/configuration/network-member-onboarding.md#the-governance-rule) lists the operating rules. `governanceThreshold(memberCount)`
|
|
40
41
|
here is the same expression for off-chain use: checking that a Safe is configured correctly, and
|
|
41
42
|
reporting what a pending operation will change the threshold to.
|
|
42
43
|
|
|
@@ -58,14 +59,23 @@ So a member never receives a transaction from anyone. Each approver derives the
|
|
|
58
59
|
same event, which is why the approvals add up: identical inputs give an identical `safeTxHash`.
|
|
59
60
|
Admission makes the new member an approver of the next candidate.
|
|
60
61
|
|
|
62
|
+
A candidate that holds its account key as a public key gets the request's key fields from
|
|
63
|
+
`memberAccountKey(publicKey)`, which accepts the 65-byte `0x04` form or the raw 64 bytes of `X || Y` and
|
|
64
|
+
returns `{ memberNetworkId, accountKeyX, accountKeyY }`.
|
|
65
|
+
|
|
61
66
|
### 1. Build the transaction from the request as the chain reported it
|
|
62
67
|
|
|
63
68
|
Building it from the event, rather than from what someone tells you, is what makes approving
|
|
64
|
-
meaningful: the digest you approve covers the
|
|
65
|
-
signer you just vetted.
|
|
69
|
+
meaningful: the digest you approve covers the account key, the Semaphore commitment and the governance
|
|
70
|
+
signer you just vetted. The `networkId` is `keccak256(X || Y)` of the account key, and the registry
|
|
71
|
+
derives it from the key's coordinates, so approving a key approves exactly one id.
|
|
66
72
|
|
|
67
73
|
```ts
|
|
68
|
-
import {
|
|
74
|
+
import {
|
|
75
|
+
NETWORK_MEMBER_JOIN_REQUESTED_EVENT,
|
|
76
|
+
buildAdmissionTransaction,
|
|
77
|
+
decodeJoinRequestLog
|
|
78
|
+
} from "@coordination-network/network-member-governance"
|
|
69
79
|
|
|
70
80
|
const contracts = { governance, registry }
|
|
71
81
|
const context = { safeAddress, chainId } // add safeVersion for a Safe older than the SDK default
|
|
@@ -73,9 +83,7 @@ const context = { safeAddress, chainId } // add safeVersion for a Safe older tha
|
|
|
73
83
|
// The request comes from the candidate's own event, read from a node you trust and decoded here.
|
|
74
84
|
const logs = await client.getLogs({
|
|
75
85
|
address: registry,
|
|
76
|
-
event:
|
|
77
|
-
"event NetworkMemberJoinRequested(bytes32 indexed memberNetworkId, uint256 semaphoreCommitment, address governanceSigner, string metadataURI)"
|
|
78
|
-
),
|
|
86
|
+
event: NETWORK_MEMBER_JOIN_REQUESTED_EVENT,
|
|
79
87
|
args: { memberNetworkId },
|
|
80
88
|
fromBlock: REGISTRY_DEPLOY_BLOCK
|
|
81
89
|
})
|
|
@@ -83,8 +91,8 @@ const logs = await client.getLogs({
|
|
|
83
91
|
// A candidate may publish several requests before it is admitted, so there can be more than one log
|
|
84
92
|
// here. `pickTheVettedRequest` is your own code: it must return the request your organisation actually
|
|
85
93
|
// vetted, not simply the newest, because approving an organisation is not the same as approving a set
|
|
86
|
-
// of keys. Store your decision against a hash of
|
|
87
|
-
// decision.
|
|
94
|
+
// of keys. Store your decision against a hash of the account key, the commitment, the signer and the
|
|
95
|
+
// URI, and treat a later request as a new decision.
|
|
88
96
|
const admission = decodeJoinRequestLog(pickTheVettedRequest(logs), registry)
|
|
89
97
|
|
|
90
98
|
// The Safe's nonce, read from the chain.
|
|
@@ -245,6 +253,6 @@ Only that member's governance signer can call it, so it reaches its own membersh
|
|
|
245
253
|
|
|
246
254
|
## References
|
|
247
255
|
|
|
248
|
-
- [Network member onboarding and governance runbook](
|
|
256
|
+
- [Network member onboarding and governance runbook](https://github.com/zk-kit/coordination-network/blob/main/packages/contracts/docs/configuration/network-member-onboarding.md)
|
|
249
257
|
- [What is Safe](https://docs.safe.global/home/what-is-safe)
|
|
250
258
|
- [Protocol Kit: signing transactions](https://docs.safe.global/sdk/protocol-kit/guides/signatures/transactions)
|
package/dist/index.cjs
CHANGED
|
@@ -335,7 +335,8 @@ const NETWORK_MEMBER_GOVERNANCE_ABI = [
|
|
|
335
335
|
name: "admitMember",
|
|
336
336
|
stateMutability: "nonpayable",
|
|
337
337
|
inputs: [
|
|
338
|
-
{ name: "
|
|
338
|
+
{ name: "accountKeyX", type: "uint256" },
|
|
339
|
+
{ name: "accountKeyY", type: "uint256" },
|
|
339
340
|
{ name: "semaphoreCommitment", type: "uint256" },
|
|
340
341
|
{ name: "governanceSigner", type: "address" },
|
|
341
342
|
{ name: "expectedMemberCount", type: "uint256" }
|
|
@@ -378,6 +379,19 @@ const NETWORK_MEMBER_GOVERNANCE_ABI = [
|
|
|
378
379
|
outputs: [{ name: "", type: "uint256" }]
|
|
379
380
|
}
|
|
380
381
|
];
|
|
382
|
+
/** The event a candidate's join request emits, for watching with `getLogs`. */
|
|
383
|
+
const NETWORK_MEMBER_JOIN_REQUESTED_EVENT = {
|
|
384
|
+
type: "event",
|
|
385
|
+
name: "NetworkMemberJoinRequested",
|
|
386
|
+
inputs: [
|
|
387
|
+
{ name: "memberNetworkId", type: "bytes32", indexed: true },
|
|
388
|
+
{ name: "accountKeyX", type: "uint256", indexed: false },
|
|
389
|
+
{ name: "accountKeyY", type: "uint256", indexed: false },
|
|
390
|
+
{ name: "semaphoreCommitment", type: "uint256", indexed: false },
|
|
391
|
+
{ name: "governanceSigner", type: "address", indexed: false },
|
|
392
|
+
{ name: "metadataURI", type: "string", indexed: false }
|
|
393
|
+
]
|
|
394
|
+
};
|
|
381
395
|
/** The `NetworkMemberRegistry` surface a member server needs directly. */
|
|
382
396
|
const NETWORK_MEMBER_REGISTRY_ABI = [
|
|
383
397
|
{
|
|
@@ -385,23 +399,15 @@ const NETWORK_MEMBER_REGISTRY_ABI = [
|
|
|
385
399
|
name: "requestToJoin",
|
|
386
400
|
stateMutability: "nonpayable",
|
|
387
401
|
inputs: [
|
|
388
|
-
{ name: "
|
|
402
|
+
{ name: "accountKeyX", type: "uint256" },
|
|
403
|
+
{ name: "accountKeyY", type: "uint256" },
|
|
389
404
|
{ name: "semaphoreCommitment", type: "uint256" },
|
|
390
405
|
{ name: "governanceSigner", type: "address" },
|
|
391
406
|
{ name: "metadataURI", type: "string" }
|
|
392
407
|
],
|
|
393
408
|
outputs: []
|
|
394
409
|
},
|
|
395
|
-
|
|
396
|
-
type: "event",
|
|
397
|
-
name: "NetworkMemberJoinRequested",
|
|
398
|
-
inputs: [
|
|
399
|
-
{ name: "memberNetworkId", type: "bytes32", indexed: true },
|
|
400
|
-
{ name: "semaphoreCommitment", type: "uint256", indexed: false },
|
|
401
|
-
{ name: "governanceSigner", type: "address", indexed: false },
|
|
402
|
-
{ name: "metadataURI", type: "string", indexed: false }
|
|
403
|
-
]
|
|
404
|
-
}
|
|
410
|
+
NETWORK_MEMBER_JOIN_REQUESTED_EVENT
|
|
405
411
|
];
|
|
406
412
|
/**
|
|
407
413
|
* Turns a `NetworkMemberJoinRequested` log into the request it announced.
|
|
@@ -428,13 +434,38 @@ function decodeJoinRequestLog(log, registry) {
|
|
|
428
434
|
data: log.data,
|
|
429
435
|
topics: log.topics
|
|
430
436
|
});
|
|
437
|
+
const keyHash = viem.keccak256(viem.concat([viem.pad(viem.toHex(args.accountKeyX)), viem.pad(viem.toHex(args.accountKeyY))]));
|
|
438
|
+
if (args.memberNetworkId !== keyHash) {
|
|
439
|
+
throw new Error(`Log's memberNetworkId ${args.memberNetworkId} is not the hash of the account key it carries`);
|
|
440
|
+
}
|
|
431
441
|
return {
|
|
432
442
|
memberNetworkId: args.memberNetworkId,
|
|
443
|
+
accountKeyX: args.accountKeyX,
|
|
444
|
+
accountKeyY: args.accountKeyY,
|
|
433
445
|
semaphoreCommitment: args.semaphoreCommitment,
|
|
434
446
|
governanceSigner: viem.getAddress(args.governanceSigner),
|
|
435
447
|
metadataURI: args.metadataURI
|
|
436
448
|
};
|
|
437
449
|
}
|
|
450
|
+
/**
|
|
451
|
+
* The networkId and key coordinates of a member's account key, the secp256k1 key whose address is its
|
|
452
|
+
* auth address.
|
|
453
|
+
*
|
|
454
|
+
* @param uncompressedPublicKey The public key: 65 bytes with the `0x04` prefix, or the raw 64 bytes of X || Y.
|
|
455
|
+
* @returns The coordinates, and `keccak256(X || Y)`, which is the id the registry derives from them.
|
|
456
|
+
*/
|
|
457
|
+
function memberAccountKey(uncompressedPublicKey) {
|
|
458
|
+
// `size` rounds an odd digit count up, so the length is checked on the digits themselves.
|
|
459
|
+
if (!/^0x(04)?[0-9a-fA-F]{128}$/.test(uncompressedPublicKey)) {
|
|
460
|
+
throw new Error(`Expected a 64-byte X || Y or a 65-byte 0x04-prefixed public key of hex digits, got ${uncompressedPublicKey.length} characters`);
|
|
461
|
+
}
|
|
462
|
+
const coordinates = viem.slice(uncompressedPublicKey, viem.size(uncompressedPublicKey) - 64);
|
|
463
|
+
return {
|
|
464
|
+
memberNetworkId: viem.keccak256(coordinates),
|
|
465
|
+
accountKeyX: viem.hexToBigInt(viem.slice(coordinates, 0, 32)),
|
|
466
|
+
accountKeyY: viem.hexToBigInt(viem.slice(coordinates, 32))
|
|
467
|
+
};
|
|
468
|
+
}
|
|
438
469
|
/**
|
|
439
470
|
* The single call that admits a network member: its record, its group leaf, and its seat at the Safe.
|
|
440
471
|
*
|
|
@@ -448,7 +479,8 @@ function encodeAdmitMember(admission, memberCount) {
|
|
|
448
479
|
abi: NETWORK_MEMBER_GOVERNANCE_ABI,
|
|
449
480
|
functionName: "admitMember",
|
|
450
481
|
args: [
|
|
451
|
-
admission.
|
|
482
|
+
admission.accountKeyX,
|
|
483
|
+
admission.accountKeyY,
|
|
452
484
|
admission.semaphoreCommitment,
|
|
453
485
|
viem.getAddress(admission.governanceSigner),
|
|
454
486
|
BigInt(memberCount)
|
|
@@ -535,7 +567,7 @@ function encodeExitNetwork(memberNetworkId, merkleProofSiblings) {
|
|
|
535
567
|
}
|
|
536
568
|
/**
|
|
537
569
|
* Calldata for a candidate's own join request, the one permissionless call in this flow. It must be
|
|
538
|
-
* sent from the
|
|
570
|
+
* sent from the address of the account key, and `governanceSigner` must be a different key.
|
|
539
571
|
*
|
|
540
572
|
* @param request The request to announce.
|
|
541
573
|
* @returns Calldata for the registry.
|
|
@@ -545,7 +577,8 @@ function encodeRequestToJoin(request) {
|
|
|
545
577
|
abi: NETWORK_MEMBER_REGISTRY_ABI,
|
|
546
578
|
functionName: "requestToJoin",
|
|
547
579
|
args: [
|
|
548
|
-
request.
|
|
580
|
+
request.accountKeyX,
|
|
581
|
+
request.accountKeyY,
|
|
549
582
|
request.semaphoreCommitment,
|
|
550
583
|
viem.getAddress(request.governanceSigner),
|
|
551
584
|
request.metadataURI ?? ""
|
|
@@ -558,6 +591,7 @@ Object.defineProperty(exports, "OperationType", {
|
|
|
558
591
|
get: function () { return typesKit.OperationType; }
|
|
559
592
|
});
|
|
560
593
|
exports.NETWORK_MEMBER_GOVERNANCE_ABI = NETWORK_MEMBER_GOVERNANCE_ABI;
|
|
594
|
+
exports.NETWORK_MEMBER_JOIN_REQUESTED_EVENT = NETWORK_MEMBER_JOIN_REQUESTED_EVENT;
|
|
561
595
|
exports.NETWORK_MEMBER_REGISTRY_ABI = NETWORK_MEMBER_REGISTRY_ABI;
|
|
562
596
|
exports.NO_REFUND = NO_REFUND;
|
|
563
597
|
exports.ZERO_ADDRESS = ZERO_ADDRESS;
|
|
@@ -576,6 +610,7 @@ exports.encodeRequestToJoin = encodeRequestToJoin;
|
|
|
576
610
|
exports.encodeSafeSignatures = encodeSafeSignatures;
|
|
577
611
|
exports.governanceThreshold = governanceThreshold;
|
|
578
612
|
exports.hashSafeTransaction = hashSafeTransaction;
|
|
613
|
+
exports.memberAccountKey = memberAccountKey;
|
|
579
614
|
exports.onChainApproval = onChainApproval;
|
|
580
615
|
exports.recoverSafeSigner = recoverSafeSigner;
|
|
581
616
|
exports.removalCall = removalCall;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getAddress, encodeFunctionData, recoverAddress, isAddressEqual, decodeEventLog } from 'viem';
|
|
1
|
+
import { getAddress, encodeFunctionData, recoverAddress, isAddressEqual, decodeEventLog, keccak256, concat, pad, toHex, slice, size, hexToBigInt } from 'viem';
|
|
2
2
|
import { encodeMultiSendData, buildSignatureBytes, calculateSafeTransactionHash, generatePreValidatedSignature, generateTypedData, EthSafeSignature, DEFAULT_SAFE_VERSION } from '@safe-global/protocol-kit';
|
|
3
3
|
import { getMultiSendCallOnlyDeployment, getSafeSingletonDeployment } from '@safe-global/safe-deployments';
|
|
4
4
|
import { OperationType } from '@safe-global/types-kit';
|
|
@@ -334,7 +334,8 @@ const NETWORK_MEMBER_GOVERNANCE_ABI = [
|
|
|
334
334
|
name: "admitMember",
|
|
335
335
|
stateMutability: "nonpayable",
|
|
336
336
|
inputs: [
|
|
337
|
-
{ name: "
|
|
337
|
+
{ name: "accountKeyX", type: "uint256" },
|
|
338
|
+
{ name: "accountKeyY", type: "uint256" },
|
|
338
339
|
{ name: "semaphoreCommitment", type: "uint256" },
|
|
339
340
|
{ name: "governanceSigner", type: "address" },
|
|
340
341
|
{ name: "expectedMemberCount", type: "uint256" }
|
|
@@ -377,6 +378,19 @@ const NETWORK_MEMBER_GOVERNANCE_ABI = [
|
|
|
377
378
|
outputs: [{ name: "", type: "uint256" }]
|
|
378
379
|
}
|
|
379
380
|
];
|
|
381
|
+
/** The event a candidate's join request emits, for watching with `getLogs`. */
|
|
382
|
+
const NETWORK_MEMBER_JOIN_REQUESTED_EVENT = {
|
|
383
|
+
type: "event",
|
|
384
|
+
name: "NetworkMemberJoinRequested",
|
|
385
|
+
inputs: [
|
|
386
|
+
{ name: "memberNetworkId", type: "bytes32", indexed: true },
|
|
387
|
+
{ name: "accountKeyX", type: "uint256", indexed: false },
|
|
388
|
+
{ name: "accountKeyY", type: "uint256", indexed: false },
|
|
389
|
+
{ name: "semaphoreCommitment", type: "uint256", indexed: false },
|
|
390
|
+
{ name: "governanceSigner", type: "address", indexed: false },
|
|
391
|
+
{ name: "metadataURI", type: "string", indexed: false }
|
|
392
|
+
]
|
|
393
|
+
};
|
|
380
394
|
/** The `NetworkMemberRegistry` surface a member server needs directly. */
|
|
381
395
|
const NETWORK_MEMBER_REGISTRY_ABI = [
|
|
382
396
|
{
|
|
@@ -384,23 +398,15 @@ const NETWORK_MEMBER_REGISTRY_ABI = [
|
|
|
384
398
|
name: "requestToJoin",
|
|
385
399
|
stateMutability: "nonpayable",
|
|
386
400
|
inputs: [
|
|
387
|
-
{ name: "
|
|
401
|
+
{ name: "accountKeyX", type: "uint256" },
|
|
402
|
+
{ name: "accountKeyY", type: "uint256" },
|
|
388
403
|
{ name: "semaphoreCommitment", type: "uint256" },
|
|
389
404
|
{ name: "governanceSigner", type: "address" },
|
|
390
405
|
{ name: "metadataURI", type: "string" }
|
|
391
406
|
],
|
|
392
407
|
outputs: []
|
|
393
408
|
},
|
|
394
|
-
|
|
395
|
-
type: "event",
|
|
396
|
-
name: "NetworkMemberJoinRequested",
|
|
397
|
-
inputs: [
|
|
398
|
-
{ name: "memberNetworkId", type: "bytes32", indexed: true },
|
|
399
|
-
{ name: "semaphoreCommitment", type: "uint256", indexed: false },
|
|
400
|
-
{ name: "governanceSigner", type: "address", indexed: false },
|
|
401
|
-
{ name: "metadataURI", type: "string", indexed: false }
|
|
402
|
-
]
|
|
403
|
-
}
|
|
409
|
+
NETWORK_MEMBER_JOIN_REQUESTED_EVENT
|
|
404
410
|
];
|
|
405
411
|
/**
|
|
406
412
|
* Turns a `NetworkMemberJoinRequested` log into the request it announced.
|
|
@@ -427,13 +433,38 @@ function decodeJoinRequestLog(log, registry) {
|
|
|
427
433
|
data: log.data,
|
|
428
434
|
topics: log.topics
|
|
429
435
|
});
|
|
436
|
+
const keyHash = keccak256(concat([pad(toHex(args.accountKeyX)), pad(toHex(args.accountKeyY))]));
|
|
437
|
+
if (args.memberNetworkId !== keyHash) {
|
|
438
|
+
throw new Error(`Log's memberNetworkId ${args.memberNetworkId} is not the hash of the account key it carries`);
|
|
439
|
+
}
|
|
430
440
|
return {
|
|
431
441
|
memberNetworkId: args.memberNetworkId,
|
|
442
|
+
accountKeyX: args.accountKeyX,
|
|
443
|
+
accountKeyY: args.accountKeyY,
|
|
432
444
|
semaphoreCommitment: args.semaphoreCommitment,
|
|
433
445
|
governanceSigner: getAddress(args.governanceSigner),
|
|
434
446
|
metadataURI: args.metadataURI
|
|
435
447
|
};
|
|
436
448
|
}
|
|
449
|
+
/**
|
|
450
|
+
* The networkId and key coordinates of a member's account key, the secp256k1 key whose address is its
|
|
451
|
+
* auth address.
|
|
452
|
+
*
|
|
453
|
+
* @param uncompressedPublicKey The public key: 65 bytes with the `0x04` prefix, or the raw 64 bytes of X || Y.
|
|
454
|
+
* @returns The coordinates, and `keccak256(X || Y)`, which is the id the registry derives from them.
|
|
455
|
+
*/
|
|
456
|
+
function memberAccountKey(uncompressedPublicKey) {
|
|
457
|
+
// `size` rounds an odd digit count up, so the length is checked on the digits themselves.
|
|
458
|
+
if (!/^0x(04)?[0-9a-fA-F]{128}$/.test(uncompressedPublicKey)) {
|
|
459
|
+
throw new Error(`Expected a 64-byte X || Y or a 65-byte 0x04-prefixed public key of hex digits, got ${uncompressedPublicKey.length} characters`);
|
|
460
|
+
}
|
|
461
|
+
const coordinates = slice(uncompressedPublicKey, size(uncompressedPublicKey) - 64);
|
|
462
|
+
return {
|
|
463
|
+
memberNetworkId: keccak256(coordinates),
|
|
464
|
+
accountKeyX: hexToBigInt(slice(coordinates, 0, 32)),
|
|
465
|
+
accountKeyY: hexToBigInt(slice(coordinates, 32))
|
|
466
|
+
};
|
|
467
|
+
}
|
|
437
468
|
/**
|
|
438
469
|
* The single call that admits a network member: its record, its group leaf, and its seat at the Safe.
|
|
439
470
|
*
|
|
@@ -447,7 +478,8 @@ function encodeAdmitMember(admission, memberCount) {
|
|
|
447
478
|
abi: NETWORK_MEMBER_GOVERNANCE_ABI,
|
|
448
479
|
functionName: "admitMember",
|
|
449
480
|
args: [
|
|
450
|
-
admission.
|
|
481
|
+
admission.accountKeyX,
|
|
482
|
+
admission.accountKeyY,
|
|
451
483
|
admission.semaphoreCommitment,
|
|
452
484
|
getAddress(admission.governanceSigner),
|
|
453
485
|
BigInt(memberCount)
|
|
@@ -534,7 +566,7 @@ function encodeExitNetwork(memberNetworkId, merkleProofSiblings) {
|
|
|
534
566
|
}
|
|
535
567
|
/**
|
|
536
568
|
* Calldata for a candidate's own join request, the one permissionless call in this flow. It must be
|
|
537
|
-
* sent from the
|
|
569
|
+
* sent from the address of the account key, and `governanceSigner` must be a different key.
|
|
538
570
|
*
|
|
539
571
|
* @param request The request to announce.
|
|
540
572
|
* @returns Calldata for the registry.
|
|
@@ -544,7 +576,8 @@ function encodeRequestToJoin(request) {
|
|
|
544
576
|
abi: NETWORK_MEMBER_REGISTRY_ABI,
|
|
545
577
|
functionName: "requestToJoin",
|
|
546
578
|
args: [
|
|
547
|
-
request.
|
|
579
|
+
request.accountKeyX,
|
|
580
|
+
request.accountKeyY,
|
|
548
581
|
request.semaphoreCommitment,
|
|
549
582
|
getAddress(request.governanceSigner),
|
|
550
583
|
request.metadataURI ?? ""
|
|
@@ -552,4 +585,4 @@ function encodeRequestToJoin(request) {
|
|
|
552
585
|
});
|
|
553
586
|
}
|
|
554
587
|
|
|
555
|
-
export { NETWORK_MEMBER_GOVERNANCE_ABI, NETWORK_MEMBER_REGISTRY_ABI, NO_REFUND, ZERO_ADDRESS, admissionCall, buildAdmissionTransaction, buildRemovalTransaction, buildSafeTransactionData, checkQuorum, decodeJoinRequestLog, encodeAdmitMember, encodeApproveHash, encodeExecTransaction, encodeExitNetwork, encodeRemoveMember, encodeRequestToJoin, encodeSafeSignatures, governanceThreshold, hashSafeTransaction, onChainApproval, recoverSafeSigner, removalCall, resolveMultiSend, signSafeTransaction };
|
|
588
|
+
export { NETWORK_MEMBER_GOVERNANCE_ABI, NETWORK_MEMBER_JOIN_REQUESTED_EVENT, NETWORK_MEMBER_REGISTRY_ABI, NO_REFUND, ZERO_ADDRESS, admissionCall, buildAdmissionTransaction, buildRemovalTransaction, buildSafeTransactionData, checkQuorum, decodeJoinRequestLog, encodeAdmitMember, encodeApproveHash, encodeExecTransaction, encodeExitNetwork, encodeRemoveMember, encodeRequestToJoin, encodeSafeSignatures, governanceThreshold, hashSafeTransaction, memberAccountKey, onChainApproval, recoverSafeSigner, removalCall, resolveMultiSend, signSafeTransaction };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coordination-network/network-member-governance",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Governance Safe transactions for the network member registry: build the admission or removal transaction, sign it as an approved network member, and aggregate the signatures the Safe accepts on chain.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { NO_REFUND, ZERO_ADDRESS } from "./constants"
|
|
|
2
2
|
export { governanceThreshold } from "./governance-rule"
|
|
3
3
|
export {
|
|
4
4
|
NETWORK_MEMBER_GOVERNANCE_ABI,
|
|
5
|
+
NETWORK_MEMBER_JOIN_REQUESTED_EVENT,
|
|
5
6
|
NETWORK_MEMBER_REGISTRY_ABI,
|
|
6
7
|
admissionCall,
|
|
7
8
|
buildAdmissionTransaction,
|
|
@@ -11,6 +12,7 @@ export {
|
|
|
11
12
|
encodeExitNetwork,
|
|
12
13
|
encodeRemoveMember,
|
|
13
14
|
encodeRequestToJoin,
|
|
15
|
+
memberAccountKey,
|
|
14
16
|
removalCall
|
|
15
17
|
} from "./network-member-calls"
|
|
16
18
|
export type {
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import type { MetaTransactionData, SafeTransactionData } from "@safe-global/types-kit"
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
concat,
|
|
4
|
+
decodeEventLog,
|
|
5
|
+
encodeFunctionData,
|
|
6
|
+
getAddress,
|
|
7
|
+
hexToBigInt,
|
|
8
|
+
isAddressEqual,
|
|
9
|
+
keccak256,
|
|
10
|
+
pad,
|
|
11
|
+
size,
|
|
12
|
+
slice,
|
|
13
|
+
toHex,
|
|
14
|
+
type Address,
|
|
15
|
+
type Hex
|
|
16
|
+
} from "viem"
|
|
3
17
|
|
|
4
18
|
import { buildSafeTransactionData } from "./safe-transaction"
|
|
5
19
|
import type { SafeContext } from "./types"
|
|
@@ -22,7 +36,8 @@ export const NETWORK_MEMBER_GOVERNANCE_ABI = [
|
|
|
22
36
|
name: "admitMember",
|
|
23
37
|
stateMutability: "nonpayable",
|
|
24
38
|
inputs: [
|
|
25
|
-
{ name: "
|
|
39
|
+
{ name: "accountKeyX", type: "uint256" },
|
|
40
|
+
{ name: "accountKeyY", type: "uint256" },
|
|
26
41
|
{ name: "semaphoreCommitment", type: "uint256" },
|
|
27
42
|
{ name: "governanceSigner", type: "address" },
|
|
28
43
|
{ name: "expectedMemberCount", type: "uint256" }
|
|
@@ -66,6 +81,20 @@ export const NETWORK_MEMBER_GOVERNANCE_ABI = [
|
|
|
66
81
|
}
|
|
67
82
|
] as const
|
|
68
83
|
|
|
84
|
+
/** The event a candidate's join request emits, for watching with `getLogs`. */
|
|
85
|
+
export const NETWORK_MEMBER_JOIN_REQUESTED_EVENT = {
|
|
86
|
+
type: "event",
|
|
87
|
+
name: "NetworkMemberJoinRequested",
|
|
88
|
+
inputs: [
|
|
89
|
+
{ name: "memberNetworkId", type: "bytes32", indexed: true },
|
|
90
|
+
{ name: "accountKeyX", type: "uint256", indexed: false },
|
|
91
|
+
{ name: "accountKeyY", type: "uint256", indexed: false },
|
|
92
|
+
{ name: "semaphoreCommitment", type: "uint256", indexed: false },
|
|
93
|
+
{ name: "governanceSigner", type: "address", indexed: false },
|
|
94
|
+
{ name: "metadataURI", type: "string", indexed: false }
|
|
95
|
+
]
|
|
96
|
+
} as const
|
|
97
|
+
|
|
69
98
|
/** The `NetworkMemberRegistry` surface a member server needs directly. */
|
|
70
99
|
export const NETWORK_MEMBER_REGISTRY_ABI = [
|
|
71
100
|
{
|
|
@@ -73,23 +102,15 @@ export const NETWORK_MEMBER_REGISTRY_ABI = [
|
|
|
73
102
|
name: "requestToJoin",
|
|
74
103
|
stateMutability: "nonpayable",
|
|
75
104
|
inputs: [
|
|
76
|
-
{ name: "
|
|
105
|
+
{ name: "accountKeyX", type: "uint256" },
|
|
106
|
+
{ name: "accountKeyY", type: "uint256" },
|
|
77
107
|
{ name: "semaphoreCommitment", type: "uint256" },
|
|
78
108
|
{ name: "governanceSigner", type: "address" },
|
|
79
109
|
{ name: "metadataURI", type: "string" }
|
|
80
110
|
],
|
|
81
111
|
outputs: []
|
|
82
112
|
},
|
|
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
|
-
}
|
|
113
|
+
NETWORK_MEMBER_JOIN_REQUESTED_EVENT
|
|
93
114
|
] as const
|
|
94
115
|
|
|
95
116
|
/** Where the governance operations live. */
|
|
@@ -108,8 +129,15 @@ export interface GovernanceContracts {
|
|
|
108
129
|
* admission is a function of the candidate's own announcement.
|
|
109
130
|
*/
|
|
110
131
|
export interface NetworkMemberJoinRequest {
|
|
111
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* The candidate's networkId, `keccak256(X || Y)` of its account key. Filled from the event topic
|
|
134
|
+
* when decoding, and ignored by the encoders, which pass the key.
|
|
135
|
+
*/
|
|
112
136
|
memberNetworkId: Hex
|
|
137
|
+
/** The X coordinate of the candidate's account key, a secp256k1 public key. */
|
|
138
|
+
accountKeyX: bigint
|
|
139
|
+
/** The Y coordinate of the candidate's account key. */
|
|
140
|
+
accountKeyY: bigint
|
|
113
141
|
/** The Semaphore identity commitment it wants in the approved-member group. */
|
|
114
142
|
semaphoreCommitment: bigint
|
|
115
143
|
/** The address it wants added to the governance Safe, so it can vote on later candidates. */
|
|
@@ -167,14 +195,49 @@ export function decodeJoinRequestLog(log: JoinRequestLog, registry?: Address): N
|
|
|
167
195
|
topics: log.topics as [signature: Hex, ...args: Hex[]]
|
|
168
196
|
})
|
|
169
197
|
|
|
198
|
+
const keyHash = keccak256(concat([pad(toHex(args.accountKeyX)), pad(toHex(args.accountKeyY))]))
|
|
199
|
+
if (args.memberNetworkId !== keyHash) {
|
|
200
|
+
throw new Error(`Log's memberNetworkId ${args.memberNetworkId} is not the hash of the account key it carries`)
|
|
201
|
+
}
|
|
202
|
+
|
|
170
203
|
return {
|
|
171
204
|
memberNetworkId: args.memberNetworkId,
|
|
205
|
+
accountKeyX: args.accountKeyX,
|
|
206
|
+
accountKeyY: args.accountKeyY,
|
|
172
207
|
semaphoreCommitment: args.semaphoreCommitment,
|
|
173
208
|
governanceSigner: getAddress(args.governanceSigner),
|
|
174
209
|
metadataURI: args.metadataURI
|
|
175
210
|
}
|
|
176
211
|
}
|
|
177
212
|
|
|
213
|
+
/**
|
|
214
|
+
* The networkId and key coordinates of a member's account key, the secp256k1 key whose address is its
|
|
215
|
+
* auth address.
|
|
216
|
+
*
|
|
217
|
+
* @param uncompressedPublicKey The public key: 65 bytes with the `0x04` prefix, or the raw 64 bytes of X || Y.
|
|
218
|
+
* @returns The coordinates, and `keccak256(X || Y)`, which is the id the registry derives from them.
|
|
219
|
+
*/
|
|
220
|
+
export function memberAccountKey(uncompressedPublicKey: Hex): {
|
|
221
|
+
memberNetworkId: Hex
|
|
222
|
+
accountKeyX: bigint
|
|
223
|
+
accountKeyY: bigint
|
|
224
|
+
} {
|
|
225
|
+
// `size` rounds an odd digit count up, so the length is checked on the digits themselves.
|
|
226
|
+
if (!/^0x(04)?[0-9a-fA-F]{128}$/.test(uncompressedPublicKey)) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Expected a 64-byte X || Y or a 65-byte 0x04-prefixed public key of hex digits, got ${uncompressedPublicKey.length} characters`
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const coordinates = slice(uncompressedPublicKey, size(uncompressedPublicKey) - 64)
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
memberNetworkId: keccak256(coordinates),
|
|
236
|
+
accountKeyX: hexToBigInt(slice(coordinates, 0, 32)),
|
|
237
|
+
accountKeyY: hexToBigInt(slice(coordinates, 32))
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
178
241
|
/**
|
|
179
242
|
* The single call that admits a network member: its record, its group leaf, and its seat at the Safe.
|
|
180
243
|
*
|
|
@@ -188,7 +251,8 @@ export function encodeAdmitMember(admission: NetworkMemberJoinRequest, memberCou
|
|
|
188
251
|
abi: NETWORK_MEMBER_GOVERNANCE_ABI,
|
|
189
252
|
functionName: "admitMember",
|
|
190
253
|
args: [
|
|
191
|
-
admission.
|
|
254
|
+
admission.accountKeyX,
|
|
255
|
+
admission.accountKeyY,
|
|
192
256
|
admission.semaphoreCommitment,
|
|
193
257
|
getAddress(admission.governanceSigner),
|
|
194
258
|
BigInt(memberCount)
|
|
@@ -302,7 +366,7 @@ export function encodeExitNetwork(memberNetworkId: Hex, merkleProofSiblings: big
|
|
|
302
366
|
|
|
303
367
|
/**
|
|
304
368
|
* Calldata for a candidate's own join request, the one permissionless call in this flow. It must be
|
|
305
|
-
* sent from the
|
|
369
|
+
* sent from the address of the account key, and `governanceSigner` must be a different key.
|
|
306
370
|
*
|
|
307
371
|
* @param request The request to announce.
|
|
308
372
|
* @returns Calldata for the registry.
|
|
@@ -312,7 +376,8 @@ export function encodeRequestToJoin(request: NetworkMemberJoinRequest): Hex {
|
|
|
312
376
|
abi: NETWORK_MEMBER_REGISTRY_ABI,
|
|
313
377
|
functionName: "requestToJoin",
|
|
314
378
|
args: [
|
|
315
|
-
request.
|
|
379
|
+
request.accountKeyX,
|
|
380
|
+
request.accountKeyY,
|
|
316
381
|
request.semaphoreCommitment,
|
|
317
382
|
getAddress(request.governanceSigner),
|
|
318
383
|
request.metadataURI ?? ""
|
package/types/index.d.ts
CHANGED
|
@@ -86,8 +86,11 @@ declare const NETWORK_MEMBER_GOVERNANCE_ABI: readonly [{
|
|
|
86
86
|
readonly name: "admitMember";
|
|
87
87
|
readonly stateMutability: "nonpayable";
|
|
88
88
|
readonly inputs: readonly [{
|
|
89
|
-
readonly name: "
|
|
90
|
-
readonly type: "
|
|
89
|
+
readonly name: "accountKeyX";
|
|
90
|
+
readonly type: "uint256";
|
|
91
|
+
}, {
|
|
92
|
+
readonly name: "accountKeyY";
|
|
93
|
+
readonly type: "uint256";
|
|
91
94
|
}, {
|
|
92
95
|
readonly name: "semaphoreCommitment";
|
|
93
96
|
readonly type: "uint256";
|
|
@@ -148,14 +151,47 @@ declare const NETWORK_MEMBER_GOVERNANCE_ABI: readonly [{
|
|
|
148
151
|
readonly type: "uint256";
|
|
149
152
|
}];
|
|
150
153
|
}];
|
|
154
|
+
/** The event a candidate's join request emits, for watching with `getLogs`. */
|
|
155
|
+
declare const NETWORK_MEMBER_JOIN_REQUESTED_EVENT: {
|
|
156
|
+
readonly type: "event";
|
|
157
|
+
readonly name: "NetworkMemberJoinRequested";
|
|
158
|
+
readonly inputs: readonly [{
|
|
159
|
+
readonly name: "memberNetworkId";
|
|
160
|
+
readonly type: "bytes32";
|
|
161
|
+
readonly indexed: true;
|
|
162
|
+
}, {
|
|
163
|
+
readonly name: "accountKeyX";
|
|
164
|
+
readonly type: "uint256";
|
|
165
|
+
readonly indexed: false;
|
|
166
|
+
}, {
|
|
167
|
+
readonly name: "accountKeyY";
|
|
168
|
+
readonly type: "uint256";
|
|
169
|
+
readonly indexed: false;
|
|
170
|
+
}, {
|
|
171
|
+
readonly name: "semaphoreCommitment";
|
|
172
|
+
readonly type: "uint256";
|
|
173
|
+
readonly indexed: false;
|
|
174
|
+
}, {
|
|
175
|
+
readonly name: "governanceSigner";
|
|
176
|
+
readonly type: "address";
|
|
177
|
+
readonly indexed: false;
|
|
178
|
+
}, {
|
|
179
|
+
readonly name: "metadataURI";
|
|
180
|
+
readonly type: "string";
|
|
181
|
+
readonly indexed: false;
|
|
182
|
+
}];
|
|
183
|
+
};
|
|
151
184
|
/** The `NetworkMemberRegistry` surface a member server needs directly. */
|
|
152
185
|
declare const NETWORK_MEMBER_REGISTRY_ABI: readonly [{
|
|
153
186
|
readonly type: "function";
|
|
154
187
|
readonly name: "requestToJoin";
|
|
155
188
|
readonly stateMutability: "nonpayable";
|
|
156
189
|
readonly inputs: readonly [{
|
|
157
|
-
readonly name: "
|
|
158
|
-
readonly type: "
|
|
190
|
+
readonly name: "accountKeyX";
|
|
191
|
+
readonly type: "uint256";
|
|
192
|
+
}, {
|
|
193
|
+
readonly name: "accountKeyY";
|
|
194
|
+
readonly type: "uint256";
|
|
159
195
|
}, {
|
|
160
196
|
readonly name: "semaphoreCommitment";
|
|
161
197
|
readonly type: "uint256";
|
|
@@ -174,6 +210,14 @@ declare const NETWORK_MEMBER_REGISTRY_ABI: readonly [{
|
|
|
174
210
|
readonly name: "memberNetworkId";
|
|
175
211
|
readonly type: "bytes32";
|
|
176
212
|
readonly indexed: true;
|
|
213
|
+
}, {
|
|
214
|
+
readonly name: "accountKeyX";
|
|
215
|
+
readonly type: "uint256";
|
|
216
|
+
readonly indexed: false;
|
|
217
|
+
}, {
|
|
218
|
+
readonly name: "accountKeyY";
|
|
219
|
+
readonly type: "uint256";
|
|
220
|
+
readonly indexed: false;
|
|
177
221
|
}, {
|
|
178
222
|
readonly name: "semaphoreCommitment";
|
|
179
223
|
readonly type: "uint256";
|
|
@@ -203,8 +247,15 @@ interface GovernanceContracts {
|
|
|
203
247
|
* admission is a function of the candidate's own announcement.
|
|
204
248
|
*/
|
|
205
249
|
interface NetworkMemberJoinRequest {
|
|
206
|
-
/**
|
|
250
|
+
/**
|
|
251
|
+
* The candidate's networkId, `keccak256(X || Y)` of its account key. Filled from the event topic
|
|
252
|
+
* when decoding, and ignored by the encoders, which pass the key.
|
|
253
|
+
*/
|
|
207
254
|
memberNetworkId: Hex;
|
|
255
|
+
/** The X coordinate of the candidate's account key, a secp256k1 public key. */
|
|
256
|
+
accountKeyX: bigint;
|
|
257
|
+
/** The Y coordinate of the candidate's account key. */
|
|
258
|
+
accountKeyY: bigint;
|
|
208
259
|
/** The Semaphore identity commitment it wants in the approved-member group. */
|
|
209
260
|
semaphoreCommitment: bigint;
|
|
210
261
|
/** The address it wants added to the governance Safe, so it can vote on later candidates. */
|
|
@@ -248,6 +299,18 @@ interface JoinRequestLog {
|
|
|
248
299
|
* @returns The request the candidate published.
|
|
249
300
|
*/
|
|
250
301
|
declare function decodeJoinRequestLog(log: JoinRequestLog, registry?: Address): NetworkMemberJoinRequest;
|
|
302
|
+
/**
|
|
303
|
+
* The networkId and key coordinates of a member's account key, the secp256k1 key whose address is its
|
|
304
|
+
* auth address.
|
|
305
|
+
*
|
|
306
|
+
* @param uncompressedPublicKey The public key: 65 bytes with the `0x04` prefix, or the raw 64 bytes of X || Y.
|
|
307
|
+
* @returns The coordinates, and `keccak256(X || Y)`, which is the id the registry derives from them.
|
|
308
|
+
*/
|
|
309
|
+
declare function memberAccountKey(uncompressedPublicKey: Hex): {
|
|
310
|
+
memberNetworkId: Hex;
|
|
311
|
+
accountKeyX: bigint;
|
|
312
|
+
accountKeyY: bigint;
|
|
313
|
+
};
|
|
251
314
|
/**
|
|
252
315
|
* The single call that admits a network member: its record, its group leaf, and its seat at the Safe.
|
|
253
316
|
*
|
|
@@ -317,7 +380,7 @@ declare function buildRemovalTransaction(contracts: GovernanceContracts, removal
|
|
|
317
380
|
declare function encodeExitNetwork(memberNetworkId: Hex, merkleProofSiblings: bigint[]): Hex;
|
|
318
381
|
/**
|
|
319
382
|
* Calldata for a candidate's own join request, the one permissionless call in this flow. It must be
|
|
320
|
-
* sent from the
|
|
383
|
+
* sent from the address of the account key, and `governanceSigner` must be a different key.
|
|
321
384
|
*
|
|
322
385
|
* @param request The request to announce.
|
|
323
386
|
* @returns Calldata for the registry.
|
|
@@ -468,5 +531,5 @@ declare function checkQuorum(owners: Address[], threshold: number, signers: Addr
|
|
|
468
531
|
*/
|
|
469
532
|
declare function encodeExecTransaction(context: SafeContext, transaction: SafeTransactionData, signatures: SafeSignature[]): Hex;
|
|
470
533
|
|
|
471
|
-
export { NETWORK_MEMBER_GOVERNANCE_ABI, NETWORK_MEMBER_REGISTRY_ABI, NO_REFUND, ZERO_ADDRESS, admissionCall, buildAdmissionTransaction, buildRemovalTransaction, buildSafeTransactionData, checkQuorum, decodeJoinRequestLog, encodeAdmitMember, encodeApproveHash, encodeExecTransaction, encodeExitNetwork, encodeRemoveMember, encodeRequestToJoin, encodeSafeSignatures, governanceThreshold, hashSafeTransaction, onChainApproval, recoverSafeSigner, removalCall, resolveMultiSend, signSafeTransaction };
|
|
534
|
+
export { NETWORK_MEMBER_GOVERNANCE_ABI, NETWORK_MEMBER_JOIN_REQUESTED_EVENT, NETWORK_MEMBER_REGISTRY_ABI, NO_REFUND, ZERO_ADDRESS, admissionCall, buildAdmissionTransaction, buildRemovalTransaction, buildSafeTransactionData, checkQuorum, decodeJoinRequestLog, encodeAdmitMember, encodeApproveHash, encodeExecTransaction, encodeExitNetwork, encodeRemoveMember, encodeRequestToJoin, encodeSafeSignatures, governanceThreshold, hashSafeTransaction, memberAccountKey, onChainApproval, recoverSafeSigner, removalCall, resolveMultiSend, signSafeTransaction };
|
|
472
535
|
export type { GovernanceContracts, JoinRequestLog, NetworkMemberJoinRequest, NetworkMemberRemoval, QuorumCheck, SafeContext, TypedDataSigner };
|