@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,439 @@
|
|
|
1
|
+
/** biome-ignore-all lint/suspicious/noBitwiseOperators: a control block's first byte is the leaf version plus the output key's parity. */
|
|
2
|
+
import { schnorr } from "@noble/curves/secp256k1.js";
|
|
3
|
+
import { bytesToHex } from "@noble/hashes/utils";
|
|
4
|
+
import * as btc from "@scure/btc-signer";
|
|
5
|
+
import { OutScript, Script } from "@scure/btc-signer";
|
|
6
|
+
import { vaultSpendSighash } from "./vault/spend.js";
|
|
7
|
+
/** The control block of a leaf: its version plus the output key's parity, the internal key, and the merkle path. */
|
|
8
|
+
export function encodeCBFromStruct(cbStruct) {
|
|
9
|
+
const parity = cbStruct.parity ?? 0;
|
|
10
|
+
return btc.TaprootControlBlock.encode({
|
|
11
|
+
version: (cbStruct.version + parity) & 0xff,
|
|
12
|
+
internalKey: cbStruct.internalKey,
|
|
13
|
+
merklePath: cbStruct.merklePath,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Does `sig` verify as `signerXonly`'s BIP340 signature over `msg`?
|
|
18
|
+
*
|
|
19
|
+
* The single answer to "did this key actually approve THIS exact transaction".
|
|
20
|
+
* Malformed input makes noble throw, which is easy to swallow by accident —
|
|
21
|
+
* always returning a boolean makes it hard for a call site to treat an
|
|
22
|
+
* unverifiable signature as an approval.
|
|
23
|
+
*/
|
|
24
|
+
export function verifyPartialSignatureAgainstMsg(msg, signerXonly, sig) {
|
|
25
|
+
// Tapscript CHECKSIG under SIGHASH_DEFAULT takes a bare 64-byte BIP340
|
|
26
|
+
// signature — no trailing sighash-flag byte.
|
|
27
|
+
if (sig.length !== 64) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
return schnorr.verify(sig, msg, signerXonly);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** The sighash a signer of `leaf` signs for input `inputIndex` of the spend. */
|
|
38
|
+
function msgHashForLeaf(spend, leaf, inputIndex) {
|
|
39
|
+
return vaultSpendSighash(spend.tx, inputIndex, leaf.script);
|
|
40
|
+
}
|
|
41
|
+
function bytesEqual(a, b) {
|
|
42
|
+
return a.length === b.length && a.every((byte, index) => byte === b[index]);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Decode the policy Bitcoin actually executes, rather than trusting metadata.
|
|
46
|
+
*
|
|
47
|
+
* The hard threshold is committed by the tapscript. `Leaf.threshold` is useful
|
|
48
|
+
* descriptive metadata, but it must never be the authority used to decide how
|
|
49
|
+
* many signatures enter a witness.
|
|
50
|
+
*/
|
|
51
|
+
export function leafScriptPolicy(leaf) {
|
|
52
|
+
try {
|
|
53
|
+
const decoded = OutScript.decode(leaf.script);
|
|
54
|
+
if (decoded.type === "tr_ms") {
|
|
55
|
+
return {
|
|
56
|
+
kind: "non-admin-required",
|
|
57
|
+
signatureKeys: [...decoded.pubkeys],
|
|
58
|
+
threshold: decoded.m,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// ADMINS_REQUIRED has a CHECKSIGVERIFY prefix, so it is not a bare tr_ms.
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const ops = Script.decode(leaf.script);
|
|
67
|
+
const adminKey = ops[0];
|
|
68
|
+
if (!(adminKey instanceof Uint8Array) ||
|
|
69
|
+
adminKey.length !== 32 ||
|
|
70
|
+
ops[1] !== "CHECKSIGVERIFY") {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const prefix = Script.encode([adminKey, "CHECKSIGVERIFY"]);
|
|
74
|
+
const body = OutScript.decode(leaf.script.slice(prefix.length));
|
|
75
|
+
if (body.type !== "tr_ms") {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const bodyKeys = [...body.pubkeys];
|
|
79
|
+
return {
|
|
80
|
+
kind: "admin-required",
|
|
81
|
+
signatureKeys: [adminKey, ...bodyKeys],
|
|
82
|
+
threshold: body.m + 1,
|
|
83
|
+
adminKey,
|
|
84
|
+
bodyKeys,
|
|
85
|
+
bodyThreshold: body.m,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Match each script key to one leaf signer slot, preserving duplicate slots. */
|
|
93
|
+
export function signerSlotsForScript(leaf, signatureKeys) {
|
|
94
|
+
const used = new Set();
|
|
95
|
+
return signatureKeys.map((key) => {
|
|
96
|
+
let matchedSigner;
|
|
97
|
+
for (let index = 0; index < leaf.signers.length; index++) {
|
|
98
|
+
const signer = leaf.signers[index];
|
|
99
|
+
if (!used.has(index) && bytesEqual(signer.xonly, key)) {
|
|
100
|
+
used.add(index);
|
|
101
|
+
matchedSigner = signer;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return matchedSigner;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
export function signatureSlotCountForLeaf(leaf) {
|
|
109
|
+
return leafScriptPolicy(leaf)?.signatureKeys.length ?? null;
|
|
110
|
+
}
|
|
111
|
+
// Build an ordered signature stack in reverse consumption order.
|
|
112
|
+
// For signer i, the signature is placed at stack[n-1-i]. Missing or invalid -> empty push.
|
|
113
|
+
//
|
|
114
|
+
// Hydration retains all verified signatures. This NAR assembler selects the
|
|
115
|
+
// exact threshold committed by the script; ADMINS_REQUIRED selects separately.
|
|
116
|
+
//
|
|
117
|
+
// A key contributes to the count AT MOST ONCE. New vaults can no longer be
|
|
118
|
+
// built with two signer slots sharing a key, but one created before that
|
|
119
|
+
// invariant existed still has a leaf where a single signature would satisfy
|
|
120
|
+
// both slots — so the counting here is by distinct key, not by slot. A
|
|
121
|
+
// legitimate spend by two different keyholders is unaffected, which is what
|
|
122
|
+
// lets the funds be migrated off such a vault.
|
|
123
|
+
export function orderedSigStackForLeaf(spend, leaf, inputIndex, bucket) {
|
|
124
|
+
const msg = msgHashForLeaf(spend, leaf, inputIndex);
|
|
125
|
+
const policy = leafScriptPolicy(leaf);
|
|
126
|
+
if (!policy || policy.kind !== "non-admin-required") {
|
|
127
|
+
return { stack: [], nonEmpty: 0 };
|
|
128
|
+
}
|
|
129
|
+
const { signatureKeys, threshold } = policy;
|
|
130
|
+
const signerSlots = signerSlotsForScript(leaf, signatureKeys);
|
|
131
|
+
const n = signatureKeys.length;
|
|
132
|
+
let taken = 0;
|
|
133
|
+
const countedKeys = new Set();
|
|
134
|
+
const stack = Array.from({ length: n }, () => new Uint8Array(0));
|
|
135
|
+
for (let i = 0; i < n; i++) {
|
|
136
|
+
const user = signerSlots[i];
|
|
137
|
+
if (!user) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const sig = bucket.get(user.id);
|
|
141
|
+
const stackIdx = n - 1 - i;
|
|
142
|
+
if (!sig || sig.length === 0) {
|
|
143
|
+
stack[stackIdx] = new Uint8Array(0);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
// Use leaf-local pubkey (per-branch derived for account-based vaults)
|
|
147
|
+
const pub = signatureKeys[i];
|
|
148
|
+
const pubHex = bytesToHex(pub);
|
|
149
|
+
// Checked before verifying: the signature would verify fine — it is the
|
|
150
|
+
// same key — and counting it again is exactly the collapse.
|
|
151
|
+
if (countedKeys.has(pubHex)) {
|
|
152
|
+
console.error(`[Leaf ${bytesToHex(leaf.leafHash)}] Refusing to reuse key ${pubHex} for ${user.id}: another signer slot already contributed it, which would double-count one signature toward the threshold.`);
|
|
153
|
+
stack[stackIdx] = new Uint8Array(0);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (!verifyPartialSignatureAgainstMsg(msg, pub, sig)) {
|
|
157
|
+
console.error(`[Leaf ${bytesToHex(leaf.leafHash)}] Sig verify failed for ${user.id}`);
|
|
158
|
+
stack[stackIdx] = new Uint8Array(0);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (taken < threshold) {
|
|
162
|
+
taken += 1;
|
|
163
|
+
countedKeys.add(pubHex);
|
|
164
|
+
stack[stackIdx] = sig;
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
stack[stackIdx] = new Uint8Array(0);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { stack, nonEmpty: taken };
|
|
171
|
+
}
|
|
172
|
+
function evaluateSignatureSlots(opts) {
|
|
173
|
+
const { spend, leaf, sigStack, signatureKeys, inputIndex } = opts;
|
|
174
|
+
const msg = msgHashForLeaf(spend, leaf, inputIndex);
|
|
175
|
+
const countedKeys = new Set();
|
|
176
|
+
const satisfied = Array.from({ length: signatureKeys.length }, () => false);
|
|
177
|
+
for (let index = 0; index < signatureKeys.length; index++) {
|
|
178
|
+
const stackIndex = signatureKeys.length - 1 - index;
|
|
179
|
+
const sig = sigStack[stackIndex];
|
|
180
|
+
// A malformed stack is an unsatisfied leaf, not an exceptional condition.
|
|
181
|
+
if (!sig || (sig.length !== 0 && sig.length !== 64)) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
if (sig.length === 0) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
// Verify against the key committed by the script, not roster metadata.
|
|
188
|
+
const pub = signatureKeys[index];
|
|
189
|
+
const pubHex = bytesToHex(pub);
|
|
190
|
+
if (countedKeys.has(pubHex)) {
|
|
191
|
+
console.error(`[Leaf ${bytesToHex(leaf.leafHash)}] Key ${pubHex} appears in more than one signed slot; not counting it twice toward the threshold.`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (!verifyPartialSignatureAgainstMsg(msg, pub, sig)) {
|
|
195
|
+
console.error(`[Leaf ${bytesToHex(leaf.leafHash)}] Sig verify failed at i=${index}`);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
countedKeys.add(pubHex);
|
|
199
|
+
satisfied[index] = true;
|
|
200
|
+
}
|
|
201
|
+
return satisfied;
|
|
202
|
+
}
|
|
203
|
+
// Sum CHECKSIGADD results across signer order and compare to threshold.
|
|
204
|
+
//
|
|
205
|
+
// Counts DISTINCT keys, matching `orderedSigStackForLeaf`. Bitcoin itself would
|
|
206
|
+
// count a repeated key twice (BIP-342 requires neither keys nor signatures in a
|
|
207
|
+
// CHECKSIGADD ladder to be distinct); refusing to here is what stops cofund
|
|
208
|
+
// from assembling a witness that meets the threshold with one signature.
|
|
209
|
+
export function sigStackSolvesLeaf(spend, leaf, sigStack, inputIndex) {
|
|
210
|
+
const policy = leafScriptPolicy(leaf);
|
|
211
|
+
if (!policy || sigStack.length !== policy.signatureKeys.length) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
// The exact-length check above also rejects oversized stacks.
|
|
215
|
+
const satisfied = evaluateSignatureSlots({
|
|
216
|
+
spend,
|
|
217
|
+
leaf,
|
|
218
|
+
sigStack,
|
|
219
|
+
signatureKeys: policy.signatureKeys,
|
|
220
|
+
inputIndex,
|
|
221
|
+
});
|
|
222
|
+
if (!satisfied) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
if (policy.kind === "admin-required") {
|
|
226
|
+
const adminSatisfied = satisfied[0];
|
|
227
|
+
const bodySum = satisfied.slice(1).filter(Boolean).length;
|
|
228
|
+
return adminSatisfied && bodySum === policy.bodyThreshold;
|
|
229
|
+
}
|
|
230
|
+
return satisfied.filter(Boolean).length === policy.threshold;
|
|
231
|
+
}
|
|
232
|
+
// <adminX> CHECKSIGVERIFY <pk> CHECKSIG (<pk> CHECKSIGADD)* <k> NUMEQUAL
|
|
233
|
+
/** An ADMINS_REQUIRED leaf: its admin, then its body signers in script order, and the body's k. */
|
|
234
|
+
export function decodeAdminRequiredLeaf(leaf) {
|
|
235
|
+
const policy = leafScriptPolicy(leaf);
|
|
236
|
+
if (!policy || policy.kind !== "admin-required") {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
const signerSlots = signerSlotsForScript(leaf, policy.signatureKeys);
|
|
240
|
+
if (signerSlots.some((signer) => !signer)) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
const [admin, ...body] = signerSlots;
|
|
244
|
+
if (!admin) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
const bodyOrderIds = [];
|
|
248
|
+
for (const signer of body) {
|
|
249
|
+
if (!signer) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
bodyOrderIds.push(signer.id);
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
adminId: admin.id,
|
|
256
|
+
adminKey: policy.adminKey,
|
|
257
|
+
bodyOrderIds,
|
|
258
|
+
bodyKeys: policy.bodyKeys,
|
|
259
|
+
k: policy.bodyThreshold,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function addEligibleSigner(selection, eligibleSignerIds, signerId, key) {
|
|
263
|
+
const keyHex = bytesToHex(key);
|
|
264
|
+
if (!eligibleSignerIds.has(signerId) || selection.countedKeys.has(keyHex)) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
selection.countedKeys.add(keyHex);
|
|
268
|
+
selection.signerIds.push(signerId);
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
function selectAdminRequiredSignerIds(leaf, eligibleSignerIds) {
|
|
272
|
+
const decoded = decodeAdminRequiredLeaf(leaf);
|
|
273
|
+
const selection = {
|
|
274
|
+
countedKeys: new Set(),
|
|
275
|
+
signerIds: [],
|
|
276
|
+
};
|
|
277
|
+
if (!decoded) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
if (!addEligibleSigner(selection, eligibleSignerIds, decoded.adminId, decoded.adminKey)) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
for (let index = 0; index < decoded.bodyOrderIds.length; index++) {
|
|
284
|
+
addEligibleSigner(selection, eligibleSignerIds, decoded.bodyOrderIds[index], decoded.bodyKeys[index]);
|
|
285
|
+
if (selection.signerIds.length === decoded.k + 1) {
|
|
286
|
+
return selection.signerIds;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
function selectNonAdminRequiredSignerIds(leaf, eligibleSignerIds) {
|
|
292
|
+
const policy = leafScriptPolicy(leaf);
|
|
293
|
+
if (!policy || policy.kind !== "non-admin-required") {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
const signerSlots = signerSlotsForScript(leaf, policy.signatureKeys);
|
|
297
|
+
const selection = {
|
|
298
|
+
countedKeys: new Set(),
|
|
299
|
+
signerIds: [],
|
|
300
|
+
};
|
|
301
|
+
for (let index = 0; index < signerSlots.length; index++) {
|
|
302
|
+
const signer = signerSlots[index];
|
|
303
|
+
if (signer) {
|
|
304
|
+
addEligibleSigner(selection, eligibleSignerIds, signer.id, policy.signatureKeys[index]);
|
|
305
|
+
}
|
|
306
|
+
if (selection.signerIds.length === policy.threshold) {
|
|
307
|
+
return selection.signerIds;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Select the exact hard-threshold signer identities available for one leaf.
|
|
314
|
+
* Returns null when the eligible set cannot satisfy the on-chain script.
|
|
315
|
+
*/
|
|
316
|
+
export function selectHardThresholdSignerIds(leaf, vaultType, eligibleSignerIds) {
|
|
317
|
+
if (vaultType === "ADMINS_REQUIRED") {
|
|
318
|
+
return selectAdminRequiredSignerIds(leaf, eligibleSignerIds);
|
|
319
|
+
}
|
|
320
|
+
return selectNonAdminRequiredSignerIds(leaf, eligibleSignerIds);
|
|
321
|
+
}
|
|
322
|
+
function stackForNAR(spend, leaf, inputIndex, bucket) {
|
|
323
|
+
const { stack } = orderedSigStackForLeaf(spend, leaf, inputIndex, bucket);
|
|
324
|
+
if (!sigStackSolvesLeaf(spend, leaf, stack, inputIndex)) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
return stack;
|
|
328
|
+
}
|
|
329
|
+
function stackForAR(spend, leaf, inputIndex, bucket) {
|
|
330
|
+
const decoded = decodeAdminRequiredLeaf(leaf);
|
|
331
|
+
if (!decoded) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
const { adminId, bodyOrderIds, bodyKeys, k } = decoded;
|
|
335
|
+
const adminSig = bucket.get(adminId);
|
|
336
|
+
if (!adminSig) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
// Collect exactly k distinct-key body signatures in script order.
|
|
340
|
+
const slots = new Array(bodyOrderIds.length).fill(new Uint8Array());
|
|
341
|
+
const countedKeys = new Set();
|
|
342
|
+
let kept = 0;
|
|
343
|
+
for (let j = 0; j < bodyOrderIds.length; j++) {
|
|
344
|
+
const sig = bucket.get(bodyOrderIds[j]);
|
|
345
|
+
const keyHex = bytesToHex(bodyKeys[j]);
|
|
346
|
+
if (sig && !countedKeys.has(keyHex) && kept < k) {
|
|
347
|
+
slots[j] = sig;
|
|
348
|
+
countedKeys.add(keyHex);
|
|
349
|
+
kept += 1;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (kept !== k) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
// witness: reverse(body) then admin
|
|
356
|
+
const stack = [...slots.slice().reverse(), adminSig];
|
|
357
|
+
if (!sigStackSolvesLeaf(spend, leaf, stack, inputIndex)) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
return stack;
|
|
361
|
+
}
|
|
362
|
+
/** Write `leaf`'s witness for input `inputIndex`: the signature stack, the leaf script and its control block. */
|
|
363
|
+
export function finalizeWithWitness(tx, inputIndex, leaf, sigStack) {
|
|
364
|
+
const expectedSlots = signatureSlotCountForLeaf(leaf);
|
|
365
|
+
if (expectedSlots === null || sigStack.length !== expectedSlots) {
|
|
366
|
+
throw new Error("signature stack length mismatch");
|
|
367
|
+
}
|
|
368
|
+
const witness = [
|
|
369
|
+
...sigStack,
|
|
370
|
+
leaf.script,
|
|
371
|
+
encodeCBFromStruct(leaf.controlBlockStruct),
|
|
372
|
+
];
|
|
373
|
+
tx.updateInput(inputIndex, { finalScriptWitness: witness });
|
|
374
|
+
return witness;
|
|
375
|
+
}
|
|
376
|
+
function restrictBucketToApprovers(bucket, approverIds) {
|
|
377
|
+
if (!approverIds) {
|
|
378
|
+
return bucket;
|
|
379
|
+
}
|
|
380
|
+
return new Map([...bucket].filter(([signerId]) => approverIds.has(signerId)));
|
|
381
|
+
}
|
|
382
|
+
function stacksForEveryInput(opts) {
|
|
383
|
+
const { spend, leaf, perLeaf, vaultType, approverIds } = opts;
|
|
384
|
+
const stacks = [];
|
|
385
|
+
for (let inputIndex = 0; inputIndex < spend.tx.inputsLength; inputIndex++) {
|
|
386
|
+
const bucket = perLeaf.get(inputIndex);
|
|
387
|
+
if (!bucket) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
const eligibleBucket = restrictBucketToApprovers(bucket, approverIds);
|
|
391
|
+
const stack = vaultType === "ADMINS_REQUIRED"
|
|
392
|
+
? stackForAR(spend, leaf, inputIndex, eligibleBucket)
|
|
393
|
+
: stackForNAR(spend, leaf, inputIndex, eligibleBucket);
|
|
394
|
+
if (!stack) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
stacks.push(stack);
|
|
398
|
+
}
|
|
399
|
+
return stacks;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Finalize every input with the first candidate leaf that the collected
|
|
403
|
+
* signatures solve on every input — the template's rules: k of the leaf's
|
|
404
|
+
* keys, and on an ADMINS_REQUIRED leaf its admin plus k − 1 of the rest.
|
|
405
|
+
* With a plan, only its leaf and its approvers' signatures count.
|
|
406
|
+
*/
|
|
407
|
+
export function tryFinalizeAny(spend, vaultType, plan) {
|
|
408
|
+
if (spend.tx.inputsLength === 0) {
|
|
409
|
+
return { finalized: false };
|
|
410
|
+
}
|
|
411
|
+
const scopedApprovers = plan ? new Set(plan.approverIds) : null;
|
|
412
|
+
const plannedLeafHash = plan?.leafHash.toLowerCase();
|
|
413
|
+
for (const leaf of spend.candidates) {
|
|
414
|
+
const key = bytesToHex(leaf.leafHash).toLowerCase();
|
|
415
|
+
if (plannedLeafHash && key !== plannedLeafHash) {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const perLeaf = spend.partials.get(key);
|
|
419
|
+
if (!perLeaf) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
// Finalization is all-or-nothing: one common leaf must solve every input.
|
|
423
|
+
const stacks = stacksForEveryInput({
|
|
424
|
+
spend,
|
|
425
|
+
leaf,
|
|
426
|
+
perLeaf,
|
|
427
|
+
vaultType,
|
|
428
|
+
approverIds: scopedApprovers,
|
|
429
|
+
});
|
|
430
|
+
if (!stacks) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
for (let inputIndex = 0; inputIndex < stacks.length; inputIndex++) {
|
|
434
|
+
finalizeWithWitness(spend.tx, inputIndex, leaf, stacks[inputIndex]);
|
|
435
|
+
}
|
|
436
|
+
return { finalized: true, usedLeaf: leaf };
|
|
437
|
+
}
|
|
438
|
+
return { finalized: false };
|
|
439
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { BtcPolicy } from "./policy.ts";
|
|
2
|
+
/**
|
|
3
|
+
* What an admin signs to approve a BTC policy's activation, shared by the
|
|
4
|
+
* webapp (`apps/cofund/src/lib/bitcoin/policy-activation-data.ts` re-exports
|
|
5
|
+
* it) and the SDK, so the server's verifier and a local signer hash the same
|
|
6
|
+
* bytes.
|
|
7
|
+
*
|
|
8
|
+
* BTC policies are soft: no contract checks this. An approval is still a
|
|
9
|
+
* signature over exactly which policy was approved, verifiable later against
|
|
10
|
+
* the admin's registered key, rather than a row anyone with database access
|
|
11
|
+
* could write.
|
|
12
|
+
*/
|
|
13
|
+
export type BtcPolicyActivationData = {
|
|
14
|
+
/** hex sha256(companyName) — matches the other chains' clientId. */
|
|
15
|
+
clientId: string;
|
|
16
|
+
policyId: string;
|
|
17
|
+
authId: string;
|
|
18
|
+
policyType: string;
|
|
19
|
+
policyTitle: string;
|
|
20
|
+
/** Hex compressed signer keys, codepoint-ordered. */
|
|
21
|
+
policySignerKeys: string[];
|
|
22
|
+
policyThreshold: number;
|
|
23
|
+
/** Wrapper binding for tx-style policies; empty strings for transfers. */
|
|
24
|
+
txWrapper: string;
|
|
25
|
+
txFunc: string;
|
|
26
|
+
isTransaction: boolean;
|
|
27
|
+
maxAmount: string;
|
|
28
|
+
tokenIdentifier: string;
|
|
29
|
+
recipients: string[];
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Length-prefixed encoding, then sha256. Every field is written as a 4-byte
|
|
33
|
+
* big-endian length followed by its UTF-8 bytes, so no choice of field values
|
|
34
|
+
* can produce the same byte string as a different choice — plain concatenation
|
|
35
|
+
* would let a title ending in a recipient address impersonate one.
|
|
36
|
+
*/
|
|
37
|
+
export declare function btcPolicyActivationDigest(data: BtcPolicyActivationData): Uint8Array;
|
|
38
|
+
/**
|
|
39
|
+
* The string an approving admin signs: the payload digest, hex-encoded.
|
|
40
|
+
*
|
|
41
|
+
* Wrapped in the Bitcoin Signed Message envelope below rather than signed raw,
|
|
42
|
+
* because that is the only thing a Ledger's Bitcoin app will sign
|
|
43
|
+
* (`SignerBtc.signMessage`). Keeping the format signer-agnostic from the start
|
|
44
|
+
* means a device branch can be added without re-collecting approvals.
|
|
45
|
+
*/
|
|
46
|
+
export declare function btcPolicyActivationMessage(data: BtcPolicyActivationData): string;
|
|
47
|
+
/** BIP-137 message hash: sha256d(prefix || varint(len) || message). */
|
|
48
|
+
export declare function bitcoinSignedMessageHash(message: string): Uint8Array;
|
|
49
|
+
/** BIP-137 message hash of the activation message: what the leaf key signs. */
|
|
50
|
+
export declare function btcPolicyActivationSigningHash(data: BtcPolicyActivationData): Uint8Array;
|
|
51
|
+
/** What an activation commits to beyond the policy. */
|
|
52
|
+
export type BtcPolicyActivationContext = {
|
|
53
|
+
/** The cofund's name, as its members sign it: `clientId` is its sha256. */
|
|
54
|
+
company: string;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* The activation of `policy`, as the app builds it for a Bitcoin transfer
|
|
58
|
+
* policy (`deriveBtcPolicyActivationData`,
|
|
59
|
+
* `apps/cofund/src/lib/bitcoin/policy-activation-data.ts:28-78`): its id as
|
|
60
|
+
* the `authId`, each signer's Cofund leaf key and the recipients in codepoint
|
|
61
|
+
* order, the cap, the token `BTC`, and no wrapper.
|
|
62
|
+
*/
|
|
63
|
+
export declare function activationDataFor(policy: BtcPolicy, context: BtcPolicyActivationContext): BtcPolicyActivationData;
|
|
64
|
+
/**
|
|
65
|
+
* Whether `signature` (64-byte compact ECDSA, hex) is the Cofund leaf of
|
|
66
|
+
* `xpub` (an account xpub) signing the activation as a Bitcoin Signed Message:
|
|
67
|
+
* the app's verifier (`verifyCofundLeafMessage`,
|
|
68
|
+
* `apps/cofund/src/lib/bitcoin/signed-message.ts:47-83`). `btcMessageSigner`
|
|
69
|
+
* (`./keys`) and a Ledger's Bitcoin app sign it.
|
|
70
|
+
*
|
|
71
|
+
* One approval, checked. Whose approvals an activation takes, and how many,
|
|
72
|
+
* is the consumer's rule: the app takes its cofund's admins, one while it is
|
|
73
|
+
* onboarding and two after, every admin for a threshold-1 policy
|
|
74
|
+
* (`lib/policies/required-activation-signatures.ts`).
|
|
75
|
+
*/
|
|
76
|
+
export declare function verifyActivationSignature(data: BtcPolicyActivationData, signature: string, xpub: string): boolean;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
2
|
+
import { sha256 } from "@noble/hashes/sha2";
|
|
3
|
+
import { bytesToHex, hexToBytes, utf8ToBytes } from "@noble/hashes/utils";
|
|
4
|
+
import { compareCodepoint } from "../btc/vault/canonical-order.js";
|
|
5
|
+
import { COFUND_LEAF_INDEX, deriveChildFromXpub } from "../btc/vault/xpub.js";
|
|
6
|
+
const DOMAIN = "cofund-btc-policy-activation-v1";
|
|
7
|
+
/**
|
|
8
|
+
* Length-prefixed encoding, then sha256. Every field is written as a 4-byte
|
|
9
|
+
* big-endian length followed by its UTF-8 bytes, so no choice of field values
|
|
10
|
+
* can produce the same byte string as a different choice — plain concatenation
|
|
11
|
+
* would let a title ending in a recipient address impersonate one.
|
|
12
|
+
*/
|
|
13
|
+
export function btcPolicyActivationDigest(data) {
|
|
14
|
+
const fields = [
|
|
15
|
+
DOMAIN,
|
|
16
|
+
data.clientId,
|
|
17
|
+
data.policyId,
|
|
18
|
+
data.authId,
|
|
19
|
+
data.policyType,
|
|
20
|
+
data.policyTitle,
|
|
21
|
+
String(data.policySignerKeys.length),
|
|
22
|
+
...data.policySignerKeys,
|
|
23
|
+
String(data.policyThreshold),
|
|
24
|
+
data.txWrapper,
|
|
25
|
+
data.txFunc,
|
|
26
|
+
data.isTransaction ? "1" : "0",
|
|
27
|
+
data.maxAmount,
|
|
28
|
+
data.tokenIdentifier,
|
|
29
|
+
String(data.recipients.length),
|
|
30
|
+
...data.recipients,
|
|
31
|
+
];
|
|
32
|
+
const encoder = new TextEncoder();
|
|
33
|
+
const parts = fields.map((field) => {
|
|
34
|
+
const body = encoder.encode(field);
|
|
35
|
+
const out = new Uint8Array(4 + body.length);
|
|
36
|
+
new DataView(out.buffer).setUint32(0, body.length, false);
|
|
37
|
+
out.set(body, 4);
|
|
38
|
+
return out;
|
|
39
|
+
});
|
|
40
|
+
const total = parts.reduce((n, part) => n + part.length, 0);
|
|
41
|
+
const buf = new Uint8Array(total);
|
|
42
|
+
let offset = 0;
|
|
43
|
+
for (const part of parts) {
|
|
44
|
+
buf.set(part, offset);
|
|
45
|
+
offset += part.length;
|
|
46
|
+
}
|
|
47
|
+
return sha256(buf);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The string an approving admin signs: the payload digest, hex-encoded.
|
|
51
|
+
*
|
|
52
|
+
* Wrapped in the Bitcoin Signed Message envelope below rather than signed raw,
|
|
53
|
+
* because that is the only thing a Ledger's Bitcoin app will sign
|
|
54
|
+
* (`SignerBtc.signMessage`). Keeping the format signer-agnostic from the start
|
|
55
|
+
* means a device branch can be added without re-collecting approvals.
|
|
56
|
+
*/
|
|
57
|
+
export function btcPolicyActivationMessage(data) {
|
|
58
|
+
return bytesToHex(btcPolicyActivationDigest(data));
|
|
59
|
+
}
|
|
60
|
+
function varint(n) {
|
|
61
|
+
if (n < 0xfd) {
|
|
62
|
+
return Uint8Array.of(n);
|
|
63
|
+
}
|
|
64
|
+
if (n <= 0xff_ff) {
|
|
65
|
+
const out = new Uint8Array(3);
|
|
66
|
+
out[0] = 0xfd;
|
|
67
|
+
new DataView(out.buffer).setUint16(1, n, true);
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
const out = new Uint8Array(5);
|
|
71
|
+
out[0] = 0xfe;
|
|
72
|
+
new DataView(out.buffer).setUint32(1, n, true);
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
const BITCOIN_SIGNED_MESSAGE_PREFIX = new TextEncoder().encode("\u0018Bitcoin Signed Message:\n");
|
|
76
|
+
/** BIP-137 message hash: sha256d(prefix || varint(len) || message). */
|
|
77
|
+
export function bitcoinSignedMessageHash(message) {
|
|
78
|
+
const bytes = new TextEncoder().encode(message);
|
|
79
|
+
const len = varint(bytes.length);
|
|
80
|
+
const buf = new Uint8Array(BITCOIN_SIGNED_MESSAGE_PREFIX.length + len.length + bytes.length);
|
|
81
|
+
buf.set(BITCOIN_SIGNED_MESSAGE_PREFIX, 0);
|
|
82
|
+
buf.set(len, BITCOIN_SIGNED_MESSAGE_PREFIX.length);
|
|
83
|
+
buf.set(bytes, BITCOIN_SIGNED_MESSAGE_PREFIX.length + len.length);
|
|
84
|
+
return sha256(sha256(buf));
|
|
85
|
+
}
|
|
86
|
+
/** BIP-137 message hash of the activation message: what the leaf key signs. */
|
|
87
|
+
export function btcPolicyActivationSigningHash(data) {
|
|
88
|
+
return bitcoinSignedMessageHash(btcPolicyActivationMessage(data));
|
|
89
|
+
}
|
|
90
|
+
const HEX_PREFIX = /^0x/;
|
|
91
|
+
/** The Cofund leaf's compressed key, `m/86'/coin'/0'/0/376864517` below an account xpub. */
|
|
92
|
+
const cofundLeafKey = (xpub) => deriveChildFromXpub(xpub, 0, COFUND_LEAF_INDEX).compressedPubKey;
|
|
93
|
+
/**
|
|
94
|
+
* The activation of `policy`, as the app builds it for a Bitcoin transfer
|
|
95
|
+
* policy (`deriveBtcPolicyActivationData`,
|
|
96
|
+
* `apps/cofund/src/lib/bitcoin/policy-activation-data.ts:28-78`): its id as
|
|
97
|
+
* the `authId`, each signer's Cofund leaf key and the recipients in codepoint
|
|
98
|
+
* order, the cap, the token `BTC`, and no wrapper.
|
|
99
|
+
*/
|
|
100
|
+
export function activationDataFor(policy, context) {
|
|
101
|
+
return {
|
|
102
|
+
clientId: bytesToHex(sha256(utf8ToBytes(context.company))),
|
|
103
|
+
policyId: policy.id,
|
|
104
|
+
authId: policy.id,
|
|
105
|
+
policyType: policy.type,
|
|
106
|
+
policyTitle: policy.name,
|
|
107
|
+
policySignerKeys: policy.signers
|
|
108
|
+
.map((xpub) => bytesToHex(cofundLeafKey(xpub)))
|
|
109
|
+
.sort(compareCodepoint),
|
|
110
|
+
policyThreshold: policy.threshold,
|
|
111
|
+
txWrapper: "",
|
|
112
|
+
txFunc: "",
|
|
113
|
+
isTransaction: false,
|
|
114
|
+
maxAmount: policy.maxAmount,
|
|
115
|
+
tokenIdentifier: "BTC",
|
|
116
|
+
recipients: [...(policy.recipients ?? [])].sort(compareCodepoint),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Whether `signature` (64-byte compact ECDSA, hex) is the Cofund leaf of
|
|
121
|
+
* `xpub` (an account xpub) signing the activation as a Bitcoin Signed Message:
|
|
122
|
+
* the app's verifier (`verifyCofundLeafMessage`,
|
|
123
|
+
* `apps/cofund/src/lib/bitcoin/signed-message.ts:47-83`). `btcMessageSigner`
|
|
124
|
+
* (`./keys`) and a Ledger's Bitcoin app sign it.
|
|
125
|
+
*
|
|
126
|
+
* One approval, checked. Whose approvals an activation takes, and how many,
|
|
127
|
+
* is the consumer's rule: the app takes its cofund's admins, one while it is
|
|
128
|
+
* onboarding and two after, every admin for a threshold-1 policy
|
|
129
|
+
* (`lib/policies/required-activation-signatures.ts`).
|
|
130
|
+
*/
|
|
131
|
+
export function verifyActivationSignature(data, signature, xpub) {
|
|
132
|
+
let sig;
|
|
133
|
+
let key;
|
|
134
|
+
try {
|
|
135
|
+
sig = hexToBytes(signature.replace(HEX_PREFIX, ""));
|
|
136
|
+
key = cofundLeafKey(xpub);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
if (sig.length !== 64) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
// The BIP-137 hash is final: no second hash.
|
|
146
|
+
return secp256k1.verify(sig, btcPolicyActivationSigningHash(data), key, {
|
|
147
|
+
prehash: false,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@getcofund/protocol/btc-policies`: Bitcoin spending policies, the soft
|
|
3
|
+
* rules a spend is checked against before it is signed, and what an admin
|
|
4
|
+
* signs to activate one. No script enforces them.
|
|
5
|
+
*/
|
|
6
|
+
export * from "./activation.ts";
|
|
7
|
+
export * from "./policy.ts";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@getcofund/protocol/btc-policies`: Bitcoin spending policies, the soft
|
|
3
|
+
* rules a spend is checked against before it is signed, and what an admin
|
|
4
|
+
* signs to activate one. No script enforces them.
|
|
5
|
+
*/
|
|
6
|
+
// biome-ignore lint/performance/noBarrelFile: the package's `./btc-policies` entry
|
|
7
|
+
export * from "./activation.js";
|
|
8
|
+
export * from "./policy.js";
|