@getcofund/protocol 0.0.0-stage → 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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +414 -2
  3. package/dist/btc/index.d.ts +12 -0
  4. package/dist/btc/index.js +13 -0
  5. package/dist/btc/psbt.d.ts +121 -0
  6. package/dist/btc/psbt.js +377 -0
  7. package/dist/btc/reader.d.ts +42 -0
  8. package/dist/btc/reader.js +85 -0
  9. package/dist/btc/select.d.ts +48 -0
  10. package/dist/btc/select.js +59 -0
  11. package/dist/btc/spend-rules.d.ts +24 -0
  12. package/dist/btc/spend-rules.js +43 -0
  13. package/dist/btc/spend.d.ts +136 -0
  14. package/dist/btc/spend.js +180 -0
  15. package/dist/btc/types.d.ts +65 -0
  16. package/dist/btc/types.js +62 -0
  17. package/dist/btc/vault/bip341-nums.d.ts +35 -0
  18. package/dist/btc/vault/bip341-nums.js +121 -0
  19. package/dist/btc/vault/build.d.ts +5 -0
  20. package/dist/btc/vault/build.js +136 -0
  21. package/dist/btc/vault/canonical-order.d.ts +10 -0
  22. package/dist/btc/vault/canonical-order.js +18 -0
  23. package/dist/btc/vault/from-contract-id.d.ts +9 -0
  24. package/dist/btc/vault/from-contract-id.js +74 -0
  25. package/dist/btc/vault/helpers.d.ts +73 -0
  26. package/dist/btc/vault/helpers.js +129 -0
  27. package/dist/btc/vault/index.d.ts +21 -0
  28. package/dist/btc/vault/index.js +21 -0
  29. package/dist/btc/vault/limits.d.ts +9 -0
  30. package/dist/btc/vault/limits.js +9 -0
  31. package/dist/btc/vault/participant-order.d.ts +69 -0
  32. package/dist/btc/vault/participant-order.js +135 -0
  33. package/dist/btc/vault/script.d.ts +13 -0
  34. package/dist/btc/vault/script.js +39 -0
  35. package/dist/btc/vault/spend.d.ts +63 -0
  36. package/dist/btc/vault/spend.js +93 -0
  37. package/dist/btc/vault/tx-size.d.ts +66 -0
  38. package/dist/btc/vault/tx-size.js +71 -0
  39. package/dist/btc/vault/types.d.ts +49 -0
  40. package/dist/btc/vault/types.js +1 -0
  41. package/dist/btc/vault/vault-policy.d.ts +109 -0
  42. package/dist/btc/vault/vault-policy.js +213 -0
  43. package/dist/btc/vault/vaultContractUtil.d.ts +80 -0
  44. package/dist/btc/vault/vaultContractUtil.js +87 -0
  45. package/dist/btc/vault/xpub.d.ts +28 -0
  46. package/dist/btc/vault/xpub.js +41 -0
  47. package/dist/btc/witness.d.ts +101 -0
  48. package/dist/btc/witness.js +439 -0
  49. package/dist/btc-policies/activation.d.ts +76 -0
  50. package/dist/btc-policies/activation.js +153 -0
  51. package/dist/btc-policies/index.d.ts +7 -0
  52. package/dist/btc-policies/index.js +8 -0
  53. package/dist/btc-policies/policy.d.ts +55 -0
  54. package/dist/btc-policies/policy.js +210 -0
  55. package/dist/evm/abis/index.d.ts +10 -0
  56. package/dist/evm/abis/index.js +11 -0
  57. package/dist/evm/abis/policies.d.ts +279 -0
  58. package/dist/evm/abis/policies.js +363 -0
  59. package/dist/evm/abis/sponsor-guard.d.ts +201 -0
  60. package/dist/evm/abis/sponsor-guard.js +263 -0
  61. package/dist/evm/abis/state.d.ts +989 -0
  62. package/dist/evm/abis/state.js +1290 -0
  63. package/dist/evm/abis/users.d.ts +408 -0
  64. package/dist/evm/abis/users.js +532 -0
  65. package/dist/evm/abis/vault-factory.d.ts +92 -0
  66. package/dist/evm/abis/vault-factory.js +126 -0
  67. package/dist/evm/abis/vault.d.ts +343 -0
  68. package/dist/evm/abis/vault.js +444 -0
  69. package/dist/evm/address.d.ts +2 -0
  70. package/dist/evm/address.js +4 -0
  71. package/dist/evm/calls/common.d.ts +30 -0
  72. package/dist/evm/calls/common.js +51 -0
  73. package/dist/evm/calls/index.d.ts +11 -0
  74. package/dist/evm/calls/index.js +4 -0
  75. package/dist/evm/calls/policies.d.ts +25 -0
  76. package/dist/evm/calls/policies.js +173 -0
  77. package/dist/evm/calls/users.d.ts +61 -0
  78. package/dist/evm/calls/users.js +353 -0
  79. package/dist/evm/calls/vault.d.ts +25 -0
  80. package/dist/evm/calls/vault.js +184 -0
  81. package/dist/evm/chain.d.ts +74 -0
  82. package/dist/evm/chain.js +48 -0
  83. package/dist/evm/company-name.d.ts +46 -0
  84. package/dist/evm/company-name.js +64 -0
  85. package/dist/evm/deployment.d.ts +47 -0
  86. package/dist/evm/deployment.js +110 -0
  87. package/dist/evm/eip712.d.ts +271 -0
  88. package/dist/evm/eip712.js +224 -0
  89. package/dist/evm/index.d.ts +20 -0
  90. package/dist/evm/index.js +21 -0
  91. package/dist/evm/reads.d.ts +64 -0
  92. package/dist/evm/reads.js +131 -0
  93. package/dist/evm/signatures.d.ts +57 -0
  94. package/dist/evm/signatures.js +105 -0
  95. package/dist/evm/simulate/balances.d.ts +76 -0
  96. package/dist/evm/simulate/balances.js +201 -0
  97. package/dist/evm/simulate/describe.d.ts +79 -0
  98. package/dist/evm/simulate/describe.js +188 -0
  99. package/dist/evm/simulate/index.d.ts +12 -0
  100. package/dist/evm/simulate/index.js +13 -0
  101. package/dist/evm/simulate/own-node.d.ts +62 -0
  102. package/dist/evm/simulate/own-node.js +122 -0
  103. package/dist/evm/simulate/request.d.ts +86 -0
  104. package/dist/evm/simulate/request.js +235 -0
  105. package/dist/evm/simulate/revert.d.ts +29 -0
  106. package/dist/evm/simulate/revert.js +192 -0
  107. package/dist/evm/simulate/run.d.ts +51 -0
  108. package/dist/evm/simulate/run.js +214 -0
  109. package/dist/evm/stocks/decode-stock-swap.d.ts +32 -0
  110. package/dist/evm/stocks/decode-stock-swap.js +79 -0
  111. package/dist/evm/stocks/encode-swap.d.ts +45 -0
  112. package/dist/evm/stocks/encode-swap.js +45 -0
  113. package/dist/evm/stocks/index.d.ts +9 -0
  114. package/dist/evm/stocks/index.js +10 -0
  115. package/dist/evm/stocks/quote.d.ts +19 -0
  116. package/dist/evm/stocks/quote.js +46 -0
  117. package/dist/evm/stocks/stock-pools.d.ts +61 -0
  118. package/dist/evm/stocks/stock-pools.js +179 -0
  119. package/dist/evm/submitters.d.ts +32 -0
  120. package/dist/evm/submitters.js +105 -0
  121. package/dist/evm/types.d.ts +146 -0
  122. package/dist/evm/types.js +128 -0
  123. package/dist/keys/index.d.ts +6 -0
  124. package/dist/keys/index.js +7 -0
  125. package/dist/keys/keys.d.ts +63 -0
  126. package/dist/keys/keys.js +80 -0
  127. package/dist/keys/xpub-ownership.d.ts +10 -0
  128. package/dist/keys/xpub-ownership.js +13 -0
  129. package/package.json +60 -3
