@buildonspark/spark-sdk 0.0.15 → 0.0.16
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/dist/services/wallet-config.d.ts +1 -0
- package/dist/services/wallet-config.js +1 -0
- package/dist/services/wallet-config.js.map +1 -1
- package/dist/spark-sdk.d.ts +1 -1
- package/dist/spark-sdk.js +3 -3
- package/dist/spark-sdk.js.map +1 -1
- package/package.json +4 -3
- package/src/examples/example.js +247 -0
- package/src/examples/example.ts +207 -0
- package/src/graphql/client.ts +282 -0
- package/src/graphql/mutations/CompleteCoopExit.ts +19 -0
- package/src/graphql/mutations/CompleteLeavesSwap.ts +17 -0
- package/src/graphql/mutations/RequestCoopExit.ts +20 -0
- package/src/graphql/mutations/RequestLightningReceive.ts +26 -0
- package/src/graphql/mutations/RequestLightningSend.ts +17 -0
- package/src/graphql/mutations/RequestSwapLeaves.ts +24 -0
- package/src/graphql/objects/BitcoinNetwork.ts +22 -0
- package/src/graphql/objects/CompleteCoopExitInput.ts +41 -0
- package/src/graphql/objects/CompleteCoopExitOutput.ts +45 -0
- package/src/graphql/objects/CompleteLeavesSwapInput.ts +45 -0
- package/src/graphql/objects/CompleteLeavesSwapOutput.ts +45 -0
- package/src/graphql/objects/CompleteSeedReleaseInput.ts +41 -0
- package/src/graphql/objects/CompleteSeedReleaseOutput.ts +43 -0
- package/src/graphql/objects/Connection.ts +90 -0
- package/src/graphql/objects/CoopExitFeeEstimateInput.ts +41 -0
- package/src/graphql/objects/CoopExitFeeEstimateOutput.ts +52 -0
- package/src/graphql/objects/CoopExitRequest.ts +118 -0
- package/src/graphql/objects/CurrencyAmount.ts +74 -0
- package/src/graphql/objects/CurrencyUnit.ts +32 -0
- package/src/graphql/objects/Entity.ts +202 -0
- package/src/graphql/objects/GetChallengeInput.ts +37 -0
- package/src/graphql/objects/GetChallengeOutput.ts +43 -0
- package/src/graphql/objects/Invoice.ts +83 -0
- package/src/graphql/objects/Leaf.ts +59 -0
- package/src/graphql/objects/LeavesSwapFeeEstimateInput.ts +37 -0
- package/src/graphql/objects/LeavesSwapFeeEstimateOutput.ts +52 -0
- package/src/graphql/objects/LeavesSwapRequest.ts +192 -0
- package/src/graphql/objects/LightningReceiveFeeEstimateInput.ts +41 -0
- package/src/graphql/objects/LightningReceiveFeeEstimateOutput.ts +52 -0
- package/src/graphql/objects/LightningReceiveRequest.ts +147 -0
- package/src/graphql/objects/LightningReceiveRequestStatus.ts +34 -0
- package/src/graphql/objects/LightningSendFeeEstimateInput.ts +37 -0
- package/src/graphql/objects/LightningSendFeeEstimateOutput.ts +52 -0
- package/src/graphql/objects/LightningSendRequest.ts +134 -0
- package/src/graphql/objects/LightningSendRequestStatus.ts +28 -0
- package/src/graphql/objects/NotifyReceiverTransferInput.ts +41 -0
- package/src/graphql/objects/PageInfo.ts +58 -0
- package/src/graphql/objects/Provider.ts +41 -0
- package/src/graphql/objects/RequestCoopExitInput.ts +41 -0
- package/src/graphql/objects/RequestCoopExitOutput.ts +45 -0
- package/src/graphql/objects/RequestLeavesSwapInput.ts +55 -0
- package/src/graphql/objects/RequestLeavesSwapOutput.ts +45 -0
- package/src/graphql/objects/RequestLightningReceiveInput.ts +58 -0
- package/src/graphql/objects/RequestLightningReceiveOutput.ts +45 -0
- package/src/graphql/objects/RequestLightningSendInput.ts +41 -0
- package/src/graphql/objects/RequestLightningSendOutput.ts +45 -0
- package/src/graphql/objects/SparkCoopExitRequestStatus.ts +20 -0
- package/src/graphql/objects/SparkLeavesSwapRequestStatus.ts +20 -0
- package/src/graphql/objects/SparkTransferToLeavesConnection.ts +79 -0
- package/src/graphql/objects/SparkWalletUser.ts +86 -0
- package/src/graphql/objects/StartSeedReleaseInput.ts +37 -0
- package/src/graphql/objects/SwapLeaf.ts +53 -0
- package/src/graphql/objects/Transfer.ts +98 -0
- package/src/graphql/objects/UserLeafInput.ts +28 -0
- package/src/graphql/objects/VerifyChallengeInput.ts +51 -0
- package/src/graphql/objects/VerifyChallengeOutput.ts +43 -0
- package/src/graphql/objects/WalletUserIdentityPublicKeyInput.ts +37 -0
- package/src/graphql/objects/WalletUserIdentityPublicKeyOutput.ts +43 -0
- package/src/graphql/objects/index.ts +67 -0
- package/src/graphql/queries/CoopExitFeeEstimate.ts +18 -0
- package/src/graphql/queries/CurrentUser.ts +10 -0
- package/src/graphql/queries/LightningReceiveFeeEstimate.ts +18 -0
- package/src/graphql/queries/LightningSendFeeEstimate.ts +16 -0
- package/src/proto/common.ts +431 -0
- package/src/proto/google/protobuf/descriptor.ts +6625 -0
- package/src/proto/google/protobuf/duration.ts +197 -0
- package/src/proto/google/protobuf/empty.ts +83 -0
- package/src/proto/google/protobuf/timestamp.ts +226 -0
- package/src/proto/mock.ts +151 -0
- package/src/proto/spark.ts +12727 -0
- package/src/proto/spark_authn.ts +673 -0
- package/src/proto/validate/validate.ts +6047 -0
- package/src/services/config.ts +71 -0
- package/src/services/connection.ts +264 -0
- package/src/services/coop-exit.ts +190 -0
- package/src/services/deposit.ts +327 -0
- package/src/services/lightning.ts +341 -0
- package/src/services/lrc20.ts +42 -0
- package/src/services/token-transactions.ts +499 -0
- package/src/services/transfer.ts +1188 -0
- package/src/services/tree-creation.ts +618 -0
- package/src/services/wallet-config.ts +141 -0
- package/src/signer/signer.ts +531 -0
- package/src/spark-sdk.ts +1644 -0
- package/src/tests/adaptor-signature.test.ts +64 -0
- package/src/tests/bitcoin.test.ts +122 -0
- package/src/tests/coop-exit.test.ts +233 -0
- package/src/tests/deposit.test.ts +98 -0
- package/src/tests/keys.test.ts +82 -0
- package/src/tests/lightning.test.ts +307 -0
- package/src/tests/secret-sharing.test.ts +63 -0
- package/src/tests/swap.test.ts +252 -0
- package/src/tests/test-util.ts +92 -0
- package/src/tests/tokens.test.ts +47 -0
- package/src/tests/transfer.test.ts +371 -0
- package/src/tests/tree-creation.test.ts +56 -0
- package/src/tests/utils/spark-testing-wallet.ts +37 -0
- package/src/tests/utils/test-faucet.ts +257 -0
- package/src/types/grpc.ts +8 -0
- package/src/types/index.ts +3 -0
- package/src/utils/adaptor-signature.ts +189 -0
- package/src/utils/bitcoin.ts +138 -0
- package/src/utils/crypto.ts +14 -0
- package/src/utils/index.ts +12 -0
- package/src/utils/keys.ts +92 -0
- package/src/utils/mempool.ts +42 -0
- package/src/utils/network.ts +70 -0
- package/src/utils/proof.ts +17 -0
- package/src/utils/response-validation.ts +26 -0
- package/src/utils/secret-sharing.ts +263 -0
- package/src/utils/signing.ts +96 -0
- package/src/utils/token-hashing.ts +163 -0
- package/src/utils/token-keyshares.ts +31 -0
- package/src/utils/token-transactions.ts +71 -0
- package/src/utils/transaction.ts +45 -0
- package/src/utils/wasm-wrapper.ts +57 -0
- package/src/utils/wasm.ts +154 -0
- package/src/wasm/spark_bindings.d.ts +208 -0
- package/src/wasm/spark_bindings.js +1161 -0
- package/src/wasm/spark_bindings_bg.wasm +0 -0
- package/src/wasm/spark_bindings_bg.wasm.d.ts +136 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { bytesToHex, hexToBytes } from "@noble/curves/abstract/utils";
|
|
2
|
+
import { schnorr, secp256k1 } from "@noble/curves/secp256k1";
|
|
3
|
+
import { Address, OutScript, SigHash, Transaction } from "@scure/btc-signer";
|
|
4
|
+
import { TransactionInput, TransactionOutput } from "@scure/btc-signer/psbt";
|
|
5
|
+
import { taprootTweakPrivKey } from "@scure/btc-signer/utils";
|
|
6
|
+
import {
|
|
7
|
+
getP2TRAddressFromPublicKey,
|
|
8
|
+
getP2TRScriptFromPublicKey,
|
|
9
|
+
} from "../../utils/bitcoin.js";
|
|
10
|
+
import { getNetwork, Network } from "../../utils/network.js";
|
|
11
|
+
import { SparkWallet } from "../../spark-sdk.js";
|
|
12
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
13
|
+
import * as btc from "@scure/btc-signer";
|
|
14
|
+
import { ripemd160 } from "@noble/hashes/ripemd160";
|
|
15
|
+
|
|
16
|
+
export type FaucetCoin = {
|
|
17
|
+
key: Uint8Array;
|
|
18
|
+
outpoint: TransactionInput;
|
|
19
|
+
txout: TransactionOutput;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const COIN_AMOUNT = 10_000_000n;
|
|
23
|
+
|
|
24
|
+
export class BitcoinFaucet {
|
|
25
|
+
private coins: FaucetCoin[] = [];
|
|
26
|
+
private static instance: BitcoinFaucet | null = null;
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
private url: string,
|
|
30
|
+
private username: string,
|
|
31
|
+
private password: string,
|
|
32
|
+
) {
|
|
33
|
+
if (BitcoinFaucet.instance) {
|
|
34
|
+
return BitcoinFaucet.instance;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
BitcoinFaucet.instance = this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async fund() {
|
|
41
|
+
// If no coins available, refill the faucet
|
|
42
|
+
if (this.coins.length === 0) {
|
|
43
|
+
await this.refill();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Take the first coin from the faucet
|
|
47
|
+
const coin = this.coins[0];
|
|
48
|
+
// Remove the used coin from the array
|
|
49
|
+
this.coins = this.coins.slice(1);
|
|
50
|
+
|
|
51
|
+
return coin;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async refill() {
|
|
55
|
+
// Generate key for initial block reward
|
|
56
|
+
const key = secp256k1.utils.randomPrivateKey();
|
|
57
|
+
const pubKey = secp256k1.getPublicKey(key);
|
|
58
|
+
const address = getP2TRAddressFromPublicKey(pubKey, Network.LOCAL);
|
|
59
|
+
|
|
60
|
+
// Mine a block to this address
|
|
61
|
+
const blockHash = await this.generateToAddress(1, address);
|
|
62
|
+
|
|
63
|
+
// Get block and funding transaction
|
|
64
|
+
const block = await this.getBlock(blockHash[0]);
|
|
65
|
+
const fundingTx = Transaction.fromRaw(hexToBytes(block.tx[0].hex), {
|
|
66
|
+
allowUnknownOutputs: true,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Mine 100 blocks to make funds spendable
|
|
70
|
+
const randomKey = secp256k1.utils.randomPrivateKey();
|
|
71
|
+
const randomPubKey = secp256k1.getPublicKey(randomKey);
|
|
72
|
+
const randomAddress = getP2TRAddressFromPublicKey(
|
|
73
|
+
randomPubKey,
|
|
74
|
+
Network.LOCAL,
|
|
75
|
+
);
|
|
76
|
+
await this.generateToAddress(100, randomAddress);
|
|
77
|
+
|
|
78
|
+
const fundingTxId = block.tx[0].txid;
|
|
79
|
+
const fundingOutpoint: TransactionInput = {
|
|
80
|
+
txid: fundingTxId,
|
|
81
|
+
index: 0,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const splitTx = new Transaction();
|
|
85
|
+
splitTx.addInput(fundingOutpoint);
|
|
86
|
+
let initialValue = fundingTx.getOutput(0)!.amount!;
|
|
87
|
+
const coinAmount = 10_000_000n;
|
|
88
|
+
const coinKeys: Uint8Array[] = [];
|
|
89
|
+
|
|
90
|
+
while (initialValue > coinAmount + 100_000n) {
|
|
91
|
+
const coinKey = secp256k1.utils.randomPrivateKey();
|
|
92
|
+
const coinPubKey = secp256k1.getPublicKey(coinKey);
|
|
93
|
+
coinKeys.push(coinKey);
|
|
94
|
+
|
|
95
|
+
const script = getP2TRScriptFromPublicKey(coinPubKey, Network.LOCAL);
|
|
96
|
+
splitTx.addOutput({
|
|
97
|
+
script,
|
|
98
|
+
amount: coinAmount,
|
|
99
|
+
});
|
|
100
|
+
initialValue -= coinAmount;
|
|
101
|
+
}
|
|
102
|
+
// Sign and broadcast
|
|
103
|
+
const signedSplitTx = await this.signFaucetCoin(
|
|
104
|
+
splitTx,
|
|
105
|
+
fundingTx.getOutput(0)!,
|
|
106
|
+
key,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
await this.broadcastTx(bytesToHex(signedSplitTx.extract()));
|
|
110
|
+
|
|
111
|
+
// Create faucet coins
|
|
112
|
+
const splitTxId = signedSplitTx.id;
|
|
113
|
+
for (let i = 0; i < signedSplitTx.outputsLength; i++) {
|
|
114
|
+
this.coins.push({
|
|
115
|
+
// @ts-ignore - It's a test file
|
|
116
|
+
key: coinKeys[i],
|
|
117
|
+
outpoint: {
|
|
118
|
+
txid: hexToBytes(splitTxId),
|
|
119
|
+
index: i,
|
|
120
|
+
},
|
|
121
|
+
txout: signedSplitTx.getOutput(i)!,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async sendFaucetCoinToP2WPKHAddress(pubKey: Uint8Array) {
|
|
126
|
+
const sendToPubKeyTx = new Transaction();
|
|
127
|
+
|
|
128
|
+
// For P2WPKH, we need to hash the public key
|
|
129
|
+
|
|
130
|
+
// Create a P2WPKH address
|
|
131
|
+
const p2wpkhAddress = btc.p2wpkh(pubKey, getNetwork(Network.LOCAL)).address;
|
|
132
|
+
if (!p2wpkhAddress) {
|
|
133
|
+
throw new Error("Invalid P2WPKH address");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Get the coin to spend
|
|
137
|
+
const coinToSend = await this.fund();
|
|
138
|
+
if (!coinToSend) {
|
|
139
|
+
throw new Error("No coins available");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Add the input
|
|
143
|
+
sendToPubKeyTx.addInput(coinToSend.outpoint);
|
|
144
|
+
|
|
145
|
+
// Add the output using the address directly
|
|
146
|
+
sendToPubKeyTx.addOutputAddress(
|
|
147
|
+
p2wpkhAddress,
|
|
148
|
+
100_000n,
|
|
149
|
+
getNetwork(Network.LOCAL),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Sign the transaction and get the signed result
|
|
153
|
+
const signedTx = await this.signFaucetCoin(
|
|
154
|
+
sendToPubKeyTx,
|
|
155
|
+
coinToSend.txout,
|
|
156
|
+
coinToSend.key,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// Broadcast the signed transaction
|
|
160
|
+
await this.broadcastTx(bytesToHex(signedTx.extract()));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async signFaucetCoin(
|
|
164
|
+
unsignedTx: Transaction,
|
|
165
|
+
fundingTxOut: TransactionOutput,
|
|
166
|
+
key: Uint8Array,
|
|
167
|
+
): Promise<Transaction> {
|
|
168
|
+
const pubKey = secp256k1.getPublicKey(key);
|
|
169
|
+
const internalKey = pubKey.slice(1); // Remove the 0x02/0x03 prefix
|
|
170
|
+
|
|
171
|
+
const script = getP2TRScriptFromPublicKey(pubKey, Network.LOCAL);
|
|
172
|
+
|
|
173
|
+
unsignedTx.updateInput(0, {
|
|
174
|
+
tapInternalKey: internalKey,
|
|
175
|
+
witnessUtxo: {
|
|
176
|
+
script,
|
|
177
|
+
amount: fundingTxOut.amount!,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const sighash = unsignedTx.preimageWitnessV1(
|
|
182
|
+
0,
|
|
183
|
+
new Array(unsignedTx.inputsLength).fill(script),
|
|
184
|
+
SigHash.DEFAULT,
|
|
185
|
+
new Array(unsignedTx.inputsLength).fill(fundingTxOut.amount!),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const merkleRoot = new Uint8Array();
|
|
189
|
+
const tweakedKey = taprootTweakPrivKey(key, merkleRoot);
|
|
190
|
+
if (!tweakedKey)
|
|
191
|
+
throw new Error("Invalid private key for taproot tweaking");
|
|
192
|
+
|
|
193
|
+
const signature = schnorr.sign(sighash, tweakedKey);
|
|
194
|
+
|
|
195
|
+
unsignedTx.updateInput(0, {
|
|
196
|
+
tapKeySig: signature,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
unsignedTx.finalize();
|
|
200
|
+
|
|
201
|
+
return unsignedTx;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// MineBlocks mines the specified number of blocks to a random address
|
|
205
|
+
// and returns the block hashes.
|
|
206
|
+
async mineBlocks(numBlocks: number) {
|
|
207
|
+
// Mine 100 blocks to make funds spendable
|
|
208
|
+
const randomKey = secp256k1.utils.randomPrivateKey();
|
|
209
|
+
const randomPubKey = secp256k1.getPublicKey(randomKey);
|
|
210
|
+
const randomAddress = getP2TRAddressFromPublicKey(
|
|
211
|
+
randomPubKey,
|
|
212
|
+
Network.LOCAL,
|
|
213
|
+
);
|
|
214
|
+
return await this.generateToAddress(100, randomAddress);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private async call(method: string, params: any[]) {
|
|
218
|
+
try {
|
|
219
|
+
const response = await fetch(this.url, {
|
|
220
|
+
method: "POST",
|
|
221
|
+
headers: {
|
|
222
|
+
"Content-Type": "application/json",
|
|
223
|
+
Authorization: "Basic " + btoa(`${this.username}:${this.password}`),
|
|
224
|
+
},
|
|
225
|
+
body: JSON.stringify({
|
|
226
|
+
jsonrpc: "1.0",
|
|
227
|
+
id: "spark-js",
|
|
228
|
+
method,
|
|
229
|
+
params,
|
|
230
|
+
}),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const data = await response.json();
|
|
234
|
+
if (data.error) {
|
|
235
|
+
console.error(`RPC Error for method ${method}:`, data.error);
|
|
236
|
+
throw new Error(`Bitcoin RPC error: ${data.error.message}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return data.result;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.error("Error calling Bitcoin RPC:", error);
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async generateToAddress(numBlocks: number, address: string) {
|
|
247
|
+
return await this.call("generatetoaddress", [numBlocks, address]);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async getBlock(blockHash: string) {
|
|
251
|
+
return await this.call("getblock", [blockHash, 2]);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async broadcastTx(txHex: string) {
|
|
255
|
+
return await this.call("sendrawtransaction", [txHex, 0]);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { mod } from "@noble/curves/abstract/modular";
|
|
2
|
+
import { bytesToNumberBE, numberToBytesBE } from "@noble/curves/abstract/utils";
|
|
3
|
+
import { schnorr, secp256k1 } from "@noble/curves/secp256k1";
|
|
4
|
+
|
|
5
|
+
export function generateSignatureFromExistingAdaptor(
|
|
6
|
+
signature: Uint8Array,
|
|
7
|
+
adaptorPrivateKeyBytes: Uint8Array,
|
|
8
|
+
): Uint8Array {
|
|
9
|
+
const { r, s } = parseSignature(signature);
|
|
10
|
+
|
|
11
|
+
const sBigInt = bytesToNumberBE(s);
|
|
12
|
+
const tBigInt = bytesToNumberBE(adaptorPrivateKeyBytes);
|
|
13
|
+
|
|
14
|
+
const newS = mod(sBigInt - tBigInt, secp256k1.CURVE.n);
|
|
15
|
+
|
|
16
|
+
const newSignature = new Uint8Array([...r, ...numberToBytesBE(newS, 32)]);
|
|
17
|
+
|
|
18
|
+
return newSignature;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function generateAdaptorFromSignature(signature: Uint8Array): {
|
|
22
|
+
adaptorSignature: Uint8Array;
|
|
23
|
+
adaptorPrivateKey: Uint8Array;
|
|
24
|
+
} {
|
|
25
|
+
const adaptorPrivateKey = secp256k1.utils.randomPrivateKey();
|
|
26
|
+
|
|
27
|
+
const { r, s } = parseSignature(signature);
|
|
28
|
+
|
|
29
|
+
const sBigInt = bytesToNumberBE(s);
|
|
30
|
+
const tBigInt = bytesToNumberBE(adaptorPrivateKey);
|
|
31
|
+
|
|
32
|
+
// Calculate s - adaptorPrivateKey
|
|
33
|
+
const newS = mod(sBigInt - tBigInt, secp256k1.CURVE.n);
|
|
34
|
+
|
|
35
|
+
const newSignature = new Uint8Array([...r, ...numberToBytesBE(newS, 32)]);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
adaptorSignature: newSignature,
|
|
39
|
+
adaptorPrivateKey: adaptorPrivateKey,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function validateOutboundAdaptorSignature(
|
|
44
|
+
pubkey: Uint8Array,
|
|
45
|
+
hash: Uint8Array,
|
|
46
|
+
signature: Uint8Array,
|
|
47
|
+
adaptorPubkey: Uint8Array,
|
|
48
|
+
): boolean {
|
|
49
|
+
return schnorrVerifyWithAdaptor(
|
|
50
|
+
signature,
|
|
51
|
+
hash,
|
|
52
|
+
pubkey,
|
|
53
|
+
adaptorPubkey,
|
|
54
|
+
false,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function applyAdaptorToSignature(
|
|
59
|
+
pubkey: Uint8Array,
|
|
60
|
+
hash: Uint8Array,
|
|
61
|
+
signature: Uint8Array,
|
|
62
|
+
adaptorPrivateKeyBytes: Uint8Array,
|
|
63
|
+
): Uint8Array {
|
|
64
|
+
// Parse the signature
|
|
65
|
+
const { r, s } = parseSignature(signature);
|
|
66
|
+
|
|
67
|
+
// Convert values to bigints
|
|
68
|
+
const sBigInt = bytesToNumberBE(s);
|
|
69
|
+
const adaptorPrivateKey = bytesToNumberBE(adaptorPrivateKeyBytes);
|
|
70
|
+
|
|
71
|
+
// Try adding adaptor to s first
|
|
72
|
+
const newS = mod(sBigInt + adaptorPrivateKey, secp256k1.CURVE.n);
|
|
73
|
+
const newSig = new Uint8Array([...r, ...numberToBytesBE(newS, 32)]);
|
|
74
|
+
|
|
75
|
+
if (schnorr.verify(newSig, hash, pubkey)) {
|
|
76
|
+
return newSig;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// If adding didn't work, try subtracting
|
|
80
|
+
const altS = mod(sBigInt - adaptorPrivateKey, secp256k1.CURVE.n);
|
|
81
|
+
const altSig = new Uint8Array([...r, ...numberToBytesBE(altS, 32)]);
|
|
82
|
+
|
|
83
|
+
if (schnorr.verify(altSig, hash, pubkey)) {
|
|
84
|
+
return altSig;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
throw new Error("Cannot apply adaptor to signature");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Step 1: P = lift_x(int(pk))
|
|
91
|
+
// Step 2: r = int(sig[0:32])
|
|
92
|
+
// Step 3: s = int(sig[32:64])
|
|
93
|
+
// Step 4: e = int(tagged_hash("BIP0340/challenge", bytes(r) || bytes(P) || m)) mod n
|
|
94
|
+
// Step 5: R = sG - eP
|
|
95
|
+
// Step 6: R = R + T
|
|
96
|
+
function schnorrVerifyWithAdaptor(
|
|
97
|
+
signature: Uint8Array,
|
|
98
|
+
hash: Uint8Array,
|
|
99
|
+
pubKeyBytes: Uint8Array,
|
|
100
|
+
adaptorPubkey: Uint8Array,
|
|
101
|
+
inbound: boolean,
|
|
102
|
+
): boolean {
|
|
103
|
+
// Step 1: Verify message length
|
|
104
|
+
if (hash.length !== 32) {
|
|
105
|
+
throw new Error(`wrong size for message (got ${hash.length}, want 32)`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Step 2: Lift x coordinate to curve point
|
|
109
|
+
const pubKey = schnorr.utils.lift_x(bytesToNumberBE(pubKeyBytes));
|
|
110
|
+
pubKey.assertValidity();
|
|
111
|
+
|
|
112
|
+
// Parse signature
|
|
113
|
+
// Step 3 and 4 is handled by parseSignature
|
|
114
|
+
const { r, s } = parseSignature(signature);
|
|
115
|
+
|
|
116
|
+
// Step 5: Compute challenge
|
|
117
|
+
const commitmenet = schnorr.utils.taggedHash(
|
|
118
|
+
"BIP0340/challenge",
|
|
119
|
+
r,
|
|
120
|
+
pubKey.toRawBytes().slice(1),
|
|
121
|
+
hash,
|
|
122
|
+
);
|
|
123
|
+
if (commitmenet.length > 32) {
|
|
124
|
+
throw new Error("hash of (r || P || m) too big");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const e = mod(bytesToNumberBE(commitmenet), secp256k1.CURVE.n);
|
|
128
|
+
const negE = mod(-e, secp256k1.CURVE.n); // Negate e before multiplication
|
|
129
|
+
|
|
130
|
+
// Step 6: Calculate R = sG - eP
|
|
131
|
+
const R = secp256k1.ProjectivePoint.BASE.multiplyAndAddUnsafe(
|
|
132
|
+
pubKey,
|
|
133
|
+
bytesToNumberBE(s),
|
|
134
|
+
negE,
|
|
135
|
+
);
|
|
136
|
+
if (!R) {
|
|
137
|
+
throw new Error("R is undefined");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
R.assertValidity();
|
|
141
|
+
|
|
142
|
+
// Step 6.5: Add adaptor public key T to R
|
|
143
|
+
const adaptorPoint = secp256k1.ProjectivePoint.fromHex(adaptorPubkey);
|
|
144
|
+
const newR = R.add(adaptorPoint);
|
|
145
|
+
|
|
146
|
+
// Step 7: Check for point at infinity (if not inbound)
|
|
147
|
+
if (!inbound && newR.equals(secp256k1.ProjectivePoint.ZERO)) {
|
|
148
|
+
throw new Error("calculated R point is the point at infinity");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Step 8: Check if R.y is odd
|
|
152
|
+
newR.assertValidity();
|
|
153
|
+
if (!newR.hasEvenY()) {
|
|
154
|
+
throw new Error("calculated R y-value is odd");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Step 9: Check if R.x == r
|
|
158
|
+
const rNum = bytesToNumberBE(r);
|
|
159
|
+
if (newR.toAffine().x !== rNum) {
|
|
160
|
+
throw new Error("calculated R point was not given R");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function parseSignature(signature: Uint8Array): {
|
|
167
|
+
r: Uint8Array;
|
|
168
|
+
s: Uint8Array;
|
|
169
|
+
} {
|
|
170
|
+
if (signature.length < 64) {
|
|
171
|
+
throw new Error(`malformed signature: too short: ${signature.length} < 64`);
|
|
172
|
+
}
|
|
173
|
+
if (signature.length > 64) {
|
|
174
|
+
throw new Error(`malformed signature: too long: ${signature.length} > 64`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const r = signature.slice(0, 32);
|
|
178
|
+
const s = signature.slice(32, 64);
|
|
179
|
+
|
|
180
|
+
if (bytesToNumberBE(r) >= secp256k1.CURVE.Fp.ORDER) {
|
|
181
|
+
throw new Error(`invalid signature: r >= field prime`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (bytesToNumberBE(s) >= secp256k1.CURVE.n) {
|
|
185
|
+
throw new Error(`invalid signature: s >= group order`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { r, s };
|
|
189
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {
|
|
2
|
+
bytesToHex,
|
|
3
|
+
bytesToNumberBE,
|
|
4
|
+
hexToBytes,
|
|
5
|
+
} from "@noble/curves/abstract/utils";
|
|
6
|
+
import { schnorr, secp256k1 } from "@noble/curves/secp256k1";
|
|
7
|
+
|
|
8
|
+
import * as btc from "@scure/btc-signer";
|
|
9
|
+
import { TransactionOutput } from "@scure/btc-signer/psbt";
|
|
10
|
+
import { sha256 } from "@scure/btc-signer/utils";
|
|
11
|
+
import { getNetwork, Network } from "./network.js";
|
|
12
|
+
|
|
13
|
+
// const t = tapTweak(pubKey, h); // t = int_from_bytes(tagged_hash("TapTweak", pubkey + h)
|
|
14
|
+
// const P = u.lift_x(u.bytesToNumberBE(pubKey)); // P = lift_x(int_from_bytes(pubkey))
|
|
15
|
+
// const Q = P.add(Point.fromPrivateKey(t)); // Q = point_add(P, point_mul(G, t))
|
|
16
|
+
export function computeTaprootKeyNoScript(pubkey: Uint8Array): Uint8Array {
|
|
17
|
+
if (pubkey.length !== 32) {
|
|
18
|
+
throw new Error("Public key must be 32 bytes");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const taggedHash = schnorr.utils.taggedHash("TapTweak", pubkey);
|
|
22
|
+
const tweak = bytesToNumberBE(taggedHash);
|
|
23
|
+
|
|
24
|
+
// Get the original point
|
|
25
|
+
const P = schnorr.utils.lift_x(schnorr.utils.bytesToNumberBE(pubkey));
|
|
26
|
+
|
|
27
|
+
// Add the tweak times the generator point
|
|
28
|
+
const Q = P.add(secp256k1.ProjectivePoint.fromPrivateKey(tweak));
|
|
29
|
+
|
|
30
|
+
return Q.toRawBytes();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getP2TRScriptFromPublicKey(
|
|
34
|
+
pubKey: Uint8Array,
|
|
35
|
+
network: Network,
|
|
36
|
+
): Uint8Array {
|
|
37
|
+
if (pubKey.length !== 33) {
|
|
38
|
+
throw new Error("Public key must be 33 bytes");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const internalKey = secp256k1.ProjectivePoint.fromHex(pubKey);
|
|
42
|
+
const script = btc.p2tr(
|
|
43
|
+
internalKey.toRawBytes().slice(1, 33),
|
|
44
|
+
undefined,
|
|
45
|
+
getNetwork(network),
|
|
46
|
+
).script;
|
|
47
|
+
if (!script) {
|
|
48
|
+
throw new Error("Failed to get P2TR address");
|
|
49
|
+
}
|
|
50
|
+
return script;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getP2TRAddressFromPublicKey(
|
|
54
|
+
pubKey: Uint8Array,
|
|
55
|
+
network: Network,
|
|
56
|
+
): string {
|
|
57
|
+
if (pubKey.length !== 33) {
|
|
58
|
+
throw new Error("Public key must be 33 bytes");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const internalKey = secp256k1.ProjectivePoint.fromHex(pubKey);
|
|
62
|
+
const address = btc.p2tr(
|
|
63
|
+
internalKey.toRawBytes().slice(1, 33),
|
|
64
|
+
undefined,
|
|
65
|
+
getNetwork(network),
|
|
66
|
+
).address;
|
|
67
|
+
if (!address) {
|
|
68
|
+
throw new Error("Failed to get P2TR address");
|
|
69
|
+
}
|
|
70
|
+
return address;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getP2TRAddressFromPkScript(
|
|
74
|
+
pkScript: Uint8Array,
|
|
75
|
+
network: Network,
|
|
76
|
+
): string {
|
|
77
|
+
if (pkScript.length !== 34 || pkScript[0] !== 0x51 || pkScript[1] !== 0x20) {
|
|
78
|
+
throw new Error("Invalid pkscript");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const parsedScript = btc.OutScript.decode(pkScript);
|
|
82
|
+
|
|
83
|
+
return btc.Address(getNetwork(network)).encode(parsedScript);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getTxFromRawTxHex(rawTxHex: string): btc.Transaction {
|
|
87
|
+
const txBytes = hexToBytes(rawTxHex);
|
|
88
|
+
const tx = btc.Transaction.fromRaw(txBytes, {
|
|
89
|
+
allowUnknownOutputs: true,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (!tx) {
|
|
93
|
+
throw new Error("Failed to parse transaction");
|
|
94
|
+
}
|
|
95
|
+
return tx;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function getTxFromRawTxBytes(rawTxBytes: Uint8Array): btc.Transaction {
|
|
99
|
+
const tx = btc.Transaction.fromRaw(rawTxBytes, {
|
|
100
|
+
allowUnknownOutputs: true,
|
|
101
|
+
});
|
|
102
|
+
if (!tx) {
|
|
103
|
+
throw new Error("Failed to parse transaction");
|
|
104
|
+
}
|
|
105
|
+
return tx;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function getSigHashFromTx(
|
|
109
|
+
tx: btc.Transaction,
|
|
110
|
+
inputIndex: number,
|
|
111
|
+
prevOutput: TransactionOutput,
|
|
112
|
+
): Uint8Array {
|
|
113
|
+
// For Taproot, we use preimageWitnessV1 with SIGHASH_DEFAULT (0x00)
|
|
114
|
+
const prevScript = prevOutput.script;
|
|
115
|
+
if (!prevScript) {
|
|
116
|
+
throw new Error("No script found in prevOutput");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const amount = prevOutput.amount;
|
|
120
|
+
if (!amount) {
|
|
121
|
+
throw new Error("No amount found in prevOutput");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return tx.preimageWitnessV1(
|
|
125
|
+
inputIndex,
|
|
126
|
+
new Array(tx.inputsLength).fill(prevScript),
|
|
127
|
+
btc.SigHash.DEFAULT,
|
|
128
|
+
new Array(tx.inputsLength).fill(amount),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function getTxId(tx: btc.Transaction): string {
|
|
133
|
+
return bytesToHex(sha256(sha256(tx.unsignedTx)).reverse());
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function getTxIdNoReverse(tx: btc.Transaction): string {
|
|
137
|
+
return bytesToHex(sha256(sha256(tx.unsignedTx)));
|
|
138
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
|
|
3
|
+
export const getCrypto = (): Crypto => {
|
|
4
|
+
// Browser environment
|
|
5
|
+
if (typeof window !== "undefined" && window.crypto) {
|
|
6
|
+
return window.crypto;
|
|
7
|
+
}
|
|
8
|
+
// Node.js environment
|
|
9
|
+
if (typeof global !== "undefined" && global.crypto) {
|
|
10
|
+
return global.crypto;
|
|
11
|
+
}
|
|
12
|
+
// Node.js environment without global.crypto (older versions)
|
|
13
|
+
return crypto as Crypto;
|
|
14
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./adaptor-signature.js";
|
|
2
|
+
export * from "./bitcoin.js";
|
|
3
|
+
export * from "./keys.js";
|
|
4
|
+
export * from "./mempool.js";
|
|
5
|
+
export * from "./network.js";
|
|
6
|
+
export * from "./proof.js";
|
|
7
|
+
export * from "./response-validation.js";
|
|
8
|
+
export * from "./secret-sharing.js";
|
|
9
|
+
export * from "./signing.js";
|
|
10
|
+
export * from "./transaction.js";
|
|
11
|
+
export * from "./wasm-wrapper.js";
|
|
12
|
+
export * from "./wasm.js";
|