@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.
- package/LICENSE +21 -0
- package/README.md +414 -2
- package/dist/btc/index.d.ts +12 -0
- package/dist/btc/index.js +13 -0
- package/dist/btc/psbt.d.ts +121 -0
- package/dist/btc/psbt.js +377 -0
- package/dist/btc/reader.d.ts +42 -0
- package/dist/btc/reader.js +85 -0
- package/dist/btc/select.d.ts +48 -0
- package/dist/btc/select.js +59 -0
- package/dist/btc/spend-rules.d.ts +24 -0
- package/dist/btc/spend-rules.js +43 -0
- package/dist/btc/spend.d.ts +136 -0
- package/dist/btc/spend.js +180 -0
- package/dist/btc/types.d.ts +65 -0
- package/dist/btc/types.js +62 -0
- package/dist/btc/vault/bip341-nums.d.ts +35 -0
- package/dist/btc/vault/bip341-nums.js +121 -0
- package/dist/btc/vault/build.d.ts +5 -0
- package/dist/btc/vault/build.js +136 -0
- package/dist/btc/vault/canonical-order.d.ts +10 -0
- package/dist/btc/vault/canonical-order.js +18 -0
- package/dist/btc/vault/from-contract-id.d.ts +9 -0
- package/dist/btc/vault/from-contract-id.js +74 -0
- package/dist/btc/vault/helpers.d.ts +73 -0
- package/dist/btc/vault/helpers.js +129 -0
- package/dist/btc/vault/index.d.ts +21 -0
- package/dist/btc/vault/index.js +21 -0
- package/dist/btc/vault/limits.d.ts +9 -0
- package/dist/btc/vault/limits.js +9 -0
- package/dist/btc/vault/participant-order.d.ts +69 -0
- package/dist/btc/vault/participant-order.js +135 -0
- package/dist/btc/vault/script.d.ts +13 -0
- package/dist/btc/vault/script.js +39 -0
- package/dist/btc/vault/spend.d.ts +63 -0
- package/dist/btc/vault/spend.js +93 -0
- package/dist/btc/vault/tx-size.d.ts +66 -0
- package/dist/btc/vault/tx-size.js +71 -0
- package/dist/btc/vault/types.d.ts +49 -0
- package/dist/btc/vault/types.js +1 -0
- package/dist/btc/vault/vault-policy.d.ts +109 -0
- package/dist/btc/vault/vault-policy.js +213 -0
- package/dist/btc/vault/vaultContractUtil.d.ts +80 -0
- package/dist/btc/vault/vaultContractUtil.js +87 -0
- package/dist/btc/vault/xpub.d.ts +28 -0
- package/dist/btc/vault/xpub.js +41 -0
- package/dist/btc/witness.d.ts +101 -0
- package/dist/btc/witness.js +439 -0
- package/dist/btc-policies/activation.d.ts +76 -0
- package/dist/btc-policies/activation.js +153 -0
- package/dist/btc-policies/index.d.ts +7 -0
- package/dist/btc-policies/index.js +8 -0
- package/dist/btc-policies/policy.d.ts +55 -0
- package/dist/btc-policies/policy.js +210 -0
- package/dist/evm/abis/index.d.ts +10 -0
- package/dist/evm/abis/index.js +11 -0
- package/dist/evm/abis/policies.d.ts +279 -0
- package/dist/evm/abis/policies.js +363 -0
- package/dist/evm/abis/sponsor-guard.d.ts +201 -0
- package/dist/evm/abis/sponsor-guard.js +263 -0
- package/dist/evm/abis/state.d.ts +989 -0
- package/dist/evm/abis/state.js +1290 -0
- package/dist/evm/abis/users.d.ts +408 -0
- package/dist/evm/abis/users.js +532 -0
- package/dist/evm/abis/vault-factory.d.ts +92 -0
- package/dist/evm/abis/vault-factory.js +126 -0
- package/dist/evm/abis/vault.d.ts +343 -0
- package/dist/evm/abis/vault.js +444 -0
- package/dist/evm/address.d.ts +2 -0
- package/dist/evm/address.js +4 -0
- package/dist/evm/calls/common.d.ts +30 -0
- package/dist/evm/calls/common.js +51 -0
- package/dist/evm/calls/index.d.ts +11 -0
- package/dist/evm/calls/index.js +4 -0
- package/dist/evm/calls/policies.d.ts +25 -0
- package/dist/evm/calls/policies.js +173 -0
- package/dist/evm/calls/users.d.ts +61 -0
- package/dist/evm/calls/users.js +353 -0
- package/dist/evm/calls/vault.d.ts +25 -0
- package/dist/evm/calls/vault.js +184 -0
- package/dist/evm/chain.d.ts +74 -0
- package/dist/evm/chain.js +48 -0
- package/dist/evm/company-name.d.ts +46 -0
- package/dist/evm/company-name.js +64 -0
- package/dist/evm/deployment.d.ts +47 -0
- package/dist/evm/deployment.js +110 -0
- package/dist/evm/eip712.d.ts +271 -0
- package/dist/evm/eip712.js +224 -0
- package/dist/evm/index.d.ts +20 -0
- package/dist/evm/index.js +21 -0
- package/dist/evm/reads.d.ts +64 -0
- package/dist/evm/reads.js +131 -0
- package/dist/evm/signatures.d.ts +57 -0
- package/dist/evm/signatures.js +105 -0
- package/dist/evm/simulate/balances.d.ts +76 -0
- package/dist/evm/simulate/balances.js +201 -0
- package/dist/evm/simulate/describe.d.ts +79 -0
- package/dist/evm/simulate/describe.js +188 -0
- package/dist/evm/simulate/index.d.ts +12 -0
- package/dist/evm/simulate/index.js +13 -0
- package/dist/evm/simulate/own-node.d.ts +62 -0
- package/dist/evm/simulate/own-node.js +122 -0
- package/dist/evm/simulate/request.d.ts +86 -0
- package/dist/evm/simulate/request.js +235 -0
- package/dist/evm/simulate/revert.d.ts +29 -0
- package/dist/evm/simulate/revert.js +192 -0
- package/dist/evm/simulate/run.d.ts +51 -0
- package/dist/evm/simulate/run.js +214 -0
- package/dist/evm/stocks/decode-stock-swap.d.ts +32 -0
- package/dist/evm/stocks/decode-stock-swap.js +79 -0
- package/dist/evm/stocks/encode-swap.d.ts +45 -0
- package/dist/evm/stocks/encode-swap.js +45 -0
- package/dist/evm/stocks/index.d.ts +9 -0
- package/dist/evm/stocks/index.js +10 -0
- package/dist/evm/stocks/quote.d.ts +19 -0
- package/dist/evm/stocks/quote.js +46 -0
- package/dist/evm/stocks/stock-pools.d.ts +61 -0
- package/dist/evm/stocks/stock-pools.js +179 -0
- package/dist/evm/submitters.d.ts +32 -0
- package/dist/evm/submitters.js +105 -0
- package/dist/evm/types.d.ts +146 -0
- package/dist/evm/types.js +128 -0
- package/dist/keys/index.d.ts +6 -0
- package/dist/keys/index.js +7 -0
- package/dist/keys/keys.d.ts +63 -0
- package/dist/keys/keys.js +80 -0
- package/dist/keys/xpub-ownership.d.ts +10 -0
- package/dist/keys/xpub-ownership.js +13 -0
- 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>;
|