@getcofund/protocol 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +413 -1
- 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 +61 -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,415 @@
|
|
|
1
1
|
# @getcofund/protocol
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Cofund protocol on chain: Bitcoin vaults and the EVM contracts behind
|
|
4
|
+
State. It talks to the chain only. There is no Cofund API in it, no database,
|
|
5
|
+
and no Cofund deployment built in.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm i @getcofund/protocol viem
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Entries
|
|
12
|
+
|
|
13
|
+
| Entry | Holds |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `@getcofund/protocol/evm` | The deployment config and its check, the EIP-712 messages (`InviteUser`, `Transfer`, `Transaction`, the policy activations and the rest) and their domain, a call builder per contract entry point with the contract's checks, State's reads, the two submitters, the contracts' ABIs, the company name and the client id it derives, a vault call's simulation over `eth_simulateV1` and how it reads, the StockSwap verb, and the values these are made of as zod schemas |
|
|
16
|
+
| `@getcofund/protocol/btc` | A vault's taproot tree and address from its definition, the vault checked against that definition, and its spends: coins selected, a PSBT built, checked and signed, combined, finalized and broadcast |
|
|
17
|
+
| `@getcofund/protocol/btc-policies` | Optional: Bitcoin spending policies (signers, a threshold, a cap, recipients) a spend is checked against before it is signed, and what an admin signs to activate one. No script enforces them; they bind only the parties that use them |
|
|
18
|
+
| `@getcofund/protocol/keys` | A member's keys (an EVM key and a BIP-86 Bitcoin account), the signers derived from them, and the proof that an xpub is the member's |
|
|
19
|
+
|
|
20
|
+
Every entry runs in a browser as well as in Node: nothing here reaches a Node
|
|
21
|
+
built-in. EVM code is written on viem.
|
|
22
|
+
|
|
23
|
+
## What you bring
|
|
24
|
+
|
|
25
|
+
The package ships no deployment and picks no provider, key store or relayer.
|
|
26
|
+
You bring the deployment, a reader, signers and a submitter. Co-signers
|
|
27
|
+
exchange signatures over whatever channel you choose: every message is plain
|
|
28
|
+
EIP-712 typed data.
|
|
29
|
+
|
|
30
|
+
### The deployment
|
|
31
|
+
|
|
32
|
+
The shape of cofund-solidity's `deployments/<chainId>.json`, which its deploy
|
|
33
|
+
script (`script/DeployCore.s.sol`) prints:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
type EvmDeployment = {
|
|
37
|
+
chainId: number;
|
|
38
|
+
state: Address; // CFHelpersStatev1, every message's verifying contract
|
|
39
|
+
users: Address; // helperContracts("users")
|
|
40
|
+
policies: Address; // helperContracts("policies")
|
|
41
|
+
vaults: Address; // the vault factory, helperContracts("vaults")
|
|
42
|
+
sponsorGuard: Address; // helperContracts("sponsorGuard")
|
|
43
|
+
eip712Domain: { name: string; version: string; chainId: number; verifyingContract: Address };
|
|
44
|
+
};
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`loadEvmDeployment(config, reader)` checks a config before anything is signed
|
|
48
|
+
under it, and refuses one that differs anywhere with every difference
|
|
49
|
+
(`DeploymentMismatchError.differences`): the domain's chain id and verifying
|
|
50
|
+
contract are the config's chain id and State, the reader is on that chain,
|
|
51
|
+
each helper is the one State registers, and the domain's name and version are
|
|
52
|
+
the ones the users helper reports (ERC-5267). Other fields in the file (gov,
|
|
53
|
+
the sponsor, dates) pass unread.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import deployment1 from "./deployments/1.json" with { type: "json" };
|
|
57
|
+
const reader = createPublicClient({ chain: mainnet, transport: http(rpcUrl) });
|
|
58
|
+
const deployment = await loadEvmDeployment(deployment1, reader);
|
|
59
|
+
const ctx = { deployment, reader };
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### The interfaces
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
type EvmReader = { // a viem PublicClient, any EIP-1193 provider
|
|
66
|
+
request(args: { method: string; params?: unknown }): Promise<unknown>;
|
|
67
|
+
};
|
|
68
|
+
type TypedDataSigner = { // a viem Account: a local key, a Ledger, a KMS adapter
|
|
69
|
+
readonly address: Address;
|
|
70
|
+
signTypedData(data: TypedData): Promise<Hex>;
|
|
71
|
+
};
|
|
72
|
+
type EvmSubmitter = { // who sends a call and pays for it
|
|
73
|
+
send(call: { to: Address; data: Hex; value?: bigint }): Promise<Hash>;
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A viem `PublicClient` is an `EvmReader` and a viem `Account` a
|
|
78
|
+
`TypedDataSigner` as they are; a `WalletClient` signs through
|
|
79
|
+
`walletSigner(client)`. An ethers provider reads through
|
|
80
|
+
`{ request: ({ method, params }) => provider.send(method, params) }`.
|
|
81
|
+
`typedData(domain, types, message)` builds what a signer signs, with the
|
|
82
|
+
primary type read from `types`; `typedDataDigest` hashes it as the contracts
|
|
83
|
+
do.
|
|
84
|
+
|
|
85
|
+
### The submitters
|
|
86
|
+
|
|
87
|
+
- **`directSubmitter(walletClient)`:** the wallet sends the call itself and
|
|
88
|
+
pays gas. The policies helper and the users helper's signature-only calls
|
|
89
|
+
(`removeUser`, `rotateUser`, `completeOnboarding`) accept any sender; a vault
|
|
90
|
+
accepts any active member of its cofund, a Cofund admin or the guard.
|
|
91
|
+
- **`guardSubmitter(walletClient, deployment)`:** the call wrapped in
|
|
92
|
+
`sponsorGuard.relay(target, data)`, sent by a wallet the deployment's guard
|
|
93
|
+
accepts as a sponsor. `createClient` and `addUser` accept only the guard or a
|
|
94
|
+
Cofund admin as their sender, so they go this way.
|
|
95
|
+
|
|
96
|
+
A call the chain refuses throws `CallRevertedError`, its `reason` the
|
|
97
|
+
contract's error by name. Sponsoring through a gas manager is an
|
|
98
|
+
`EvmSubmitter` you write.
|
|
99
|
+
|
|
100
|
+
## Calls
|
|
101
|
+
|
|
102
|
+
One builder per contract entry point. Each reads what it needs through the
|
|
103
|
+
reader, checks the call as the contract will, and returns `{ to, data }` for a
|
|
104
|
+
submitter; a call the contract would revert is refused before anything is
|
|
105
|
+
sent, with `CallRefusedError`, its `reason` the error the contract would revert
|
|
106
|
+
with. Multi-signature arrays are put in the order the contracts walk them
|
|
107
|
+
(ascending by recovered address); a signer counted twice is refused. Every
|
|
108
|
+
signature is recovered as OpenZeppelin's `ECDSA` recovers it (65 bytes, low
|
|
109
|
+
`s`, `v` 27 or 28). The checks that turn on `msg.sender` are the submitter's.
|
|
110
|
+
|
|
111
|
+
| Builder | Takes | Refuses, as the contract does |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| `createClientCall` | the founder's `InviteUser` over themselves, the founder's address and signature, a tier | an invite window not open now or more than 30 days long (`InviteExpired`, `InvalidParams`); an empty `authId`; a message not naming the founder as admin, a signature not the founder's (`InvalidSignature`); a spent `authId` (`KeyExists`); an existing cofund (`ClientExists`); an unknown tier (`InvalidTier`); a taken seat or key (`UserExists`, `AddressExists`) |
|
|
114
|
+
| `addUserCall` | an `InviteUser`, the inviting admin's signature, co-signatures, the invitee's address and signature | the window; an inviting signature that is not the `callerId`'s seat (`UnauthorizedCaller`), an inactive or non-admin inviter; a spent `authId`; for an admin seat fewer than `requiredAdminInviteSignatures` (`ThresholdNotMet`) or a co-signer who is the inviter or no active admin; an invitee signature not the invitee's; a taken seat or key |
|
|
115
|
+
| `removeUserCall` | a `RemoveUser`, admin signatures | fewer than two for an admin while more than one admin is active (`ThresholdNotMet`); a signer who is no active admin; a spent `authId`; an unknown or removed user; the last admin (`InvalidSetting`) |
|
|
116
|
+
| `rotateUserCall` | a `RotateUser`, the new key's signature, and the current key's or (for a lost key) admins' | a new-key signature not the new address's; both authorizations at once (`InvalidParams`); a current key not the one State holds; too few admins; a new key that holds a seat |
|
|
117
|
+
| `completeOnboardingCall` | a `CompleteOnboarding`, an admin's signature | a stranger (`UnauthorizedCaller`), an inactive admin, a member; a spent `authId`; a closed phase (`OnboardingAlreadyComplete`) |
|
|
118
|
+
| `activateTransferPolicyCall`, `activateTransactionPolicyCall` | the activation, admin signatures | an unsupported type, an empty title, a zero threshold, fewer signers than the threshold, a zero token or wrapper; a spent `authId`; fewer than `requiredPolicyActivationSignatures` (`SignerCountMismatch`); a signer who is no active admin; an existing policy id, a signer id that is no active user, listed twice, a zero recipient |
|
|
119
|
+
| `deactivatePolicyCall` | a `PolicyDeactivation`, an admin's signature | a spent `authId`; a stranger, an inactive admin, a member (`UnauthorizedCaller`, `InactiveUser`, `InvalidAdmin`); a policy that is not active |
|
|
120
|
+
| `executeTransferCall` | a `Transfer`, the signers' signatures | a company with no vault; an inactive policy, a transaction policy, an amount over the cap, another token, another type, fewer signatures than the threshold, a recipient off the allowlist; a spent `authId`; a signer State does not resolve to an active signer of the policy; a vault short of the amount and the tier's fee on top |
|
|
121
|
+
| `executeTransactionCall` | a `Transaction`, its instructions, the signers' signatures | the same policy checks with the policy's wrapper; instructions other than the signed hash; an allowance of the zero address or of 0 |
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const message = { company, callerId: me, userId, userIsAdmin: false, userPosition: "Engineer", expiresAt, authId };
|
|
125
|
+
const call = await addUserCall(ctx, {
|
|
126
|
+
message,
|
|
127
|
+
adminSignature: await admin.signTypedData(typedData(deployment.eip712Domain, INVITE_USER_TYPES, message)),
|
|
128
|
+
invitee: member.address,
|
|
129
|
+
inviteeSignature: await member.signTypedData(typedData(deployment.eip712Domain, INVITE_USER_TYPES, message)),
|
|
130
|
+
});
|
|
131
|
+
await guardSubmitter(sponsorWallet, deployment).send(call);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Reads
|
|
135
|
+
|
|
136
|
+
Typed State getters through the reader: `readUser`, `readUserIdByAddress`,
|
|
137
|
+
`readPolicy`, `readClientVault`, `readOnboardingComplete`, `readActiveAdmins`,
|
|
138
|
+
`readHelperContract`, `readAuthIdSpent`, the helpers' rules
|
|
139
|
+
(`readRequiredAdminInviteSignatures`, `readRequiredPolicyActivationSignatures`)
|
|
140
|
+
and `readPredictedVault`. There is no index: history or speed is yours to
|
|
141
|
+
bring. The ABIs (`stateAbi`, `usersAbi`, `policiesAbi`, `vaultAbi`,
|
|
142
|
+
`vaultFactoryAbi`, `sponsorGuardAbi`) are generated from cofund-solidity's
|
|
143
|
+
build by `scripts/gen-evm-abis.mjs`.
|
|
144
|
+
|
|
145
|
+
## An open guard
|
|
146
|
+
|
|
147
|
+
Which guard a deployment registers, and whom it accepts, is the deployer's
|
|
148
|
+
choice. For a deployment where anyone may found a cofund or seat a member
|
|
149
|
+
(the signatures still authorize each call), a guard without the sponsor
|
|
150
|
+
allowlist:
|
|
151
|
+
|
|
152
|
+
```solidity
|
|
153
|
+
// SPDX-License-Identifier: MIT
|
|
154
|
+
pragma solidity ^0.8.28;
|
|
155
|
+
|
|
156
|
+
interface IState {
|
|
157
|
+
function helperContracts(string calldata) external view returns (address);
|
|
158
|
+
function clientVaults(bytes32) external view returns (address, string memory);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// CFSponsorGuardv1 without `isAuthorizedSponsor`: anyone relays, and pays gas.
|
|
162
|
+
contract OpenGuard {
|
|
163
|
+
error CallNotAllowed();
|
|
164
|
+
error GuardPaused();
|
|
165
|
+
error InvalidCall();
|
|
166
|
+
error UnauthorizedCaller();
|
|
167
|
+
|
|
168
|
+
IState public immutable state;
|
|
169
|
+
bool public paused;
|
|
170
|
+
|
|
171
|
+
constructor(address _state) { state = IState(_state); }
|
|
172
|
+
|
|
173
|
+
function setPaused(bool value) external {
|
|
174
|
+
if (msg.sender != state.helperContracts("gov")) revert UnauthorizedCaller();
|
|
175
|
+
paused = value;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function relay(address target, bytes calldata data) external payable returns (bytes memory) {
|
|
179
|
+
if (paused) revert GuardPaused();
|
|
180
|
+
if (data.length < 4) revert InvalidCall();
|
|
181
|
+
if (!_isHelper(target) && !_isKnownVault(target)) revert CallNotAllowed();
|
|
182
|
+
(bool ok, bytes memory ret) = target.call{value: msg.value}(data);
|
|
183
|
+
if (!ok) assembly { revert(add(ret, 0x20), mload(ret)) }
|
|
184
|
+
return ret;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function _isHelper(address target) internal view returns (bool) {
|
|
188
|
+
return target != address(0)
|
|
189
|
+
&& (target == state.helperContracts("users") || target == state.helperContracts("policies"));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function _isKnownVault(address target) internal view returns (bool) {
|
|
193
|
+
if (target == address(0) || target.code.length == 0) return false;
|
|
194
|
+
(bool ok, bytes memory ret) = target.staticcall(abi.encodeWithSignature("clientId()"));
|
|
195
|
+
if (!ok || ret.length != 32) return false;
|
|
196
|
+
(address registered, ) = state.clientVaults(abi.decode(ret, (bytes32)));
|
|
197
|
+
return registered == target;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Register it with `gov.setHelperContract("sponsorGuard", openGuard)` and put
|
|
203
|
+
its address in the deployment's `sponsorGuard`; `guardSubmitter` then works
|
|
204
|
+
with any wallet. It keeps the guard's one rule: it reaches the users and
|
|
205
|
+
policies helpers and the cofunds' own vaults, nothing else, never State.
|
|
206
|
+
|
|
207
|
+
## Tests
|
|
208
|
+
|
|
209
|
+
`pnpm test` runs the unit tests: each builder's calldata against vectors
|
|
210
|
+
`cast calldata` encoded (`test/vectors/gen-calls.mjs`), each refusal, the
|
|
211
|
+
deployment check, the submitters, and `cofund-protocol`'s EVM message vectors
|
|
212
|
+
(`test/vectors/conformance`). The integration test deploys the contracts to a
|
|
213
|
+
throwaway Anvil with cofund-solidity's `script/e2e-deploy.sh` and runs a
|
|
214
|
+
cofund's life through this package, every call sent directly where the
|
|
215
|
+
contracts allow it and through the guard:
|
|
216
|
+
|
|
217
|
+
```sh
|
|
218
|
+
PROTOCOL_ANVIL=1 COFUND_SOLIDITY_DIR=../cofund-solidity pnpm --filter @getcofund/protocol test
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
It needs Foundry (`anvil`, `forge`, `cast`); the contracts build into
|
|
222
|
+
`COFUND_FOUNDRY_CACHE_DIR` (default under the OS temp dir), never into the
|
|
223
|
+
checkout.
|
|
224
|
+
|
|
225
|
+
The Bitcoin integration test builds a vault of each template from fresh keys,
|
|
226
|
+
funds it, spends it through PSBTs signed by separate signers, and checks the
|
|
227
|
+
outputs on chain. It mines, so give it a regtest bitcoind of its own:
|
|
228
|
+
|
|
229
|
+
```sh
|
|
230
|
+
PROTOCOL_REGTEST_RPC=http://user:pass@127.0.0.1:18443 pnpm --filter @getcofund/protocol test
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Bitcoin spends
|
|
234
|
+
|
|
235
|
+
A spend is five plain functions on the vault verified from its definition
|
|
236
|
+
(`verifyBtcVault`): select the coins, build a PSBT, sign it, combine and
|
|
237
|
+
finalize it, broadcast it. Co-signers pass the PSBT around as bytes over any
|
|
238
|
+
channel; each checks it against its own copy of the vault, and against what it
|
|
239
|
+
was told the spend pays, before it signs.
|
|
240
|
+
|
|
241
|
+
### A 2-of-3 vault, three signers on three machines
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import { base64 } from "@scure/base";
|
|
245
|
+
import {
|
|
246
|
+
broadcast,
|
|
247
|
+
buildSpendPsbt,
|
|
248
|
+
combinePsbts,
|
|
249
|
+
esploraReader,
|
|
250
|
+
extractTx,
|
|
251
|
+
finalizePsbt,
|
|
252
|
+
localLeafSigner,
|
|
253
|
+
selectCoins,
|
|
254
|
+
signPsbt,
|
|
255
|
+
vaultSpendInputVbytes,
|
|
256
|
+
verifyBtcVault,
|
|
257
|
+
} from "@getcofund/protocol/btc";
|
|
258
|
+
|
|
259
|
+
// Every machine: the vault, rebuilt from the definition its members share.
|
|
260
|
+
const vault = verifyBtcVault({ id: "treasury", address, definition }, "mainnet");
|
|
261
|
+
const reader = esploraReader("https://mempool.space/api");
|
|
262
|
+
|
|
263
|
+
// What the spend pays, agreed out of band. Each signer checks the PSBT against it.
|
|
264
|
+
const expectations = {
|
|
265
|
+
payees: [{ address: "bc1q…", amount: 1_000_000n }],
|
|
266
|
+
extraOutputs: [{ address: "bc1p…", amount: 5_000n }], // e.g. a fee you charge
|
|
267
|
+
maxFeeRate: 50, // sat/vB
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// Machine A selects coins and builds the PSBT.
|
|
271
|
+
const utxos = (await reader.utxos(vault.address)).filter((u) => u.confirmed);
|
|
272
|
+
const { selected, fee } = selectCoins({
|
|
273
|
+
utxos,
|
|
274
|
+
amount: 1_000_000n,
|
|
275
|
+
extraOutputs: [5_000n],
|
|
276
|
+
feeRate: await reader.feeRate(),
|
|
277
|
+
inputVbytes: vaultSpendInputVbytes(vault),
|
|
278
|
+
});
|
|
279
|
+
const unsigned = base64.encode(
|
|
280
|
+
buildSpendPsbt({
|
|
281
|
+
vault,
|
|
282
|
+
inputs: selected,
|
|
283
|
+
payees: expectations.payees,
|
|
284
|
+
extraOutputs: expectations.extraOutputs,
|
|
285
|
+
fee,
|
|
286
|
+
}),
|
|
287
|
+
);
|
|
288
|
+
// …and sends `unsigned` to B and C.
|
|
289
|
+
|
|
290
|
+
// A, B and C each sign their own copy with their own key, and send it back.
|
|
291
|
+
const signer = localLeafSigner(accountXprv); // or a wallet, or a Ledger
|
|
292
|
+
const signed = base64.encode(
|
|
293
|
+
await signPsbt(base64.decode(unsigned), vault, signer, expectations),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
// Whoever collects them: two signatures meet the threshold; a third is ignored.
|
|
297
|
+
const final = finalizePsbt(
|
|
298
|
+
combinePsbts([signedA, signedB, signedC].map((psbt) => base64.decode(psbt))),
|
|
299
|
+
vault,
|
|
300
|
+
);
|
|
301
|
+
const txid = await broadcast(extractTx(final), reader);
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Each step
|
|
305
|
+
|
|
306
|
+
| Function | Does |
|
|
307
|
+
|---|---|
|
|
308
|
+
| `selectCoins({ utxos, amount, feeRate, inputVbytes, payeeCount?, extraOutputs?, consolidate? })` | Coins for the spend, largest first, until they cover the outputs and the fee for that many inputs; with `consolidate`, every coin. The fee is the spend's vsize with a change output, each input sized by the vault's largest leaf (`vaultSpendInputVbytes`), at `feeRate`, times `FEE_SAFETY_MULTIPLIER`. It locks nothing: keeping two spends off the same coins is yours |
|
|
309
|
+
| `buildSpendPsbt({ vault, inputs, payees, extraOutputs?, fee })` | The unsigned PSBT: the payees, then the extra outputs, then change to the vault unless it is below `VAULT_CHANGE_DUST` (then it goes to the fee). The inputs are in their canonical order, and each carries its prevout (`witnessUtxo`), every leaf with its control block (`tapLeafScript`), the internal key and merkle root, and every leaf key's origin (`tapBip32Derivation`). The package adds no output of its own |
|
|
310
|
+
| `checkPsbt(psbt, vault, expectations)` | Every input spends the vault's script, none twice; the outputs are the payees, then the extra outputs, then at most change to the vault; the network fee is at most `maxFeeRate` (default 1000) sat/vB. Returns what the spend pays; throws on the first thing that does not hold |
|
|
311
|
+
| `signPsbt(psbt, vault, signer, expectations)` | `checkPsbt`, then a BIP-340 signature (`tapScriptSig`, SIGHASH_DEFAULT) per input for each leaf that holds the signer's key, each verified against that key before it is added |
|
|
312
|
+
| `combinePsbts(psbts)` | One PSBT with every signature; refuses PSBTs of different spends, or that disagree on a prevout |
|
|
313
|
+
| `finalizePsbt(psbt, vault)` | Each input's witness, once one leaf has its threshold on every input: k of the leaf's keys, and on an `ADMINS_REQUIRED` leaf its admin and k − 1 more. Only signatures that verify count. `ADMINS_INCLUDED` has the same script as `ALL`: its admin rule binds only those who apply it. Throws, saying what each leaf lacks, when none is met |
|
|
314
|
+
| `extractTx(psbt)` | The finalized transaction, as hex |
|
|
315
|
+
| `broadcast(hex, reader)` | Sends it through the reader; returns its txid |
|
|
316
|
+
|
|
317
|
+
The witness assembly the finalizer uses (`tryFinalizeAny`, `leafScriptPolicy`,
|
|
318
|
+
`finalizeWithWitness` and the rest) is exported too, for signatures collected
|
|
319
|
+
some other way.
|
|
320
|
+
|
|
321
|
+
### What you bring
|
|
322
|
+
|
|
323
|
+
A **reader**, `BtcReader`: `utxos(address)`, `tx(txid)`, `feeRate()` and
|
|
324
|
+
`broadcast(hex)`. `esploraReader(baseUrl, { feeTarget? })` is one over any
|
|
325
|
+
Esplora API with `fetch` alone; a node's RPC or another indexer fits the same
|
|
326
|
+
four functions.
|
|
327
|
+
|
|
328
|
+
A **signer** per member, `BtcLeafSigner`: `xpub()`, the account xpub the
|
|
329
|
+
vault's definition lists for the member, and `signTaprootLeaf(sighash,
|
|
330
|
+
derivation)`, a BIP-340 signature by the key at `derivation` below that account
|
|
331
|
+
(`[branch, 376864517]`). `localLeafSigner(accountXprv)` holds an account key in
|
|
332
|
+
memory; derive one from a seed with `@scure/bip32`, e.g.
|
|
333
|
+
`HDKey.fromMasterSeed(seed).derive("m/86'/0'/0'").privateExtendedKey` (with
|
|
334
|
+
`BIP32_VERSIONS.testnet` off mainnet).
|
|
335
|
+
|
|
336
|
+
A vault's definition holds account xpubs, not their wallets' master
|
|
337
|
+
fingerprints, so each `tapBip32Derivation` names its key's origin from the
|
|
338
|
+
account: the account xpub's fingerprint, then `branch/376864517`. A wallet that
|
|
339
|
+
looks its keys up by master fingerprint finds them at
|
|
340
|
+
`m/86'/coin'/0'/branch/376864517`.
|
|
341
|
+
|
|
342
|
+
## Bitcoin policies
|
|
343
|
+
|
|
344
|
+
`@getcofund/protocol/btc-policies` gives Bitcoin the policy shape the EVM
|
|
345
|
+
policies helper has: signers, a threshold, a cap per spend and an optional
|
|
346
|
+
recipient allowlist. No script checks them: a vault's script asks for k of its
|
|
347
|
+
keys and nothing else. A policy binds only the parties that check each spend
|
|
348
|
+
against it before they sign and wait for its signers before they finalize.
|
|
349
|
+
The checks are the ones Cofund's app makes on its Bitcoin transfers.
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
import { checkPsbt, combinePsbts, finalizePsbt, signPsbt } from "@getcofund/protocol/btc";
|
|
353
|
+
import {
|
|
354
|
+
activationDataFor,
|
|
355
|
+
BtcPolicySchema,
|
|
356
|
+
btcPolicyActivationMessage,
|
|
357
|
+
checkSpendAgainstPolicy,
|
|
358
|
+
policySignersSigned,
|
|
359
|
+
verifyActivationSignature,
|
|
360
|
+
} from "@getcofund/protocol/btc-policies";
|
|
361
|
+
import { btcMessageSigner } from "@getcofund/protocol/keys";
|
|
362
|
+
|
|
363
|
+
// A plain value you store and share with its signers.
|
|
364
|
+
const policy = BtcPolicySchema.parse({
|
|
365
|
+
id: crypto.randomUUID(),
|
|
366
|
+
name: "Contractor payouts",
|
|
367
|
+
type: "Contractor_Stipend",
|
|
368
|
+
signers: [aliceXpub, bobXpub, carolXpub], // account xpubs the vault commits to
|
|
369
|
+
threshold: 2,
|
|
370
|
+
maxAmount: "5000000", // sats per spend
|
|
371
|
+
recipients: ["bc1q…"], // absent or empty: any
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// Activation: an admin signs with their BTC key's Cofund leaf, and anyone
|
|
375
|
+
// checks the approval against that admin's account xpub.
|
|
376
|
+
const data = activationDataFor(policy, { company: "Acme Inc" });
|
|
377
|
+
const approval = await btcMessageSigner(keystore).signMessage(btcPolicyActivationMessage(data));
|
|
378
|
+
verifyActivationSignature(data, approval, adminXpub); // true
|
|
379
|
+
|
|
380
|
+
// Before signing: the PSBT checked, then the spend against the policy.
|
|
381
|
+
const summary = checkPsbt(psbt, vault, expectations);
|
|
382
|
+
checkSpendAgainstPolicy(summary, policy, vault); // throws, saying why
|
|
383
|
+
const signed = await signPsbt(psbt, vault, signer, expectations);
|
|
384
|
+
|
|
385
|
+
// Before finalizing: enough of the policy's signers, not any k of the vault's.
|
|
386
|
+
const combined = combinePsbts(copies);
|
|
387
|
+
if (policySignersSigned(combined, policy, vault)) {
|
|
388
|
+
finalizePsbt(combined, vault);
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
| Function | Does |
|
|
393
|
+
|---|---|
|
|
394
|
+
| `BtcPolicySchema` | The policy: a UUID id, a name, a type, its signers (distinct account xpubs), a threshold no higher than their count, `maxAmount` in sats, and `recipients`, each once |
|
|
395
|
+
| `checkPolicyForVault(policy, vault)` | Throws unless the policy parses, every signer is a member of the vault, the threshold is at least the vault's k, and on an `ADMINS_INCLUDED` or `ADMINS_REQUIRED` vault an admin is among the signers |
|
|
396
|
+
| `checkSpendAgainstPolicy(summary, policy, vault)` | `checkPolicyForVault`, then: the spend (`checkPsbt`'s summary) is the vault's, its payees' total is at most `maxAmount`, and with recipients listed every payee is one of them, compared case-insensitively. The extra outputs, the change and the network fee are not counted |
|
|
397
|
+
| `policySignersSigned(psbt, policy, vault)` | The first leaf on which at least `threshold` of the policy's signers each have a signature that verifies over every input, each key counted once, and among whom the leaf's own rule can be met; `{ leafHash, signers }`, or null |
|
|
398
|
+
| `activationDataFor(policy, { company })` | The activation: `clientId` the company's sha256, the policy's id as `authId`, each signer's Cofund leaf key and the recipients in codepoint order, the cap, the token `BTC`, no wrapper |
|
|
399
|
+
| `verifyActivationSignature(data, signature, xpub)` | Whether the signature (64-byte compact, hex) is the Cofund leaf of `xpub` signing the activation message as a Bitcoin Signed Message; false, never a throw, on a malformed one |
|
|
400
|
+
|
|
401
|
+
`btcPolicyActivationDigest`, `btcPolicyActivationMessage`,
|
|
402
|
+
`btcPolicyActivationSigningHash` and `bitcoinSignedMessageHash` give the bytes
|
|
403
|
+
underneath: a length-prefixed sha256 of the fields, its hex as the message,
|
|
404
|
+
and the message's BIP-137 hash, which a Ledger's Bitcoin app signs too.
|
|
405
|
+
|
|
406
|
+
One call verifies one approval. Whose approvals activate a policy, and how
|
|
407
|
+
many, is yours: Cofund's app takes its cofund's admins, one while it is
|
|
408
|
+
onboarding and two after (every admin for a threshold-1 policy).
|
|
409
|
+
|
|
410
|
+
`finalizePsbt` uses every signature that verifies; to put only the policy's
|
|
411
|
+
signers on the witness, combine only their PSBTs.
|
|
412
|
+
|
|
413
|
+
## License
|
|
414
|
+
|
|
415
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@getcofund/protocol/btc`: a vault's taproot tree from its definition, and
|
|
3
|
+
* its spends: coins selected, built into a PSBT, checked and signed, combined,
|
|
4
|
+
* finalized and broadcast.
|
|
5
|
+
*/
|
|
6
|
+
export * from "./psbt.ts";
|
|
7
|
+
export * from "./reader.ts";
|
|
8
|
+
export * from "./select.ts";
|
|
9
|
+
export * from "./spend.ts";
|
|
10
|
+
export * from "./types.ts";
|
|
11
|
+
export * from "./vault/index.ts";
|
|
12
|
+
export * from "./witness.ts";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@getcofund/protocol/btc`: a vault's taproot tree from its definition, and
|
|
3
|
+
* its spends: coins selected, built into a PSBT, checked and signed, combined,
|
|
4
|
+
* finalized and broadcast.
|
|
5
|
+
*/
|
|
6
|
+
// biome-ignore lint/performance/noBarrelFile: the package's `./btc` entry
|
|
7
|
+
export * from "./psbt.js";
|
|
8
|
+
export * from "./reader.js";
|
|
9
|
+
export * from "./select.js";
|
|
10
|
+
export * from "./spend.js";
|
|
11
|
+
export * from "./types.js";
|
|
12
|
+
export * from "./vault/index.js";
|
|
13
|
+
export * from "./witness.js";
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { VerifiedBtcVault } from "./spend.ts";
|
|
2
|
+
/**
|
|
3
|
+
* A vault spend as a PSBT (BIP-174, with BIP-371's taproot fields): what
|
|
4
|
+
* co-signers pass around, over whatever channel they choose, until one leaf
|
|
5
|
+
* has its threshold.
|
|
6
|
+
*
|
|
7
|
+
* select (`selectCoins`) → `buildSpendPsbt` → `signPsbt`, once per signer →
|
|
8
|
+
* `combinePsbts` → `finalizePsbt` → `extractTx` → `broadcast`
|
|
9
|
+
*
|
|
10
|
+
* Every step takes the vault verified against its definition
|
|
11
|
+
* (`verifyBtcVault`): a signer reads the vault's script and leaves off its
|
|
12
|
+
* own rebuild of the tree, never off the PSBT.
|
|
13
|
+
*/
|
|
14
|
+
/** An output: an address on the vault's network and the sats it is paid. */
|
|
15
|
+
export type BtcPayment = {
|
|
16
|
+
address: string;
|
|
17
|
+
amount: bigint | number;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Signs for a member's account key: a wallet, a Ledger, a local key. `xpub` is
|
|
21
|
+
* the account xpub the vault's definition lists for the member, and
|
|
22
|
+
* `signTaprootLeaf` returns a BIP-340 signature over `sighash` by the key at
|
|
23
|
+
* `derivation` below that account: `[branch, COFUND_LEAF_INDEX]`, the leaf's
|
|
24
|
+
* branch and the Cofund leaf index.
|
|
25
|
+
*/
|
|
26
|
+
export type BtcLeafSigner = {
|
|
27
|
+
xpub(): Promise<string>;
|
|
28
|
+
signTaprootLeaf(sighash: Uint8Array, derivation: number[]): Promise<Uint8Array>;
|
|
29
|
+
};
|
|
30
|
+
/** A {@link BtcLeafSigner} over an account xprv (tprv off mainnet), `m/86'/coin'/0'`, held in memory. */
|
|
31
|
+
export declare function localLeafSigner(accountXprv: string): BtcLeafSigner;
|
|
32
|
+
/** Change below this many sats is left to the fee: a P2TR output under it is dust. */
|
|
33
|
+
export declare const VAULT_CHANGE_DUST = 330n;
|
|
34
|
+
export type SpendPsbtRequest = {
|
|
35
|
+
vault: VerifiedBtcVault;
|
|
36
|
+
/** The vault's coins it spends (`selectCoins`); `value` in sats. */
|
|
37
|
+
inputs: readonly {
|
|
38
|
+
txid: string;
|
|
39
|
+
vout: number;
|
|
40
|
+
value: bigint | number;
|
|
41
|
+
}[];
|
|
42
|
+
/** Paid first, in this order. */
|
|
43
|
+
payees: readonly BtcPayment[];
|
|
44
|
+
/** Paid after the payees, in this order: a fee to an address, say. The package adds none of its own. */
|
|
45
|
+
extraOutputs?: readonly BtcPayment[];
|
|
46
|
+
/** The network fee, in sats (`selectCoins`). */
|
|
47
|
+
fee: bigint | number;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The unsigned spend as a PSBT: the payees, then the extra outputs, then
|
|
51
|
+
* change back to the vault when it is not dust (the inputs less the outputs
|
|
52
|
+
* less `fee`); the inputs in their canonical order (`buildVaultSpend`, by
|
|
53
|
+
* `txid:vout`). Each input carries its prevout (`witnessUtxo`), every leaf of
|
|
54
|
+
* the vault with its control block (`tapLeafScript`), the internal key and
|
|
55
|
+
* merkle root, and each leaf key's origin (`tapBip32Derivation`).
|
|
56
|
+
*/
|
|
57
|
+
export declare function buildSpendPsbt(request: SpendPsbtRequest): Uint8Array;
|
|
58
|
+
/** What a signer expects a spend to pay. */
|
|
59
|
+
export type PsbtExpectations = {
|
|
60
|
+
/** The first outputs, in this order. */
|
|
61
|
+
payees: readonly BtcPayment[];
|
|
62
|
+
/** The outputs right after the payees, in this order. */
|
|
63
|
+
extraOutputs?: readonly BtcPayment[];
|
|
64
|
+
/** The network fee's ceiling, in sat/vB of the spend's vsize (default 1000). */
|
|
65
|
+
maxFeeRate?: number;
|
|
66
|
+
};
|
|
67
|
+
/** What a checked PSBT spends and pays; sats throughout. */
|
|
68
|
+
export type PsbtSpendSummary = {
|
|
69
|
+
vault: string;
|
|
70
|
+
inputs: {
|
|
71
|
+
txid: string;
|
|
72
|
+
vout: number;
|
|
73
|
+
value: bigint;
|
|
74
|
+
}[];
|
|
75
|
+
payees: {
|
|
76
|
+
address: string;
|
|
77
|
+
amount: bigint;
|
|
78
|
+
}[];
|
|
79
|
+
extraOutputs: {
|
|
80
|
+
address: string;
|
|
81
|
+
amount: bigint;
|
|
82
|
+
}[];
|
|
83
|
+
/** Back to the vault; 0 without a change output. */
|
|
84
|
+
change: bigint;
|
|
85
|
+
fee: bigint;
|
|
86
|
+
/** As the fee is priced: every input witnessed by the vault's largest leaf. */
|
|
87
|
+
vsize: number;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Check a PSBT against the verified vault and what the consumer expects;
|
|
91
|
+
* throws on the first thing that does not hold:
|
|
92
|
+
*
|
|
93
|
+
* - every input spends the vault's script (`witnessUtxo`), and none twice: a
|
|
94
|
+
* prevout the PSBT misstates only makes every signature over it invalid,
|
|
95
|
+
* since BIP-341 commits to each prevout's script and amount;
|
|
96
|
+
* - the outputs are the payees, then the extra outputs, each its address and
|
|
97
|
+
* amount, then at most one more: change back to the vault;
|
|
98
|
+
* - the network fee (the inputs less the outputs) is at most `maxFeeRate`
|
|
99
|
+
* sat/vB of the spend's vsize as it is priced.
|
|
100
|
+
*/
|
|
101
|
+
export declare function checkPsbt(psbt: Uint8Array, vault: VerifiedBtcVault, expectations: PsbtExpectations): PsbtSpendSummary;
|
|
102
|
+
/**
|
|
103
|
+
* The signer's BIP-340 signatures added to the PSBT (`tapScriptSig`), one per
|
|
104
|
+
* input for each leaf that holds the signer's key, over the BIP-341 sighash
|
|
105
|
+
* (SIGHASH_DEFAULT) of the spend with the verified leaf's script. First the
|
|
106
|
+
* PSBT is checked (`checkPsbt`); each signature is verified against the
|
|
107
|
+
* signer's key in the leaf before it is added.
|
|
108
|
+
*/
|
|
109
|
+
export declare function signPsbt(psbt: Uint8Array, vault: VerifiedBtcVault, signer: BtcLeafSigner, expectations: PsbtExpectations): Promise<Uint8Array>;
|
|
110
|
+
/** One PSBT with the signatures of all of them; they must be the same spend of the same prevouts. */
|
|
111
|
+
export declare function combinePsbts(psbts: readonly Uint8Array[]): Uint8Array;
|
|
112
|
+
/**
|
|
113
|
+
* Assemble each input's witness once one leaf has its threshold on every
|
|
114
|
+
* input, by the template's rules (`tryFinalizeAny`): k of the leaf's keys, and
|
|
115
|
+
* on an ADMINS_REQUIRED leaf its admin plus k − 1 of the rest. Only signatures
|
|
116
|
+
* that verify over the spend count. Returns the finalized PSBT, its signing
|
|
117
|
+
* fields cleared; throws when no leaf is met.
|
|
118
|
+
*/
|
|
119
|
+
export declare function finalizePsbt(psbt: Uint8Array, vault: VerifiedBtcVault): Uint8Array;
|
|
120
|
+
/** A finalized PSBT's transaction, as raw hex to broadcast. */
|
|
121
|
+
export declare function extractTx(psbt: Uint8Array): string;
|