getcofund 0.1.0 → 0.2.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/README.md +57 -0
- package/dist/api-types.d.ts +201 -0
- package/dist/api-types.js +124 -5
- package/dist/cli/context.d.ts +1 -1
- package/dist/cli/context.js +5 -0
- package/dist/cli/invite.d.ts +10 -1
- package/dist/cli/invite.js +81 -3
- package/dist/cli/policies.d.ts +17 -0
- package/dist/cli/policies.js +100 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +25 -1
- package/dist/client.d.ts +63 -1
- package/dist/client.js +25 -1
- package/dist/eip712.d.ts +14 -0
- package/dist/eip712.js +16 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/invite.d.ts +28 -1
- package/dist/invite.js +40 -1
- package/dist/policies.d.ts +69 -0
- package/dist/policies.js +132 -0
- package/dist/requests.d.ts +1 -1
- package/dist/requests.js +21 -60
- package/package.json +1 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { ApiPolicyTypeSchema, } from "../api-types.js";
|
|
3
|
+
import { WRAPPER_ENTRYPOINT } from "../eip712.js";
|
|
4
|
+
import { btcMessageSigner, evmWallet, readKeystore } from "../keys.js";
|
|
5
|
+
import { signPolicyCreation } from "../policies.js";
|
|
6
|
+
import { approval, chainPins, client, keystorePath, pickCofund, pinnedCompany, USAGE, } from "./context.js";
|
|
7
|
+
/** The members `list` names, by id or email, in that order. */
|
|
8
|
+
function pickSigners(members, list) {
|
|
9
|
+
return list.split(",").map((wanted) => {
|
|
10
|
+
const key = wanted.trim().toLowerCase();
|
|
11
|
+
const member = members.find((m) => m.id === key || m.email.toLowerCase() === key);
|
|
12
|
+
if (!member) {
|
|
13
|
+
throw new Error(`No member ${wanted.trim()} in this cofund`);
|
|
14
|
+
}
|
|
15
|
+
return member;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/** A transfer policy by default, a call policy with --wrapper. */
|
|
19
|
+
function policyType(values) {
|
|
20
|
+
if (values.type) {
|
|
21
|
+
return ApiPolicyTypeSchema.parse(values.type);
|
|
22
|
+
}
|
|
23
|
+
return values.wrapper ? "Treasury_Management" : "Operational_Expense";
|
|
24
|
+
}
|
|
25
|
+
function policyLines(policy, company) {
|
|
26
|
+
const names = policy.signers.map((s) => s.name ?? s.email).join(", ");
|
|
27
|
+
return [
|
|
28
|
+
`policy ${policy.name} (${policy.type})`,
|
|
29
|
+
`vault ${policy.vault.address} on ${policy.vault.chain}`,
|
|
30
|
+
`signers ${names}; ${policy.threshold} of ${policy.signers.length} must sign`,
|
|
31
|
+
policy.wrapper
|
|
32
|
+
? `calls ${policy.wrapper}, function ${WRAPPER_ENTRYPOINT}`
|
|
33
|
+
: `cap ${policy.maxAmount} base units of ${policy.token} per transfer`,
|
|
34
|
+
`company "${company}"`,
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* `cofund policies create`: build the policy's activation from the flags, sign
|
|
39
|
+
* it with the local key and create the policy. It activates at once when one
|
|
40
|
+
* admin may activate it, else it awaits the other admins.
|
|
41
|
+
*/
|
|
42
|
+
export async function policiesCreate(io, values) {
|
|
43
|
+
const threshold = Number(values.threshold);
|
|
44
|
+
const transfer = values.token && values["max-amount"];
|
|
45
|
+
if (!(values.vault &&
|
|
46
|
+
values.name &&
|
|
47
|
+
values.signers &&
|
|
48
|
+
Number.isInteger(threshold)) ||
|
|
49
|
+
Boolean(transfer) === Boolean(values.wrapper)) {
|
|
50
|
+
io.err(USAGE);
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
const keystore = readKeystore(keystorePath(io, values.keystore));
|
|
54
|
+
const api = client(io);
|
|
55
|
+
const cofundId = pickCofund(await api.me(), values.cofund);
|
|
56
|
+
const company = pinnedCompany(keystore, cofundId);
|
|
57
|
+
const [vaults, members] = await Promise.all([
|
|
58
|
+
api.vaults(cofundId),
|
|
59
|
+
api.members(cofundId),
|
|
60
|
+
]);
|
|
61
|
+
const vault = vaults.find((v) => v.id === values.vault);
|
|
62
|
+
if (!vault) {
|
|
63
|
+
io.err(`No vault ${values.vault} in this cofund`);
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
const common = {
|
|
67
|
+
id: randomUUID(),
|
|
68
|
+
vault,
|
|
69
|
+
name: values.name,
|
|
70
|
+
type: policyType(values),
|
|
71
|
+
signers: pickSigners(members, values.signers),
|
|
72
|
+
threshold,
|
|
73
|
+
};
|
|
74
|
+
const policy = values.wrapper
|
|
75
|
+
? { ...common, wrapper: values.wrapper }
|
|
76
|
+
: {
|
|
77
|
+
...common,
|
|
78
|
+
token: values.token,
|
|
79
|
+
maxAmount: values["max-amount"],
|
|
80
|
+
};
|
|
81
|
+
for (const line of policyLines(policy, company)) {
|
|
82
|
+
io.out(line);
|
|
83
|
+
}
|
|
84
|
+
const approve = approval(io, values.yes, "Sign and create this policy?");
|
|
85
|
+
if (!approve) {
|
|
86
|
+
io.err("Pass --yes to create without a prompt");
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
if (!(await approve())) {
|
|
90
|
+
io.err("Not created");
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
const body = await signPolicyCreation(policy, company, chainPins(io), {
|
|
94
|
+
evm: evmWallet(keystore),
|
|
95
|
+
btc: btcMessageSigner(keystore),
|
|
96
|
+
});
|
|
97
|
+
const created = await api.createPolicy(cofundId, body);
|
|
98
|
+
io.out(`created ${created.id} (${created.status})`);
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
* cofund keys init [--force]
|
|
6
6
|
* cofund me
|
|
7
7
|
* cofund invite accept [--invite <id>] [--company <name>]
|
|
8
|
+
* cofund invite create --email <email> --first-name <name> --last-name <name>
|
|
9
|
+
* --position <title> [--admin] [--chains A,B] [--yes]
|
|
10
|
+
* cofund policies create --vault <id> --name <name> --signers <id|email,…>
|
|
11
|
+
* --threshold <n> (--token <address|BTC> --max-amount <base units> |
|
|
12
|
+
* --wrapper <address>) [--type <PolicyType>] [--yes]
|
|
8
13
|
* cofund requests [list]
|
|
9
14
|
* cofund requests show <id>
|
|
10
15
|
* cofund requests sign <id> [--yes]
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
* cofund keys init [--force]
|
|
6
6
|
* cofund me
|
|
7
7
|
* cofund invite accept [--invite <id>] [--company <name>]
|
|
8
|
+
* cofund invite create --email <email> --first-name <name> --last-name <name>
|
|
9
|
+
* --position <title> [--admin] [--chains A,B] [--yes]
|
|
10
|
+
* cofund policies create --vault <id> --name <name> --signers <id|email,…>
|
|
11
|
+
* --threshold <n> (--token <address|BTC> --max-amount <base units> |
|
|
12
|
+
* --wrapper <address>) [--type <PolicyType>] [--yes]
|
|
8
13
|
* cofund requests [list]
|
|
9
14
|
* cofund requests show <id>
|
|
10
15
|
* cofund requests sign <id> [--yes]
|
|
@@ -27,8 +32,9 @@ import { createInterface } from "node:readline/promises";
|
|
|
27
32
|
import { pathToFileURL } from "node:url";
|
|
28
33
|
import { parseArgs } from "node:util";
|
|
29
34
|
import { client, USAGE } from "./cli/context.js";
|
|
30
|
-
import { inviteAccept } from "./cli/invite.js";
|
|
35
|
+
import { inviteAccept, inviteCreate } from "./cli/invite.js";
|
|
31
36
|
import { keysInit } from "./cli/keys.js";
|
|
37
|
+
import { policiesCreate } from "./cli/policies.js";
|
|
32
38
|
import { requestsCommand } from "./cli/requests.js";
|
|
33
39
|
import { stocksBuy } from "./cli/stocks.js";
|
|
34
40
|
import { webhooksCommand } from "./cli/webhooks.js";
|
|
@@ -45,6 +51,20 @@ export async function runCli(argv, io) {
|
|
|
45
51
|
policy: { type: "string" },
|
|
46
52
|
spend: { type: "string" },
|
|
47
53
|
events: { type: "string" },
|
|
54
|
+
vault: { type: "string" },
|
|
55
|
+
name: { type: "string" },
|
|
56
|
+
type: { type: "string" },
|
|
57
|
+
signers: { type: "string" },
|
|
58
|
+
threshold: { type: "string" },
|
|
59
|
+
token: { type: "string" },
|
|
60
|
+
"max-amount": { type: "string" },
|
|
61
|
+
wrapper: { type: "string" },
|
|
62
|
+
email: { type: "string" },
|
|
63
|
+
"first-name": { type: "string" },
|
|
64
|
+
"last-name": { type: "string" },
|
|
65
|
+
position: { type: "string" },
|
|
66
|
+
admin: { type: "boolean" },
|
|
67
|
+
chains: { type: "string" },
|
|
48
68
|
yes: { type: "boolean", short: "y" },
|
|
49
69
|
help: { type: "boolean", short: "h" },
|
|
50
70
|
},
|
|
@@ -73,6 +93,10 @@ export async function runCli(argv, io) {
|
|
|
73
93
|
return 0;
|
|
74
94
|
case "invite accept":
|
|
75
95
|
return await inviteAccept(io, values);
|
|
96
|
+
case "invite create":
|
|
97
|
+
return await inviteCreate(io, values);
|
|
98
|
+
case "policies create":
|
|
99
|
+
return await policiesCreate(io, values);
|
|
76
100
|
default:
|
|
77
101
|
io.err(USAGE);
|
|
78
102
|
return 1;
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AcceptInviteRequest, type CreateWebhookRequest, type ProposeCallRequest, type ProposeTransferRequest } from "./api-types.ts";
|
|
1
|
+
import { type AcceptInviteRequest, type CreateInviteRequest, type CreatePolicyRequest, type CreateWebhookRequest, type ProposeCallRequest, type ProposeTransferRequest } from "./api-types.ts";
|
|
2
2
|
export declare class CofundApiError extends Error {
|
|
3
3
|
readonly status: number;
|
|
4
4
|
constructor(status: number, message: string);
|
|
@@ -168,9 +168,71 @@ export declare class CofundClient {
|
|
|
168
168
|
wrapper: string | null;
|
|
169
169
|
func: "entry" | null;
|
|
170
170
|
}>;
|
|
171
|
+
/**
|
|
172
|
+
* Admins only: create a policy with the creator's activation signature
|
|
173
|
+
* (`signPolicyCreation`). It activates at once when the activation rule
|
|
174
|
+
* lets one admin do it, else it awaits the other admins' approvals as a
|
|
175
|
+
* `policy` request with the same id. Safe to retry: the same `id` returns
|
|
176
|
+
* the same policy.
|
|
177
|
+
*/
|
|
178
|
+
createPolicy(cofundId: string, body: CreatePolicyRequest): Promise<{
|
|
179
|
+
id: string;
|
|
180
|
+
name: string;
|
|
181
|
+
type: "Contractor_Stipend" | "Operational_Expense" | "Treasury_Management";
|
|
182
|
+
status: "PENDING" | "ACTIVE" | "INACTIVE" | "FAILED" | "AWAITING_SIGNATURES";
|
|
183
|
+
threshold: number;
|
|
184
|
+
vault: {
|
|
185
|
+
id: string;
|
|
186
|
+
chain: "ETHEREUM" | "BASE" | "ARBITRUM" | "ROBINHOOD";
|
|
187
|
+
address: string;
|
|
188
|
+
} | {
|
|
189
|
+
id: string;
|
|
190
|
+
chain: "BTC";
|
|
191
|
+
address: string;
|
|
192
|
+
};
|
|
193
|
+
signers: {
|
|
194
|
+
id: string;
|
|
195
|
+
name: string | null;
|
|
196
|
+
}[];
|
|
197
|
+
token: {
|
|
198
|
+
address: string;
|
|
199
|
+
symbol: string;
|
|
200
|
+
decimals: number;
|
|
201
|
+
} | null;
|
|
202
|
+
maxAmount: string | null;
|
|
203
|
+
recipients: string[];
|
|
204
|
+
wrapper: string | null;
|
|
205
|
+
func: "entry" | null;
|
|
206
|
+
}>;
|
|
207
|
+
/** Admins only: the user id an invite to `email` must seat; null for a new account. */
|
|
208
|
+
inviteUserId(cofundId: string, email: string): Promise<{
|
|
209
|
+
userId: string | null;
|
|
210
|
+
}>;
|
|
211
|
+
/**
|
|
212
|
+
* Admins only: invite a member with the admin's `InviteUser` per chain
|
|
213
|
+
* (`signInviteCreation`). Safe to retry: the same `id` returns the same
|
|
214
|
+
* invite.
|
|
215
|
+
*/
|
|
216
|
+
createInvite(cofundId: string, body: CreateInviteRequest): Promise<{
|
|
217
|
+
id: string;
|
|
218
|
+
cofundId: string;
|
|
219
|
+
member: {
|
|
220
|
+
id: string;
|
|
221
|
+
email: string;
|
|
222
|
+
isAdmin: boolean;
|
|
223
|
+
position: string;
|
|
224
|
+
};
|
|
225
|
+
chains: {
|
|
226
|
+
chain: "ETHEREUM" | "BASE" | "ARBITRUM" | "ROBINHOOD";
|
|
227
|
+
id: string;
|
|
228
|
+
status: "FAILED" | "AWAITING_SIGNATURES" | "COMPLETED" | "AWAITING_SUBMISSION" | "PROCESSING" | "EXPIRED" | "CANCELED" | "REJECTED";
|
|
229
|
+
expiresAt: string;
|
|
230
|
+
}[];
|
|
231
|
+
}>;
|
|
171
232
|
/** Admins only. */
|
|
172
233
|
members(cofundId: string): Promise<{
|
|
173
234
|
id: string;
|
|
235
|
+
onchainId: string;
|
|
174
236
|
email: string;
|
|
175
237
|
name: string | null;
|
|
176
238
|
isAdmin: boolean;
|
package/dist/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { AcceptInviteResultSchema, ApiCallRequestSchema, ApiErrorSchema, ApiInviteSchema, ApiMemberSchema, ApiMeSchema, ApiPolicySchema, ApiRequestSchema, ApiTransferRequestSchema, ApiVaultDetailSchema, ApiVaultSchema, ApiWebhookSchema, CreatedWebhookSchema, DeleteWebhookResultSchema, RejectResultSchema, } from "./api-types.js";
|
|
2
|
+
import { AcceptInviteResultSchema, ApiCallRequestSchema, ApiCreatedInviteSchema, ApiErrorSchema, ApiInviteSchema, ApiMemberSchema, ApiMeSchema, ApiPolicySchema, ApiRequestSchema, ApiTransferRequestSchema, ApiVaultDetailSchema, ApiVaultSchema, ApiWebhookSchema, CreatedWebhookSchema, DeleteWebhookResultSchema, InviteUserIdSchema, RejectResultSchema, } from "./api-types.js";
|
|
3
3
|
const TRAILING_SLASHES = /\/+$/;
|
|
4
4
|
/** Gateway errors a retry of an idempotent proposal may outlast. */
|
|
5
5
|
const RETRYABLE = new Set([502, 503, 504]);
|
|
@@ -106,6 +106,30 @@ export class CofundClient {
|
|
|
106
106
|
policy(cofundId, policyId) {
|
|
107
107
|
return this.call(ApiPolicySchema, `/cofunds/${cofundId}/policies/${policyId}`);
|
|
108
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Admins only: create a policy with the creator's activation signature
|
|
111
|
+
* (`signPolicyCreation`). It activates at once when the activation rule
|
|
112
|
+
* lets one admin do it, else it awaits the other admins' approvals as a
|
|
113
|
+
* `policy` request with the same id. Safe to retry: the same `id` returns
|
|
114
|
+
* the same policy.
|
|
115
|
+
*/
|
|
116
|
+
createPolicy(cofundId, body) {
|
|
117
|
+
return this.call(ApiPolicySchema, `/cofunds/${cofundId}/policies`, body, {
|
|
118
|
+
retries: 2,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/** Admins only: the user id an invite to `email` must seat; null for a new account. */
|
|
122
|
+
inviteUserId(cofundId, email) {
|
|
123
|
+
return this.call(InviteUserIdSchema, `/cofunds/${cofundId}/invites/user-id`, { email });
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Admins only: invite a member with the admin's `InviteUser` per chain
|
|
127
|
+
* (`signInviteCreation`). Safe to retry: the same `id` returns the same
|
|
128
|
+
* invite.
|
|
129
|
+
*/
|
|
130
|
+
createInvite(cofundId, body) {
|
|
131
|
+
return this.call(ApiCreatedInviteSchema, `/cofunds/${cofundId}/invites`, body, { retries: 2 });
|
|
132
|
+
}
|
|
109
133
|
/** Admins only. */
|
|
110
134
|
members(cofundId) {
|
|
111
135
|
return this.call(z.array(ApiMemberSchema), `/cofunds/${cofundId}/members`);
|
package/dist/eip712.d.ts
CHANGED
|
@@ -87,6 +87,20 @@ export declare function buildCofundDomain({ chainId, state, }: {
|
|
|
87
87
|
export declare const INVITE_USER_TYPES: TypeSet;
|
|
88
88
|
/** `CFHelpersUsersv1.MAX_INVITE_LIFETIME`: the furthest out an `InviteUser`'s `expiresAt` may be. */
|
|
89
89
|
export declare const MAX_INVITE_LIFETIME_SECONDS: number;
|
|
90
|
+
/**
|
|
91
|
+
* Headroom under the contract's ceiling. `_requireWindow` rejects
|
|
92
|
+
* `expiresAt > block.timestamp + MAX_INVITE_LIFETIME`, and `block.timestamp`
|
|
93
|
+
* at inclusion is the sequencer's clock, not the signer's: signing exactly
|
|
94
|
+
* `now + 30 days` reverts with `InvalidParams` whenever the chain is even one
|
|
95
|
+
* second behind (clock skew, a lagging L2 sequencer). Anvil stamps blocks from
|
|
96
|
+
* the same wall clock, so no local stack ever shows it. A full day of headroom
|
|
97
|
+
* (so 29 days signed) is far beyond any skew.
|
|
98
|
+
*/
|
|
99
|
+
export declare const CHAIN_CLOCK_SKEW_MARGIN_SECONDS: number;
|
|
100
|
+
/** What an inviting admin signs: the ceiling minus the skew margin. */
|
|
101
|
+
export declare const INVITE_LIFETIME_SECONDS: number;
|
|
102
|
+
/** An invite's `expiresAt`: unix SECONDS, as `uint256 expiresAt` wants them. */
|
|
103
|
+
export declare function inviteExpiresAt(now?: Date, lifetimeSeconds?: number): string;
|
|
90
104
|
export declare const REMOVE_USER_TYPES: TypeSet;
|
|
91
105
|
/**
|
|
92
106
|
* Closing a client's onboarding phase (§4.3, round 4).
|
package/dist/eip712.js
CHANGED
|
@@ -51,6 +51,22 @@ export const INVITE_USER_TYPES = Object.freeze({
|
|
|
51
51
|
});
|
|
52
52
|
/** `CFHelpersUsersv1.MAX_INVITE_LIFETIME`: the furthest out an `InviteUser`'s `expiresAt` may be. */
|
|
53
53
|
export const MAX_INVITE_LIFETIME_SECONDS = 30 * 24 * 60 * 60;
|
|
54
|
+
/**
|
|
55
|
+
* Headroom under the contract's ceiling. `_requireWindow` rejects
|
|
56
|
+
* `expiresAt > block.timestamp + MAX_INVITE_LIFETIME`, and `block.timestamp`
|
|
57
|
+
* at inclusion is the sequencer's clock, not the signer's: signing exactly
|
|
58
|
+
* `now + 30 days` reverts with `InvalidParams` whenever the chain is even one
|
|
59
|
+
* second behind (clock skew, a lagging L2 sequencer). Anvil stamps blocks from
|
|
60
|
+
* the same wall clock, so no local stack ever shows it. A full day of headroom
|
|
61
|
+
* (so 29 days signed) is far beyond any skew.
|
|
62
|
+
*/
|
|
63
|
+
export const CHAIN_CLOCK_SKEW_MARGIN_SECONDS = 24 * 60 * 60;
|
|
64
|
+
/** What an inviting admin signs: the ceiling minus the skew margin. */
|
|
65
|
+
export const INVITE_LIFETIME_SECONDS = MAX_INVITE_LIFETIME_SECONDS - CHAIN_CLOCK_SKEW_MARGIN_SECONDS;
|
|
66
|
+
/** An invite's `expiresAt`: unix SECONDS, as `uint256 expiresAt` wants them. */
|
|
67
|
+
export function inviteExpiresAt(now = new Date(), lifetimeSeconds = INVITE_LIFETIME_SECONDS) {
|
|
68
|
+
return String(Math.floor(now.getTime() / 1000) + lifetimeSeconds);
|
|
69
|
+
}
|
|
54
70
|
export const REMOVE_USER_TYPES = Object.freeze({
|
|
55
71
|
RemoveUser: [
|
|
56
72
|
{ name: "company", type: "string" },
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/invite.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Wallet } from "ethers";
|
|
2
|
-
import type
|
|
2
|
+
import { type AcceptInviteRequest, type ApiInvite, type CreateInviteRequest, type EvmChainName } from "./api-types.ts";
|
|
3
3
|
import { type ChainPins } from "./chains.ts";
|
|
4
4
|
import { type InviteUserMessage } from "./eip712.ts";
|
|
5
|
+
import type { TypedDataSigner } from "./requests.ts";
|
|
5
6
|
/** What the invitee expects before signing anything. */
|
|
6
7
|
export type InviteExpectations = {
|
|
7
8
|
/** The caller's own user id (from `/me`). */
|
|
@@ -25,3 +26,29 @@ export declare function checkInvite(invite: ApiInvite, expected: InviteExpectati
|
|
|
25
26
|
}[];
|
|
26
27
|
/** Sign each checked `InviteUser` with the local EVM key. */
|
|
27
28
|
export declare function signInviteAcceptance(checked: ReturnType<typeof checkInvite>, wallet: Wallet): Promise<AcceptInviteRequest["evm"]>;
|
|
29
|
+
/** An invite to create, as the inviting admin states it. */
|
|
30
|
+
export type NewInvite = {
|
|
31
|
+
/** A fresh UUID: the invite's id and every chain's `authId`; a retry reuses it. */
|
|
32
|
+
id: string;
|
|
33
|
+
/** The inviting admin's own user id (`/me`), signed as `callerId`. */
|
|
34
|
+
callerId: string;
|
|
35
|
+
/** `inviteUserId(email)`'s answer, else `id`. */
|
|
36
|
+
userId: string;
|
|
37
|
+
email: string;
|
|
38
|
+
firstName: string;
|
|
39
|
+
lastName: string;
|
|
40
|
+
position: string;
|
|
41
|
+
isAdmin: boolean;
|
|
42
|
+
/** The cofund's EVM vault chains: one seat on each. */
|
|
43
|
+
chains: EvmChainName[];
|
|
44
|
+
/** Unix seconds; 29 days out by default (`INVITE_LIFETIME_SECONDS`). */
|
|
45
|
+
expiresAt?: string;
|
|
46
|
+
now?: Date;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* The POST /invites body for `invite`: on each chain the `InviteUser` built
|
|
50
|
+
* here from the admin's own inputs, under the pinned domain and the pinned
|
|
51
|
+
* `company`, signed with the local EVM key. The invitee and any co-signing
|
|
52
|
+
* admins later sign the same message.
|
|
53
|
+
*/
|
|
54
|
+
export declare function signInviteCreation(invite: NewInvite, company: string, pins: ChainPins, wallet: TypedDataSigner): Promise<CreateInviteRequest>;
|
package/dist/invite.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { CreateInviteRequestSchema, } from "./api-types.js";
|
|
1
2
|
import { pinnedDomain, sameDomain } from "./chains.js";
|
|
2
|
-
import { inviteUserTypes, MAX_INVITE_LIFETIME_SECONDS, } from "./eip712.js";
|
|
3
|
+
import { inviteExpiresAt, inviteUserTypes, MAX_INVITE_LIFETIME_SECONDS, } from "./eip712.js";
|
|
3
4
|
/** The decoded message, field by field: nothing the types do not name. */
|
|
4
5
|
function rebuildMessage(m) {
|
|
5
6
|
return {
|
|
@@ -69,3 +70,41 @@ export async function signInviteAcceptance(checked, wallet) {
|
|
|
69
70
|
signature: await wallet.signTypedData(domain, inviteUserTypes(), message),
|
|
70
71
|
})));
|
|
71
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* The POST /invites body for `invite`: on each chain the `InviteUser` built
|
|
75
|
+
* here from the admin's own inputs, under the pinned domain and the pinned
|
|
76
|
+
* `company`, signed with the local EVM key. The invitee and any co-signing
|
|
77
|
+
* admins later sign the same message.
|
|
78
|
+
*/
|
|
79
|
+
export async function signInviteCreation(invite, company, pins, wallet) {
|
|
80
|
+
const now = invite.now ?? new Date();
|
|
81
|
+
const expiresAt = invite.expiresAt ?? inviteExpiresAt(now);
|
|
82
|
+
const problem = inviteWindowProblem(expiresAt, now);
|
|
83
|
+
if (problem) {
|
|
84
|
+
throw new Error(`The invite cannot be signed: ${problem}`);
|
|
85
|
+
}
|
|
86
|
+
const message = {
|
|
87
|
+
company,
|
|
88
|
+
callerId: invite.callerId,
|
|
89
|
+
userId: invite.userId,
|
|
90
|
+
userIsAdmin: invite.isAdmin,
|
|
91
|
+
userPosition: invite.position,
|
|
92
|
+
expiresAt,
|
|
93
|
+
authId: invite.id,
|
|
94
|
+
};
|
|
95
|
+
const chains = await Promise.all(invite.chains.map(async (chain) => ({
|
|
96
|
+
chain,
|
|
97
|
+
expiresAt,
|
|
98
|
+
signature: await wallet.signTypedData(pinnedDomain(pins, chain), inviteUserTypes(), message),
|
|
99
|
+
})));
|
|
100
|
+
return CreateInviteRequestSchema.parse({
|
|
101
|
+
id: invite.id,
|
|
102
|
+
email: invite.email,
|
|
103
|
+
firstName: invite.firstName,
|
|
104
|
+
lastName: invite.lastName,
|
|
105
|
+
position: invite.position,
|
|
106
|
+
isAdmin: invite.isAdmin,
|
|
107
|
+
userId: invite.userId,
|
|
108
|
+
chains,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type ApiMember, type ApiPolicyType, type ApiVault, type CreatePolicyRequest, type PolicyType } from "./api-types.ts";
|
|
2
|
+
import { type BtcPolicyActivationData } from "./btc-policy-activation.ts";
|
|
3
|
+
import { type ChainPins } from "./chains.ts";
|
|
4
|
+
import type { LocalSigners } from "./requests.ts";
|
|
5
|
+
/**
|
|
6
|
+
* Policy activations: what an admin signs to create a policy or to approve one.
|
|
7
|
+
* One builder for both, so the creator signs exactly what each approving admin
|
|
8
|
+
* later rebuilds and the app verifies.
|
|
9
|
+
*/
|
|
10
|
+
/** What an activation commits to, besides its signers. */
|
|
11
|
+
export type PolicyActivationFields = {
|
|
12
|
+
/** The policy's id, also its activation's `authId`. */
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
type: PolicyType;
|
|
16
|
+
threshold: number;
|
|
17
|
+
/** Transfer policies: the token (an EVM address, or `BTC`), the cap per transfer and the allowlist (empty: any). */
|
|
18
|
+
token: string | null;
|
|
19
|
+
maxAmount: string | null;
|
|
20
|
+
recipients: string[];
|
|
21
|
+
/** Call policies: the wrapper; its one function is `entry`. */
|
|
22
|
+
wrapper: string | null;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The EVM activation: a call policy names its wrapper and `entry`, a transfer
|
|
26
|
+
* policy its token, cap and allowlist. `signerIds` are the signers' on-chain
|
|
27
|
+
* user ids.
|
|
28
|
+
*/
|
|
29
|
+
export declare function evmPolicyActivation(company: string, fields: PolicyActivationFields & {
|
|
30
|
+
signerIds: string[];
|
|
31
|
+
}): {
|
|
32
|
+
types: Record<string, {
|
|
33
|
+
name: string;
|
|
34
|
+
type: string;
|
|
35
|
+
}[]>;
|
|
36
|
+
message: Record<string, unknown>;
|
|
37
|
+
};
|
|
38
|
+
/** The BTC activation; each signer's Cofund leaf key is derived here from their account xpub. */
|
|
39
|
+
export declare function btcPolicyActivation(company: string, fields: PolicyActivationFields & {
|
|
40
|
+
signerXpubs: string[];
|
|
41
|
+
}): BtcPolicyActivationData;
|
|
42
|
+
/** A policy to create, as its creator states it. */
|
|
43
|
+
export type NewPolicy = {
|
|
44
|
+
/** A fresh UUID: the policy's id and its activation's `authId`; a retry reuses it. */
|
|
45
|
+
id: string;
|
|
46
|
+
/** From `vaults()`. */
|
|
47
|
+
vault: ApiVault;
|
|
48
|
+
name: string;
|
|
49
|
+
type: ApiPolicyType;
|
|
50
|
+
/** The members who sign, from `members()`: the activation names their `onchainId` (EVM) or BTC key. */
|
|
51
|
+
signers: ApiMember[];
|
|
52
|
+
threshold: number;
|
|
53
|
+
} & ({
|
|
54
|
+
token: string;
|
|
55
|
+
maxAmount: string;
|
|
56
|
+
wrapper?: undefined;
|
|
57
|
+
} | {
|
|
58
|
+
wrapper: string;
|
|
59
|
+
token?: undefined;
|
|
60
|
+
maxAmount?: undefined;
|
|
61
|
+
});
|
|
62
|
+
/**
|
|
63
|
+
* The POST /policies body for `policy`: its activation built here from the
|
|
64
|
+
* creator's own inputs, under the pinned domain (EVM) and the pinned
|
|
65
|
+
* `company`, and signed with the local key: EIP-712 on EVM, the BTC key's
|
|
66
|
+
* Cofund leaf on BTC. The server builds the same activation and checks the
|
|
67
|
+
* signature against the creator's key.
|
|
68
|
+
*/
|
|
69
|
+
export declare function signPolicyCreation(policy: NewPolicy, company: string, pins: ChainPins, signers: LocalSigners): Promise<CreatePolicyRequest>;
|
package/dist/policies.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { sha256 } from "@noble/hashes/sha2";
|
|
2
|
+
import { bytesToHex } from "@noble/hashes/utils";
|
|
3
|
+
import { ethers } from "ethers";
|
|
4
|
+
import { CreatePolicyRequestSchema, } from "./api-types.js";
|
|
5
|
+
import { btcPolicyActivationMessage, } from "./btc-policy-activation.js";
|
|
6
|
+
import { pinnedDomain } from "./chains.js";
|
|
7
|
+
import { transactionPolicyActivationTypes, transferPolicyActivationTypes, WRAPPER_ENTRYPOINT, } from "./eip712.js";
|
|
8
|
+
import { btcCofundLeafKey } from "./keys.js";
|
|
9
|
+
const byCodepoint = (a, b) => (a < b ? -1 : Number(a > b));
|
|
10
|
+
/**
|
|
11
|
+
* The EVM activation: a call policy names its wrapper and `entry`, a transfer
|
|
12
|
+
* policy its token, cap and allowlist. `signerIds` are the signers' on-chain
|
|
13
|
+
* user ids.
|
|
14
|
+
*/
|
|
15
|
+
export function evmPolicyActivation(company, fields) {
|
|
16
|
+
const common = {
|
|
17
|
+
company,
|
|
18
|
+
policyId: fields.id,
|
|
19
|
+
title: fields.name,
|
|
20
|
+
policyType: fields.type,
|
|
21
|
+
signerIds: [...fields.signerIds].sort(byCodepoint),
|
|
22
|
+
threshold: String(fields.threshold),
|
|
23
|
+
authId: fields.id,
|
|
24
|
+
};
|
|
25
|
+
if (fields.wrapper) {
|
|
26
|
+
return {
|
|
27
|
+
types: transactionPolicyActivationTypes(),
|
|
28
|
+
message: { ...common, wrapper: fields.wrapper, func: WRAPPER_ENTRYPOINT },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
types: transferPolicyActivationTypes(),
|
|
33
|
+
message: {
|
|
34
|
+
...common,
|
|
35
|
+
token: fields.token ?? ethers.ZeroAddress,
|
|
36
|
+
maxAmount: fields.maxAmount ?? "0",
|
|
37
|
+
recipients: fields.recipients,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** The BTC activation; each signer's Cofund leaf key is derived here from their account xpub. */
|
|
42
|
+
export function btcPolicyActivation(company, fields) {
|
|
43
|
+
const isTransaction = fields.wrapper !== null;
|
|
44
|
+
return {
|
|
45
|
+
clientId: bytesToHex(sha256(new TextEncoder().encode(company))),
|
|
46
|
+
policyId: fields.id,
|
|
47
|
+
authId: fields.id,
|
|
48
|
+
policyType: fields.type,
|
|
49
|
+
policyTitle: fields.name,
|
|
50
|
+
policySignerKeys: fields.signerXpubs
|
|
51
|
+
.map(btcCofundLeafKey)
|
|
52
|
+
.sort(byCodepoint),
|
|
53
|
+
policyThreshold: fields.threshold,
|
|
54
|
+
txWrapper: fields.wrapper ?? "",
|
|
55
|
+
txFunc: isTransaction ? WRAPPER_ENTRYPOINT : "",
|
|
56
|
+
isTransaction,
|
|
57
|
+
maxAmount: isTransaction ? "0" : (fields.maxAmount ?? "0"),
|
|
58
|
+
tokenIdentifier: isTransaction ? "" : (fields.token ?? ""),
|
|
59
|
+
recipients: isTransaction ? [] : [...fields.recipients].sort(byCodepoint),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Each signer's BTC account xpub in this cofund. */
|
|
63
|
+
function btcXpubs(signers) {
|
|
64
|
+
return signers.map((signer) => {
|
|
65
|
+
const key = signer.keys.find(({ chain }) => chain === "BTC");
|
|
66
|
+
if (!key) {
|
|
67
|
+
throw new Error(`${signer.name ?? signer.id} has no Bitcoin key`);
|
|
68
|
+
}
|
|
69
|
+
return key.pubKey;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The POST /policies body for `policy`: its activation built here from the
|
|
74
|
+
* creator's own inputs, under the pinned domain (EVM) and the pinned
|
|
75
|
+
* `company`, and signed with the local key: EIP-712 on EVM, the BTC key's
|
|
76
|
+
* Cofund leaf on BTC. The server builds the same activation and checks the
|
|
77
|
+
* signature against the creator's key.
|
|
78
|
+
*/
|
|
79
|
+
export async function signPolicyCreation(policy, company, pins, signers) {
|
|
80
|
+
const fields = {
|
|
81
|
+
id: policy.id,
|
|
82
|
+
name: policy.name,
|
|
83
|
+
type: policy.type,
|
|
84
|
+
threshold: policy.threshold,
|
|
85
|
+
token: policy.token ?? null,
|
|
86
|
+
maxAmount: policy.maxAmount ?? null,
|
|
87
|
+
recipients: [],
|
|
88
|
+
wrapper: policy.wrapper ?? null,
|
|
89
|
+
};
|
|
90
|
+
const common = {
|
|
91
|
+
id: policy.id,
|
|
92
|
+
vaultId: policy.vault.id,
|
|
93
|
+
name: policy.name,
|
|
94
|
+
type: policy.type,
|
|
95
|
+
signers: policy.signers.map(({ id }) => id),
|
|
96
|
+
threshold: policy.threshold,
|
|
97
|
+
};
|
|
98
|
+
const { vault } = policy;
|
|
99
|
+
if (vault.chain === "BTC") {
|
|
100
|
+
if (policy.wrapper !== undefined || policy.token !== "BTC") {
|
|
101
|
+
throw new Error("A Bitcoin policy sends BTC, and binds no wrapper");
|
|
102
|
+
}
|
|
103
|
+
if (!signers.btc) {
|
|
104
|
+
throw new Error("A Bitcoin policy needs the BTC key");
|
|
105
|
+
}
|
|
106
|
+
const data = btcPolicyActivation(company, {
|
|
107
|
+
...fields,
|
|
108
|
+
signerXpubs: btcXpubs(policy.signers),
|
|
109
|
+
});
|
|
110
|
+
return CreatePolicyRequestSchema.parse({
|
|
111
|
+
...common,
|
|
112
|
+
chain: "BTC",
|
|
113
|
+
token: "BTC",
|
|
114
|
+
maxAmount: policy.maxAmount,
|
|
115
|
+
signature: await signers.btc.signMessage(btcPolicyActivationMessage(data)),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const { types, message } = evmPolicyActivation(company, {
|
|
119
|
+
...fields,
|
|
120
|
+
signerIds: policy.signers.map(({ onchainId }) => onchainId),
|
|
121
|
+
});
|
|
122
|
+
const signature = await signers.evm.signTypedData(pinnedDomain(pins, vault.chain), types, message);
|
|
123
|
+
return CreatePolicyRequestSchema.parse(policy.wrapper === undefined
|
|
124
|
+
? {
|
|
125
|
+
...common,
|
|
126
|
+
chain: vault.chain,
|
|
127
|
+
token: policy.token,
|
|
128
|
+
maxAmount: policy.maxAmount,
|
|
129
|
+
signature,
|
|
130
|
+
}
|
|
131
|
+
: { ...common, chain: vault.chain, wrapper: policy.wrapper, signature });
|
|
132
|
+
}
|
package/dist/requests.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ApiInviteRequest, ApiPolicy, ApiPolicyRequest, ApiRequest, ApiToken, ChainName, EvmChainName, PolicyType } from "./api-types.ts";
|
|
2
2
|
import { type ChainPins } from "./chains.ts";
|
|
3
3
|
import { type AllowanceMessage, type Eip712Domain } from "./eip712.ts";
|
|
4
|
-
import {
|
|
4
|
+
import type { BtcMessageSigner } from "./keys.ts";
|
|
5
5
|
import { type DecodedStockSwap } from "./stocks/index.ts";
|
|
6
6
|
/**
|
|
7
7
|
* Local signers for transfers, calls, policy activations and admin invites.
|