@getcofund/cli 0.0.0 → 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,293 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { BtcVaultDefinitionSchema, BtcVaultTypeSchema, BtcXpub, broadcast, buildSpendPsbt, buildTaprootForNetwork, COFUND_LEAF_INDEX, checkPsbt, combinePsbts, createBip341NumsRecipe, esploraReader, extractTx, finalizePsbt, ORDERING_XPUB_CODEPOINT_V2, selectCoins, serializeAccountBasedData, signPsbt, vaultSpendInputVbytes, verifyBtcVault, } from "@getcofund/protocol/btc";
4
+ import { btcLeafSigner } from "@getcofund/protocol/keys";
5
+ import { z } from "zod";
6
+ import { approved, keystore, line, setting, } from "../context.js";
7
+ import { USAGE } from "../usage.js";
8
+ /**
9
+ * `btc vault create`: a vault's definition from its members' account xpubs,
10
+ * and the address it derives. `btc spend build`: the vault's coins selected
11
+ * through Esplora and an unsigned PSBT paying one address. `sign`, `combine`,
12
+ * `finalize` and `broadcast`: the rest of the spend. Every command that takes
13
+ * a vault file rebuilds the vault from its definition first, and refuses one
14
+ * that does not derive its address.
15
+ */
16
+ /** Each network's address and key prefixes, as the vault builder takes them. */
17
+ const NETWORKS = {
18
+ mainnet: { bech32: "bc", pubKeyHash: 0x00, scriptHash: 0x05, wif: 0x80 },
19
+ testnet: { bech32: "tb", pubKeyHash: 0x6f, scriptHash: 0xc4, wif: 0xef },
20
+ regtest: { bech32: "bcrt", pubKeyHash: 0x6f, scriptHash: 0xc4, wif: 0xef },
21
+ };
22
+ const POSITIVE = /^[1-9]\d*$/;
23
+ const PSBT_MAGIC = "70736274ff";
24
+ const HEX = /^(?:[0-9a-fA-F]{2})+$/;
25
+ const list = (text) => (text ?? "")
26
+ .split(",")
27
+ .map((each) => each.trim())
28
+ .filter(Boolean);
29
+ /** Sats from a flag: a positive whole number. */
30
+ function sats(value, flag) {
31
+ if (!(value && POSITIVE.test(value))) {
32
+ throw new Error(`${flag} takes sats, a positive whole number`);
33
+ }
34
+ return BigInt(value);
35
+ }
36
+ /** Write a file the command made; an existing one is never overwritten. */
37
+ function writeNew(path, text) {
38
+ try {
39
+ writeFileSync(path, text, { flag: "wx" });
40
+ }
41
+ catch (error) {
42
+ if (error.code === "EEXIST") {
43
+ throw new Error(`${path} exists; pass another --out`);
44
+ }
45
+ throw error;
46
+ }
47
+ }
48
+ const VaultFileSchema = z.object({
49
+ id: z.string().min(1),
50
+ address: z.string().min(1),
51
+ definition: BtcVaultDefinitionSchema,
52
+ });
53
+ /** A vault file, rebuilt from its definition: refused unless it derives its address. */
54
+ function loadVault(path) {
55
+ if (!path) {
56
+ throw new Error("Pass --vault <file>, the vault's definition");
57
+ }
58
+ let json;
59
+ try {
60
+ json = JSON.parse(readFileSync(path, "utf8"));
61
+ }
62
+ catch (error) {
63
+ throw new Error(`${path}: not a vault file (${error instanceof Error ? error.message : String(error)})`);
64
+ }
65
+ const parsed = VaultFileSchema.safeParse(json);
66
+ if (!parsed.success) {
67
+ const issue = parsed.error.issues[0];
68
+ throw new Error(`${path}: not a vault file (${issue.path.join(".") || "file"}: ${issue.message})`);
69
+ }
70
+ return verifyBtcVault(parsed.data, parsed.data.definition.network);
71
+ }
72
+ function vaultLine(vault) {
73
+ const { vaultType, threshold, members } = vault.definition;
74
+ return line("vault", `${vault.address}: ${threshold} of ${members.length} (${vaultType})`);
75
+ }
76
+ /** A PSBT file: base64, as wallets and bitcoind write them, or the raw bytes. */
77
+ function readPsbt(path) {
78
+ const bytes = readFileSync(path);
79
+ if (bytes.subarray(0, 5).toString("hex") === PSBT_MAGIC) {
80
+ return new Uint8Array(bytes);
81
+ }
82
+ const decoded = Buffer.from(bytes.toString("utf8").trim(), "base64");
83
+ if (decoded.subarray(0, 5).toString("hex") !== PSBT_MAGIC) {
84
+ throw new Error(`${path} is not a PSBT`);
85
+ }
86
+ return new Uint8Array(decoded);
87
+ }
88
+ const writePsbt = (path, psbt) => writeNew(path, `${Buffer.from(psbt).toString("base64")}\n`);
89
+ /** What a spend pays, as the build and every signer show it. */
90
+ function spendLines(summary) {
91
+ const held = summary.inputs.reduce((sum, { value }) => sum + value, 0n);
92
+ const rate = Number(summary.fee) / summary.vsize;
93
+ return [
94
+ ...summary.payees.map(({ address, amount }) => line("pay", `${amount} sats to ${address}`)),
95
+ line("inputs", `${summary.inputs.length} of the vault's coins, ${held} sats`),
96
+ line("fee", `${summary.fee} sats (${rate.toFixed(2)} sat/vB of ${summary.vsize} vB)`),
97
+ line("change", summary.change === 0n
98
+ ? "none"
99
+ : `${summary.change} sats back to the vault`),
100
+ ];
101
+ }
102
+ /** The network a new vault's address is on: COFUND_BTC_NETWORK, else its xpubs' (xpub mainnet, tpub testnet). */
103
+ function networkFor(io, xpubs) {
104
+ const mainnet = xpubs[0].startsWith("xpub");
105
+ const network = io.env.COFUND_BTC_NETWORK ?? (mainnet ? "mainnet" : "testnet");
106
+ if (!(network in NETWORKS)) {
107
+ throw new Error("COFUND_BTC_NETWORK is mainnet, testnet or regtest");
108
+ }
109
+ if (xpubs.some((xpub) => xpub.startsWith("xpub") !== (network === "mainnet"))) {
110
+ throw new Error(`A ${network} vault takes ${network === "mainnet" ? "xpubs" : "tpubs"} only`);
111
+ }
112
+ return network;
113
+ }
114
+ export function btcVaultCreate(io, flags) {
115
+ const xpubs = list(flags.xpubs);
116
+ const admins = list(flags.admins);
117
+ const { out, threshold } = flags;
118
+ if (xpubs.length === 0 || !threshold || !out) {
119
+ io.err(USAGE);
120
+ return 1;
121
+ }
122
+ for (const xpub of [...xpubs, ...admins]) {
123
+ if (!BtcXpub.safeParse(xpub).success) {
124
+ throw new Error(`Not an account xpub: ${xpub}`);
125
+ }
126
+ }
127
+ if (new Set(xpubs).size !== xpubs.length) {
128
+ throw new Error("--xpubs lists a member twice");
129
+ }
130
+ const stray = admins.find((admin) => !xpubs.includes(admin));
131
+ if (stray) {
132
+ throw new Error(`--admins names ${stray}, which --xpubs does not list`);
133
+ }
134
+ const network = networkFor(io, xpubs);
135
+ const recipe = createBip341NumsRecipe();
136
+ const definition = BtcVaultDefinitionSchema.parse({
137
+ vaultType: BtcVaultTypeSchema.parse(flags.type ?? "ALL"),
138
+ network: network === "mainnet" ? "mainnet" : "testnet",
139
+ threshold: Number(threshold),
140
+ participantOrdering: ORDERING_XPUB_CODEPOINT_V2,
141
+ nums: { scheme: recipe.scheme, version: recipe.version, r: recipe.r },
142
+ members: xpubs.map((accountXpub) => ({
143
+ id: randomUUID(),
144
+ role: admins.includes(accountXpub) ? "admin" : "member",
145
+ accountXpub,
146
+ })),
147
+ });
148
+ const { members, ...rest } = definition;
149
+ const { address } = buildTaprootForNetwork(serializeAccountBasedData({
150
+ accountBased: true,
151
+ ...rest,
152
+ companyUsers: members,
153
+ }), NETWORKS[network]);
154
+ const vault = { id: randomUUID(), address, definition };
155
+ const verified = verifyBtcVault(vault, definition.network);
156
+ writeNew(out, `${JSON.stringify(vault, null, 2)}\n`);
157
+ io.out(vaultLine(verified));
158
+ for (const member of definition.members) {
159
+ io.out(line(member.role, member.accountXpub));
160
+ }
161
+ io.out(line("wrote", `${out}: every member keeps it, and signs against it`));
162
+ return 0;
163
+ }
164
+ export async function btcSpendBuild(io, flags) {
165
+ const { to, out } = flags;
166
+ if (!(to && out)) {
167
+ io.err(USAGE);
168
+ return 1;
169
+ }
170
+ const amount = sats(flags.amount, "--amount");
171
+ const vault = loadVault(flags.vault);
172
+ const reader = esploraReader(setting(io, flags, "esplora"));
173
+ const feeRate = flags["fee-rate"] === undefined
174
+ ? await reader.feeRate()
175
+ : Number(flags["fee-rate"]);
176
+ if (!(Number.isFinite(feeRate) && feeRate > 0)) {
177
+ throw new Error("--fee-rate takes sat/vB, above 0");
178
+ }
179
+ const coins = await reader.utxos(vault.address);
180
+ const confirmed = coins.filter((coin) => coin.confirmed);
181
+ if (confirmed.length === 0 && coins.length > 0) {
182
+ throw new Error(`The vault's ${coins.length} coin(s) are unconfirmed: wait for a block`);
183
+ }
184
+ const { selected, fee } = selectCoins({
185
+ utxos: confirmed,
186
+ amount,
187
+ feeRate,
188
+ inputVbytes: vaultSpendInputVbytes(vault),
189
+ });
190
+ const payees = [{ address: to, amount }];
191
+ const psbt = buildSpendPsbt({ vault, inputs: selected, payees, fee });
192
+ for (const text of [
193
+ vaultLine(vault),
194
+ ...spendLines(checkPsbt(psbt, vault, { payees })),
195
+ ]) {
196
+ io.out(text);
197
+ }
198
+ writePsbt(out, psbt);
199
+ io.out(line("wrote", `${out}: each signer signs it with \`btc spend sign\``));
200
+ return 0;
201
+ }
202
+ /**
203
+ * The keystore's Bitcoin account as a PSBT signer: its xpub, and the
204
+ * Cofund leaf key at a leaf's branch (`m/86'/coin'/0'/<branch>/<leaf index>`).
205
+ */
206
+ function keystoreSigner(keys) {
207
+ const leaves = btcLeafSigner(keys);
208
+ return {
209
+ xpub: () => Promise.resolve(keys.btc.accountXpub),
210
+ signTaprootLeaf(sighash, [branch, index]) {
211
+ if (index !== COFUND_LEAF_INDEX) {
212
+ throw new Error(`The keystore signs Cofund leaves only, not index ${index}`);
213
+ }
214
+ return Promise.resolve(leaves.signDigest(branch, sighash));
215
+ },
216
+ };
217
+ }
218
+ export async function btcSpendSign(io, flags, [path, ...rest]) {
219
+ const { to, out } = flags;
220
+ if (!(path && rest.length === 0 && to && out)) {
221
+ io.err(USAGE);
222
+ return 1;
223
+ }
224
+ const amount = sats(flags.amount, "--amount");
225
+ const vault = loadVault(flags.vault);
226
+ const keys = keystore(io, flags);
227
+ const member = vault.definition.members.find(({ accountXpub }) => accountXpub === keys.btc.accountXpub);
228
+ if (!member) {
229
+ throw new Error(`This keystore's xpub is not a member of vault ${vault.address}`);
230
+ }
231
+ const psbt = readPsbt(path);
232
+ // What the signer was told the spend pays: anything else is refused.
233
+ const expectations = { payees: [{ address: to, amount }] };
234
+ for (const text of [
235
+ vaultLine(vault),
236
+ ...spendLines(checkPsbt(psbt, vault, expectations)),
237
+ line("you", `sign as ${member.role} ${keys.btc.accountXpub}`),
238
+ ]) {
239
+ io.out(text);
240
+ }
241
+ if (!(await approved(io, flags, {
242
+ question: "Sign this spend?",
243
+ action: "sign it",
244
+ }))) {
245
+ return 1;
246
+ }
247
+ writePsbt(out, await signPsbt(psbt, vault, keystoreSigner(keys), expectations));
248
+ io.out(line("wrote", `${out}: combine it with the other signers'`));
249
+ return 0;
250
+ }
251
+ export function btcSpendCombine(io, flags, paths) {
252
+ const { out } = flags;
253
+ if (paths.length === 0 || !out) {
254
+ io.err(USAGE);
255
+ return 1;
256
+ }
257
+ writePsbt(out, combinePsbts(paths.map(readPsbt)));
258
+ io.out(line("wrote", `${out}: ${paths.length} PSBT(s) combined`));
259
+ return 0;
260
+ }
261
+ export function btcSpendFinalize(io, flags, [path, ...rest]) {
262
+ const { out } = flags;
263
+ if (!(path && rest.length === 0 && out)) {
264
+ io.err(USAGE);
265
+ return 1;
266
+ }
267
+ const vault = loadVault(flags.vault);
268
+ const hex = extractTx(finalizePsbt(readPsbt(path), vault));
269
+ io.out(vaultLine(vault));
270
+ writeNew(out, `${hex}\n`);
271
+ io.out(line("wrote", `${out}: the signed transaction, ${hex.length / 2} bytes; \`btc spend broadcast\` sends it`));
272
+ return 0;
273
+ }
274
+ export async function btcSpendBroadcast(io, flags, [arg, ...rest]) {
275
+ if (!arg || rest.length > 0) {
276
+ io.err(USAGE);
277
+ return 1;
278
+ }
279
+ const hex = (existsSync(arg) ? readFileSync(arg, "utf8") : arg).trim();
280
+ if (!HEX.test(hex)) {
281
+ throw new Error(`${arg} is neither a transaction's hex nor a file of it`);
282
+ }
283
+ const url = setting(io, flags, "esplora");
284
+ io.out(line("send", `a ${hex.length / 2}-byte transaction through Esplora`));
285
+ if (!(await approved(io, flags, {
286
+ question: "Broadcast it?",
287
+ action: "broadcast it",
288
+ }))) {
289
+ return 1;
290
+ }
291
+ io.out(line("sent", await broadcast(hex, esploraReader(url))));
292
+ return 0;
293
+ }
@@ -0,0 +1,194 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { allowanceBound, cofundClientId, describeSimulation, executeTransactionCall, formatUnitsAmount, readClientVault, simulateCall, simulationCaveat, simulationTokens, vaultAbi, } from "@getcofund/protocol/evm";
3
+ import { encodeFunctionData, getAddress, isHex, keccak256, } from "viem";
4
+ import { amountText, baseUnits, tokenInfo } from "../chain.js";
5
+ import { evmChain, keystore, line, send, sender, } from "../context.js";
6
+ import { pickCompany, requirePinned } from "../keystore.js";
7
+ import { loadSignatures } from "../signature-file.js";
8
+ import { USAGE } from "../usage.js";
9
+ import { cofundLine, signedByLines } from "./lines.js";
10
+ import { policyLine, signVaultMessage, spendingPolicy, vaultLine, } from "./transfers.js";
11
+ /**
12
+ * `calls sign`: a signer of a call policy signs a call through its wrapper,
13
+ * new from the flags or the one in a file. `calls send`: sent from the vault
14
+ * once signed often enough. `calls simulate`: its effect, simulated before
15
+ * it is sent.
16
+ */
17
+ async function callLines(evm, message, instructions) {
18
+ const allowances = [];
19
+ for (const { asset, amount } of message.allowances) {
20
+ allowances.push(line("allows", `${amountText(BigInt(amount), await tokenInfo(evm, getAddress(asset)))} to the wrapper`));
21
+ }
22
+ return [
23
+ cofundLine(message.company),
24
+ await policyLine(evm, message.company, message.policyId),
25
+ line("calls", `wrapper ${message.wrapper}, function ${message.func}`),
26
+ line("with", `${(instructions.length - 2) / 2} bytes of instructions, keccak256 ${message.instructionsHash}`),
27
+ ...(allowances.length > 0 ? allowances : [line("allows", "nothing")]),
28
+ await vaultLine(evm, message.company),
29
+ ];
30
+ }
31
+ /** `--allowance <token>:<amount>`: whole tokens of the token, approved to the wrapper for the call. */
32
+ async function allowancesFrom(evm, flags) {
33
+ const allowances = [];
34
+ for (const each of flags.allowance ?? []) {
35
+ const [asset, amount] = each.split(":");
36
+ if (!(asset && amount)) {
37
+ throw new Error(`--allowance takes <token>:<amount>, not ${each}`);
38
+ }
39
+ const token = await tokenInfo(evm, getAddress(asset));
40
+ allowances.push({
41
+ asset: token.address,
42
+ amount: baseUnits(amount, token, "--allowance").toString(),
43
+ });
44
+ }
45
+ return allowances;
46
+ }
47
+ export async function callsSign(io, flags, files) {
48
+ const keys = keystore(io, flags);
49
+ const evm = await evmChain(io, flags);
50
+ let message;
51
+ let instructions;
52
+ let signed = [];
53
+ if (files.length > 0) {
54
+ signed = await loadSignatures(files, evm.deployment, ["Transaction"]);
55
+ message = signed[0].message;
56
+ instructions = signed[0].instructions;
57
+ requirePinned(keys, message.company, flags.company);
58
+ await spendingPolicy(evm, message.company, message.policyId, "call");
59
+ }
60
+ else {
61
+ const { policy: policyId } = flags;
62
+ if (!(policyId && flags.instructions)) {
63
+ io.err(USAGE);
64
+ return 1;
65
+ }
66
+ if (!isHex(flags.instructions, { strict: true }) ||
67
+ flags.instructions.length % 2) {
68
+ throw new Error("--instructions takes the wrapper's instructions as 0x hex");
69
+ }
70
+ instructions = flags.instructions;
71
+ const company = pickCompany(keys, flags.company);
72
+ const policy = await spendingPolicy(evm, company, policyId, "call");
73
+ message = {
74
+ company,
75
+ policyId,
76
+ messageType: policy.policyType,
77
+ wrapper: policy.transaction.wrapper,
78
+ func: policy.transaction.func,
79
+ allowances: await allowancesFrom(evm, flags),
80
+ instructionsHash: keccak256(instructions),
81
+ authId: randomUUID(),
82
+ };
83
+ }
84
+ return await signVaultMessage(io, flags, {
85
+ evm,
86
+ keys,
87
+ type: "Transaction",
88
+ message,
89
+ signed,
90
+ lines: await callLines(evm, message, instructions),
91
+ instructions,
92
+ });
93
+ }
94
+ export async function callsSend(io, flags, files) {
95
+ if (files.length === 0) {
96
+ io.err(USAGE);
97
+ return 1;
98
+ }
99
+ const evm = await evmChain(io, flags);
100
+ const signed = await loadSignatures(files, evm.deployment, ["Transaction"]);
101
+ const message = signed[0].message;
102
+ const instructions = signed[0].instructions;
103
+ const via = sender(io, flags, evm);
104
+ for (const text of [
105
+ ...(await callLines(evm, message, instructions)),
106
+ ...(await signedByLines(evm, message.company, signed)),
107
+ ]) {
108
+ io.out(text);
109
+ }
110
+ const call = await executeTransactionCall(evm, {
111
+ message,
112
+ instructions,
113
+ signatures: signed.map(({ signature }) => signature),
114
+ });
115
+ io.out(line("call", `executeTransaction on the vault ${call.to}`));
116
+ return await send(io, flags, { evm, via, call }, {
117
+ question: "Send this call?",
118
+ action: "send it",
119
+ });
120
+ }
121
+ /**
122
+ * `calls simulate <file>`: the call as its sender would send it (the vault
123
+ * from a member's key, or the guard's relay), run on the node with
124
+ * `eth_simulateV1` before the signers' threshold is met: the policy's
125
+ * threshold is overridden, and the balances it moves read before and after.
126
+ */
127
+ export async function callsSimulate(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, ["Transaction"]);
134
+ const message = signed[0].message;
135
+ const instructions = signed[0].instructions;
136
+ const clientId = cofundClientId(message.company);
137
+ const vault = await readClientVault(evm.reader, evm.deployment, clientId);
138
+ if (!vault) {
139
+ throw new Error(`No cofund "${message.company}" on this chain`);
140
+ }
141
+ const via = sender(io, flags, evm);
142
+ const tx = via.transaction({
143
+ to: vault.vault,
144
+ data: encodeFunctionData({
145
+ abi: vaultAbi,
146
+ functionName: "executeTransaction",
147
+ args: [
148
+ message.company,
149
+ message.policyId,
150
+ message.messageType,
151
+ getAddress(message.wrapper),
152
+ message.func,
153
+ instructions,
154
+ message.allowances.map(({ asset, amount }) => ({
155
+ asset: getAddress(asset),
156
+ amount: BigInt(amount),
157
+ })),
158
+ message.authId,
159
+ [],
160
+ ],
161
+ }),
162
+ });
163
+ for (const text of await callLines(evm, message, instructions)) {
164
+ io.out(text);
165
+ }
166
+ const simulation = await simulateCall(evm.reader, {
167
+ state: evm.deployment.state,
168
+ clientId,
169
+ policyId: message.policyId,
170
+ vault: vault.vault,
171
+ wrapper: message.wrapper,
172
+ tx: { from: via.address, to: tx.to, data: tx.data },
173
+ skipSignatures: true,
174
+ });
175
+ const view = describeSimulation(simulation, formatUnitsAmount, {
176
+ withAddress: true,
177
+ });
178
+ const ran = view.status === "success";
179
+ io.out(line("simulated", `${view.block === null ? view.at : `block ${view.block} ${view.at}`}: ${ran ? "success" : view.notice}`));
180
+ for (const holder of view.holders) {
181
+ for (const change of holder.changes) {
182
+ io.out(line("", `${holder.label} ${change}`));
183
+ }
184
+ }
185
+ for (const note of [
186
+ ...(ran && view.notice ? [view.notice] : []),
187
+ ...view.notes,
188
+ ...simulationCaveat(false),
189
+ allowanceBound(message.allowances, simulationTokens(simulation), formatUnitsAmount),
190
+ ]) {
191
+ io.out(line("", note));
192
+ }
193
+ return ran ? 0 : 1;
194
+ }
@@ -0,0 +1,61 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { cofundClientId, createClientCall, INVITE_USER_TYPES, inviteExpiresAt, normalizeCompanyName, readPredictedVault, typedData, } from "@getcofund/protocol/evm";
3
+ import { evmWallet } from "@getcofund/protocol/keys";
4
+ import { evmChain, line, send, sender, } from "../context.js";
5
+ import { keystorePath, pinCompany, readKeystore } from "../keystore.js";
6
+ import { USAGE } from "../usage.js";
7
+ import { cofundLine, expiryText } from "./lines.js";
8
+ /** The tier a cofund is founded on unless --tier names another: the one Cofund's app founds on. */
9
+ const DEFAULT_TIER = "BUSINESS";
10
+ /**
11
+ * `found`: the founder signs their own admin seat (`InviteUser` over
12
+ * themselves) and sends `createClient`, which creates the cofund and its
13
+ * vault. The company is pinned in the keystore once it lands.
14
+ */
15
+ export async function found(io, flags) {
16
+ if (!(flags.name || flags.company)) {
17
+ io.err(USAGE);
18
+ return 1;
19
+ }
20
+ const company = flags.company ?? normalizeCompanyName(flags.name ?? "");
21
+ if (!company) {
22
+ io.err(`--name "${flags.name}" makes no company: pass --company`);
23
+ return 1;
24
+ }
25
+ const path = keystorePath(io.env, flags.keystore);
26
+ const founder = evmWallet(readKeystore(path));
27
+ const evm = await evmChain(io, flags);
28
+ const via = sender(io, flags, evm);
29
+ const userId = randomUUID();
30
+ const message = {
31
+ company,
32
+ callerId: userId,
33
+ userId,
34
+ userIsAdmin: true,
35
+ userPosition: "admin",
36
+ expiresAt: inviteExpiresAt(),
37
+ authId: randomUUID(),
38
+ };
39
+ const tier = flags.tier ?? DEFAULT_TIER;
40
+ const vault = await readPredictedVault(evm.reader, evm.deployment, cofundClientId(company));
41
+ io.out(cofundLine(company));
42
+ io.out(line("founder", `${userId} (admin) at ${founder.address}`));
43
+ io.out(line("vault", `${vault}, tier ${tier}`));
44
+ io.out(line("signed", `your own seat, valid ${expiryText(message.expiresAt)}`));
45
+ const call = await createClientCall(evm, {
46
+ message,
47
+ founder: founder.address,
48
+ signature: await founder.signTypedData(typedData(evm.deployment.eip712Domain, INVITE_USER_TYPES, message)),
49
+ tier,
50
+ });
51
+ io.out(line("call", `createClient on the users helper ${call.to}`));
52
+ const sent = await send(io, flags, { evm, via, call }, {
53
+ question: "Found this cofund?",
54
+ action: "found it",
55
+ });
56
+ if (sent === 0) {
57
+ pinCompany(path, company);
58
+ io.out(line("pinned", `company "${company}" in ${path}`));
59
+ }
60
+ return sent;
61
+ }