getcofund 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +200 -2
- package/dist/api-types.d.ts +1794 -0
- package/dist/api-types.js +737 -0
- package/dist/btc-policy-activation.d.ts +49 -0
- package/dist/btc-policy-activation.js +86 -0
- package/dist/chains.d.ts +16 -0
- package/dist/chains.js +26 -0
- package/dist/cli/context.d.ts +27 -0
- package/dist/cli/context.js +69 -0
- package/dist/cli/invite.d.ts +15 -0
- package/dist/cli/invite.js +165 -0
- package/dist/cli/keys.d.ts +6 -0
- package/dist/cli/keys.js +22 -0
- package/dist/cli/policies.d.ts +17 -0
- package/dist/cli/policies.js +100 -0
- package/dist/cli/request-lines.d.ts +7 -0
- package/dist/cli/request-lines.js +116 -0
- package/dist/cli/requests.d.ts +3 -0
- package/dist/cli/requests.js +52 -0
- package/dist/cli/stocks.d.ts +9 -0
- package/dist/cli/stocks.js +81 -0
- package/dist/cli/webhooks.d.ts +5 -0
- package/dist/cli/webhooks.js +39 -0
- package/dist/cli.d.ts +32 -0
- package/dist/cli.js +131 -0
- package/dist/client.d.ts +1172 -0
- package/dist/client.js +174 -0
- package/dist/eip712.d.ts +234 -0
- package/dist/eip712.js +215 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/invite.d.ts +54 -0
- package/dist/invite.js +110 -0
- package/dist/keys.d.ts +51 -0
- package/dist/keys.js +97 -0
- package/dist/policies.d.ts +69 -0
- package/dist/policies.js +132 -0
- package/dist/requests.d.ts +114 -0
- package/dist/requests.js +249 -0
- package/dist/stocks/decode-stock-swap.d.ts +26 -0
- package/dist/stocks/decode-stock-swap.js +93 -0
- package/dist/stocks/encode-swap.d.ts +30 -0
- package/dist/stocks/encode-swap.js +36 -0
- package/dist/stocks/index.d.ts +9 -0
- package/dist/stocks/index.js +10 -0
- package/dist/stocks/quote.d.ts +19 -0
- package/dist/stocks/quote.js +38 -0
- package/dist/stocks/stock-pools.d.ts +61 -0
- package/dist/stocks/stock-pools.js +179 -0
- package/dist/webhooks.d.ts +19 -0
- package/dist/webhooks.js +47 -0
- package/dist/xpub-ownership.d.ts +10 -0
- package/dist/xpub-ownership.js +13 -0
- package/package.json +77 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cofund
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,201 @@
|
|
|
1
|
-
#
|
|
1
|
+
# getcofund
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Client, local signers and the `cofund` CLI for the Cofund API (`/api/v1`).
|
|
4
|
+
|
|
5
|
+
Two kinds of key:
|
|
6
|
+
- **The API key is access.** Cofund issues it.
|
|
7
|
+
- **The signing key is authority.** It is generated on your machine and never leaves it. The API receives only addresses, xpubs and signatures.
|
|
8
|
+
|
|
9
|
+
## CLI
|
|
10
|
+
|
|
11
|
+
Run it with `npx getcofund`, or install it globally (`npm i -g getcofund`) and run `cofund`. The `cofund` package on npm is unrelated, so never use `npx cofund`.
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npx getcofund keys init
|
|
15
|
+
# or
|
|
16
|
+
npm i -g getcofund
|
|
17
|
+
cofund keys init
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
With npx, put `--` first when passing flags, or npm takes flags such as `--help` and `--force` for itself: `npx -- getcofund invite accept --company "Acme"`.
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
cofund keys init [--force]
|
|
24
|
+
cofund me
|
|
25
|
+
cofund invite accept [--invite <id>] [--company <name>]
|
|
26
|
+
cofund invite create --email <email> --first-name <name> --last-name <name> --position <title> [--admin] [--chains A,B] [--yes]
|
|
27
|
+
cofund policies create --vault <id> --name <name> --signers <id|email,...> --threshold <n>
|
|
28
|
+
(--token <address|BTC> --max-amount <base units> | --wrapper <address>) [--type <PolicyType>] [--yes]
|
|
29
|
+
cofund requests [list]
|
|
30
|
+
cofund requests show <id>
|
|
31
|
+
cofund requests sign <id> [--yes]
|
|
32
|
+
cofund requests reject <id> [--yes]
|
|
33
|
+
cofund stocks buy <SYMBOL> --spend <USDG> [--policy <id>]
|
|
34
|
+
cofund webhooks [list]
|
|
35
|
+
cofund webhooks add <url> [--events a,b]
|
|
36
|
+
cofund webhooks remove <id>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Commands on a cofund take `--cofund <id>` when the key is a member of several.
|
|
40
|
+
|
|
41
|
+
Environment:
|
|
42
|
+
- `COFUND_API_URL`, `COFUND_API_KEY`: the API and your key.
|
|
43
|
+
- `COFUND_KEYSTORE`: the keystore file. Default `~/.cofund/keystore.json`, mode 0600.
|
|
44
|
+
- `COFUND_ROBINHOOD_RPC_URL`: the Robinhood Chain RPC `stocks buy` quotes from. Default the public one.
|
|
45
|
+
|
|
46
|
+
Cofund runs on mainnet only. For local development, `COFUND_CHAINS` pins other chain ids and State addresses (JSON), and `COFUND_BTC_NETWORK=testnet` makes testnet BTC keys.
|
|
47
|
+
|
|
48
|
+
`invite accept` checks the invite before signing anything:
|
|
49
|
+
- it shows and pins the company;
|
|
50
|
+
- the seat is yours;
|
|
51
|
+
- it expires within 30 days.
|
|
52
|
+
|
|
53
|
+
It rebuilds the EIP-712 message itself under the pinned domain, signs locally, and sends the BTC xpub with its ownership proof.
|
|
54
|
+
|
|
55
|
+
`requests` lists what awaits you, as the app's Requests page shows it: transfers, calls and, for an admin, policy activations and admin invites (one per chain).
|
|
56
|
+
|
|
57
|
+
`requests sign` signs nothing it has not read:
|
|
58
|
+
- the domain is the pinned one, and `company` is the one pinned at `invite accept`;
|
|
59
|
+
- the message is rebuilt from the request's fields (a call's `instructionsHash` is hashed here), and must match the server's;
|
|
60
|
+
- a call goes through its verb's decoder: the StockSwap wrapper only, the instructions must re-encode from the decoded swap, and the allowances must be exactly its input;
|
|
61
|
+
- a policy activation is rebuilt from its name, type, signers, threshold and bounds (token, cap, recipients, or wrapper); a BTC one is signed with the BTC key's Cofund leaf;
|
|
62
|
+
- an admin invite must seat an admin, and its authorization must be open and at most 30 days out;
|
|
63
|
+
- it shows the decoded request and asks, or takes `--yes`.
|
|
64
|
+
|
|
65
|
+
What it takes from the server: which on-chain user id (EVM) or account xpub (BTC) belongs to which named policy signer, and an invite's `callerId` and `authId` (a wrong one only makes the chain refuse it). `approve` receives each signer with its id or xpub, so a script holding its own roster can check the pairing.
|
|
66
|
+
|
|
67
|
+
An admin creates policies and invites with keys of their own:
|
|
68
|
+
- `policies create` builds the activation from the flags: the signers by member id or email (an EVM activation names their `onchainId`, a BTC one their account xpubs), the threshold, and the token and cap per transfer in base units or the wrapper (function `entry`). A transfer policy is `Operational_Expense` and a call policy `Treasury_Management` unless `--type` says otherwise. It signs under the pinned domain and company, with the BTC key's Cofund leaf on a Bitcoin vault. The policy activates at once when one admin may activate it; otherwise it awaits the other admins as a `policy` request.
|
|
69
|
+
- `invite create` asks which user id the email's seat registers (an existing account keeps its own), signs `InviteUser` on each of the cofund's EVM vault chains (or `--chains`), 29 days out, and sends the invite. A member seat is claimable at once; an admin seat awaits the other admins' co-signatures where the chain asks for them, one `invite` request per chain.
|
|
70
|
+
- Both show what they will sign and ask, or take `--yes`.
|
|
71
|
+
|
|
72
|
+
`stocks buy` quotes the pool, sets the minimum output 1% under the quote, signs the swap and proposes it on the cofund's Stocks policy. The other signers approve it in the app or with `requests sign`.
|
|
73
|
+
|
|
74
|
+
## SDK
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
import {
|
|
78
|
+
btcMessageSigner,
|
|
79
|
+
CofundClient,
|
|
80
|
+
evmWallet,
|
|
81
|
+
readKeystore,
|
|
82
|
+
signRequest,
|
|
83
|
+
} from "getcofund";
|
|
84
|
+
|
|
85
|
+
const api = new CofundClient({ baseUrl, apiKey });
|
|
86
|
+
const me = await api.me();
|
|
87
|
+
|
|
88
|
+
// Sign what awaits you and matches your intent; refuse the rest unsigned.
|
|
89
|
+
const keystore = readKeystore(path);
|
|
90
|
+
for (const request of await api.requests(cofundId)) {
|
|
91
|
+
const signature = await signRequest(
|
|
92
|
+
request,
|
|
93
|
+
{
|
|
94
|
+
company: keystore.cofunds[cofundId].company,
|
|
95
|
+
approve: (decoded) => decoded.kind === "transfer" && decoded.amount === "1000000",
|
|
96
|
+
},
|
|
97
|
+
pins,
|
|
98
|
+
{ evm: evmWallet(keystore), btc: btcMessageSigner(keystore) },
|
|
99
|
+
).catch(() => null); // not approved, or not what its fields say
|
|
100
|
+
if (signature) {
|
|
101
|
+
await api.signRequest(cofundId, request.id, signature);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`proposeTransfer` and `proposeCall` take an id the caller picks (`id`, `authId`); a retry with the same id returns the same request, so the client retries them on a lost response. `signTransferProposal` and `signCallProposal` sign the proposer's own intent.
|
|
107
|
+
|
|
108
|
+
An admin's writes work the same way: `signPolicyCreation` and `signInviteCreation` build the activation or each chain's `InviteUser` from your inputs and sign it locally, and `createPolicy` and `createInvite` send it under the id you picked.
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
import { randomUUID } from "node:crypto";
|
|
112
|
+
import { signInviteCreation, signPolicyCreation } from "getcofund";
|
|
113
|
+
|
|
114
|
+
const company = keystore.cofunds[cofundId].company;
|
|
115
|
+
const signers = { evm: evmWallet(keystore), btc: btcMessageSigner(keystore) };
|
|
116
|
+
const [vault] = await api.vaults(cofundId);
|
|
117
|
+
const roster = await api.members(cofundId);
|
|
118
|
+
|
|
119
|
+
const policy = await signPolicyCreation(
|
|
120
|
+
{
|
|
121
|
+
id: randomUUID(),
|
|
122
|
+
vault,
|
|
123
|
+
name: "Payouts",
|
|
124
|
+
type: "Operational_Expense",
|
|
125
|
+
signers: roster.filter((m) => m.isAdmin),
|
|
126
|
+
threshold: 2,
|
|
127
|
+
token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
128
|
+
maxAmount: "1000000000", // 1,000 USDC per transfer
|
|
129
|
+
},
|
|
130
|
+
company,
|
|
131
|
+
pins,
|
|
132
|
+
signers,
|
|
133
|
+
);
|
|
134
|
+
await api.createPolicy(cofundId, policy); // AWAITING_SIGNATURES, or active at once
|
|
135
|
+
|
|
136
|
+
const id = randomUUID();
|
|
137
|
+
const { userId } = await api.inviteUserId(cofundId, "nia@example.com");
|
|
138
|
+
const invite = await signInviteCreation(
|
|
139
|
+
{
|
|
140
|
+
id,
|
|
141
|
+
callerId: me.id,
|
|
142
|
+
userId: userId ?? id,
|
|
143
|
+
email: "nia@example.com",
|
|
144
|
+
firstName: "Nia",
|
|
145
|
+
lastName: "Okafor",
|
|
146
|
+
position: "Treasurer",
|
|
147
|
+
isAdmin: false,
|
|
148
|
+
chains: ["ETHEREUM", "ROBINHOOD"],
|
|
149
|
+
},
|
|
150
|
+
company,
|
|
151
|
+
pins,
|
|
152
|
+
signers.evm,
|
|
153
|
+
);
|
|
154
|
+
await api.createInvite(cofundId, invite);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Webhooks
|
|
158
|
+
|
|
159
|
+
`cofund webhooks add https://bot.example.com/cofund` subscribes an endpoint for your key's agent, to every event or those named with `--events`, and prints its signing secret once. The URL must be https, on a host that resolves only to public addresses.
|
|
160
|
+
|
|
161
|
+
| Event | When |
|
|
162
|
+
|---|---|
|
|
163
|
+
| `request.signature_needed` | A transfer, call, policy activation or admin invite awaits your signature |
|
|
164
|
+
| `request.executed` | It went through: executed on chain; a Bitcoin policy once approved; an admin invite's chain row once its last co-signature lands |
|
|
165
|
+
| `request.failed` | It failed on chain; or its simulation reverted and nothing was sent, and it stays open; or it expired unsent (a swap's quote, an admin invite). `reason` says why |
|
|
166
|
+
| `request.rejected` | An admin rejected it |
|
|
167
|
+
| `invite.received` | You were invited to a cofund (`inviteId`) |
|
|
168
|
+
|
|
169
|
+
The body is `{id, type, createdAt, cofundId}` and, by type: every `request.*` event its `requestId` and `kind` (`transfer`, `call`, `policy` or `invite`), `request.failed` an optional `reason`, `invite.received` its `inviteId`. Never amounts or addresses: fetch the request with `api.request(cofundId, requestId)` and check it as `signRequest` does. `WebhookEventSchema` parses it, narrowed by `type`. Delivery is at least once and in no particular order. Answer 2xx within 10 seconds; anything else is retried at 1m, 5m, 30m, 2h, 6h and 24h, and three days of failures disable the subscription (your key's owner is emailed).
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
import { createServer } from "node:http";
|
|
173
|
+
import { verifyWebhookSignature, WebhookEventSchema } from "getcofund";
|
|
174
|
+
|
|
175
|
+
const seen = new Set(); // use your database in production
|
|
176
|
+
|
|
177
|
+
createServer((req, res) => {
|
|
178
|
+
let rawBody = "";
|
|
179
|
+
req.setEncoding("utf8");
|
|
180
|
+
req.on("data", (chunk) => (rawBody += chunk));
|
|
181
|
+
req.on("end", async () => {
|
|
182
|
+
// The exact bytes received, before any JSON parsing; refuses a stale timestamp.
|
|
183
|
+
if (!verifyWebhookSignature(req.headers["cofund-signature"], rawBody, process.env.COFUND_WEBHOOK_SECRET)) {
|
|
184
|
+
res.writeHead(401).end();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const event = WebhookEventSchema.parse(JSON.parse(rawBody));
|
|
188
|
+
res.writeHead(200).end(); // acknowledge first, then work
|
|
189
|
+
if (seen.has(event.id)) return; // a retry of one already handled
|
|
190
|
+
seen.add(event.id);
|
|
191
|
+
if (event.type === "request.signature_needed") {
|
|
192
|
+
const request = await api.request(event.cofundId, event.requestId);
|
|
193
|
+
// ...decide, then sign it as in the SDK example above
|
|
194
|
+
} else if (event.type === "invite.received") {
|
|
195
|
+
// ...accept event.inviteId with `cofund invite accept --invite <id>`
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}).listen(8080);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Node 20 or later, ESM.
|