@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,184 @@
1
+ import { encodeFunctionData, erc20Abi, isAddressEqual, keccak256, zeroAddress, } from "viem";
2
+ import { stateAbi } from "../abis/state.js";
3
+ import { vaultAbi } from "../abis/vault.js";
4
+ import { readContract } from "../chain.js";
5
+ import { cofundClientId, TRANSACTION_TYPES, TRANSFER_TYPES, typedData, typedDataDigest, } from "../eip712.js";
6
+ import { readClientVault, readPolicy } from "../reads.js";
7
+ import { ascendingSigners, refuse } from "../signatures.js";
8
+ import { requireAuthIdUnspent, uint } from "./common.js";
9
+ /**
10
+ * A vault's calls (`CFHelpersVaultTemplatev1`), each checked as the vault
11
+ * and State would check it, then encoded for the cofund's vault. The vault
12
+ * accepts the guard, a Cofund admin or any active member of the cofund as the
13
+ * sender: that is the submitter's to be.
14
+ */
15
+ /** The cofund's vault, which State names: there is no call without one. */
16
+ async function vaultOf(ctx, clientId, company) {
17
+ const vault = await readClientVault(ctx.reader, ctx.deployment, clientId);
18
+ if (!vault) {
19
+ refuse("InvalidClient", `no cofund is named "${company}"`);
20
+ }
21
+ return vault;
22
+ }
23
+ /** The policy's checks both executions share: active, of this kind, of this type, signed often enough. */
24
+ function checkPolicy(policy, message, { isTransaction, signatures }, more) {
25
+ if (!policy?.active) {
26
+ refuse("InactivePolicy", `policy ${message.policyId} is not active`);
27
+ }
28
+ if (policy.isTransaction !== isTransaction) {
29
+ refuse("InvalidType", `policy ${message.policyId} is a ${policy.isTransaction ? "transaction" : "transfer"} policy`);
30
+ }
31
+ more(policy);
32
+ if (policy.policyType !== message.messageType) {
33
+ refuse("InvalidPolicyType", `the message is a "${message.messageType}", the policy a "${policy.policyType}"`);
34
+ }
35
+ if (signatures < policy.threshold) {
36
+ refuse("ThresholdNotMet", `policy ${message.policyId} needs ${policy.threshold} signatures; ${signatures} given`);
37
+ }
38
+ return policy;
39
+ }
40
+ /**
41
+ * The signatures in the order the vault walks them, each by an active signer
42
+ * of the policy as State resolves them now (`resolvePolicySigner`).
43
+ */
44
+ async function policySigners(ctx, clientId, policyId, digest, signatures) {
45
+ const ordered = await ascendingSigners(digest, signatures, {
46
+ what: "signature",
47
+ errors: "vault",
48
+ });
49
+ for (const { signer } of ordered) {
50
+ const [ok] = await readContract(ctx.reader, {
51
+ address: ctx.deployment.state,
52
+ abi: stateAbi,
53
+ functionName: "resolvePolicySigner",
54
+ args: [clientId, policyId, signer],
55
+ });
56
+ if (!ok) {
57
+ refuse("InvalidSigner", `${signer} is not an active signer of policy ${policyId}`);
58
+ }
59
+ }
60
+ return ordered.map(({ signature }) => signature);
61
+ }
62
+ /**
63
+ * `executeTransfer`: tokens out of the vault under a transfer policy, on its
64
+ * signers' `Transfer`: within the cap, in the policy's token, to an allowed
65
+ * recipient, with the threshold met, and with the vault holding the amount
66
+ * and the fee the cofund's tier charges on top.
67
+ */
68
+ export async function executeTransferCall(ctx, input) {
69
+ const { deployment, reader } = ctx;
70
+ const { message, signatures } = input;
71
+ const clientId = cofundClientId(message.company);
72
+ const amount = uint(message.amount, "amount");
73
+ const token = message.token;
74
+ const recipient = message.recipient;
75
+ const { vault, tier } = await vaultOf(ctx, clientId, message.company);
76
+ const policy = checkPolicy(await readPolicy(reader, deployment, clientId, message.policyId), message, { isTransaction: false, signatures: signatures.length }, ({ transfer }) => {
77
+ if (amount > transfer.maxAmount) {
78
+ refuse("AmountExceedsPolicy", `${amount} is over the policy's ${transfer.maxAmount}`);
79
+ }
80
+ if (!isAddressEqual(token, transfer.token)) {
81
+ refuse("InvalidToken", `the policy moves ${transfer.token}, not ${token}`);
82
+ }
83
+ });
84
+ const { recipients } = policy.transfer;
85
+ if (recipients.length > 0 &&
86
+ !recipients.some((r) => isAddressEqual(r, recipient))) {
87
+ refuse("RecipientNotAllowed", `${recipient} is not on the policy's allowlist`);
88
+ }
89
+ await requireAuthIdUnspent(ctx, clientId, message.authId);
90
+ const ordered = await policySigners(ctx, clientId, message.policyId, typedDataDigest(typedData(deployment.eip712Domain, TRANSFER_TYPES, message)), signatures);
91
+ const [bps, balance] = await Promise.all([
92
+ readContract(reader, {
93
+ address: deployment.state,
94
+ abi: stateAbi,
95
+ functionName: "subscriptionTierBps",
96
+ args: [tier],
97
+ }),
98
+ readContract(reader, {
99
+ address: token,
100
+ abi: erc20Abi,
101
+ functionName: "balanceOf",
102
+ args: [vault],
103
+ }),
104
+ ]);
105
+ const raw = amount * BigInt(bps);
106
+ const fee = raw > 0n && raw / 10000n === 0n ? 1n : raw / 10000n;
107
+ if (fee !== 0n && balance < fee) {
108
+ refuse("fee transfer failed", `the vault holds ${balance}, under the fee of ${fee}`);
109
+ }
110
+ if (balance - fee < amount) {
111
+ refuse("ERC20 transfer failed", `the vault holds ${balance}, under ${amount}${fee === 0n ? "" : ` and the fee of ${fee}`}`);
112
+ }
113
+ return {
114
+ to: vault,
115
+ data: encodeFunctionData({
116
+ abi: vaultAbi,
117
+ functionName: "executeTransfer",
118
+ args: [
119
+ message.company,
120
+ message.policyId,
121
+ message.messageType,
122
+ amount,
123
+ token,
124
+ recipient,
125
+ message.authId,
126
+ ordered,
127
+ ],
128
+ }),
129
+ };
130
+ }
131
+ /**
132
+ * `executeTransaction`: a call through the policy's wrapper, on its signers'
133
+ * `Transaction` over these `instructions`, with the threshold met and every
134
+ * allowance a non-zero amount of a real token. The vault does not compare
135
+ * `func` with the policy's; neither does this.
136
+ */
137
+ export async function executeTransactionCall(ctx, input) {
138
+ const { deployment, reader } = ctx;
139
+ const { message, instructions, signatures } = input;
140
+ const clientId = cofundClientId(message.company);
141
+ const wrapper = message.wrapper;
142
+ const { vault } = await vaultOf(ctx, clientId, message.company);
143
+ checkPolicy(await readPolicy(reader, deployment, clientId, message.policyId), message, { isTransaction: true, signatures: signatures.length }, ({ transaction }) => {
144
+ if (!isAddressEqual(wrapper, transaction.wrapper)) {
145
+ refuse("InvalidType", `the policy's wrapper is ${transaction.wrapper}, not ${wrapper}`);
146
+ }
147
+ });
148
+ await requireAuthIdUnspent(ctx, clientId, message.authId);
149
+ // The vault hashes the instructions it is given; signatures over another
150
+ // hash recover to strangers.
151
+ if (keccak256(instructions) !== message.instructionsHash.toLowerCase()) {
152
+ refuse("InvalidSigner", "the instructions are not the ones signed: their keccak256 is not the message's instructionsHash");
153
+ }
154
+ const ordered = await policySigners(ctx, clientId, message.policyId, typedDataDigest(typedData(deployment.eip712Domain, TRANSACTION_TYPES, message)), signatures);
155
+ const allowances = message.allowances.map((allowance, i) => {
156
+ const asset = allowance.asset;
157
+ const amount = uint(allowance.amount, `allowance ${i + 1}`);
158
+ if (isAddressEqual(asset, zeroAddress)) {
159
+ refuse("InvalidToken", `allowance ${i + 1} is of the zero address`);
160
+ }
161
+ if (amount === 0n) {
162
+ refuse("InvalidAmount", `allowance ${i + 1} is 0`);
163
+ }
164
+ return { asset, amount };
165
+ });
166
+ return {
167
+ to: vault,
168
+ data: encodeFunctionData({
169
+ abi: vaultAbi,
170
+ functionName: "executeTransaction",
171
+ args: [
172
+ message.company,
173
+ message.policyId,
174
+ message.messageType,
175
+ wrapper,
176
+ message.func,
177
+ instructions,
178
+ allowances,
179
+ message.authId,
180
+ ordered,
181
+ ],
182
+ }),
183
+ };
184
+ }
@@ -0,0 +1,74 @@
1
+ import { type Abi, type Account, type Address, type Chain, type ContractFunctionArgs, type ContractFunctionName, type ContractFunctionReturnType, type Hash, type Hex } from "viem";
2
+ import type { TypedData } from "./eip712.ts";
3
+ /**
4
+ * What the consumer brings to reach an EVM chain: a reader, a signer and a
5
+ * submitter. Each is a small structural type a viem client or account fits
6
+ * as it is.
7
+ */
8
+ /**
9
+ * JSON-RPC reads: an EIP-1193 `request`. A viem `PublicClient` fits, as does
10
+ * any EIP-1193 provider; an ethers provider fits through
11
+ * `{ request: ({ method, params }) => provider.send(method, params) }`.
12
+ */
13
+ export type EvmReader = {
14
+ request(args: {
15
+ method: string;
16
+ params?: unknown;
17
+ }): Promise<unknown>;
18
+ };
19
+ /**
20
+ * Signs EIP-712 typed data: a viem `Account` (a local key, a Ledger, a KMS
21
+ * adapter) as it is, or a viem `WalletClient` through `walletSigner`.
22
+ */
23
+ export type TypedDataSigner = {
24
+ readonly address: Address;
25
+ signTypedData(data: TypedData): Promise<Hex>;
26
+ };
27
+ /** One call: where it goes and what it carries. Every call builder returns one. */
28
+ export type EvmCall = {
29
+ to: Address;
30
+ data: Hex;
31
+ value?: bigint;
32
+ };
33
+ /** Sends a call and pays for it: `directSubmitter`, `guardSubmitter`, or the consumer's own (a gas manager). */
34
+ export type EvmSubmitter = {
35
+ send(call: EvmCall): Promise<Hash>;
36
+ };
37
+ /**
38
+ * What a submitter needs of a wallet: a viem `WalletClient` with its account
39
+ * fits as it is.
40
+ */
41
+ export type EvmWallet = {
42
+ readonly account: Account | undefined;
43
+ readonly chain: Chain | undefined;
44
+ sendTransaction(args: {
45
+ account: Account;
46
+ chain: Chain | null;
47
+ to: Address;
48
+ data: Hex;
49
+ value?: bigint;
50
+ }): Promise<Hash>;
51
+ };
52
+ /** A viem `WalletClient` as a `TypedDataSigner`: its account signs. */
53
+ export declare function walletSigner(client: {
54
+ readonly account: Account | undefined;
55
+ signTypedData(args: TypedData & {
56
+ account: Account;
57
+ }): Promise<Hex>;
58
+ }): TypedDataSigner;
59
+ /** `eth_call` at the latest block: the return data. */
60
+ export declare function ethCall(reader: EvmReader, call: {
61
+ to: Address;
62
+ data: Hex;
63
+ }): Promise<Hex>;
64
+ /** A view function of `abi` at `address`, read at the latest block and decoded. */
65
+ export declare function readContract<const abi extends Abi, functionName extends ContractFunctionName<abi, "view" | "pure">, const args extends ContractFunctionArgs<abi, "view" | "pure", functionName>>(reader: EvmReader, read: {
66
+ address: Address;
67
+ abi: abi;
68
+ functionName: functionName;
69
+ args: args;
70
+ }): Promise<ContractFunctionReturnType<abi, "view" | "pure", functionName, args>>;
71
+ /** The chain id the reader is on. */
72
+ export declare function readChainId(reader: EvmReader): Promise<number>;
73
+ /** The latest block's timestamp, unix seconds: the clock a deadline is checked on. */
74
+ export declare function readLatestTimestamp(reader: EvmReader): Promise<bigint>;
@@ -0,0 +1,48 @@
1
+ import { decodeFunctionResult, encodeFunctionData, } from "viem";
2
+ /** A viem `WalletClient` as a `TypedDataSigner`: its account signs. */
3
+ export function walletSigner(client) {
4
+ const account = client.account;
5
+ if (!account) {
6
+ throw new Error("The wallet client has no account to sign with");
7
+ }
8
+ return {
9
+ address: account.address,
10
+ signTypedData: (data) => client.signTypedData({ ...data, account }),
11
+ };
12
+ }
13
+ /** `eth_call` at the latest block: the return data. */
14
+ export async function ethCall(reader, call) {
15
+ return (await reader.request({
16
+ method: "eth_call",
17
+ params: [call, "latest"],
18
+ }));
19
+ }
20
+ /** A view function of `abi` at `address`, read at the latest block and decoded. */
21
+ export async function readContract(reader, read) {
22
+ const data = encodeFunctionData({
23
+ abi: read.abi,
24
+ functionName: read.functionName,
25
+ args: read.args,
26
+ });
27
+ const result = await ethCall(reader, { to: read.address, data });
28
+ return decodeFunctionResult({
29
+ abi: read.abi,
30
+ functionName: read.functionName,
31
+ data: result,
32
+ });
33
+ }
34
+ /** The chain id the reader is on. */
35
+ export async function readChainId(reader) {
36
+ return Number(BigInt((await reader.request({ method: "eth_chainId" }))));
37
+ }
38
+ /** The latest block's timestamp, unix seconds: the clock a deadline is checked on. */
39
+ export async function readLatestTimestamp(reader) {
40
+ const block = (await reader.request({
41
+ method: "eth_getBlockByNumber",
42
+ params: ["latest", false],
43
+ }));
44
+ if (!block) {
45
+ throw new Error("The node returned no latest block");
46
+ }
47
+ return BigInt(block.timestamp);
48
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Company-name normalization.
3
+ *
4
+ * The user types a free-form display name (spaces, mixed case). We derive a
5
+ * stable normalized identifier from it that seeds the on-chain client ID
6
+ * (`sha256(name)`) and enforces org uniqueness. This MUST stay deterministic
7
+ * and is shared between the client form (taken-check), the server action
8
+ * (the value actually persisted as `Company.name`) and the SDK (the `company`
9
+ * a founder signs), so it has no server deps.
10
+ */
11
+ export declare const COMPANY_NAME_MIN_LENGTH = 3;
12
+ export declare const COMPANY_NAME_MAX_LENGTH = 255;
13
+ /**
14
+ * Lowercase, trim, collapse any run of non-alphanumeric characters into a
15
+ * single hyphen, and strip leading/trailing hyphens. Idempotent.
16
+ *
17
+ * Examples:
18
+ * "Acme Corp" -> "acme-corp"
19
+ * " Foo__Bar!! " -> "foo-bar"
20
+ * "acme-co" -> "acme-co" (already-normalized handles are a no-op)
21
+ */
22
+ export declare function normalizeCompanyName(input: string): string;
23
+ /**
24
+ * Scope a normalized identifier to a deployment.
25
+ *
26
+ * `clientId = sha256(Company.name)` has no environment discriminator, so two
27
+ * deployments sharing a state contract derive the SAME clientId for the same
28
+ * org name. Prefixing the stored identifier separates them, and because every
29
+ * clientId in the app is derived from `Company.name`, doing it here covers all
30
+ * of those call sites without touching any of them.
31
+ *
32
+ * Pure and namespace-as-argument so it can be tested without env, and so the
33
+ * prefix cannot silently differ between the two places that must agree: the
34
+ * value persisted at signup, and the invite-link token the form previews.
35
+ *
36
+ * Idempotent for the same namespace, matching `normalizeCompanyName`, so
37
+ * re-applying it to an already-scoped identifier is a no-op.
38
+ */
39
+ export declare function applyCompanyNamespace(normalized: string, namespace: string | null | undefined): string;
40
+ /** A deployment's namespace as it prefixes `Company.name`: normalized; null when there is none. */
41
+ export declare function normalizeCompanyNamespace(namespace: string | null | undefined): string | null;
42
+ /**
43
+ * The `Company.name` a deployment with `namespace` stores for a cofund named
44
+ * `displayName`: the app's at signup, and the `company` a founder signs.
45
+ */
46
+ export declare function companyIdentifierFor(displayName: string, namespace: string | null | undefined): string;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Company-name normalization.
3
+ *
4
+ * The user types a free-form display name (spaces, mixed case). We derive a
5
+ * stable normalized identifier from it that seeds the on-chain client ID
6
+ * (`sha256(name)`) and enforces org uniqueness. This MUST stay deterministic
7
+ * and is shared between the client form (taken-check), the server action
8
+ * (the value actually persisted as `Company.name`) and the SDK (the `company`
9
+ * a founder signs), so it has no server deps.
10
+ */
11
+ export const COMPANY_NAME_MIN_LENGTH = 3;
12
+ export const COMPANY_NAME_MAX_LENGTH = 255;
13
+ /**
14
+ * Lowercase, trim, collapse any run of non-alphanumeric characters into a
15
+ * single hyphen, and strip leading/trailing hyphens. Idempotent.
16
+ *
17
+ * Examples:
18
+ * "Acme Corp" -> "acme-corp"
19
+ * " Foo__Bar!! " -> "foo-bar"
20
+ * "acme-co" -> "acme-co" (already-normalized handles are a no-op)
21
+ */
22
+ export function normalizeCompanyName(input) {
23
+ return input
24
+ .trim()
25
+ .toLowerCase()
26
+ .replace(/[^a-z0-9]+/g, "-")
27
+ .replace(/^-+|-+$/g, "");
28
+ }
29
+ /**
30
+ * Scope a normalized identifier to a deployment.
31
+ *
32
+ * `clientId = sha256(Company.name)` has no environment discriminator, so two
33
+ * deployments sharing a state contract derive the SAME clientId for the same
34
+ * org name. Prefixing the stored identifier separates them, and because every
35
+ * clientId in the app is derived from `Company.name`, doing it here covers all
36
+ * of those call sites without touching any of them.
37
+ *
38
+ * Pure and namespace-as-argument so it can be tested without env, and so the
39
+ * prefix cannot silently differ between the two places that must agree: the
40
+ * value persisted at signup, and the invite-link token the form previews.
41
+ *
42
+ * Idempotent for the same namespace, matching `normalizeCompanyName`, so
43
+ * re-applying it to an already-scoped identifier is a no-op.
44
+ */
45
+ export function applyCompanyNamespace(normalized, namespace) {
46
+ const prefix = normalizeCompanyNamespace(namespace);
47
+ if (!prefix) {
48
+ return normalized;
49
+ }
50
+ return normalized.startsWith(`${prefix}-`)
51
+ ? normalized
52
+ : `${prefix}-${normalized}`;
53
+ }
54
+ /** A deployment's namespace as it prefixes `Company.name`: normalized; null when there is none. */
55
+ export function normalizeCompanyNamespace(namespace) {
56
+ return normalizeCompanyName(namespace ?? "") || null;
57
+ }
58
+ /**
59
+ * The `Company.name` a deployment with `namespace` stores for a cofund named
60
+ * `displayName`: the app's at signup, and the `company` a founder signs.
61
+ */
62
+ export function companyIdentifierFor(displayName, namespace) {
63
+ return applyCompanyNamespace(normalizeCompanyName(displayName), namespace);
64
+ }
@@ -0,0 +1,47 @@
1
+ import { type Address as AddressType } from "viem";
2
+ import { z } from "zod";
3
+ import { type EvmReader } from "./chain.ts";
4
+ /** A `deployments/<chainId>.json`: the fields the package reads; any others (gov, sponsor, dates) pass unread. */
5
+ export declare const EvmDeploymentSchema: z.ZodObject<{
6
+ chainId: z.ZodNumber;
7
+ state: z.ZodPipe<z.ZodString, z.ZodTransform<`0x${string}`, string>>;
8
+ users: z.ZodPipe<z.ZodString, z.ZodTransform<`0x${string}`, string>>;
9
+ policies: z.ZodPipe<z.ZodString, z.ZodTransform<`0x${string}`, string>>;
10
+ vaults: z.ZodPipe<z.ZodString, z.ZodTransform<`0x${string}`, string>>;
11
+ sponsorGuard: z.ZodPipe<z.ZodString, z.ZodTransform<`0x${string}`, string>>;
12
+ eip712Domain: z.ZodObject<{
13
+ name: z.ZodString;
14
+ version: z.ZodString;
15
+ chainId: z.ZodNumber;
16
+ verifyingContract: z.ZodPipe<z.ZodString, z.ZodTransform<`0x${string}`, string>>;
17
+ }, z.core.$strip>;
18
+ }, z.core.$strip>;
19
+ export type EvmDeployment = {
20
+ chainId: number;
21
+ state: AddressType;
22
+ users: AddressType;
23
+ policies: AddressType;
24
+ vaults: AddressType;
25
+ sponsorGuard: AddressType;
26
+ eip712Domain: {
27
+ name: string;
28
+ version: string;
29
+ chainId: number;
30
+ verifyingContract: AddressType;
31
+ };
32
+ };
33
+ /** A deployment config that disagrees with itself or with its chain; `differences` says where. */
34
+ export declare class DeploymentMismatchError extends Error {
35
+ readonly differences: string[];
36
+ constructor(differences: string[]);
37
+ }
38
+ /**
39
+ * `config` checked against itself and its chain, before anything is signed
40
+ * under it: the domain's chain id and verifying contract are the
41
+ * deployment's chain id and State; the reader is on that chain; each helper
42
+ * is the one State registers (the users and policies helpers, the guard, and
43
+ * the vault factory under `"vaults"`); and the domain's name and version are
44
+ * the ones the registered users helper reports (ERC-5267). A config that
45
+ * differs anywhere is refused with every difference.
46
+ */
47
+ export declare function loadEvmDeployment(config: unknown, reader: EvmReader): Promise<EvmDeployment>;
@@ -0,0 +1,110 @@
1
+ import { getAddress, isAddressEqual } from "viem";
2
+ import { z } from "zod";
3
+ import { usersAbi } from "./abis/users.js";
4
+ import { readChainId, readContract } from "./chain.js";
5
+ import { readHelperContract } from "./reads.js";
6
+ import { Address } from "./types.js";
7
+ /**
8
+ * A deployment of the contracts: the shape of cofund-solidity's
9
+ * `deployments/<chainId>.json`, which the deploy script writes. The package
10
+ * ships none; the consumer passes the one they deployed or trust.
11
+ */
12
+ const ChecksummedAddress = Address.transform((a) => getAddress(a));
13
+ /** A `deployments/<chainId>.json`: the fields the package reads; any others (gov, sponsor, dates) pass unread. */
14
+ export const EvmDeploymentSchema = z.object({
15
+ chainId: z.number().int().positive(),
16
+ /** `CFHelpersStatev1`: the registry, and every message's verifying contract. */
17
+ state: ChecksummedAddress,
18
+ /** `CFHelpersUsersv1`, registered as `helperContracts("users")`. */
19
+ users: ChecksummedAddress,
20
+ /** `CFHelpersPoliciesv1`, registered as `helperContracts("policies")`. */
21
+ policies: ChecksummedAddress,
22
+ /** `CFHelpersVaultFactoryv1`, registered as `helperContracts("vaults")`. */
23
+ vaults: ChecksummedAddress,
24
+ /** `CFSponsorGuardv1`, registered as `helperContracts("sponsorGuard")`. */
25
+ sponsorGuard: ChecksummedAddress,
26
+ eip712Domain: z.object({
27
+ name: z.string(),
28
+ version: z.string(),
29
+ chainId: z.number().int().positive(),
30
+ verifyingContract: ChecksummedAddress,
31
+ }),
32
+ });
33
+ /** A deployment config that disagrees with itself or with its chain; `differences` says where. */
34
+ export class DeploymentMismatchError extends Error {
35
+ differences;
36
+ constructor(differences) {
37
+ super(`The deployment does not match its chain: ${differences.join("; ")}`);
38
+ this.name = "DeploymentMismatchError";
39
+ this.differences = differences;
40
+ }
41
+ }
42
+ /** The config's address of each helper, by the key State registers it under. */
43
+ const HELPERS = [
44
+ ["users", "users"],
45
+ ["policies", "policies"],
46
+ ["vaults", "vaults"],
47
+ ["sponsorGuard", "sponsorGuard"],
48
+ ];
49
+ /**
50
+ * `config` checked against itself and its chain, before anything is signed
51
+ * under it: the domain's chain id and verifying contract are the
52
+ * deployment's chain id and State; the reader is on that chain; each helper
53
+ * is the one State registers (the users and policies helpers, the guard, and
54
+ * the vault factory under `"vaults"`); and the domain's name and version are
55
+ * the ones the registered users helper reports (ERC-5267). A config that
56
+ * differs anywhere is refused with every difference.
57
+ */
58
+ export async function loadEvmDeployment(config, reader) {
59
+ const parsed = EvmDeploymentSchema.safeParse(config);
60
+ if (!parsed.success) {
61
+ throw new Error(`Not a deployment: ${parsed.error.issues
62
+ .map((issue) => `${issue.path.join(".") || "config"}: ${issue.message}`)
63
+ .join("; ")}`);
64
+ }
65
+ const deployment = parsed.data;
66
+ const { eip712Domain: domain } = deployment;
67
+ const differences = [];
68
+ if (domain.chainId !== deployment.chainId) {
69
+ differences.push(`eip712Domain.chainId is ${domain.chainId}, not the deployment's ${deployment.chainId}`);
70
+ }
71
+ if (!isAddressEqual(domain.verifyingContract, deployment.state)) {
72
+ differences.push(`eip712Domain.verifyingContract is ${domain.verifyingContract}, not State ${deployment.state}`);
73
+ }
74
+ const chainId = await readChainId(reader);
75
+ if (chainId !== deployment.chainId) {
76
+ differences.push(`the reader is on chain ${chainId}, not ${deployment.chainId}`);
77
+ throw new DeploymentMismatchError(differences);
78
+ }
79
+ let registered;
80
+ try {
81
+ registered = await Promise.all(HELPERS.map(([kind]) => readHelperContract(reader, deployment, kind)));
82
+ }
83
+ catch {
84
+ differences.push(`State at ${deployment.state} could not be read`);
85
+ throw new DeploymentMismatchError(differences);
86
+ }
87
+ HELPERS.forEach(([kind, field], i) => {
88
+ if (!isAddressEqual(registered[i], deployment[field])) {
89
+ differences.push(`${field} is ${deployment[field]}, but State registers ${registered[i]} as "${kind}"`);
90
+ }
91
+ });
92
+ const users = registered[0];
93
+ try {
94
+ const [, name, version, onChainId, verifyingContract] = await readContract(reader, { address: users, abi: usersAbi, functionName: "eip712Domain", args: [] });
95
+ if (name !== domain.name || version !== domain.version) {
96
+ differences.push(`eip712Domain is ("${domain.name}", "${domain.version}"), the contracts sign under ("${name}", "${version}")`);
97
+ }
98
+ if (Number(onChainId) !== deployment.chainId ||
99
+ !isAddressEqual(verifyingContract, deployment.state)) {
100
+ differences.push(`the users helper's domain is chain ${onChainId} and ${verifyingContract}`);
101
+ }
102
+ }
103
+ catch {
104
+ differences.push(`the users helper at ${users} reports no EIP-712 domain`);
105
+ }
106
+ if (differences.length > 0) {
107
+ throw new DeploymentMismatchError(differences);
108
+ }
109
+ return deployment;
110
+ }