@@ -0,0 +1,173 @@
1
+ import { encodeFunctionData, isAddressEqual, zeroAddress, } from "viem";
2
+ import { policiesAbi } from "../abis/policies.js";
3
+ import { stateAbi } from "../abis/state.js";
4
+ import { readContract } from "../chain.js";
5
+ import { cofundClientId, POLICY_DEACTIVATION_TYPES, TRANSACTION_POLICY_ACTIVATION_TYPES, TRANSFER_POLICY_ACTIVATION_TYPES, typedData, typedDataDigest, } from "../eip712.js";
6
+ import { readPolicy, readRequiredPolicyActivationSignatures, readUser, } from "../reads.js";
7
+ import { ascendingSigners, recoverSigner, refuse } from "../signatures.js";
8
+ import { requireActiveAdmin, requireAuthIdUnspent, uint, } from "./common.js";
9
+ /**
10
+ * What both activations check, in the helper's order: the type is supported,
11
+ * the shape holds, the `authId` is unspent, enough active admins signed
12
+ * (`requiredPolicyActivationSignatures`), and State accepts the policy. The
13
+ * signatures come back in the order the helper walks them.
14
+ */
15
+ async function checkActivation(ctx, message, types, signatures, option) {
16
+ const { deployment, reader } = ctx;
17
+ const clientId = cofundClientId(message.company);
18
+ const threshold = uint(message.threshold, "threshold");
19
+ const supported = await readContract(reader, {
20
+ address: deployment.state,
21
+ abi: stateAbi,
22
+ functionName: "isSupportedType",
23
+ args: [message.policyType],
24
+ });
25
+ if (!supported) {
26
+ refuse("InvalidPolicyType", `State does not support "${message.policyType}"`);
27
+ }
28
+ if (message.title === "") {
29
+ refuse("InvalidTitle", "the title is empty");
30
+ }
31
+ if (threshold === 0n) {
32
+ refuse("InvalidThreshold", "the threshold is 0");
33
+ }
34
+ if (message.signerIds.length === 0 ||
35
+ BigInt(message.signerIds.length) < threshold) {
36
+ refuse("SignersLength", `${message.signerIds.length} signers cannot meet a threshold of ${threshold}`);
37
+ }
38
+ if (isAddressEqual(option.address, zeroAddress)) {
39
+ refuse("InvalidType", `the ${option.what} is the zero address`);
40
+ }
41
+ await requireAuthIdUnspent(ctx, clientId, message.authId);
42
+ const required = await readRequiredPolicyActivationSignatures(reader, deployment, clientId, threshold);
43
+ if (signatures.length < required) {
44
+ refuse("SignerCountMismatch", `activating a ${threshold}-signature policy needs ${required} admin signatures; ${signatures.length} given`);
45
+ }
46
+ const digest = typedDataDigest(typedData(deployment.eip712Domain, types, message));
47
+ const ordered = await ascendingSigners(digest, signatures, {
48
+ what: "signature",
49
+ });
50
+ for (const { signer } of ordered) {
51
+ await requireActiveAdmin(ctx, clientId, signer, {
52
+ unknown: "InvalidSigner",
53
+ inactive: "InvalidSigner",
54
+ notAdmin: "InvalidSigner",
55
+ }, "signer");
56
+ }
57
+ if (await readPolicy(reader, deployment, clientId, message.policyId)) {
58
+ refuse("InvalidPolicy", `policy ${message.policyId} was already activated`);
59
+ }
60
+ const seen = new Set();
61
+ for (const signerId of message.signerIds) {
62
+ const user = await readUser(reader, deployment, clientId, signerId);
63
+ if (!user?.active) {
64
+ refuse("InvalidPolicySigner", `${signerId} is not an active user of this cofund`);
65
+ }
66
+ if (seen.has(signerId)) {
67
+ refuse("DuplicatePolicySigner", `${signerId} is listed twice`);
68
+ }
69
+ seen.add(signerId);
70
+ }
71
+ return {
72
+ clientId,
73
+ threshold,
74
+ ordered: ordered.map(({ signature }) => signature),
75
+ };
76
+ }
77
+ /**
78
+ * `activateTransferPolicy`: a transfer policy (signers, threshold, token, cap,
79
+ * recipient allowlist), on active admins' `TransferPolicyActivation`.
80
+ */
81
+ export async function activateTransferPolicyCall(ctx, input) {
82
+ const { message } = input;
83
+ const { clientId, threshold, ordered } = await checkActivation(ctx, message, TRANSFER_POLICY_ACTIVATION_TYPES, input.signatures, { address: message.token, what: "token" });
84
+ for (const recipient of message.recipients) {
85
+ if (isAddressEqual(recipient, zeroAddress)) {
86
+ refuse("InvalidRecipient", "a recipient is the zero address");
87
+ }
88
+ }
89
+ return {
90
+ to: ctx.deployment.policies,
91
+ data: encodeFunctionData({
92
+ abi: policiesAbi,
93
+ functionName: "activateTransferPolicy",
94
+ args: [
95
+ {
96
+ clientId,
97
+ company: message.company,
98
+ policyId: message.policyId,
99
+ title: message.title,
100
+ policyType: message.policyType,
101
+ signerIds: message.signerIds,
102
+ threshold,
103
+ token: message.token,
104
+ maxAmount: uint(message.maxAmount, "maxAmount"),
105
+ recipients: message.recipients,
106
+ },
107
+ message.authId,
108
+ ordered,
109
+ ],
110
+ }),
111
+ };
112
+ }
113
+ /**
114
+ * `activateTransactionPolicy`: a transaction policy (signers, threshold, the
115
+ * wrapper and its function), on active admins' `TransactionPolicyActivation`.
116
+ */
117
+ export async function activateTransactionPolicyCall(ctx, input) {
118
+ const { message } = input;
119
+ const { clientId, threshold, ordered } = await checkActivation(ctx, message, TRANSACTION_POLICY_ACTIVATION_TYPES, input.signatures, { address: message.wrapper, what: "wrapper" });
120
+ return {
121
+ to: ctx.deployment.policies,
122
+ data: encodeFunctionData({
123
+ abi: policiesAbi,
124
+ functionName: "activateTransactionPolicy",
125
+ args: [
126
+ {
127
+ clientId,
128
+ company: message.company,
129
+ policyId: message.policyId,
130
+ title: message.title,
131
+ policyType: message.policyType,
132
+ signerIds: message.signerIds,
133
+ threshold,
134
+ wrapper: message.wrapper,
135
+ func: message.func,
136
+ },
137
+ message.authId,
138
+ ordered,
139
+ ],
140
+ }),
141
+ };
142
+ }
143
+ /** `deactivatePolicy`: an active policy off, on one active admin's `PolicyDeactivation`. */
144
+ export async function deactivatePolicyCall(ctx, input) {
145
+ const { deployment, reader } = ctx;
146
+ const { message, signature } = input;
147
+ const clientId = cofundClientId(message.company);
148
+ await requireAuthIdUnspent(ctx, clientId, message.authId);
149
+ const signer = await recoverSigner(typedDataDigest(typedData(deployment.eip712Domain, POLICY_DEACTIVATION_TYPES, message)), signature, { what: "the admin's signature" });
150
+ await requireActiveAdmin(ctx, clientId, signer, {
151
+ unknown: "UnauthorizedCaller",
152
+ inactive: "InactiveUser",
153
+ notAdmin: "InvalidAdmin",
154
+ }, "the signer");
155
+ const policy = await readPolicy(reader, deployment, clientId, message.policyId);
156
+ if (!policy?.active) {
157
+ refuse("InvalidPolicy", `policy ${message.policyId} is not active`);
158
+ }
159
+ return {
160
+ to: deployment.policies,
161
+ data: encodeFunctionData({
162
+ abi: policiesAbi,
163
+ functionName: "deactivatePolicy",
164
+ args: [
165
+ clientId,
166
+ message.company,
167
+ message.policyId,
168
+ message.authId,
169
+ signature,
170
+ ],
171
+ }),
172
+ };
173
+ }
@@ -0,0 +1,61 @@
1
+ import { type Address, type Hex } from "viem";
2
+ import { type EvmCall } from "../chain.ts";
3
+ import { type CompleteOnboardingMessage, type InviteUserMessage, type RemoveUserMessage, type RotateUserMessage } from "../eip712.ts";
4
+ import { type DeadlineOptions, type EvmContext } from "./common.ts";
5
+ /**
6
+ * `createClient`: a cofund and its founding admin, on the founder's own
7
+ * `InviteUser` over themselves (`callerId` and `userId` both the admin's id,
8
+ * an admin, position "admin"). Refused unless the window is open, the
9
+ * signature is the founder's, the `authId` is unspent, no cofund has this
10
+ * company yet and `tier` is one State knows.
11
+ */
12
+ export declare function createClientCall(ctx: EvmContext, input: {
13
+ message: InviteUserMessage;
14
+ founder: Address;
15
+ signature: Hex;
16
+ /** A subscription tier State knows (`subscriptionTierExists`). */
17
+ tier: string;
18
+ }, options?: DeadlineOptions): Promise<EvmCall>;
19
+ /**
20
+ * `addUser`: a seat, on the inviting admin's `InviteUser`, co-signed by more
21
+ * admins for an admin seat (`requiredAdminInviteSignatures`, the inviter's
22
+ * own counted), and accepted by the invitee's key over the same message.
23
+ * Co-signatures are put in the order the helper walks them.
24
+ */
25
+ export declare function addUserCall(ctx: EvmContext, input: {
26
+ message: InviteUserMessage;
27
+ adminSignature: Hex;
28
+ /** Other admins' signatures over the same message; read only for an admin seat. */
29
+ coSignatures?: Hex[];
30
+ invitee: Address;
31
+ inviteeSignature: Hex;
32
+ }, options?: DeadlineOptions): Promise<EvmCall>;
33
+ /**
34
+ * `removeUser`: a member out, on active admins' `RemoveUser`: one for a
35
+ * member, two for an admin while the cofund has more than one. Signatures are
36
+ * put in the order the helper walks them.
37
+ */
38
+ export declare function removeUserCall(ctx: EvmContext, input: {
39
+ message: RemoveUserMessage;
40
+ signatures: Hex[];
41
+ }): Promise<EvmCall>;
42
+ /**
43
+ * `rotateUser`: a user's seat moved to a new key, which always signs
44
+ * `RotateUser`, and either the current key signs it too (`currentKeySignature`)
45
+ * or, for a lost key, active admins do (`adminSignatures`: two while the
46
+ * cofund has more than one admin), never both.
47
+ */
48
+ export declare function rotateUserCall(ctx: EvmContext, input: {
49
+ message: RotateUserMessage;
50
+ newKeySignature: Hex;
51
+ currentKeySignature?: Hex;
52
+ adminSignatures?: Hex[];
53
+ }): Promise<EvmCall>;
54
+ /**
55
+ * `completeOnboarding`: the cofund's onboarding phase closed, on one active
56
+ * admin's `CompleteOnboarding`. Irreversible: refused once it is closed.
57
+ */
58
+ export declare function completeOnboardingCall(ctx: EvmContext, input: {
59
+ message: CompleteOnboardingMessage;
60
+ signature: Hex;
61
+ }): Promise<EvmCall>;
@@ -0,0 +1,353 @@
1
+ import { encodeFunctionData, isAddressEqual, zeroAddress, } from "viem";
2
+ import { stateAbi } from "../abis/state.js";
3
+ import { usersAbi } from "../abis/users.js";
4
+ import { readContract } from "../chain.js";
5
+ import { COMPLETE_ONBOARDING_TYPES, cofundClientId, INVITE_USER_TYPES, REMOVE_USER_TYPES, ROTATE_USER_TYPES, typedData, typedDataDigest, } from "../eip712.js";
6
+ import { readActiveAdmins, readClientVault, readOnboardingComplete, readRequiredAdminInviteSignatures, readUser, readUserIdByAddress, } from "../reads.js";
7
+ import { ascendingSigners, recoverSigner, refuse } from "../signatures.js";
8
+ import { requireActiveAdmin, requireAuthIdUnspent, requireWindow, uint, } from "./common.js";
9
+ /**
10
+ * The users helper's calls (`CFHelpersUsersv1`), each checked as the helper
11
+ * and State would check it, then encoded. None checks `msg.sender`, which is
12
+ * the submitter's: `createClient` and `addUser` only accept the sponsor guard
13
+ * or a Cofund admin as the sender (`_requirePlatform`), so they go through
14
+ * `guardSubmitter`; the rest accept anyone.
15
+ */
16
+ /** `Users._requireSignerIsActiveAdmin`. */
17
+ const ADMIN_SIGNER = {
18
+ unknown: "InvalidSigner",
19
+ inactive: "InactiveSigner",
20
+ notAdmin: "InvalidSigner",
21
+ };
22
+ function inviteDigest(ctx, message) {
23
+ return typedDataDigest(typedData(ctx.deployment.eip712Domain, INVITE_USER_TYPES, message));
24
+ }
25
+ /** `State.addUser` / `State.newClient`: the seat is free, and the address holds no other. */
26
+ async function requireFreeSeat(ctx, clientId, userId, address) {
27
+ const { deployment, reader } = ctx;
28
+ if (await readUser(reader, deployment, clientId, userId)) {
29
+ refuse("UserExists", `user ${userId} already has a seat in this cofund`);
30
+ }
31
+ const holder = await readUserIdByAddress(reader, deployment, clientId, address);
32
+ if (holder !== null) {
33
+ refuse("AddressExists", `${address} already holds seat ${holder}`);
34
+ }
35
+ }
36
+ /**
37
+ * `createClient`: a cofund and its founding admin, on the founder's own
38
+ * `InviteUser` over themselves (`callerId` and `userId` both the admin's id,
39
+ * an admin, position "admin"). Refused unless the window is open, the
40
+ * signature is the founder's, the `authId` is unspent, no cofund has this
41
+ * company yet and `tier` is one State knows.
42
+ */
43
+ export async function createClientCall(ctx, input, options = {}) {
44
+ const { deployment, reader } = ctx;
45
+ const { message, founder, signature, tier } = input;
46
+ const clientId = cofundClientId(message.company);
47
+ const expiresAt = uint(message.expiresAt, "expiresAt");
48
+ await requireWindow(reader, expiresAt, options);
49
+ if (message.authId === "") {
50
+ refuse("InvalidParams", "the authId is empty");
51
+ }
52
+ if (isAddressEqual(founder, zeroAddress)) {
53
+ refuse("InvalidParams", "the founder is the zero address");
54
+ }
55
+ if (message.callerId !== message.userId ||
56
+ !message.userIsAdmin ||
57
+ message.userPosition !== "admin") {
58
+ refuse("InvalidSignature", 'the founder signs InviteUser over themselves: callerId = userId, an admin, position "admin"');
59
+ }
60
+ const signer = await recoverSigner(inviteDigest(ctx, message), signature, {
61
+ what: "the founder's signature",
62
+ });
63
+ if (!isAddressEqual(signer, founder)) {
64
+ refuse("InvalidSignature", `the founder's signature is ${signer}'s`);
65
+ }
66
+ await requireAuthIdUnspent(ctx, clientId, message.authId);
67
+ if (await readClientVault(reader, deployment, clientId)) {
68
+ refuse("ClientExists", `a cofund named "${message.company}" exists`);
69
+ }
70
+ const tierExists = await readContract(reader, {
71
+ address: deployment.state,
72
+ abi: stateAbi,
73
+ functionName: "subscriptionTierExists",
74
+ args: [tier],
75
+ });
76
+ if (!tierExists) {
77
+ refuse("InvalidTier", `State has no tier "${tier}"`);
78
+ }
79
+ await requireFreeSeat(ctx, clientId, message.userId, founder);
80
+ if (isAddressEqual(deployment.vaults, zeroAddress)) {
81
+ refuse("InvalidHelper", "no vault factory is registered");
82
+ }
83
+ return {
84
+ to: deployment.users,
85
+ data: encodeFunctionData({
86
+ abi: usersAbi,
87
+ functionName: "createClient",
88
+ args: [
89
+ clientId,
90
+ message.company,
91
+ message.userId,
92
+ founder,
93
+ tier,
94
+ expiresAt,
95
+ message.authId,
96
+ signature,
97
+ ],
98
+ }),
99
+ };
100
+ }
101
+ /**
102
+ * `addUser`: a seat, on the inviting admin's `InviteUser`, co-signed by more
103
+ * admins for an admin seat (`requiredAdminInviteSignatures`, the inviter's
104
+ * own counted), and accepted by the invitee's key over the same message.
105
+ * Co-signatures are put in the order the helper walks them.
106
+ */
107
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: the helper's checks, one by one in its order
108
+ export async function addUserCall(ctx, input, options = {}) {
109
+ const { deployment, reader } = ctx;
110
+ const { message, adminSignature, invitee, inviteeSignature } = input;
111
+ const clientId = cofundClientId(message.company);
112
+ const expiresAt = uint(message.expiresAt, "expiresAt");
113
+ await requireWindow(reader, expiresAt, options);
114
+ if (message.authId === "") {
115
+ refuse("InvalidParams", "the authId is empty");
116
+ }
117
+ const digest = inviteDigest(ctx, message);
118
+ const inviter = await recoverSigner(digest, adminSignature, {
119
+ what: "the inviting admin's signature",
120
+ });
121
+ const derived = await readUserIdByAddress(reader, deployment, clientId, inviter);
122
+ if (derived === null || derived !== message.callerId) {
123
+ refuse("UnauthorizedCaller", `the inviting signature is ${inviter}'s, which is not ${message.callerId}'s seat`);
124
+ }
125
+ const admin = await readUser(reader, deployment, clientId, message.callerId);
126
+ if (!admin?.active) {
127
+ refuse("InactiveSigner", `the inviting admin ${message.callerId} is not active`);
128
+ }
129
+ if (!admin.isAdmin) {
130
+ refuse("InvalidSigner", `the inviter ${message.callerId} is not an admin`);
131
+ }
132
+ await requireAuthIdUnspent(ctx, clientId, message.authId);
133
+ let coSignatures = input.coSignatures ?? [];
134
+ if (message.userIsAdmin) {
135
+ const required = await readRequiredAdminInviteSignatures(reader, deployment, clientId);
136
+ const ordered = await ascendingSigners(digest, coSignatures, {
137
+ what: "co-signature",
138
+ });
139
+ for (const { signer } of ordered) {
140
+ if (isAddressEqual(signer, inviter)) {
141
+ refuse("InvalidSignature", "the inviting admin also co-signed");
142
+ }
143
+ await requireActiveAdmin(ctx, clientId, signer, ADMIN_SIGNER, "co-signer");
144
+ }
145
+ if (ordered.length + 1 < required) {
146
+ refuse("ThresholdNotMet", `an admin seat needs ${required} admin signatures, the inviter's included; ${ordered.length + 1} given`);
147
+ }
148
+ coSignatures = ordered.map(({ signature }) => signature);
149
+ }
150
+ if (isAddressEqual(invitee, zeroAddress)) {
151
+ refuse("InvalidSignature", "the invitee is the zero address");
152
+ }
153
+ const accepted = await recoverSigner(digest, inviteeSignature, {
154
+ what: "the invitee's signature",
155
+ });
156
+ if (!isAddressEqual(accepted, invitee)) {
157
+ refuse("InvalidSignature", `the invitee's signature is ${accepted}'s, not ${invitee}'s`);
158
+ }
159
+ await requireFreeSeat(ctx, clientId, message.userId, invitee);
160
+ return {
161
+ to: deployment.users,
162
+ data: encodeFunctionData({
163
+ abi: usersAbi,
164
+ functionName: "addUser",
165
+ args: [
166
+ {
167
+ clientId,
168
+ company: message.company,
169
+ callerId: message.callerId,
170
+ userId: message.userId,
171
+ isAdmin: message.userIsAdmin,
172
+ userPosition: message.userPosition,
173
+ expiresAt,
174
+ authId: message.authId,
175
+ },
176
+ adminSignature,
177
+ coSignatures,
178
+ invitee,
179
+ inviteeSignature,
180
+ ],
181
+ }),
182
+ };
183
+ }
184
+ /**
185
+ * `removeUser`: a member out, on active admins' `RemoveUser`: one for a
186
+ * member, two for an admin while the cofund has more than one. Signatures are
187
+ * put in the order the helper walks them.
188
+ */
189
+ export async function removeUserCall(ctx, input) {
190
+ const { deployment, reader } = ctx;
191
+ const { message } = input;
192
+ const clientId = cofundClientId(message.company);
193
+ if (message.authId === "") {
194
+ refuse("InvalidParams", "the authId is empty");
195
+ }
196
+ const [removed, admins] = await Promise.all([
197
+ readUser(reader, deployment, clientId, message.removedUserId),
198
+ readActiveAdmins(reader, deployment, clientId),
199
+ ]);
200
+ const required = removed?.isAdmin && admins > 1 ? 2 : 1;
201
+ if (input.signatures.length < required) {
202
+ refuse("ThresholdNotMet", `removing ${message.removedUserId} needs ${required} admin signatures; ${input.signatures.length} given`);
203
+ }
204
+ const digest = typedDataDigest(typedData(deployment.eip712Domain, REMOVE_USER_TYPES, message));
205
+ const ordered = await ascendingSigners(digest, input.signatures, {
206
+ what: "signature",
207
+ });
208
+ for (const { signer } of ordered) {
209
+ await requireActiveAdmin(ctx, clientId, signer, ADMIN_SIGNER, "signer");
210
+ }
211
+ await requireAuthIdUnspent(ctx, clientId, message.authId);
212
+ if (!removed) {
213
+ refuse("InvalidAddress", `${message.removedUserId} has no seat in this cofund`);
214
+ }
215
+ if (!removed.active) {
216
+ refuse("InactiveUser", `${message.removedUserId} is already removed`);
217
+ }
218
+ if (removed.isAdmin && admins <= 1) {
219
+ refuse("InvalidSetting", `${message.removedUserId} is the last admin`);
220
+ }
221
+ return {
222
+ to: deployment.users,
223
+ data: encodeFunctionData({
224
+ abi: usersAbi,
225
+ functionName: "removeUser",
226
+ args: [
227
+ clientId,
228
+ message.company,
229
+ message.removedUserId,
230
+ message.authId,
231
+ ordered.map(({ signature }) => signature),
232
+ ],
233
+ }),
234
+ };
235
+ }
236
+ /**
237
+ * `rotateUser`: a user's seat moved to a new key, which always signs
238
+ * `RotateUser`, and either the current key signs it too (`currentKeySignature`)
239
+ * or, for a lost key, active admins do (`adminSignatures`: two while the
240
+ * cofund has more than one admin), never both.
241
+ */
242
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: the helper's checks, one by one in its order
243
+ export async function rotateUserCall(ctx, input) {
244
+ const { deployment, reader } = ctx;
245
+ const { message, newKeySignature } = input;
246
+ const currentKeySignature = input.currentKeySignature ?? "0x";
247
+ let adminSignatures = input.adminSignatures ?? [];
248
+ const clientId = cofundClientId(message.company);
249
+ const newAddress = message.newAddress;
250
+ if (message.authId === "") {
251
+ refuse("InvalidParams", "the authId is empty");
252
+ }
253
+ if (isAddressEqual(newAddress, zeroAddress)) {
254
+ refuse("InvalidSignature", "the new address is the zero address");
255
+ }
256
+ const digest = typedDataDigest(typedData(deployment.eip712Domain, ROTATE_USER_TYPES, message));
257
+ const newKey = await recoverSigner(digest, newKeySignature, {
258
+ what: "the new key's signature",
259
+ });
260
+ if (!isAddressEqual(newKey, newAddress)) {
261
+ refuse("InvalidSignature", `the new key's signature is ${newKey}'s, not ${newAddress}'s`);
262
+ }
263
+ const user = await readUser(reader, deployment, clientId, message.userId);
264
+ if (currentKeySignature !== "0x") {
265
+ if (adminSignatures.length !== 0) {
266
+ refuse("InvalidParams", "the current key and admins cannot both authorize it");
267
+ }
268
+ if (!user?.active) {
269
+ refuse("InactiveSigner", `${message.userId} has no active seat`);
270
+ }
271
+ const current = await recoverSigner(digest, currentKeySignature, {
272
+ what: "the current key's signature",
273
+ });
274
+ if (!isAddressEqual(current, user.address)) {
275
+ refuse("InvalidSignature", `the current key's signature is ${current}'s; State holds ${user.address} for ${message.userId}`);
276
+ }
277
+ }
278
+ else {
279
+ const admins = await readActiveAdmins(reader, deployment, clientId);
280
+ const required = admins > 1 ? 2 : 1;
281
+ if (adminSignatures.length < required) {
282
+ refuse("ThresholdNotMet", `a rotation without the current key needs ${required} admin signatures; ${adminSignatures.length} given`);
283
+ }
284
+ const ordered = await ascendingSigners(digest, adminSignatures, {
285
+ what: "admin signature",
286
+ });
287
+ for (const { signer } of ordered) {
288
+ await requireActiveAdmin(ctx, clientId, signer, ADMIN_SIGNER, "signer");
289
+ }
290
+ adminSignatures = ordered.map(({ signature }) => signature);
291
+ }
292
+ await requireAuthIdUnspent(ctx, clientId, message.authId);
293
+ if (!user) {
294
+ refuse("InvalidUser", `${message.userId} has no seat in this cofund`);
295
+ }
296
+ if (!user.active) {
297
+ refuse("InactiveUser", `${message.userId} is removed`);
298
+ }
299
+ const holder = await readUserIdByAddress(reader, deployment, clientId, newAddress);
300
+ if (holder !== null) {
301
+ refuse("AddressExists", `${newAddress} already holds seat ${holder}`);
302
+ }
303
+ return {
304
+ to: deployment.users,
305
+ data: encodeFunctionData({
306
+ abi: usersAbi,
307
+ functionName: "rotateUser",
308
+ args: [
309
+ clientId,
310
+ message.company,
311
+ message.userId,
312
+ newAddress,
313
+ message.authId,
314
+ currentKeySignature,
315
+ newKeySignature,
316
+ adminSignatures,
317
+ ],
318
+ }),
319
+ };
320
+ }
321
+ /**
322
+ * `completeOnboarding`: the cofund's onboarding phase closed, on one active
323
+ * admin's `CompleteOnboarding`. Irreversible: refused once it is closed.
324
+ */
325
+ export async function completeOnboardingCall(ctx, input) {
326
+ const { deployment, reader } = ctx;
327
+ const { message, signature } = input;
328
+ const clientId = cofundClientId(message.company);
329
+ if (message.authId === "") {
330
+ refuse("InvalidParams", "the authId is empty");
331
+ }
332
+ const signer = await recoverSigner(typedDataDigest(typedData(deployment.eip712Domain, COMPLETE_ONBOARDING_TYPES, message)), signature, { what: "the admin's signature" });
333
+ await requireActiveAdmin(ctx, clientId, signer, {
334
+ unknown: "UnauthorizedCaller",
335
+ inactive: "InactiveSigner",
336
+ notAdmin: "InvalidSigner",
337
+ }, "the signer");
338
+ await requireAuthIdUnspent(ctx, clientId, message.authId);
339
+ if (!(await readClientVault(reader, deployment, clientId))) {
340
+ refuse("InvalidClient", `no cofund is named "${message.company}"`);
341
+ }
342
+ if (await readOnboardingComplete(reader, deployment, clientId)) {
343
+ refuse("OnboardingAlreadyComplete", "the onboarding phase is already closed");
344
+ }
345
+ return {
346
+ to: deployment.users,
347
+ data: encodeFunctionData({
348
+ abi: usersAbi,
349
+ functionName: "completeOnboarding",
350
+ args: [clientId, message.company, message.authId, signature],
351
+ }),
352
+ };
353
+ }
@@ -0,0 +1,25 @@
1
+ import { type Hex } from "viem";
2
+ import { type EvmCall } from "../chain.ts";
3
+ import { type TransactionMessage, type TransferMessage } from "../eip712.ts";
4
+ import { type EvmContext } from "./common.ts";
5
+ /**
6
+ * `executeTransfer`: tokens out of the vault under a transfer policy, on its
7
+ * signers' `Transfer`: within the cap, in the policy's token, to an allowed
8
+ * recipient, with the threshold met, and with the vault holding the amount
9
+ * and the fee the cofund's tier charges on top.
10
+ */
11
+ export declare function executeTransferCall(ctx: EvmContext, input: {
12
+ message: TransferMessage;
13
+ signatures: Hex[];
14
+ }): Promise<EvmCall>;
15
+ /**
16
+ * `executeTransaction`: a call through the policy's wrapper, on its signers'
17
+ * `Transaction` over these `instructions`, with the threshold met and every
18
+ * allowance a non-zero amount of a real token. The vault does not compare
19
+ * `func` with the policy's; neither does this.
20
+ */
21
+ export declare function executeTransactionCall(ctx: EvmContext, input: {
22
+ message: TransactionMessage;
23
+ instructions: Hex;
24
+ signatures: Hex[];
25
+ }): Promise<EvmCall>;