|
1 | | -import { taproot } from 'bitcoinjs-lib'; |
2 | | -import { PsbtOutputUpdate } from 'bip174/src/lib/interfaces'; |
| 1 | +import * as assert from 'assert'; |
| 2 | + |
| 3 | +import { Payment, taproot } from 'bitcoinjs-lib'; |
| 4 | +import { PsbtOutput, PsbtOutputUpdate } from 'bip174/src/lib/interfaces'; |
3 | 5 | import { UtxoPsbt } from '../UtxoPsbt'; |
4 | | -import { RootWalletKeys } from './WalletKeys'; |
| 6 | +import { RootWalletKeys, DerivedWalletKeys } from './WalletKeys'; |
5 | 7 | import { ChainCode, scriptTypeForChain } from './chains'; |
| 8 | +import { getScriptIdFromPath, ScriptId } from './ScriptId'; |
6 | 9 | import { createOutputScript2of3, createPaymentP2tr, createPaymentP2trMusig2, toXOnlyPublicKey } from '../outputScripts'; |
7 | 10 |
|
8 | 11 | /** |
9 | | - * Add a verifiable wallet output to the PSBT. The output and all data |
10 | | - * needed to verify it from public keys only are added to the PSBT. |
11 | | - * Typically these are change outputs. |
| 12 | + * Get the BIP32 derivation data for a PSBT output. |
12 | 13 | * |
13 | | - * @param psbt the PSBT to add change output to |
14 | | - * @param rootWalletKeys keys that will be able to spend the output |
15 | | - * @param chain chain code to use for deriving scripts (and to determine script |
16 | | - * type) chain is an API parameter in the BitGo API, and may be |
17 | | - * any valid ChainCode |
18 | | - * @param index derivation index for the change address |
19 | | - * @param value value of the change output |
| 14 | + * @param rootWalletKeys root wallet keys used for master fingerprints |
| 15 | + * @param walletKeys derived wallet keys for the specific chain and index |
| 16 | + * @param scriptType the script type to determine whether to use regular or taproot derivation |
| 17 | + * @param payment optional payment object for taproot scripts to calculate leaf hashes |
| 18 | + * @returns Object containing BIP32 derivation data |
20 | 19 | */ |
21 | | -export function addWalletOutputToPsbt( |
22 | | - psbt: UtxoPsbt, |
| 20 | +export function getPsbtBip32DerivationOutputUpdate( |
23 | 21 | rootWalletKeys: RootWalletKeys, |
24 | | - chain: ChainCode, |
25 | | - index: number, |
26 | | - value: bigint |
27 | | -): void { |
28 | | - const walletKeys = rootWalletKeys.deriveForChainAndIndex(chain, index); |
29 | | - const scriptType = scriptTypeForChain(chain); |
| 22 | + walletKeys: DerivedWalletKeys, |
| 23 | + scriptType: string, |
| 24 | + payment?: Payment |
| 25 | +): PsbtOutputUpdate { |
| 26 | + const update: PsbtOutputUpdate = {}; |
| 27 | + |
30 | 28 | if (scriptType === 'p2tr' || scriptType === 'p2trMusig2') { |
31 | | - const payment = |
32 | | - scriptType === 'p2tr' ? createPaymentP2tr(walletKeys.publicKeys) : createPaymentP2trMusig2(walletKeys.publicKeys); |
33 | | - psbt.addOutput({ script: payment.output!, value }); |
| 29 | + if (!payment || !payment.redeems) { |
| 30 | + throw new Error('Payment object with redeems is required for taproot derivation'); |
| 31 | + } |
| 32 | + |
| 33 | + const allLeafHashes = payment.redeems.map((r) => taproot.hashTapLeaf(r.output!)); |
| 34 | + |
| 35 | + update.tapBip32Derivation = [0, 1, 2].map((idx) => { |
| 36 | + const pubkey = toXOnlyPublicKey(walletKeys.triple[idx].publicKey); |
| 37 | + const leafHashes: Buffer[] = []; |
| 38 | + |
| 39 | + assert(payment.redeems); |
| 40 | + payment.redeems.forEach((r: any, redeemIdx: number) => { |
| 41 | + if (r.pubkeys!.find((pk: Buffer) => pk.equals(pubkey))) { |
| 42 | + leafHashes.push(allLeafHashes[redeemIdx]); |
| 43 | + } |
| 44 | + }); |
| 45 | + |
| 46 | + return { |
| 47 | + leafHashes, |
| 48 | + pubkey, |
| 49 | + path: walletKeys.paths[idx], |
| 50 | + masterFingerprint: rootWalletKeys.triple[idx].fingerprint, |
| 51 | + }; |
| 52 | + }); |
34 | 53 | } else { |
35 | | - const { scriptPubKey: script } = createOutputScript2of3(walletKeys.publicKeys, scriptType); |
36 | | - psbt.addOutput({ script, value }); |
| 54 | + update.bip32Derivation = [0, 1, 2].map((idx) => ({ |
| 55 | + pubkey: walletKeys.triple[idx].publicKey, |
| 56 | + path: walletKeys.paths[idx], |
| 57 | + masterFingerprint: rootWalletKeys.triple[idx].fingerprint, |
| 58 | + })); |
37 | 59 | } |
38 | | - updateWalletOutputForPsbt(psbt, rootWalletKeys, psbt.data.outputs.length - 1, chain, index); |
| 60 | + |
| 61 | + return update; |
39 | 62 | } |
40 | 63 |
|
41 | 64 | /** |
42 | | - * Update the wallet output with the required information when necessary. If the |
43 | | - * information is there already, it will skip over it. |
| 65 | + * Get the PSBT output update object from a PSBT output and output script. |
44 | 66 | * |
45 | | - * This function assumes that the output script and value have already been set. |
46 | | - * |
47 | | - * @param psbt the PSBT to update change output at |
| 67 | + * @param output the PSBT output to get update for |
| 68 | + * @param outputScript the output script |
48 | 69 | * @param rootWalletKeys keys that will be able to spend the output |
49 | | - * @param outputIndex output index where to update the output |
50 | | - * @param chain chain code to use for deriving scripts (and to determine script |
51 | | - * type) chain is an API parameter in the BitGo API, and may be |
52 | | - * any valid ChainCode |
| 70 | + * @param chain chain code to use for deriving scripts (and to determine script type) |
53 | 71 | * @param index derivation index for the change address |
54 | | - * @param value value of the change output |
| 72 | + * @returns PsbtOutputUpdate object with the required information |
55 | 73 | */ |
56 | | -export function updateWalletOutputForPsbt( |
57 | | - psbt: UtxoPsbt, |
| 74 | +export function getPsbtOutputUpdateFromPsbtOutput( |
| 75 | + output: PsbtOutput, |
| 76 | + outputScript: Buffer, |
58 | 77 | rootWalletKeys: RootWalletKeys, |
59 | | - outputIndex: number, |
60 | 78 | chain: ChainCode, |
61 | 79 | index: number |
62 | | -): void { |
63 | | - if (psbt.data.outputs.length <= outputIndex) { |
64 | | - throw new Error( |
65 | | - `outputIndex (${outputIndex}) is too large for the number of outputs (${psbt.data.outputs.length})` |
66 | | - ); |
67 | | - } |
68 | | - |
69 | | - const outputScript = psbt.getOutputScript(outputIndex); |
70 | | - |
| 80 | +): PsbtOutputUpdate { |
71 | 81 | const walletKeys = rootWalletKeys.deriveForChainAndIndex(chain, index); |
72 | 82 | const scriptType = scriptTypeForChain(chain); |
73 | | - const output = psbt.data.outputs[outputIndex]; |
74 | 83 | const update: PsbtOutputUpdate = {}; |
| 84 | + |
75 | 85 | if (scriptType === 'p2tr' || scriptType === 'p2trMusig2') { |
76 | 86 | const payment = |
77 | 87 | scriptType === 'p2tr' ? createPaymentP2tr(walletKeys.publicKeys) : createPaymentP2trMusig2(walletKeys.publicKeys); |
78 | 88 | if (!payment.output || !payment.output.equals(outputScript)) { |
79 | 89 | throw new Error(`cannot update a p2tr output where the scripts do not match - Failing.`); |
80 | 90 | } |
81 | | - const allLeafHashes = payment.redeems!.map((r) => taproot.hashTapLeaf(r.output!)); |
82 | 91 |
|
83 | 92 | if (!output.tapTree) { |
84 | 93 | update.tapTree = payment.tapTree; |
85 | 94 | } |
86 | 95 | if (!output.tapInternalKey) { |
87 | 96 | update.tapInternalKey = payment.internalPubkey; |
88 | 97 | } |
| 98 | + |
89 | 99 | if (!output.tapBip32Derivation) { |
90 | | - update.tapBip32Derivation = [0, 1, 2].map((idx) => { |
91 | | - const pubkey = toXOnlyPublicKey(walletKeys.triple[idx].publicKey); |
92 | | - const leafHashes: Buffer[] = []; |
93 | | - payment.redeems!.forEach((r, idx) => { |
94 | | - if (r.pubkeys!.find((pk) => pk.equals(pubkey))) { |
95 | | - leafHashes.push(allLeafHashes[idx]); |
96 | | - } |
97 | | - }); |
98 | | - return { |
99 | | - leafHashes, |
100 | | - pubkey, |
101 | | - path: walletKeys.paths[idx], |
102 | | - masterFingerprint: rootWalletKeys.triple[idx].fingerprint, |
103 | | - }; |
104 | | - }); |
| 100 | + const derivationUpdate = getPsbtBip32DerivationOutputUpdate(rootWalletKeys, walletKeys, scriptType, payment); |
| 101 | + update.tapBip32Derivation = derivationUpdate.tapBip32Derivation; |
105 | 102 | } |
106 | 103 | } else { |
107 | 104 | const { scriptPubKey, witnessScript, redeemScript } = createOutputScript2of3(walletKeys.publicKeys, scriptType); |
108 | 105 | if (!scriptPubKey.equals(outputScript)) { |
109 | 106 | throw new Error(`cannot update an output where the scripts do not match - Failing.`); |
110 | 107 | } |
| 108 | + |
111 | 109 | if (!output.bip32Derivation) { |
112 | | - update.bip32Derivation = [0, 1, 2].map((idx) => ({ |
113 | | - pubkey: walletKeys.triple[idx].publicKey, |
114 | | - path: walletKeys.paths[idx], |
115 | | - masterFingerprint: rootWalletKeys.triple[idx].fingerprint, |
116 | | - })); |
| 110 | + const derivationUpdate = getPsbtBip32DerivationOutputUpdate(rootWalletKeys, walletKeys, scriptType); |
| 111 | + update.bip32Derivation = derivationUpdate.bip32Derivation; |
117 | 112 | } |
| 113 | + |
118 | 114 | if (!output.witnessScript && witnessScript) { |
119 | 115 | update.witnessScript = witnessScript; |
120 | 116 | } |
121 | 117 | if (!output.redeemScript && redeemScript) { |
122 | 118 | update.redeemScript = redeemScript; |
123 | 119 | } |
124 | 120 | } |
125 | | - psbt.updateOutput(outputIndex, update); |
| 121 | + |
| 122 | + return update; |
| 123 | +} |
| 124 | + |
| 125 | +/** |
| 126 | + * Get the PSBT output update object with the required information. |
| 127 | + * |
| 128 | + * @param psbt the PSBT to get output update for |
| 129 | + * @param rootWalletKeys keys that will be able to spend the output |
| 130 | + * @param outputIndex output index where to update the output |
| 131 | + * @param chain chain code to use for deriving scripts (and to determine script |
| 132 | + * type) chain is an API parameter in the BitGo API, and may be |
| 133 | + * any valid ChainCode |
| 134 | + * @param index derivation index for the change address |
| 135 | + * @returns PsbtOutputUpdate object with the required information |
| 136 | + */ |
| 137 | +export function getPsbtOutputUpdate( |
| 138 | + psbt: UtxoPsbt, |
| 139 | + rootWalletKeys: RootWalletKeys, |
| 140 | + outputIndex: number, |
| 141 | + chain: ChainCode, |
| 142 | + index: number |
| 143 | +): PsbtOutputUpdate { |
| 144 | + if (psbt.data.outputs.length <= outputIndex) { |
| 145 | + throw new Error( |
| 146 | + `outputIndex (${outputIndex}) is too large for the number of outputs (${psbt.data.outputs.length})` |
| 147 | + ); |
| 148 | + } |
| 149 | + |
| 150 | + const outputScript = psbt.getOutputScript(outputIndex); |
| 151 | + const output = psbt.data.outputs[outputIndex]; |
| 152 | + |
| 153 | + return getPsbtOutputUpdateFromPsbtOutput(output, outputScript, rootWalletKeys, chain, index); |
| 154 | +} |
| 155 | + |
| 156 | +/** |
| 157 | + * Update the wallet output with the required information when necessary. If the |
| 158 | + * information is there already, it will skip over it. |
| 159 | + * |
| 160 | + * This function assumes that the output script and value have already been set. |
| 161 | + * |
| 162 | + * @param psbt the PSBT to update change output at |
| 163 | + * @param rootWalletKeys keys that will be able to spend the output |
| 164 | + * @param outputIndex output index where to update the output |
| 165 | + * @param chain chain code to use for deriving scripts (and to determine script |
| 166 | + * type) chain is an API parameter in the BitGo API, and may be |
| 167 | + * any valid ChainCode |
| 168 | + * @param index derivation index for the change address |
| 169 | + */ |
| 170 | +export function updateWalletOutputForPsbt( |
| 171 | + psbt: UtxoPsbt, |
| 172 | + rootWalletKeys: RootWalletKeys, |
| 173 | + outputIndex: number, |
| 174 | + chain: ChainCode, |
| 175 | + index: number |
| 176 | +): void { |
| 177 | + psbt.updateOutput(outputIndex, getPsbtOutputUpdate(psbt, rootWalletKeys, outputIndex, chain, index)); |
| 178 | +} |
| 179 | + |
| 180 | +/** |
| 181 | + * Add a verifiable wallet output to the PSBT. The output and all data |
| 182 | + * needed to verify it from public keys only are added to the PSBT. |
| 183 | + * Typically these are change outputs. |
| 184 | + * |
| 185 | + * @param psbt the PSBT to add change output to |
| 186 | + * @param rootWalletKeys keys that will be able to spend the output |
| 187 | + * @param chain chain code to use for deriving scripts (and to determine script |
| 188 | + * type) chain is an API parameter in the BitGo API, and may be |
| 189 | + * any valid ChainCode |
| 190 | + * @param index derivation index for the change address |
| 191 | + * @param value value of the change output |
| 192 | + */ |
| 193 | +export function addWalletOutputToPsbt( |
| 194 | + psbt: UtxoPsbt, |
| 195 | + rootWalletKeys: RootWalletKeys, |
| 196 | + chain: ChainCode, |
| 197 | + index: number, |
| 198 | + value: bigint |
| 199 | +): void { |
| 200 | + const walletKeys = rootWalletKeys.deriveForChainAndIndex(chain, index); |
| 201 | + const scriptType = scriptTypeForChain(chain); |
| 202 | + if (scriptType === 'p2tr' || scriptType === 'p2trMusig2') { |
| 203 | + const payment = |
| 204 | + scriptType === 'p2tr' ? createPaymentP2tr(walletKeys.publicKeys) : createPaymentP2trMusig2(walletKeys.publicKeys); |
| 205 | + psbt.addOutput({ script: payment.output!, value }); |
| 206 | + } else { |
| 207 | + const { scriptPubKey: script } = createOutputScript2of3(walletKeys.publicKeys, scriptType); |
| 208 | + psbt.addOutput({ script, value }); |
| 209 | + } |
| 210 | + updateWalletOutputForPsbt(psbt, rootWalletKeys, psbt.data.outputs.length - 1, chain, index); |
| 211 | +} |
| 212 | + |
| 213 | +/** |
| 214 | + * Fold the script ids into a single script id, if they are all the same. |
| 215 | + * @param scriptIds |
| 216 | + */ |
| 217 | +function foldScriptIds(scriptIds: ScriptId[]): ScriptId { |
| 218 | + if (scriptIds.length === 0) { |
| 219 | + throw new Error('cannot fold empty script ids'); |
| 220 | + } |
| 221 | + scriptIds.forEach((scriptId, i) => { |
| 222 | + if (scriptId.chain !== scriptIds[0].chain) { |
| 223 | + throw new Error(`chain mismatch: ${scriptId.chain} != ${scriptIds[0].chain}`); |
| 224 | + } |
| 225 | + if (scriptId.index !== scriptIds[0].index) { |
| 226 | + throw new Error(`index mismatch: ${scriptId.index} != ${scriptIds[0].index}`); |
| 227 | + } |
| 228 | + }); |
| 229 | + return scriptIds[0]; |
| 230 | +} |
| 231 | + |
| 232 | +/** |
| 233 | + * Get the script id from the output. |
| 234 | + * The output can have either bip32Derivation or tapBip32Derivation, but not both. |
| 235 | + * @param output |
| 236 | + * @throws Error if neither or both bip32Derivation and tapBip32Derivation are present |
| 237 | + * @throws Error if the output is empty |
| 238 | + * @throws Error if we cannot fold the script ids into a single script id |
| 239 | + */ |
| 240 | +export function getScriptIdFromOutput(output: { |
| 241 | + bip32Derivation?: { path: string }[]; |
| 242 | + tapBip32Derivation?: { path: string }[]; |
| 243 | +}): ScriptId { |
| 244 | + if (output.bip32Derivation && output.tapBip32Derivation) { |
| 245 | + throw new Error('cannot get script id from output with both bip32Derivation and tapBip32Derivation'); |
| 246 | + } |
| 247 | + if (output.bip32Derivation) { |
| 248 | + return foldScriptIds(output.bip32Derivation.map((d) => getScriptIdFromPath(d.path))); |
| 249 | + } |
| 250 | + if (output.tapBip32Derivation) { |
| 251 | + return foldScriptIds(output.tapBip32Derivation.map((d) => getScriptIdFromPath(d.path))); |
| 252 | + } |
| 253 | + throw new Error('cannot get script id from output without bip32Derivation or tapBip32Derivation'); |
126 | 254 | } |
0 commit comments