@getcofund/cli 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.
@@ -0,0 +1,153 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { addUserCall, cofundClientId, INVITE_USER_TYPES, inviteExpiresAt, readRequiredAdminInviteSignatures, readUser, sameAddress, typedData, } from "@getcofund/protocol/evm";
3
+ import { evmWallet } from "@getcofund/protocol/keys";
4
+ import { requireAdmin, seatText } from "../chain.js";
5
+ import { approved, evmChain, keystore, line, send, sender, } from "../context.js";
6
+ import { keystorePath, pickCompany, pinCompany, readKeystore, requirePinned, } from "../keystore.js";
7
+ import { loadSignatures, signMessage, writeSignatureFile, } from "../signature-file.js";
8
+ import { USAGE } from "../usage.js";
9
+ import { cofundLine, expiryText, signedByLines, userText } from "./lines.js";
10
+ /**
11
+ * `invite`: an admin signs a seat. `invite cosign`: another admin co-signs an
12
+ * admin seat. `join`: the invitee signs the same message and sends `addUser`
13
+ * with every signature, which seats them.
14
+ */
15
+ /** What an invite seats, as every step of it shows. */
16
+ async function seatLines(evm, message) {
17
+ return [
18
+ cofundLine(message.company),
19
+ line("seat", `${message.userId}, ${message.userIsAdmin ? "admin" : "member"}, "${message.userPosition}"`),
20
+ line("inviter", await userText(evm, message.company, message.callerId)),
21
+ line("valid", expiryText(message.expiresAt)),
22
+ ];
23
+ }
24
+ /** An admin seat's signature rule, said. */
25
+ async function adminRuleLine(evm, message) {
26
+ if (!message.userIsAdmin) {
27
+ return [];
28
+ }
29
+ const required = await readRequiredAdminInviteSignatures(evm.reader, evm.deployment, cofundClientId(message.company));
30
+ return [
31
+ line("needs", `${required} admin signature${required === 1 ? "" : "s"}, the inviter's included; other admins add theirs with \`invite cosign\``),
32
+ ];
33
+ }
34
+ export async function invite(io, flags) {
35
+ const userId = flags["user-id"];
36
+ const { position, out } = flags;
37
+ if (!(userId && position && out)) {
38
+ io.err(USAGE);
39
+ return 1;
40
+ }
41
+ const keys = keystore(io, flags);
42
+ const company = pickCompany(keys, flags.company);
43
+ const evm = await evmChain(io, flags);
44
+ const admin = evmWallet(keys);
45
+ const seat = await requireAdmin(evm, cofundClientId(company), admin.address);
46
+ const message = {
47
+ company,
48
+ callerId: seat.userId,
49
+ userId,
50
+ userIsAdmin: Boolean(flags.admin),
51
+ userPosition: position,
52
+ expiresAt: inviteExpiresAt(),
53
+ authId: randomUUID(),
54
+ };
55
+ for (const text of [
56
+ ...(await seatLines(evm, message)),
57
+ ...(await adminRuleLine(evm, message)),
58
+ ]) {
59
+ io.out(text);
60
+ }
61
+ if (!(await approved(io, flags, {
62
+ question: "Sign this invite?",
63
+ action: "sign it",
64
+ }))) {
65
+ return 1;
66
+ }
67
+ writeSignatureFile(out, await signMessage(admin, evm.deployment, { type: "InviteUser", message }));
68
+ io.out(line("wrote", `${out}: the invitee joins with it`));
69
+ return 0;
70
+ }
71
+ export async function inviteCosign(io, flags, files) {
72
+ const { out } = flags;
73
+ if (files.length === 0 || !out) {
74
+ io.err(USAGE);
75
+ return 1;
76
+ }
77
+ const evm = await evmChain(io, flags);
78
+ const signed = await loadSignatures(files, evm.deployment, ["InviteUser"]);
79
+ const message = signed[0].message;
80
+ const keys = keystore(io, flags);
81
+ requirePinned(keys, message.company, flags.company);
82
+ if (!message.userIsAdmin) {
83
+ throw new Error("A member's seat takes the inviter's signature alone: there is nothing to co-sign");
84
+ }
85
+ const admin = evmWallet(keys);
86
+ const seat = await requireAdmin(evm, cofundClientId(message.company), admin.address);
87
+ if (seat.userId === message.callerId) {
88
+ throw new Error("You are the inviter: a co-signature is another admin's");
89
+ }
90
+ for (const text of [
91
+ ...(await seatLines(evm, message)),
92
+ ...(await signedByLines(evm, message.company, signed)),
93
+ line("you", seatText(seat)),
94
+ ...(await adminRuleLine(evm, message)),
95
+ ]) {
96
+ io.out(text);
97
+ }
98
+ if (!(await approved(io, flags, {
99
+ question: "Co-sign this admin seat?",
100
+ action: "co-sign it",
101
+ }))) {
102
+ return 1;
103
+ }
104
+ writeSignatureFile(out, await signMessage(admin, evm.deployment, { type: "InviteUser", message }));
105
+ io.out(line("wrote", `${out}: the invitee joins with it and the others`));
106
+ return 0;
107
+ }
108
+ export async function join(io, flags, files) {
109
+ if (files.length === 0) {
110
+ io.err(USAGE);
111
+ return 1;
112
+ }
113
+ const evm = await evmChain(io, flags);
114
+ const signed = await loadSignatures(files, evm.deployment, ["InviteUser"]);
115
+ const message = signed[0].message;
116
+ if (flags.company !== undefined && flags.company !== message.company) {
117
+ throw new Error(`The invite is to company "${message.company}", not "${flags.company}"`);
118
+ }
119
+ const path = keystorePath(io.env, flags.keystore);
120
+ const invitee = evmWallet(readKeystore(path));
121
+ const via = sender(io, flags, evm);
122
+ const inviter = await readUser(evm.reader, evm.deployment, cofundClientId(message.company), message.callerId);
123
+ const inviting = signed.find(({ signer }) => inviter && sameAddress(signer, inviter.address));
124
+ if (!inviting) {
125
+ throw new Error(`None of the files is the inviting admin's (${message.callerId}${inviter ? ` at ${inviter.address}` : ", who holds no seat"})`);
126
+ }
127
+ for (const text of [
128
+ ...(await seatLines(evm, message)),
129
+ ...(await signedByLines(evm, message.company, signed)),
130
+ line("you", `${invitee.address} takes the seat`),
131
+ ]) {
132
+ io.out(text);
133
+ }
134
+ const call = await addUserCall(evm, {
135
+ message,
136
+ adminSignature: inviting.signature,
137
+ coSignatures: signed
138
+ .filter((each) => each !== inviting)
139
+ .map(({ signature }) => signature),
140
+ invitee: invitee.address,
141
+ inviteeSignature: await invitee.signTypedData(typedData(evm.deployment.eip712Domain, INVITE_USER_TYPES, message)),
142
+ });
143
+ io.out(line("call", `addUser on the users helper ${call.to}`));
144
+ const sent = await send(io, flags, { evm, via, call }, {
145
+ question: `Join "${message.company}" and pin its company?`,
146
+ action: "join",
147
+ });
148
+ if (sent === 0) {
149
+ pinCompany(path, message.company);
150
+ io.out(line("pinned", `company "${message.company}" in ${path}`));
151
+ }
152
+ return sent;
153
+ }
@@ -0,0 +1,36 @@
1
+ import { existsSync } from "node:fs";
2
+ import { generateKeystore } from "@getcofund/protocol/keys";
3
+ import { keystore, line } from "../context.js";
4
+ import { keystorePath, pinnedCompanies, writeKeystore } from "../keystore.js";
5
+ /** `keys init`: an EVM key and a BIP-86 Bitcoin account, generated here and kept in the keystore. */
6
+ export function keysInit(io, flags) {
7
+ const path = keystorePath(io.env, flags.keystore);
8
+ if (existsSync(path) && !flags.force) {
9
+ io.err(`${path} already exists; pass --force to replace it`);
10
+ return 1;
11
+ }
12
+ const network = io.env.COFUND_BTC_NETWORK ?? "mainnet";
13
+ if (!["mainnet", "testnet", "regtest"].includes(network)) {
14
+ io.err("COFUND_BTC_NETWORK is mainnet, testnet or regtest");
15
+ return 1;
16
+ }
17
+ // Regtest keys are testnet's: tpubs.
18
+ const keys = generateKeystore(network === "mainnet" ? "mainnet" : "testnet");
19
+ writeKeystore(path, keys);
20
+ io.out(line("keystore", path));
21
+ io.out(line("evm", keys.evm.address));
22
+ io.out(line("btc xpub", `${keys.btc.accountXpub} (${keys.btc.network})`));
23
+ return 0;
24
+ }
25
+ /** `keys show`: the keystore's EVM address, its Bitcoin account xpub, and the cofunds it pins. */
26
+ export function keysShow(io, flags) {
27
+ const keys = keystore(io, flags);
28
+ io.out(line("keystore", keystorePath(io.env, flags.keystore)));
29
+ io.out(line("evm", keys.evm.address));
30
+ io.out(line("btc xpub", `${keys.btc.accountXpub} (${keys.btc.network})`));
31
+ const companies = pinnedCompanies(keys);
32
+ io.out(line("cofunds", companies.length === 0
33
+ ? "none pinned"
34
+ : companies.map((company) => `"${company}"`).join(", ")));
35
+ return 0;
36
+ }
@@ -0,0 +1,29 @@
1
+ import { cofundClientId, readUser } from "@getcofund/protocol/evm";
2
+ import { seatOf, seatText } from "../chain.js";
3
+ import { line } from "../context.js";
4
+ /** Preview lines several commands print. */
5
+ export const cofundLine = (company) => line("cofund", `"${company}" (client id ${cofundClientId(company)})`);
6
+ /** An `expiresAt` (unix seconds) as a preview says it. */
7
+ export function expiryText(expiresAt) {
8
+ return `until ${new Date(Number(expiresAt) * 1000).toISOString()}`;
9
+ }
10
+ /** Who `address` is in the cofund: their seat, or that they hold none. */
11
+ export async function whoText(evm, company, address) {
12
+ const seat = await seatOf(evm, cofundClientId(company), address);
13
+ return seat ? seatText(seat) : `${address} (no seat in this cofund)`;
14
+ }
15
+ /** A user id as the preview names it: their seat, or that there is none. */
16
+ export async function userText(evm, company, userId) {
17
+ const user = await readUser(evm.reader, evm.deployment, cofundClientId(company), userId);
18
+ return user
19
+ ? seatText({ userId, user })
20
+ : `${userId} (no seat in this cofund)`;
21
+ }
22
+ /** Each signature's signer, by their seat. */
23
+ export async function signedByLines(evm, company, signed) {
24
+ const lines = [];
25
+ for (const { signer } of signed) {
26
+ lines.push(line("signed by", await whoText(evm, company, signer)));
27
+ }
28
+ return lines;
29
+ }
@@ -0,0 +1,145 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { cofundClientId, readUser, removeUserCall, rotateUserCall, sameAddress, } from "@getcofund/protocol/evm";
3
+ import { evmWallet } from "@getcofund/protocol/keys";
4
+ import { getAddress } from "viem";
5
+ import { seatOf } from "../chain.js";
6
+ import { line } from "../context.js";
7
+ import { pickCompany } from "../keystore.js";
8
+ import { USAGE } from "../usage.js";
9
+ import { userText } from "./lines.js";
10
+ import { signOrSend } from "./sign-or-send.js";
11
+ /** `members remove`, `members rotate`: signed by admins (and keys), sent with the signatures given. */
12
+ /** Whether `me` is an active admin who has not signed among `signed`. */
13
+ async function adminRole(evm, company, me, signed) {
14
+ if (signed.some(({ signer }) => sameAddress(signer, me))) {
15
+ return { not: "the files hold your signature already" };
16
+ }
17
+ const seat = await seatOf(evm, cofundClientId(company), me);
18
+ return seat?.user.active && seat.user.isAdmin
19
+ ? { as: `admin ${seat.userId}` }
20
+ : { not: `${me} is no active admin of this cofund` };
21
+ }
22
+ export function membersRemove(io, flags, userId, files) {
23
+ if (!userId) {
24
+ io.err(USAGE);
25
+ return 1;
26
+ }
27
+ return signOrSend(io, flags, {
28
+ type: "RemoveUser",
29
+ files,
30
+ fresh: async (keys) => ({
31
+ company: pickCompany(keys, flags.company),
32
+ removedUserId: userId,
33
+ authId: randomUUID(),
34
+ }),
35
+ matches: (message) => {
36
+ if (message.removedUserId !== userId) {
37
+ throw new Error(`The files remove ${message.removedUserId}, not ${userId}`);
38
+ }
39
+ },
40
+ role: (evm, message, me, signed) => adminRole(evm, message.company, me, signed),
41
+ lines: async (evm, message) => [
42
+ line("remove", await userText(evm, message.company, message.removedUserId)),
43
+ ],
44
+ build: (evm, message, signed) => removeUserCall(evm, {
45
+ message,
46
+ signatures: signed.map(({ signature }) => signature),
47
+ }),
48
+ entry: "removeUser on the users helper",
49
+ question: `Remove ${userId}?`,
50
+ });
51
+ }
52
+ /**
53
+ * The seat a rotation moves, and who holds it now; null when there is none
54
+ * (the builder refuses it).
55
+ */
56
+ async function rotated(evm, message) {
57
+ return await readUser(evm.reader, evm.deployment, cofundClientId(message.company), message.userId);
58
+ }
59
+ /**
60
+ * `members rotate`: a seat moved to a new key. The new key always signs,
61
+ * with the current key or (for a lost one) admins: each signs with `--out`,
62
+ * and the new key sends last with the others' files.
63
+ */
64
+ export function membersRotate(io, flags, files) {
65
+ const userIdFlag = flags["user-id"];
66
+ const newAddressFlag = flags["new-address"];
67
+ return signOrSend(io, flags, {
68
+ type: "RotateUser",
69
+ files,
70
+ fresh: async (keys, evm) => {
71
+ if (!newAddressFlag) {
72
+ throw new Error("A new rotation names the key it moves to: pass --new-address");
73
+ }
74
+ const company = pickCompany(keys, flags.company);
75
+ let userId = userIdFlag;
76
+ if (!userId) {
77
+ const seat = await seatOf(evm, cofundClientId(company), evmWallet(keys).address);
78
+ if (!seat) {
79
+ throw new Error("This key holds no seat to rotate: pass --user-id to rotate another's as an admin");
80
+ }
81
+ userId = seat.userId;
82
+ }
83
+ return {
84
+ company,
85
+ userId,
86
+ newAddress: getAddress(newAddressFlag),
87
+ authId: randomUUID(),
88
+ };
89
+ },
90
+ matches: (message) => {
91
+ if (userIdFlag !== undefined && message.userId !== userIdFlag) {
92
+ throw new Error(`The files rotate ${message.userId}, not ${userIdFlag}`);
93
+ }
94
+ if (newAddressFlag !== undefined &&
95
+ !sameAddress(message.newAddress, newAddressFlag)) {
96
+ throw new Error(`The files rotate to ${message.newAddress}, not ${newAddressFlag}`);
97
+ }
98
+ },
99
+ role: async (evm, message, me, signed) => {
100
+ if (signed.some(({ signer }) => sameAddress(signer, me))) {
101
+ return { not: "the files hold your signature already" };
102
+ }
103
+ if (sameAddress(me, message.newAddress)) {
104
+ return { as: "new key" };
105
+ }
106
+ const user = await rotated(evm, message);
107
+ const byAdmins = signed.some(({ signer }) => !(sameAddress(signer, message.newAddress) ||
108
+ (user && sameAddress(signer, user.address))));
109
+ if (user && sameAddress(me, user.address)) {
110
+ return byAdmins
111
+ ? { not: "admins authorize this rotation, not the current key" }
112
+ : { as: "current key" };
113
+ }
114
+ if (user &&
115
+ signed.some(({ signer }) => sameAddress(signer, user.address))) {
116
+ return { not: "the current key authorizes this rotation, not admins" };
117
+ }
118
+ return await adminRole(evm, message.company, me, signed);
119
+ },
120
+ lines: async (evm, message) => [
121
+ line("rotate", await userText(evm, message.company, message.userId)),
122
+ line("to", message.newAddress),
123
+ ],
124
+ build: async (evm, message, signed) => {
125
+ const user = await rotated(evm, message);
126
+ const newKey = signed.find(({ signer }) => sameAddress(signer, message.newAddress));
127
+ if (!newKey) {
128
+ throw new Error(`The new key ${message.newAddress} has not signed: it sends last, with the others' files`);
129
+ }
130
+ const current = signed.find(({ signer }) => user && sameAddress(signer, user.address));
131
+ // Both authorizations given at once are refused by the builder, as by the helper.
132
+ return rotateUserCall(evm, {
133
+ message,
134
+ newKeySignature: newKey.signature,
135
+ currentKeySignature: current?.signature,
136
+ adminSignatures: signed
137
+ .filter((each) => each !== newKey && each !== current)
138
+ .map(({ signature }) => signature),
139
+ });
140
+ },
141
+ entry: "rotateUser on the users helper",
142
+ question: "Rotate this seat to the new key?",
143
+ pinOnSend: true,
144
+ });
145
+ }
@@ -0,0 +1,40 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { cofundClientId, completeOnboardingCall, } from "@getcofund/protocol/evm";
3
+ import { requireAdmin } from "../chain.js";
4
+ import { line } from "../context.js";
5
+ import { pickCompany } from "../keystore.js";
6
+ import { signOrSend } from "./sign-or-send.js";
7
+ /**
8
+ * `onboarding complete [<file>]`: one active admin's `CompleteOnboarding`,
9
+ * this key's unless a file gives one, sent. It closes the founding phase for
10
+ * good: after it, seating an admin and activating a policy take the admins'
11
+ * consensus.
12
+ */
13
+ export function onboardingComplete(io, flags, files) {
14
+ return signOrSend(io, flags, {
15
+ type: "CompleteOnboarding",
16
+ files,
17
+ fresh: async (keys) => ({
18
+ company: pickCompany(keys, flags.company),
19
+ authId: randomUUID(),
20
+ }),
21
+ matches: () => {
22
+ if (files.length > 1) {
23
+ throw new Error("It takes one admin's signature: one file");
24
+ }
25
+ },
26
+ role: async (evm, message, me, signed) => {
27
+ if (signed.length > 0) {
28
+ return { not: "it takes one admin's signature, and the file has it" };
29
+ }
30
+ const seat = await requireAdmin(evm, cofundClientId(message.company), me);
31
+ return { as: `admin ${seat.userId}` };
32
+ },
33
+ lines: async () => [
34
+ line("closes", "the onboarding phase, for good: admin seats and policies then take the admins' consensus"),
35
+ ],
36
+ build: (evm, message, [first]) => completeOnboardingCall(evm, { message, signature: first.signature }),
37
+ entry: "completeOnboarding on the users helper",
38
+ question: "Complete onboarding?",
39
+ });
40
+ }
@@ -0,0 +1,193 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { activateTransactionPolicyCall, activateTransferPolicyCall, cofundClientId, deactivatePolicyCall, readPolicy, readRequiredPolicyActivationSignatures, sameAddress, WRAPPER_ENTRYPOINT, } from "@getcofund/protocol/evm";
3
+ import { evmWallet } from "@getcofund/protocol/keys";
4
+ import { getAddress } from "viem";
5
+ import { baseUnits, requireAdmin, seatText, tokenInfo } from "../chain.js";
6
+ import { approved, evmChain, keystore, line, send, sender, } from "../context.js";
7
+ import { pickCompany, requirePinned } from "../keystore.js";
8
+ import { loadSignatures, signMessage, writeSignatureFile, } from "../signature-file.js";
9
+ import { USAGE } from "../usage.js";
10
+ import { cofundLine, signedByLines, userText } from "./lines.js";
11
+ import { boundsLines } from "./reads.js";
12
+ import { signOrSend } from "./sign-or-send.js";
13
+ const ACTIVATIONS = [
14
+ "TransferPolicyActivation",
15
+ "TransactionPolicyActivation",
16
+ ];
17
+ const isTransaction = (message) => "wrapper" in message;
18
+ /** The policy an activation activates, as every step of it shows. */
19
+ async function activationLines(evm, message) {
20
+ const signers = [];
21
+ for (const id of message.signerIds) {
22
+ signers.push(await userText(evm, message.company, id));
23
+ }
24
+ const required = await readRequiredPolicyActivationSignatures(evm.reader, evm.deployment, cofundClientId(message.company), BigInt(message.threshold));
25
+ const bounds = await boundsLines(evm, isTransaction(message)
26
+ ? { wrapper: getAddress(message.wrapper), func: message.func }
27
+ : {
28
+ token: getAddress(message.token),
29
+ maxAmount: BigInt(message.maxAmount),
30
+ recipients: message.recipients.map((r) => getAddress(r)),
31
+ });
32
+ return [
33
+ cofundLine(message.company),
34
+ line("policy", `${message.policyId}: "${message.title}" (${message.policyType})`),
35
+ line("signers", `${signers.join(", ")}; ${message.threshold} of ${message.signerIds.length} must sign`),
36
+ ...bounds,
37
+ line("activate", `takes ${required} admin signature${required === 1 ? "" : "s"}`),
38
+ ];
39
+ }
40
+ const POSITIVE = /^[1-9]\d*$/;
41
+ const list = (text) => text
42
+ .split(",")
43
+ .map((each) => each.trim())
44
+ .filter(Boolean);
45
+ /** A new activation from the flags: a transfer policy (token and cap), or a call policy (wrapper). */
46
+ async function activationFrom(flags, company, evm) {
47
+ const { name, signers, threshold, token, wrapper } = flags;
48
+ const maxAmount = flags["max-amount"];
49
+ const transfer = Boolean(token && maxAmount);
50
+ if (!(name && signers && threshold && POSITIVE.test(threshold)) ||
51
+ transfer === Boolean(wrapper) ||
52
+ (flags.recipients && wrapper)) {
53
+ return null;
54
+ }
55
+ const common = {
56
+ company,
57
+ policyId: randomUUID(),
58
+ title: name,
59
+ policyType: flags.type ?? (wrapper ? "Treasury_Management" : "Operational_Expense"),
60
+ signerIds: list(signers),
61
+ threshold,
62
+ };
63
+ if (wrapper) {
64
+ return {
65
+ ...common,
66
+ wrapper: getAddress(wrapper),
67
+ func: WRAPPER_ENTRYPOINT,
68
+ authId: randomUUID(),
69
+ };
70
+ }
71
+ const address = getAddress(token);
72
+ return {
73
+ ...common,
74
+ token: address,
75
+ maxAmount: baseUnits(maxAmount, await tokenInfo(evm, address), "--max-amount").toString(),
76
+ recipients: list(flags.recipients ?? "").map((r) => getAddress(r)),
77
+ authId: randomUUID(),
78
+ };
79
+ }
80
+ export async function policiesSign(io, flags, files) {
81
+ const { out } = flags;
82
+ if (!out) {
83
+ io.err(USAGE);
84
+ return 1;
85
+ }
86
+ const keys = keystore(io, flags);
87
+ const evm = await evmChain(io, flags);
88
+ let message;
89
+ let signed = [];
90
+ if (files.length > 0) {
91
+ signed = await loadSignatures(files, evm.deployment, ACTIVATIONS);
92
+ message = signed[0].message;
93
+ requirePinned(keys, message.company, flags.company);
94
+ }
95
+ else {
96
+ message = await activationFrom(flags, pickCompany(keys, flags.company), evm);
97
+ if (!message) {
98
+ io.err(USAGE);
99
+ return 1;
100
+ }
101
+ }
102
+ const admin = evmWallet(keys);
103
+ const seat = await requireAdmin(evm, cofundClientId(message.company), admin.address);
104
+ if (signed.some(({ signer }) => sameAddress(signer, admin.address))) {
105
+ throw new Error("The files hold your signature already");
106
+ }
107
+ for (const text of [
108
+ ...(await activationLines(evm, message)),
109
+ ...(await signedByLines(evm, message.company, signed)),
110
+ line("you", seatText(seat)),
111
+ ]) {
112
+ io.out(text);
113
+ }
114
+ if (!(await approved(io, flags, {
115
+ question: "Sign this policy's activation?",
116
+ action: "sign it",
117
+ }))) {
118
+ return 1;
119
+ }
120
+ const type = isTransaction(message)
121
+ ? "TransactionPolicyActivation"
122
+ : "TransferPolicyActivation";
123
+ writeSignatureFile(out, await signMessage(admin, evm.deployment, { type, message }));
124
+ io.out(line("wrote", `${out}: \`policies activate\` sends it with the others`));
125
+ return 0;
126
+ }
127
+ export async function policiesActivate(io, flags, files) {
128
+ if (files.length === 0) {
129
+ io.err(USAGE);
130
+ return 1;
131
+ }
132
+ const evm = await evmChain(io, flags);
133
+ const signed = await loadSignatures(files, evm.deployment, ACTIVATIONS);
134
+ const message = signed[0].message;
135
+ const via = sender(io, flags, evm);
136
+ for (const text of [
137
+ ...(await activationLines(evm, message)),
138
+ ...(await signedByLines(evm, message.company, signed)),
139
+ ]) {
140
+ io.out(text);
141
+ }
142
+ const signatures = signed.map(({ signature }) => signature);
143
+ const call = isTransaction(message)
144
+ ? await activateTransactionPolicyCall(evm, { message, signatures })
145
+ : await activateTransferPolicyCall(evm, { message, signatures });
146
+ io.out(line("call", `${isTransaction(message) ? "activateTransactionPolicy" : "activateTransferPolicy"} on the policies helper ${call.to}`));
147
+ return await send(io, flags, { evm, via, call }, {
148
+ question: "Activate this policy?",
149
+ action: "activate it",
150
+ });
151
+ }
152
+ /** `policies deactivate <id> [<file>]`: one active admin's signature, this key's unless a file gives one. */
153
+ export function policiesDeactivate(io, flags, policyId, files) {
154
+ if (!policyId) {
155
+ io.err(USAGE);
156
+ return 1;
157
+ }
158
+ return signOrSend(io, flags, {
159
+ type: "PolicyDeactivation",
160
+ files,
161
+ fresh: async (keys) => ({
162
+ company: pickCompany(keys, flags.company),
163
+ policyId,
164
+ authId: randomUUID(),
165
+ }),
166
+ matches: (message) => {
167
+ if (message.policyId !== policyId) {
168
+ throw new Error(`The files deactivate ${message.policyId}, not ${policyId}`);
169
+ }
170
+ if (files.length > 1) {
171
+ throw new Error("A deactivation takes one admin's signature: one file");
172
+ }
173
+ },
174
+ role: async (evm, message, me, signed) => {
175
+ if (signed.length > 0) {
176
+ return { not: "it takes one admin's signature, and the file has it" };
177
+ }
178
+ const seat = await requireAdmin(evm, cofundClientId(message.company), me);
179
+ return { as: `admin ${seat.userId}` };
180
+ },
181
+ lines: async (evm, message) => {
182
+ const policy = await readPolicy(evm.reader, evm.deployment, cofundClientId(message.company), message.policyId);
183
+ return [
184
+ line("policy", policy
185
+ ? `${message.policyId}: "${policy.title}", ${policy.active ? "active" : "inactive"}`
186
+ : `${message.policyId} (no such policy)`),
187
+ ];
188
+ },
189
+ build: (evm, message, [first]) => deactivatePolicyCall(evm, { message, signature: first.signature }),
190
+ entry: "deactivatePolicy on the policies helper",
191
+ question: `Deactivate policy ${policyId}?`,
192
+ });
193
+ }