@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,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical participant ordering for account-based BTC vaults.
|
|
3
|
+
*
|
|
4
|
+
* A vault's taproot address is not stored anywhere — `Vault.contractId` holds
|
|
5
|
+
* only the roster (xpubs + roles), and every read path re-derives the address
|
|
6
|
+
* from it. Participant order decides the order of pushed keys inside the
|
|
7
|
+
* `CHECKSIGADD` tapscript, and for ADMINS_REQUIRED it also decides which admin
|
|
8
|
+
* owns which derivation branch (`2 * leafIdx`). So the comparator used to order
|
|
9
|
+
* the roster is part of the address: change it and a live vault silently
|
|
10
|
+
* repoints at an address nobody funded.
|
|
11
|
+
*
|
|
12
|
+
* That makes `String.prototype.localeCompare` unusable here, which is what this
|
|
13
|
+
* module replaces. With no explicit locale it resolves the runtime's default
|
|
14
|
+
* collation — the browser's UI language on the client, `LANG`/`LC_ALL` (or ICU's
|
|
15
|
+
* `en-US` fallback) on the server. Base58 is mixed-case alphanumeric, and
|
|
16
|
+
* several CLDR tailorings reorder plain ASCII:
|
|
17
|
+
*
|
|
18
|
+
* - `cs` treats `ch` as one letter sorting after `h`, so `ch…` > `d…`
|
|
19
|
+
* - `da` / `nb` fold `aa` to `å`, which sorts after `z`
|
|
20
|
+
* - `et` reorders `z` before `t`…`w`; `lt` reorders `y`; `hu` has digraphs
|
|
21
|
+
*
|
|
22
|
+
* Measured against a corpus of realistic `tpub…` strings, `lt` disagrees with
|
|
23
|
+
* `en` on ~3.5% of pairs and `et` on ~1.5%. A vault created by the server and
|
|
24
|
+
* displayed to a Czech-locale browser therefore rendered two different valid
|
|
25
|
+
* bech32m addresses for one `contractId`.
|
|
26
|
+
*
|
|
27
|
+
* Both orderings below are pure, total and frozen. Neither consults ICU, so
|
|
28
|
+
* they return the same result in every runtime, browser locale and ICU version.
|
|
29
|
+
*/
|
|
30
|
+
import { compareCodepoint } from "./canonical-order.js";
|
|
31
|
+
/**
|
|
32
|
+
* Ordering rule a vault's address was derived under.
|
|
33
|
+
*
|
|
34
|
+
* Persisted in the `contractId` so a vault always re-derives under the rule it
|
|
35
|
+
* was created with. An absent field means {@link ORDERING_ICU_EN_V1} — every
|
|
36
|
+
* vault created before this field existed was ordered by `localeCompare`.
|
|
37
|
+
*/
|
|
38
|
+
export const ORDERING_ICU_EN_V1 = "icu-en-v1";
|
|
39
|
+
/** Ordering rule for vaults created from now on. */
|
|
40
|
+
export const ORDERING_XPUB_CODEPOINT_V2 = "role-then-xpub-codepoint-v2";
|
|
41
|
+
/** The rule new vaults are created under. */
|
|
42
|
+
export const CURRENT_PARTICIPANT_ORDERING = ORDERING_XPUB_CODEPOINT_V2;
|
|
43
|
+
/**
|
|
44
|
+
* Read the ordering rule off a deserialized contract.
|
|
45
|
+
*
|
|
46
|
+
* Fails OPEN to v1 rather than throwing: an unrecognised value can only come
|
|
47
|
+
* from a contract written by a newer revision, and v1 is what every vault
|
|
48
|
+
* predating the field used. Callers that must not guess should compare the
|
|
49
|
+
* derived address against a known-good one instead.
|
|
50
|
+
*/
|
|
51
|
+
export function participantOrderingOf(value) {
|
|
52
|
+
return value === ORDERING_XPUB_CODEPOINT_V2
|
|
53
|
+
? ORDERING_XPUB_CODEPOINT_V2
|
|
54
|
+
: ORDERING_ICU_EN_V1;
|
|
55
|
+
}
|
|
56
|
+
// v2 orders by code point — `compareCodepoint` from `./canonical-order.ts`,
|
|
57
|
+
// the same comparator every other consensus-critical sort in the app uses.
|
|
58
|
+
// Base58 is ASCII, so code-unit order is byte order: digits, then `A`–`Z`, then
|
|
59
|
+
// `a`–`z`. Preferred over comparing the decoded 78-byte extended key because it
|
|
60
|
+
// cannot fail — a malformed xpub still orders deterministically instead of
|
|
61
|
+
// throwing inside a read path that renders a page.
|
|
62
|
+
// ── v1: frozen replica of the ICU en collation ───────────────────────────
|
|
63
|
+
/**
|
|
64
|
+
* Primary collation weight of one Base58 character.
|
|
65
|
+
*
|
|
66
|
+
* CLDR root sorts digits before letters and gives a letter one weight
|
|
67
|
+
* regardless of case — case is a tertiary difference, applied only after the
|
|
68
|
+
* whole primary sequence ties. That two-level structure is why a per-character
|
|
69
|
+
* rank table cannot stand in for this: under a flat table a case difference at
|
|
70
|
+
* position 0 would outrank a letter difference at position 1, and ICU orders
|
|
71
|
+
* `aC` after `Ab` precisely because it does not.
|
|
72
|
+
*/
|
|
73
|
+
function primaryWeight(code) {
|
|
74
|
+
// "1"–"9" → 1–9. Base58 has no "0".
|
|
75
|
+
if (code >= 0x31 && code <= 0x39) {
|
|
76
|
+
return code - 0x30;
|
|
77
|
+
}
|
|
78
|
+
// "A"–"Z" → 100–125, case-folded onto the same weights as "a"–"z".
|
|
79
|
+
if (code >= 0x41 && code <= 0x5a) {
|
|
80
|
+
return 100 + (code - 0x41);
|
|
81
|
+
}
|
|
82
|
+
// "a"–"z" → 100–125.
|
|
83
|
+
if (code >= 0x61 && code <= 0x7a) {
|
|
84
|
+
return 100 + (code - 0x61);
|
|
85
|
+
}
|
|
86
|
+
// Anything else (never present in Base58) keeps a stable relative position
|
|
87
|
+
// by its own code point, above every letter.
|
|
88
|
+
return 1000 + code;
|
|
89
|
+
}
|
|
90
|
+
function isUpperAscii(code) {
|
|
91
|
+
return code >= 0x41 && code <= 0x5a;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The ordering that `localeCompare()` produced for Base58 strings on an
|
|
95
|
+
* `en`-family runtime — the rule every pre-v2 vault address was built under.
|
|
96
|
+
*
|
|
97
|
+
* Exists ONLY so those vaults keep re-deriving the address their UTXOs are
|
|
98
|
+
* locked to. Do not use it for anything new; new vaults use
|
|
99
|
+
* {@link compareCodepoint} via {@link ORDERING_XPUB_CODEPOINT_V2}.
|
|
100
|
+
*
|
|
101
|
+
* Verified equal to `Intl.Collator` for `en-US`, `en-GB`, `de-DE`, `fr-FR`,
|
|
102
|
+
* `ja-JP`, `zh-CN`, `ar-EG`, `he-IL`, `ru-RU`, `el-GR` and `hi-IN` across
|
|
103
|
+
* exhaustive two-character Base58 pairs and randomised full-length `tpub`
|
|
104
|
+
* corpora — see `participant-order.test.ts`. Those locales all inherit root
|
|
105
|
+
* collation for ASCII; the tailorings that diverge (`cs`, `da`, `nb`, `et`,
|
|
106
|
+
* `lt`, `hu`) are the bug this replaces, not behaviour to reproduce.
|
|
107
|
+
*/
|
|
108
|
+
export function compareBase58IcuEnV1(a, b) {
|
|
109
|
+
const shared = Math.min(a.length, b.length);
|
|
110
|
+
// Primary pass: letter and digit identity across the whole string.
|
|
111
|
+
for (let i = 0; i < shared; i++) {
|
|
112
|
+
const diff = primaryWeight(a.charCodeAt(i)) - primaryWeight(b.charCodeAt(i));
|
|
113
|
+
if (diff !== 0) {
|
|
114
|
+
return diff < 0 ? -1 : 1;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (a.length !== b.length) {
|
|
118
|
+
return a.length < b.length ? -1 : 1;
|
|
119
|
+
}
|
|
120
|
+
// Tertiary pass: case, lowercase before uppercase.
|
|
121
|
+
for (let i = 0; i < shared; i++) {
|
|
122
|
+
const aUpper = isUpperAscii(a.charCodeAt(i));
|
|
123
|
+
const bUpper = isUpperAscii(b.charCodeAt(i));
|
|
124
|
+
if (aUpper !== bUpper) {
|
|
125
|
+
return aUpper ? 1 : -1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
/** The xpub comparator for a given ordering rule. */
|
|
131
|
+
export function xpubComparatorFor(ordering) {
|
|
132
|
+
return ordering === ORDERING_XPUB_CODEPOINT_V2
|
|
133
|
+
? compareCodepoint
|
|
134
|
+
: compareBase58IcuEnV1;
|
|
135
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ScriptType } from "@scure/btc-signer";
|
|
2
|
+
/**
|
|
3
|
+
* The k-of-n CHECKSIG/CHECKSIGADD ladder, as an op list.
|
|
4
|
+
*
|
|
5
|
+
* This is the single implementation used by both the direct k-of-n builder and
|
|
6
|
+
* the ADMINS_REQUIRED builder in `build.ts`.
|
|
7
|
+
*
|
|
8
|
+
* Keys must already be sorted: ordering is the caller's policy decision, and it
|
|
9
|
+
* is mirrored into `Leaf.signers`, which is what the witness assembler walks.
|
|
10
|
+
*/
|
|
11
|
+
export declare function kOfNOps(xonlys: Uint8Array[], k: number): ScriptType;
|
|
12
|
+
/** Serialise the k-of-n ladder to tapscript bytes. */
|
|
13
|
+
export declare function buildKofNScript(xonlys: Uint8Array[], k: number): Uint8Array;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Script } from "@scure/btc-signer";
|
|
2
|
+
import { BTC_VAULT_MAX_PARTICIPANTS } from "./limits.js";
|
|
3
|
+
/**
|
|
4
|
+
* The k-of-n CHECKSIG/CHECKSIGADD ladder, as an op list.
|
|
5
|
+
*
|
|
6
|
+
* This is the single implementation used by both the direct k-of-n builder and
|
|
7
|
+
* the ADMINS_REQUIRED builder in `build.ts`.
|
|
8
|
+
*
|
|
9
|
+
* Keys must already be sorted: ordering is the caller's policy decision, and it
|
|
10
|
+
* is mirrored into `Leaf.signers`, which is what the witness assembler walks.
|
|
11
|
+
*/
|
|
12
|
+
export function kOfNOps(xonlys, k) {
|
|
13
|
+
// Script.encode truncates fractional numbers in the OP_1..OP_16 range.
|
|
14
|
+
if (!Number.isInteger(k) || k < 1 || k > xonlys.length) {
|
|
15
|
+
throw new Error(`invalid threshold ${k}: must be an integer in 1..${xonlys.length}`);
|
|
16
|
+
}
|
|
17
|
+
// Cofund caps every vault at fourteen participants so newly created account
|
|
18
|
+
// vaults remain within Ledger's fifteen-key policy after the NUMS internal key.
|
|
19
|
+
if (xonlys.length > BTC_VAULT_MAX_PARTICIPANTS) {
|
|
20
|
+
throw new Error(`too many keys: ${xonlys.length} exceeds the vault participant limit of ${BTC_VAULT_MAX_PARTICIPANTS}`);
|
|
21
|
+
}
|
|
22
|
+
const ops = [];
|
|
23
|
+
xonlys.forEach((pk, i) => {
|
|
24
|
+
// BIP-342 assigns non-32-byte pushes unknown public-key semantics, so reject
|
|
25
|
+
// them before they can become an unintended successful CHECKSIGADD.
|
|
26
|
+
if (pk.length !== 32) {
|
|
27
|
+
throw new Error(`x-only key #${i} must be 32 bytes, got ${pk.length}`);
|
|
28
|
+
}
|
|
29
|
+
ops.push(pk);
|
|
30
|
+
ops.push(i === 0 ? "CHECKSIG" : "CHECKSIGADD");
|
|
31
|
+
});
|
|
32
|
+
ops.push(k);
|
|
33
|
+
ops.push("NUMEQUAL");
|
|
34
|
+
return ops;
|
|
35
|
+
}
|
|
36
|
+
/** Serialise the k-of-n ladder to tapscript bytes. */
|
|
37
|
+
export function buildKofNScript(xonlys, k) {
|
|
38
|
+
return Script.encode(kOfNOps(xonlys, k));
|
|
39
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as btc from "@scure/btc-signer";
|
|
2
|
+
import type { Leaf } from "./types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* The unsigned spend of a vault, built once for everyone who signs or
|
|
5
|
+
* verifies it: the app's signing session (browser and server), the Ledger
|
|
6
|
+
* PSBT and the SDK's signer. A BIP-341 signature commits to every input's
|
|
7
|
+
* outpoint, amount, script and sequence, to the outputs and to the version and
|
|
8
|
+
* lock time, so these parties agree byte for byte or no signature verifies.
|
|
9
|
+
*/
|
|
10
|
+
/** Opt-in RBF on every input. */
|
|
11
|
+
export declare const VAULT_SPEND_SEQUENCE = 4294967293;
|
|
12
|
+
export declare const VAULT_SPEND_VERSION = 2;
|
|
13
|
+
export declare const VAULT_SPEND_LOCKTIME = 0;
|
|
14
|
+
/** A vault UTXO the spend consumes: its id names it in the signing protocol. */
|
|
15
|
+
export type VaultSpendInput = {
|
|
16
|
+
id: string;
|
|
17
|
+
txid: string;
|
|
18
|
+
vout: number;
|
|
19
|
+
/** Sats. */
|
|
20
|
+
amount: number | bigint;
|
|
21
|
+
};
|
|
22
|
+
export type VaultSpendOutput = {
|
|
23
|
+
script: Uint8Array;
|
|
24
|
+
amount: bigint;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The inputs in the order every party signs: by utxo id, code point order.
|
|
28
|
+
* The input index is part of what each signature commits to.
|
|
29
|
+
*/
|
|
30
|
+
export declare function vaultSpendInputOrder<T extends {
|
|
31
|
+
id: string;
|
|
32
|
+
}>(inputs: readonly T[]): T[];
|
|
33
|
+
/**
|
|
34
|
+
* `outputs` then the change back to the vault: the inputs less the outputs
|
|
35
|
+
* less `fee`, when that is positive. Throws when the inputs cannot pay.
|
|
36
|
+
*/
|
|
37
|
+
export declare function withVaultChange(inputs: readonly VaultSpendInput[], outputs: readonly VaultSpendOutput[], vaultScript: Uint8Array, fee: number | bigint): VaultSpendOutput[];
|
|
38
|
+
/**
|
|
39
|
+
* The unsigned transaction spending vault UTXOs: inputs in
|
|
40
|
+
* {@link vaultSpendInputOrder}, each with sequence
|
|
41
|
+
* {@link VAULT_SPEND_SEQUENCE} and its witness UTXO (the vault's script and
|
|
42
|
+
* the input's amount), then `outputs` in the order given; version
|
|
43
|
+
* {@link VAULT_SPEND_VERSION}, lock time {@link VAULT_SPEND_LOCKTIME}. With
|
|
44
|
+
* `leaves`, each input also lists them as its tapleaf scripts (a PSBT's
|
|
45
|
+
* `tapLeafScript`), which the finalizer and a hardware signer read.
|
|
46
|
+
*
|
|
47
|
+
* Returns the inputs in transaction order, so input `i` is `inputs[i]`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildVaultSpend<Input extends VaultSpendInput>({ vaultScript, inputs, outputs, leaves, }: {
|
|
50
|
+
vaultScript: Uint8Array;
|
|
51
|
+
inputs: readonly Input[];
|
|
52
|
+
outputs: readonly VaultSpendOutput[];
|
|
53
|
+
leaves?: readonly Leaf[];
|
|
54
|
+
}): {
|
|
55
|
+
tx: btc.Transaction;
|
|
56
|
+
inputs: Input[];
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The BIP-341 script-path sighash (SIGHASH_DEFAULT) a signer of `leafScript`
|
|
60
|
+
* signs for input `index` of a {@link buildVaultSpend} transaction, every
|
|
61
|
+
* input's prevout read from its witness UTXO.
|
|
62
|
+
*/
|
|
63
|
+
export declare function vaultSpendSighash(tx: btc.Transaction, index: number, leafScript: Uint8Array): Uint8Array;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as btc from "@scure/btc-signer";
|
|
2
|
+
import { TAP_LEAF_VERSION } from "@scure/btc-signer/payment.js";
|
|
3
|
+
import { compareCodepoint } from "./canonical-order.js";
|
|
4
|
+
/**
|
|
5
|
+
* The unsigned spend of a vault, built once for everyone who signs or
|
|
6
|
+
* verifies it: the app's signing session (browser and server), the Ledger
|
|
7
|
+
* PSBT and the SDK's signer. A BIP-341 signature commits to every input's
|
|
8
|
+
* outpoint, amount, script and sequence, to the outputs and to the version and
|
|
9
|
+
* lock time, so these parties agree byte for byte or no signature verifies.
|
|
10
|
+
*/
|
|
11
|
+
/** Opt-in RBF on every input. */
|
|
12
|
+
export const VAULT_SPEND_SEQUENCE = 0xff_ff_ff_fd;
|
|
13
|
+
export const VAULT_SPEND_VERSION = 2;
|
|
14
|
+
export const VAULT_SPEND_LOCKTIME = 0;
|
|
15
|
+
/**
|
|
16
|
+
* The inputs in the order every party signs: by utxo id, code point order.
|
|
17
|
+
* The input index is part of what each signature commits to.
|
|
18
|
+
*/
|
|
19
|
+
export function vaultSpendInputOrder(inputs) {
|
|
20
|
+
return [...inputs].sort((a, b) => compareCodepoint(a.id, b.id));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* `outputs` then the change back to the vault: the inputs less the outputs
|
|
24
|
+
* less `fee`, when that is positive. Throws when the inputs cannot pay.
|
|
25
|
+
*/
|
|
26
|
+
export function withVaultChange(inputs, outputs, vaultScript, fee) {
|
|
27
|
+
const total = inputs.reduce((sum, input) => sum + BigInt(input.amount), 0n);
|
|
28
|
+
const spent = outputs.reduce((sum, output) => sum + output.amount, 0n);
|
|
29
|
+
const change = total - spent - BigInt(fee);
|
|
30
|
+
if (change < 0n) {
|
|
31
|
+
throw new Error(`Insufficient funds: have ${total}, need ${spent + BigInt(fee)}`);
|
|
32
|
+
}
|
|
33
|
+
return change > 0n
|
|
34
|
+
? [...outputs, { script: vaultScript, amount: change }]
|
|
35
|
+
: [...outputs];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The unsigned transaction spending vault UTXOs: inputs in
|
|
39
|
+
* {@link vaultSpendInputOrder}, each with sequence
|
|
40
|
+
* {@link VAULT_SPEND_SEQUENCE} and its witness UTXO (the vault's script and
|
|
41
|
+
* the input's amount), then `outputs` in the order given; version
|
|
42
|
+
* {@link VAULT_SPEND_VERSION}, lock time {@link VAULT_SPEND_LOCKTIME}. With
|
|
43
|
+
* `leaves`, each input also lists them as its tapleaf scripts (a PSBT's
|
|
44
|
+
* `tapLeafScript`), which the finalizer and a hardware signer read.
|
|
45
|
+
*
|
|
46
|
+
* Returns the inputs in transaction order, so input `i` is `inputs[i]`.
|
|
47
|
+
*/
|
|
48
|
+
export function buildVaultSpend({ vaultScript, inputs, outputs, leaves, }) {
|
|
49
|
+
const ordered = vaultSpendInputOrder(inputs);
|
|
50
|
+
const tx = new btc.Transaction({
|
|
51
|
+
version: VAULT_SPEND_VERSION,
|
|
52
|
+
lockTime: VAULT_SPEND_LOCKTIME,
|
|
53
|
+
});
|
|
54
|
+
for (const input of ordered) {
|
|
55
|
+
tx.addInput({
|
|
56
|
+
txid: input.txid,
|
|
57
|
+
index: input.vout,
|
|
58
|
+
sequence: VAULT_SPEND_SEQUENCE,
|
|
59
|
+
witnessUtxo: { script: vaultScript, amount: BigInt(input.amount) },
|
|
60
|
+
...(leaves && {
|
|
61
|
+
tapLeafScript: leaves.map((leaf) => [
|
|
62
|
+
{
|
|
63
|
+
// A control block's first byte is the leaf version plus the
|
|
64
|
+
// output key's parity, which the struct keeps apart.
|
|
65
|
+
version: leaf.controlBlockStruct.version + leaf.controlBlockStruct.parity,
|
|
66
|
+
internalKey: leaf.controlBlockStruct.internalKey,
|
|
67
|
+
merklePath: leaf.controlBlockStruct.merklePath,
|
|
68
|
+
},
|
|
69
|
+
new Uint8Array([...leaf.script, TAP_LEAF_VERSION]),
|
|
70
|
+
]),
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
for (const output of outputs) {
|
|
75
|
+
tx.addOutput({ script: output.script, amount: output.amount });
|
|
76
|
+
}
|
|
77
|
+
return { tx, inputs: ordered };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The BIP-341 script-path sighash (SIGHASH_DEFAULT) a signer of `leafScript`
|
|
81
|
+
* signs for input `index` of a {@link buildVaultSpend} transaction, every
|
|
82
|
+
* input's prevout read from its witness UTXO.
|
|
83
|
+
*/
|
|
84
|
+
export function vaultSpendSighash(tx, index, leafScript) {
|
|
85
|
+
const prevouts = Array.from({ length: tx.inputsLength }, (_, i) => {
|
|
86
|
+
const { witnessUtxo } = tx.getInput(i);
|
|
87
|
+
if (!witnessUtxo) {
|
|
88
|
+
throw new Error(`Input ${i} of the spend has no witness UTXO`);
|
|
89
|
+
}
|
|
90
|
+
return witnessUtxo;
|
|
91
|
+
});
|
|
92
|
+
return tx.preimageWitnessV1(index, prevouts.map(({ script }) => script), btc.SigHash.DEFAULT, prevouts.map(({ amount }) => amount), undefined, leafScript, TAP_LEAF_VERSION);
|
|
93
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accurate BTC virtual-size (vbytes) estimation for cofund vault spends.
|
|
3
|
+
*
|
|
4
|
+
* The old flat `INPUT_VBYTES = 78` under-counted the real taproot SCRIPT-PATH
|
|
5
|
+
* witness (a 2-of-2 spend is ~108 vB/input: 2 schnorr sigs + the leaf script +
|
|
6
|
+
* the control block), so at 1 sat/vB (regtest/devnet) the estimated fee fell
|
|
7
|
+
* below the node's min-relay floor and broadcasts were rejected. These helpers
|
|
8
|
+
* size each input from the vault's REAL tapscript leaves instead.
|
|
9
|
+
*
|
|
10
|
+
* The sizing is exact for the witness, not an upper bound: a k-of-n leaf is
|
|
11
|
+
* sized for its k signatures and (n − k) empty slots, which is the only shape
|
|
12
|
+
* that can spend it. What headroom remains is deliberate and lives elsewhere —
|
|
13
|
+
* the app's `FEE_SAFETY_MULTIPLIER`, and its choice to price every output as
|
|
14
|
+
* P2TR even when a recipient is a cheaper type.
|
|
15
|
+
*/
|
|
16
|
+
export declare const TX_OVERHEAD_VBYTES = 11;
|
|
17
|
+
/** vbytes of one output: 8 (value) + 1 (script-length prefix) + script bytes. */
|
|
18
|
+
export declare function outputVbytes(scriptLen: number): number;
|
|
19
|
+
type SizingLeaf = {
|
|
20
|
+
/** Signers in script order. Each occupies one witness slot, signed or not. */
|
|
21
|
+
signers: unknown[];
|
|
22
|
+
/** How many of those slots carry a real signature — the leaf's k. */
|
|
23
|
+
threshold: number;
|
|
24
|
+
script: Uint8Array;
|
|
25
|
+
controlBlockStruct: {
|
|
26
|
+
merklePath: Uint8Array[];
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* vsize of one taproot SCRIPT-PATH input spending `leaf`.
|
|
31
|
+
*
|
|
32
|
+
* The witness always has exactly one slot per signer, of which exactly
|
|
33
|
+
* `threshold` carry a 64-byte schnorr signature and the rest are EMPTY pushes
|
|
34
|
+
* costing a single length byte:
|
|
35
|
+
*
|
|
36
|
+
* [ k × 64-byte sig | (n − k) × empty | leaf script | control block ]
|
|
37
|
+
*
|
|
38
|
+
* Sizing for k rather than n is not optimistic — k is the only spendable shape.
|
|
39
|
+
* `orderedSigStackForLeaf` caps non-empty slots at the threshold and blanks any
|
|
40
|
+
* extra valid signature, and the leaf script ends in `OP_NUMEQUAL k`, which
|
|
41
|
+
* rejects a sum above k outright. Verified against the on-chain witness of tx
|
|
42
|
+
* `d512fd27…fc8c` (2-of-5, 172-byte script, depth 0): this predicts 341 witness
|
|
43
|
+
* bytes and the transaction carries exactly 341.
|
|
44
|
+
*
|
|
45
|
+
* `threshold` is clamped into [0, n], so a malformed value can only ever make
|
|
46
|
+
* this over-count — never under-count into a min-relay rejection.
|
|
47
|
+
*
|
|
48
|
+
* For a full-threshold leaf (k === n, e.g. 2-of-2) there are no empty slots and
|
|
49
|
+
* this is identical to the old all-signers sizing. It is only k < n vaults that
|
|
50
|
+
* change, where the old estimate carried (n − k) × 16 vB of phantom size per
|
|
51
|
+
* input — 48 vB/input on a 2-of-5.
|
|
52
|
+
*
|
|
53
|
+
* Control block = 1 (parity+version) + 32 (internal key) + 32 × merkle depth.
|
|
54
|
+
*/
|
|
55
|
+
export declare function leafInputVbytes(leaf: SizingLeaf): number;
|
|
56
|
+
/**
|
|
57
|
+
* Total tx vsize: overhead + inputs (all spending the same vault) + outputs.
|
|
58
|
+
* `outputScriptLens` lets callers size mixed output types exactly; the common
|
|
59
|
+
* cofund output is P2TR (34-byte script → 43 vB).
|
|
60
|
+
*/
|
|
61
|
+
export declare function vaultTxVbytes({ inputVbytes, nIn, outputScriptLens, }: {
|
|
62
|
+
inputVbytes: number;
|
|
63
|
+
nIn: number;
|
|
64
|
+
outputScriptLens: number[];
|
|
65
|
+
}): number;
|
|
66
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accurate BTC virtual-size (vbytes) estimation for cofund vault spends.
|
|
3
|
+
*
|
|
4
|
+
* The old flat `INPUT_VBYTES = 78` under-counted the real taproot SCRIPT-PATH
|
|
5
|
+
* witness (a 2-of-2 spend is ~108 vB/input: 2 schnorr sigs + the leaf script +
|
|
6
|
+
* the control block), so at 1 sat/vB (regtest/devnet) the estimated fee fell
|
|
7
|
+
* below the node's min-relay floor and broadcasts were rejected. These helpers
|
|
8
|
+
* size each input from the vault's REAL tapscript leaves instead.
|
|
9
|
+
*
|
|
10
|
+
* The sizing is exact for the witness, not an upper bound: a k-of-n leaf is
|
|
11
|
+
* sized for its k signatures and (n − k) empty slots, which is the only shape
|
|
12
|
+
* that can spend it. What headroom remains is deliberate and lives elsewhere —
|
|
13
|
+
* the app's `FEE_SAFETY_MULTIPLIER`, and its choice to price every output as
|
|
14
|
+
* P2TR even when a recipient is a cheaper type.
|
|
15
|
+
*/
|
|
16
|
+
// A taproot input's non-witness bytes (counted ×4 in weight): 36-byte outpoint
|
|
17
|
+
// + 1-byte empty scriptSig length + 4-byte sequence.
|
|
18
|
+
const NON_WITNESS_INPUT_VBYTES = 41;
|
|
19
|
+
// version(4) + segwit marker+flag(2 weight units = 0.5 vB) + in/out compactSize
|
|
20
|
+
// (1 each) + locktime(4), rounded up — exact for the small input/output counts
|
|
21
|
+
// cofund produces (well under the 253 threshold where compactSize grows).
|
|
22
|
+
export const TX_OVERHEAD_VBYTES = 11;
|
|
23
|
+
/** vbytes of one output: 8 (value) + 1 (script-length prefix) + script bytes. */
|
|
24
|
+
export function outputVbytes(scriptLen) {
|
|
25
|
+
return 8 + 1 + scriptLen;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* vsize of one taproot SCRIPT-PATH input spending `leaf`.
|
|
29
|
+
*
|
|
30
|
+
* The witness always has exactly one slot per signer, of which exactly
|
|
31
|
+
* `threshold` carry a 64-byte schnorr signature and the rest are EMPTY pushes
|
|
32
|
+
* costing a single length byte:
|
|
33
|
+
*
|
|
34
|
+
* [ k × 64-byte sig | (n − k) × empty | leaf script | control block ]
|
|
35
|
+
*
|
|
36
|
+
* Sizing for k rather than n is not optimistic — k is the only spendable shape.
|
|
37
|
+
* `orderedSigStackForLeaf` caps non-empty slots at the threshold and blanks any
|
|
38
|
+
* extra valid signature, and the leaf script ends in `OP_NUMEQUAL k`, which
|
|
39
|
+
* rejects a sum above k outright. Verified against the on-chain witness of tx
|
|
40
|
+
* `d512fd27…fc8c` (2-of-5, 172-byte script, depth 0): this predicts 341 witness
|
|
41
|
+
* bytes and the transaction carries exactly 341.
|
|
42
|
+
*
|
|
43
|
+
* `threshold` is clamped into [0, n], so a malformed value can only ever make
|
|
44
|
+
* this over-count — never under-count into a min-relay rejection.
|
|
45
|
+
*
|
|
46
|
+
* For a full-threshold leaf (k === n, e.g. 2-of-2) there are no empty slots and
|
|
47
|
+
* this is identical to the old all-signers sizing. It is only k < n vaults that
|
|
48
|
+
* change, where the old estimate carried (n − k) × 16 vB of phantom size per
|
|
49
|
+
* input — 48 vB/input on a 2-of-5.
|
|
50
|
+
*
|
|
51
|
+
* Control block = 1 (parity+version) + 32 (internal key) + 32 × merkle depth.
|
|
52
|
+
*/
|
|
53
|
+
export function leafInputVbytes(leaf) {
|
|
54
|
+
const controlBlockLen = 33 + 32 * leaf.controlBlockStruct.merklePath.length;
|
|
55
|
+
const signed = Math.max(0, Math.min(leaf.threshold, leaf.signers.length));
|
|
56
|
+
const witnessBytes = 1 + // stack item count
|
|
57
|
+
signed * (1 + 64) + // a 64-byte sig plus its length prefix
|
|
58
|
+
(leaf.signers.length - signed) + // empty push: length prefix only
|
|
59
|
+
(1 + leaf.script.length) + // leaf script
|
|
60
|
+
(1 + controlBlockLen); // control block
|
|
61
|
+
return NON_WITNESS_INPUT_VBYTES + Math.ceil(witnessBytes / 4);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Total tx vsize: overhead + inputs (all spending the same vault) + outputs.
|
|
65
|
+
* `outputScriptLens` lets callers size mixed output types exactly; the common
|
|
66
|
+
* cofund output is P2TR (34-byte script → 43 vB).
|
|
67
|
+
*/
|
|
68
|
+
export function vaultTxVbytes({ inputVbytes, nIn, outputScriptLens, }) {
|
|
69
|
+
const outputs = outputScriptLens.reduce((s, len) => s + outputVbytes(len), 0);
|
|
70
|
+
return TX_OVERHEAD_VBYTES + nIn * inputVbytes + outputs;
|
|
71
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type Role = "admin" | "member";
|
|
2
|
+
export interface User {
|
|
3
|
+
id: string;
|
|
4
|
+
role: Role;
|
|
5
|
+
xonly: Uint8Array;
|
|
6
|
+
pubCompressed: Uint8Array;
|
|
7
|
+
}
|
|
8
|
+
export type VaultType = "ALL" | "ADMINS_INCLUDED" | "ADMINS_REQUIRED";
|
|
9
|
+
export interface VaultContract {
|
|
10
|
+
vaultType: VaultType;
|
|
11
|
+
internalKeyXonly: Uint8Array;
|
|
12
|
+
}
|
|
13
|
+
export interface Leaf {
|
|
14
|
+
threshold: number;
|
|
15
|
+
signers: User[];
|
|
16
|
+
script: Uint8Array;
|
|
17
|
+
leafHash: Uint8Array;
|
|
18
|
+
controlBlockStruct: {
|
|
19
|
+
parity: number;
|
|
20
|
+
version: number;
|
|
21
|
+
internalKey: Uint8Array;
|
|
22
|
+
merklePath: Uint8Array[];
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export type BuildVaultOptions = {
|
|
26
|
+
/** Default 2 keeps pre-threshold vaults (contractId without the field)
|
|
27
|
+
* re-deriving to the same address. */
|
|
28
|
+
threshold?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Rebuild a vault that ALREADY EXISTS on chain, skipping the leaf-key
|
|
31
|
+
* uniqueness invariant.
|
|
32
|
+
*
|
|
33
|
+
* Only `buildTaprootFromContractId` may set this. A vault created before the
|
|
34
|
+
* invariant existed may hold a duplicated roster; its address must still
|
|
35
|
+
* resolve or every read path (balances, deposit address, PDFs, cron, the
|
|
36
|
+
* sweep that moves the funds to safety) breaks and the coins become
|
|
37
|
+
* unreachable. Never set it on a creation path — that is the one place the
|
|
38
|
+
* invariant is load-bearing.
|
|
39
|
+
*/
|
|
40
|
+
reconstructingExistingVault?: boolean;
|
|
41
|
+
};
|
|
42
|
+
export interface BuiltVault {
|
|
43
|
+
merkleRoot?: Uint8Array;
|
|
44
|
+
tweakedPubkey: Uint8Array;
|
|
45
|
+
scriptPubKey: Uint8Array;
|
|
46
|
+
address: string;
|
|
47
|
+
leaves: Leaf[];
|
|
48
|
+
internalPubKey: Uint8Array;
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account-Based Vault Builder
|
|
3
|
+
*
|
|
4
|
+
* Builds BTC vaults from account-level xpubs. Everything is deterministic:
|
|
5
|
+
* given xpubs + roles + vaultType + network + NUMS recipe, the vault address
|
|
6
|
+
* is reproducible.
|
|
7
|
+
*
|
|
8
|
+
* Key differences from the legacy MuSig2 builder (build.ts):
|
|
9
|
+
* - Internal key: unspendable NUMS point (script-path-only, no key-path spend)
|
|
10
|
+
* - ADMINS_REQUIRED: each admin leaf uses a unique branch namespace
|
|
11
|
+
* (leaf i → branch 2*i) so Ledger sees distinct keys per script
|
|
12
|
+
* - All keys are derived from xpubs at runtime via deriveChildFromXpub
|
|
13
|
+
*/
|
|
14
|
+
/** biome-ignore-all lint/suspicious/noBitwiseOperators: Taproot control-block bytes encode parity with bit masks. */
|
|
15
|
+
import type { BTC_NETWORK } from "@scure/btc-signer/utils.js";
|
|
16
|
+
import { type Bip341NumsRecipe } from "./bip341-nums.ts";
|
|
17
|
+
import { type ParticipantOrdering } from "./participant-order.ts";
|
|
18
|
+
import type { BuiltVault, Leaf, VaultType } from "./types.ts";
|
|
19
|
+
/** Minimal user input — everything else is derived deterministically. */
|
|
20
|
+
export type AccountVaultUser = {
|
|
21
|
+
id: string;
|
|
22
|
+
role: "admin" | "member";
|
|
23
|
+
accountXpub: string;
|
|
24
|
+
};
|
|
25
|
+
export type AccountVaultBuildOptions = {
|
|
26
|
+
nums: Bip341NumsRecipe;
|
|
27
|
+
btcNetwork?: BTC_NETWORK;
|
|
28
|
+
/**
|
|
29
|
+
* Signatures required to spend. The default preserves addresses for vaults
|
|
30
|
+
* created before the serialized contract included a threshold.
|
|
31
|
+
*/
|
|
32
|
+
threshold?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Rebuild a vault that ALREADY EXISTS on chain, skipping the leaf-key
|
|
35
|
+
* uniqueness invariant.
|
|
36
|
+
*
|
|
37
|
+
* Only `buildTaprootFromContractId` may set this. A vault created before the
|
|
38
|
+
* invariant existed may hold a duplicated roster; its address must still
|
|
39
|
+
* resolve or every read path (balances, deposit address, PDFs, cron, the
|
|
40
|
+
* sweep that moves the funds to safety) breaks and the coins become
|
|
41
|
+
* unreachable. Never set it on a creation path — that is the one place the
|
|
42
|
+
* invariant is load-bearing.
|
|
43
|
+
*/
|
|
44
|
+
reconstructingExistingVault?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Which participant-ordering rule to sort the roster under.
|
|
47
|
+
*
|
|
48
|
+
* Defaults to {@link ORDERING_ICU_EN_V1} because that is what every vault
|
|
49
|
+
* created before the rule was recorded used — so a caller that forgets to
|
|
50
|
+
* pass it reproduces a historical address rather than inventing a new one.
|
|
51
|
+
* Creation paths must pass {@link CURRENT_PARTICIPANT_ORDERING} explicitly and
|
|
52
|
+
* persist it in the contract.
|
|
53
|
+
*/
|
|
54
|
+
participantOrdering?: ParticipantOrdering;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Sort account vault users deterministically:
|
|
58
|
+
* admins first, then members. Within each group, sort by accountXpub.
|
|
59
|
+
*
|
|
60
|
+
* `ordering` is not optional and must come from the vault's own contract (see
|
|
61
|
+
* {@link participantOrderingOf}). This order IS part of the vault's address —
|
|
62
|
+
* it fixes the key order inside the tapscript, and for ADMINS_REQUIRED it fixes
|
|
63
|
+
* which admin owns which derivation branch — so sorting a roster under the
|
|
64
|
+
* wrong rule yields a different, unfunded address.
|
|
65
|
+
*/
|
|
66
|
+
export declare function sortAccountUsers(users: AccountVaultUser[], ordering: ParticipantOrdering): AccountVaultUser[];
|
|
67
|
+
/**
|
|
68
|
+
* Build an account-based vault from xpubs.
|
|
69
|
+
*
|
|
70
|
+
* Returns the standard `BuiltVault` so all downstream code
|
|
71
|
+
* (taprootUtils, getVaultDetails, finalizeTx, cron) works unchanged.
|
|
72
|
+
*
|
|
73
|
+
* For ADMINS_REQUIRED vaults, leaf `i` derives keys at branch `2*i`,
|
|
74
|
+
* producing a unique key namespace per leaf. This satisfies the Ledger
|
|
75
|
+
* restriction that the same key must not appear in multiple scripts.
|
|
76
|
+
*
|
|
77
|
+
* For NAR vaults (ALL / ADMINS_INCLUDED), a single leaf at branch 0.
|
|
78
|
+
*/
|
|
79
|
+
export declare function buildAccountBasedVault(users: AccountVaultUser[], vaultType: VaultType, network: "mainnet" | "testnet", options: AccountVaultBuildOptions): BuiltVault;
|
|
80
|
+
/**
|
|
81
|
+
* Map each built leaf back to the derivation branch its keys came from.
|
|
82
|
+
*
|
|
83
|
+
* The forward assignment lives in `buildAccountBasedVault` above: admin `i`
|
|
84
|
+
* in *sorted-admin* order owns branch `2*i`, and every signer's key inside
|
|
85
|
+
* that leaf is derived from that leaf's branch. The inverse is not the array
|
|
86
|
+
* index — `btc.taprootListToTree` returns leaves in Huffman tree-walk order,
|
|
87
|
+
* which diverges from creation order at 3 or more leaves. A leaf's branch can
|
|
88
|
+
* only be recovered from its *contents*, so this matches the leaf's admin key
|
|
89
|
+
* (script position 0) against each sorted admin's candidate key.
|
|
90
|
+
*
|
|
91
|
+
* Every consumer that needs a leaf's branch must go through here: the
|
|
92
|
+
* software signer's key derivation, and the Ledger PSBT's BIP-371
|
|
93
|
+
* tap-bip32-derivation records (which are malformed if a key is paired with a
|
|
94
|
+
* leaf that does not contain it).
|
|
95
|
+
*
|
|
96
|
+
* NAR vaults (ALL / ADMINS_INCLUDED) are a single leaf at branch 0.
|
|
97
|
+
*
|
|
98
|
+
* `ordering` must be the rule the vault was CREATED under, read off its own
|
|
99
|
+
* contract. Sorting the admins under a different rule permutes the branch
|
|
100
|
+
* assignment, so the signer derives a key that is not in the leaf it is paired
|
|
101
|
+
* with — a malformed PSBT on the Ledger path and a signature that cannot
|
|
102
|
+
* satisfy the script on the software path.
|
|
103
|
+
*/
|
|
104
|
+
export declare function buildLeafBranchMap(leaves: Leaf[], vaultType: VaultType, accountXpubs: AccountVaultUser[] | undefined, ordering: ParticipantOrdering): Map<Leaf, number>;
|
|
105
|
+
/**
|
|
106
|
+
* Compute the unspendable internal key x-only for a given network.
|
|
107
|
+
* Used by the serializer to store a backward-compatible aggregateKey.
|
|
108
|
+
*/
|
|
109
|
+
export declare function getUnspendableInternalKeyXonly(network: "mainnet" | "testnet", numsRecipe: Bip341NumsRecipe): Uint8Array;
|