@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,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
|
+
}
|