@aibtc/mcp-server 1.9.0 → 1.13.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/dist/config/bitcoin-constants.d.ts +32 -0
- package/dist/config/bitcoin-constants.d.ts.map +1 -0
- package/dist/config/bitcoin-constants.js +32 -0
- package/dist/config/bitcoin-constants.js.map +1 -0
- package/dist/config/contracts.d.ts +12 -3
- package/dist/config/contracts.d.ts.map +1 -1
- package/dist/config/contracts.js +7 -1
- package/dist/config/contracts.js.map +1 -1
- package/dist/services/bitflow.service.d.ts +2 -1
- package/dist/services/bitflow.service.d.ts.map +1 -1
- package/dist/services/bitflow.service.js +3 -1
- package/dist/services/bitflow.service.js.map +1 -1
- package/dist/services/bns.service.d.ts +21 -5
- package/dist/services/bns.service.d.ts.map +1 -1
- package/dist/services/bns.service.js +145 -51
- package/dist/services/bns.service.js.map +1 -1
- package/dist/services/defi.service.d.ts +16 -6
- package/dist/services/defi.service.d.ts.map +1 -1
- package/dist/services/defi.service.js +99 -27
- package/dist/services/defi.service.js.map +1 -1
- package/dist/services/hiro-api.d.ts +26 -0
- package/dist/services/hiro-api.d.ts.map +1 -1
- package/dist/services/hiro-api.js +8 -0
- package/dist/services/hiro-api.js.map +1 -1
- package/dist/services/inscription-parser.d.ts +122 -0
- package/dist/services/inscription-parser.d.ts.map +1 -0
- package/dist/services/inscription-parser.js +163 -0
- package/dist/services/inscription-parser.js.map +1 -0
- package/dist/services/nft.service.d.ts +2 -1
- package/dist/services/nft.service.d.ts.map +1 -1
- package/dist/services/nft.service.js +3 -1
- package/dist/services/nft.service.js.map +1 -1
- package/dist/services/ordinal-indexer.d.ts +128 -0
- package/dist/services/ordinal-indexer.d.ts.map +1 -0
- package/dist/services/ordinal-indexer.js +128 -0
- package/dist/services/ordinal-indexer.js.map +1 -0
- package/dist/services/sbtc.service.d.ts +2 -1
- package/dist/services/sbtc.service.d.ts.map +1 -1
- package/dist/services/sbtc.service.js +3 -1
- package/dist/services/sbtc.service.js.map +1 -1
- package/dist/services/tokens.service.d.ts +2 -1
- package/dist/services/tokens.service.d.ts.map +1 -1
- package/dist/services/tokens.service.js +3 -1
- package/dist/services/tokens.service.js.map +1 -1
- package/dist/services/wallet-manager.d.ts +5 -0
- package/dist/services/wallet-manager.d.ts.map +1 -1
- package/dist/services/wallet-manager.js +23 -9
- package/dist/services/wallet-manager.js.map +1 -1
- package/dist/tools/bitcoin.tools.d.ts.map +1 -1
- package/dist/tools/bitcoin.tools.js +214 -34
- package/dist/tools/bitcoin.tools.js.map +1 -1
- package/dist/tools/bitflow.tools.d.ts.map +1 -1
- package/dist/tools/bitflow.tools.js +8 -3
- package/dist/tools/bitflow.tools.js.map +1 -1
- package/dist/tools/bns.tools.d.ts.map +1 -1
- package/dist/tools/bns.tools.js +87 -7
- package/dist/tools/bns.tools.js.map +1 -1
- package/dist/tools/contract.tools.d.ts.map +1 -1
- package/dist/tools/contract.tools.js +19 -4
- package/dist/tools/contract.tools.js.map +1 -1
- package/dist/tools/defi.tools.d.ts.map +1 -1
- package/dist/tools/defi.tools.js +43 -0
- package/dist/tools/defi.tools.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +4 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/nft.tools.d.ts.map +1 -1
- package/dist/tools/nft.tools.js +8 -3
- package/dist/tools/nft.tools.js.map +1 -1
- package/dist/tools/ordinals.tools.d.ts +15 -0
- package/dist/tools/ordinals.tools.d.ts.map +1 -0
- package/dist/tools/ordinals.tools.js +401 -0
- package/dist/tools/ordinals.tools.js.map +1 -0
- package/dist/tools/query.tools.d.ts.map +1 -1
- package/dist/tools/query.tools.js +71 -0
- package/dist/tools/query.tools.js.map +1 -1
- package/dist/tools/sbtc.tools.d.ts.map +1 -1
- package/dist/tools/sbtc.tools.js +8 -3
- package/dist/tools/sbtc.tools.js.map +1 -1
- package/dist/tools/signing.tools.d.ts +25 -0
- package/dist/tools/signing.tools.d.ts.map +1 -0
- package/dist/tools/signing.tools.js +776 -0
- package/dist/tools/signing.tools.js.map +1 -0
- package/dist/tools/tokens.tools.d.ts.map +1 -1
- package/dist/tools/tokens.tools.js +8 -3
- package/dist/tools/tokens.tools.js.map +1 -1
- package/dist/tools/transfer.tools.d.ts.map +1 -1
- package/dist/tools/transfer.tools.js +8 -3
- package/dist/tools/transfer.tools.js.map +1 -1
- package/dist/tools/wallet-management.tools.d.ts.map +1 -1
- package/dist/tools/wallet-management.tools.js +34 -39
- package/dist/tools/wallet-management.tools.js.map +1 -1
- package/dist/tools/wallet.tools.d.ts.map +1 -1
- package/dist/tools/wallet.tools.js +25 -16
- package/dist/tools/wallet.tools.js.map +1 -1
- package/dist/tools/yield-hunter.tools.d.ts.map +1 -1
- package/dist/tools/yield-hunter.tools.js +53 -48
- package/dist/tools/yield-hunter.tools.js.map +1 -1
- package/dist/transactions/bitcoin-builder.d.ts.map +1 -1
- package/dist/transactions/bitcoin-builder.js +1 -16
- package/dist/transactions/bitcoin-builder.js.map +1 -1
- package/dist/transactions/builder.d.ts +7 -2
- package/dist/transactions/builder.d.ts.map +1 -1
- package/dist/transactions/builder.js +8 -2
- package/dist/transactions/builder.js.map +1 -1
- package/dist/transactions/inscription-builder.d.ts +183 -0
- package/dist/transactions/inscription-builder.d.ts.map +1 -0
- package/dist/transactions/inscription-builder.js +230 -0
- package/dist/transactions/inscription-builder.js.map +1 -0
- package/dist/utils/bitcoin.d.ts +38 -0
- package/dist/utils/bitcoin.d.ts.map +1 -1
- package/dist/utils/bitcoin.js +59 -0
- package/dist/utils/bitcoin.js.map +1 -1
- package/dist/utils/fee.d.ts +41 -0
- package/dist/utils/fee.d.ts.map +1 -0
- package/dist/utils/fee.js +69 -0
- package/dist/utils/fee.js.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/storage.d.ts +4 -7
- package/dist/utils/storage.d.ts.map +1 -1
- package/dist/utils/storage.js +0 -30
- package/dist/utils/storage.js.map +1 -1
- package/dist/utils/tokens.d.ts +13 -0
- package/dist/utils/tokens.d.ts.map +1 -0
- package/dist/utils/tokens.js +24 -0
- package/dist/utils/tokens.js.map +1 -0
- package/dist/yield-hunter/index.d.ts.map +1 -1
- package/dist/yield-hunter/index.js +10 -15
- package/dist/yield-hunter/index.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Signing Tools
|
|
3
|
+
*
|
|
4
|
+
* These tools provide message signing capabilities for agent identity and authentication:
|
|
5
|
+
*
|
|
6
|
+
* SIP-018 (Structured Data Signing):
|
|
7
|
+
* - sip018_sign: Sign structured Clarity data (SIP-018 standard)
|
|
8
|
+
* - sip018_verify: Verify SIP-018 signature and recover signer
|
|
9
|
+
* - sip018_hash: Compute SIP-018 message hash without signing
|
|
10
|
+
*
|
|
11
|
+
* Stacks Message Signing (SIWS-Compatible):
|
|
12
|
+
* - stacks_sign_message: Sign plain text messages with Stacks prefix
|
|
13
|
+
* - stacks_verify_message: Verify message signature and recover signer
|
|
14
|
+
*
|
|
15
|
+
* Bitcoin Message Signing (BIP-137):
|
|
16
|
+
* - btc_sign_message: Sign messages with Bitcoin private key (BIP-137 format)
|
|
17
|
+
* - btc_verify_message: Verify Bitcoin message signatures
|
|
18
|
+
*
|
|
19
|
+
* SIP-018 signatures can be verified both off-chain and on-chain by smart contracts.
|
|
20
|
+
* Stacks message signatures are SIWS-compatible for web authentication flows.
|
|
21
|
+
* Bitcoin signatures use BIP-137 format compatible with most Bitcoin wallets.
|
|
22
|
+
*/
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
import { signStructuredData, hashStructuredData, encodeStructuredDataBytes, publicKeyFromSignatureRsv, getAddressFromPublicKey, signMessageHashRsv, tupleCV, stringAsciiCV, stringUtf8CV, uintCV, intCV, principalCV, bufferCV, listCV, noneCV, someCV, trueCV, falseCV, } from "@stacks/transactions";
|
|
25
|
+
import { hashMessage, verifyMessageSignatureRsv } from "@stacks/encryption";
|
|
26
|
+
import { bytesToHex } from "@stacks/common";
|
|
27
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
28
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
29
|
+
import { hex } from "@scure/base";
|
|
30
|
+
import { NETWORK } from "../config/networks.js";
|
|
31
|
+
import { createJsonResponse, createErrorResponse } from "../utils/index.js";
|
|
32
|
+
import { getWalletManager } from "../services/wallet-manager.js";
|
|
33
|
+
/**
|
|
34
|
+
* Chain IDs for SIP-018 domain (from SIP-005)
|
|
35
|
+
*/
|
|
36
|
+
const CHAIN_IDS = {
|
|
37
|
+
mainnet: 1,
|
|
38
|
+
testnet: 2147483648, // 0x80000000
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* SIP-018 structured data prefix as hex.
|
|
42
|
+
* ASCII "SIP018" = 0x534950303138
|
|
43
|
+
* Included in responses to show how the verification hash is constructed.
|
|
44
|
+
*/
|
|
45
|
+
const SIP018_MSG_PREFIX = "0x534950303138";
|
|
46
|
+
/**
|
|
47
|
+
* Stacks message signing prefix (SIWS-compatible)
|
|
48
|
+
* 'Stacks Signed Message:\n'.length === 23 (0x17 in hex)
|
|
49
|
+
* The hashMessage function from @stacks/encryption applies this internally
|
|
50
|
+
*/
|
|
51
|
+
const STACKS_MSG_PREFIX = "\x17Stacks Signed Message:\n";
|
|
52
|
+
/**
|
|
53
|
+
* Bitcoin message signing prefix (BIP-137)
|
|
54
|
+
* '\x18Bitcoin Signed Message:\n' where 0x18 = 24 (length of "Bitcoin Signed Message:\n")
|
|
55
|
+
*/
|
|
56
|
+
const BITCOIN_MSG_PREFIX = "\x18Bitcoin Signed Message:\n";
|
|
57
|
+
/**
|
|
58
|
+
* BIP-137 header byte base values for different address types.
|
|
59
|
+
* The actual header = base + recoveryId (0-3)
|
|
60
|
+
*
|
|
61
|
+
* - 27-30: P2PKH uncompressed
|
|
62
|
+
* - 31-34: P2PKH compressed
|
|
63
|
+
* - 35-38: P2SH-P2WPKH (SegWit wrapped)
|
|
64
|
+
* - 39-42: P2WPKH native SegWit (bech32)
|
|
65
|
+
*/
|
|
66
|
+
const BIP137_HEADER_BASE = {
|
|
67
|
+
P2PKH_UNCOMPRESSED: 27,
|
|
68
|
+
P2PKH_COMPRESSED: 31,
|
|
69
|
+
P2SH_P2WPKH: 35,
|
|
70
|
+
P2WPKH: 39,
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Encode a variable-length integer (Bitcoin varint format)
|
|
74
|
+
* Used for encoding message length in BIP-137
|
|
75
|
+
*/
|
|
76
|
+
function encodeVarInt(n) {
|
|
77
|
+
if (n < 0xfd) {
|
|
78
|
+
return new Uint8Array([n]);
|
|
79
|
+
}
|
|
80
|
+
else if (n <= 0xffff) {
|
|
81
|
+
const buf = new Uint8Array(3);
|
|
82
|
+
buf[0] = 0xfd;
|
|
83
|
+
buf[1] = n & 0xff;
|
|
84
|
+
buf[2] = (n >> 8) & 0xff;
|
|
85
|
+
return buf;
|
|
86
|
+
}
|
|
87
|
+
else if (n <= 0xffffffff) {
|
|
88
|
+
const buf = new Uint8Array(5);
|
|
89
|
+
buf[0] = 0xfe;
|
|
90
|
+
buf[1] = n & 0xff;
|
|
91
|
+
buf[2] = (n >> 8) & 0xff;
|
|
92
|
+
buf[3] = (n >> 16) & 0xff;
|
|
93
|
+
buf[4] = (n >> 24) & 0xff;
|
|
94
|
+
return buf;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
throw new Error("Message too long for varint encoding");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Format a message for Bitcoin signing (BIP-137)
|
|
102
|
+
* Returns: prefix || varint(message.length) || message
|
|
103
|
+
*/
|
|
104
|
+
function formatBitcoinMessage(message) {
|
|
105
|
+
const prefixBytes = new TextEncoder().encode(BITCOIN_MSG_PREFIX);
|
|
106
|
+
const messageBytes = new TextEncoder().encode(message);
|
|
107
|
+
const lengthBytes = encodeVarInt(messageBytes.length);
|
|
108
|
+
const result = new Uint8Array(prefixBytes.length + lengthBytes.length + messageBytes.length);
|
|
109
|
+
result.set(prefixBytes, 0);
|
|
110
|
+
result.set(lengthBytes, prefixBytes.length);
|
|
111
|
+
result.set(messageBytes, prefixBytes.length + lengthBytes.length);
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Double SHA-256 hash (Bitcoin standard)
|
|
116
|
+
*/
|
|
117
|
+
function doubleSha256(data) {
|
|
118
|
+
return sha256(sha256(data));
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Get Bitcoin address type from BIP-137 header byte
|
|
122
|
+
*/
|
|
123
|
+
function getAddressTypeFromHeader(header) {
|
|
124
|
+
if (header >= 27 && header <= 30)
|
|
125
|
+
return "P2PKH (uncompressed)";
|
|
126
|
+
if (header >= 31 && header <= 34)
|
|
127
|
+
return "P2PKH (compressed)";
|
|
128
|
+
if (header >= 35 && header <= 38)
|
|
129
|
+
return "P2SH-P2WPKH (SegWit wrapped)";
|
|
130
|
+
if (header >= 39 && header <= 42)
|
|
131
|
+
return "P2WPKH (native SegWit)";
|
|
132
|
+
return "Unknown";
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Extract recovery ID from BIP-137 header byte
|
|
136
|
+
*/
|
|
137
|
+
function getRecoveryIdFromHeader(header) {
|
|
138
|
+
if (header >= 27 && header <= 30)
|
|
139
|
+
return header - 27;
|
|
140
|
+
if (header >= 31 && header <= 34)
|
|
141
|
+
return header - 31;
|
|
142
|
+
if (header >= 35 && header <= 38)
|
|
143
|
+
return header - 35;
|
|
144
|
+
if (header >= 39 && header <= 42)
|
|
145
|
+
return header - 39;
|
|
146
|
+
throw new Error(`Invalid BIP-137 header byte: ${header}`);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Type guard for explicit Clarity type hint objects.
|
|
150
|
+
* Checks if value is an object with a "type" string property.
|
|
151
|
+
*/
|
|
152
|
+
function isTypedValue(value) {
|
|
153
|
+
return (value !== null &&
|
|
154
|
+
typeof value === "object" &&
|
|
155
|
+
"type" in value &&
|
|
156
|
+
typeof value.type === "string");
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Convert a JSON value to a ClarityValue.
|
|
160
|
+
*
|
|
161
|
+
* Supports explicit type hints for typed arguments:
|
|
162
|
+
* - { type: "uint", value: 100 }
|
|
163
|
+
* - { type: "int", value: -50 }
|
|
164
|
+
* - { type: "principal", value: "SP..." }
|
|
165
|
+
* - { type: "ascii", value: "hello" }
|
|
166
|
+
* - { type: "utf8", value: "hello" }
|
|
167
|
+
* - { type: "buff", value: "0x1234" }
|
|
168
|
+
* - { type: "bool", value: true }
|
|
169
|
+
* - { type: "none" }
|
|
170
|
+
* - { type: "some", value: ... }
|
|
171
|
+
* - { type: "list", value: [...] }
|
|
172
|
+
* - { type: "tuple", value: {...} }
|
|
173
|
+
*
|
|
174
|
+
* Also supports implicit conversion:
|
|
175
|
+
* - string -> stringUtf8CV
|
|
176
|
+
* - number -> intCV (signed)
|
|
177
|
+
* - boolean -> trueCV/falseCV
|
|
178
|
+
* - null -> noneCV
|
|
179
|
+
* - array -> listCV
|
|
180
|
+
* - object -> tupleCV (recursively)
|
|
181
|
+
*/
|
|
182
|
+
function jsonToClarityValue(value) {
|
|
183
|
+
// Handle explicit type hints
|
|
184
|
+
if (isTypedValue(value)) {
|
|
185
|
+
switch (value.type) {
|
|
186
|
+
case "uint":
|
|
187
|
+
if (typeof value.value !== "number" && typeof value.value !== "string") {
|
|
188
|
+
throw new Error("uint type requires a numeric value");
|
|
189
|
+
}
|
|
190
|
+
return uintCV(BigInt(value.value));
|
|
191
|
+
case "int":
|
|
192
|
+
if (typeof value.value !== "number" && typeof value.value !== "string") {
|
|
193
|
+
throw new Error("int type requires a numeric value");
|
|
194
|
+
}
|
|
195
|
+
return intCV(BigInt(value.value));
|
|
196
|
+
case "principal":
|
|
197
|
+
if (typeof value.value !== "string") {
|
|
198
|
+
throw new Error("principal type requires a string value");
|
|
199
|
+
}
|
|
200
|
+
return principalCV(value.value);
|
|
201
|
+
case "ascii":
|
|
202
|
+
if (typeof value.value !== "string") {
|
|
203
|
+
throw new Error("ascii type requires a string value");
|
|
204
|
+
}
|
|
205
|
+
return stringAsciiCV(value.value);
|
|
206
|
+
case "utf8":
|
|
207
|
+
if (typeof value.value !== "string") {
|
|
208
|
+
throw new Error("utf8 type requires a string value");
|
|
209
|
+
}
|
|
210
|
+
return stringUtf8CV(value.value);
|
|
211
|
+
case "buff":
|
|
212
|
+
case "buffer":
|
|
213
|
+
if (typeof value.value !== "string") {
|
|
214
|
+
throw new Error("buff type requires a hex string value");
|
|
215
|
+
}
|
|
216
|
+
// Support both "0x..." and raw hex
|
|
217
|
+
const hexStr = value.value.startsWith("0x")
|
|
218
|
+
? value.value.slice(2)
|
|
219
|
+
: value.value;
|
|
220
|
+
return bufferCV(Uint8Array.from(Buffer.from(hexStr, "hex")));
|
|
221
|
+
case "bool":
|
|
222
|
+
return value.value ? trueCV() : falseCV();
|
|
223
|
+
case "none":
|
|
224
|
+
return noneCV();
|
|
225
|
+
case "some":
|
|
226
|
+
return someCV(jsonToClarityValue(value.value));
|
|
227
|
+
case "list":
|
|
228
|
+
if (!Array.isArray(value.value)) {
|
|
229
|
+
throw new Error("list type requires an array value");
|
|
230
|
+
}
|
|
231
|
+
return listCV(value.value.map(jsonToClarityValue));
|
|
232
|
+
case "tuple":
|
|
233
|
+
if (typeof value.value !== "object" || value.value === null) {
|
|
234
|
+
throw new Error("tuple type requires an object value");
|
|
235
|
+
}
|
|
236
|
+
const tupleData = {};
|
|
237
|
+
for (const [k, v] of Object.entries(value.value)) {
|
|
238
|
+
tupleData[k] = jsonToClarityValue(v);
|
|
239
|
+
}
|
|
240
|
+
return tupleCV(tupleData);
|
|
241
|
+
default:
|
|
242
|
+
throw new Error(`Unknown type hint: ${value.type}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// Implicit conversion for primitives
|
|
246
|
+
if (value === null || value === undefined) {
|
|
247
|
+
return noneCV();
|
|
248
|
+
}
|
|
249
|
+
if (typeof value === "boolean") {
|
|
250
|
+
return value ? trueCV() : falseCV();
|
|
251
|
+
}
|
|
252
|
+
if (typeof value === "number") {
|
|
253
|
+
// Use intCV for implicit numbers (can be negative)
|
|
254
|
+
return intCV(BigInt(Math.floor(value)));
|
|
255
|
+
}
|
|
256
|
+
if (typeof value === "string") {
|
|
257
|
+
// Default to UTF-8 string
|
|
258
|
+
return stringUtf8CV(value);
|
|
259
|
+
}
|
|
260
|
+
if (Array.isArray(value)) {
|
|
261
|
+
return listCV(value.map(jsonToClarityValue));
|
|
262
|
+
}
|
|
263
|
+
if (typeof value === "object") {
|
|
264
|
+
const tupleData = {};
|
|
265
|
+
for (const [k, v] of Object.entries(value)) {
|
|
266
|
+
tupleData[k] = jsonToClarityValue(v);
|
|
267
|
+
}
|
|
268
|
+
return tupleCV(tupleData);
|
|
269
|
+
}
|
|
270
|
+
throw new Error(`Cannot convert value to ClarityValue: ${typeof value}`);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Build the standard SIP-018 domain tuple
|
|
274
|
+
*/
|
|
275
|
+
function buildDomainCV(name, version, chainId) {
|
|
276
|
+
return tupleCV({
|
|
277
|
+
name: stringAsciiCV(name),
|
|
278
|
+
version: stringAsciiCV(version),
|
|
279
|
+
"chain-id": uintCV(chainId),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Get the active wallet account or throw a consistent error message
|
|
284
|
+
*/
|
|
285
|
+
function requireUnlockedWallet() {
|
|
286
|
+
const walletManager = getWalletManager();
|
|
287
|
+
const account = walletManager.getActiveAccount();
|
|
288
|
+
if (!account) {
|
|
289
|
+
throw new Error("Wallet is not unlocked. Use wallet_unlock first to enable signing.");
|
|
290
|
+
}
|
|
291
|
+
return account;
|
|
292
|
+
}
|
|
293
|
+
export function registerSigningTools(server) {
|
|
294
|
+
// Sign structured data (SIP-018)
|
|
295
|
+
server.registerTool("sip018_sign", {
|
|
296
|
+
description: "Sign structured Clarity data using SIP-018 standard. " +
|
|
297
|
+
"Creates a signature that can be verified both off-chain and on-chain by smart contracts. " +
|
|
298
|
+
"Use cases: meta-transactions, off-chain voting, permits, proving address control. " +
|
|
299
|
+
"Requires an unlocked wallet.",
|
|
300
|
+
inputSchema: {
|
|
301
|
+
message: z
|
|
302
|
+
.record(z.string(), z.unknown())
|
|
303
|
+
.describe("The structured data to sign as a JSON object. " +
|
|
304
|
+
"Use type hints for explicit types: {type: 'uint', value: 100}, " +
|
|
305
|
+
"{type: 'principal', value: 'SP...'}, etc. " +
|
|
306
|
+
"Implicit conversion: strings->utf8, numbers->int, booleans->bool."),
|
|
307
|
+
domain: z
|
|
308
|
+
.object({
|
|
309
|
+
name: z.string().describe("Application name (e.g., 'My App')"),
|
|
310
|
+
version: z.string().describe("Application version (e.g., '1.0.0')"),
|
|
311
|
+
})
|
|
312
|
+
.describe("Domain binding for the signature. Prevents cross-app and cross-version replay."),
|
|
313
|
+
},
|
|
314
|
+
}, async ({ message, domain }) => {
|
|
315
|
+
try {
|
|
316
|
+
const account = requireUnlockedWallet();
|
|
317
|
+
// Build domain CV with chain-id
|
|
318
|
+
const chainId = CHAIN_IDS[NETWORK];
|
|
319
|
+
const domainCV = buildDomainCV(domain.name, domain.version, chainId);
|
|
320
|
+
// Convert message to ClarityValue
|
|
321
|
+
const messageCV = jsonToClarityValue(message);
|
|
322
|
+
// Sign the structured data
|
|
323
|
+
const signature = signStructuredData({
|
|
324
|
+
message: messageCV,
|
|
325
|
+
domain: domainCV,
|
|
326
|
+
privateKey: account.privateKey,
|
|
327
|
+
});
|
|
328
|
+
// Compute hashes for reference and verification
|
|
329
|
+
const messageHash = hashStructuredData(messageCV);
|
|
330
|
+
const domainHash = hashStructuredData(domainCV);
|
|
331
|
+
// Compute the full encoded bytes and its sha256 hash (used for signing/verification)
|
|
332
|
+
const encodedBytes = encodeStructuredDataBytes({
|
|
333
|
+
message: messageCV,
|
|
334
|
+
domain: domainCV,
|
|
335
|
+
});
|
|
336
|
+
const encodedHex = bytesToHex(encodedBytes);
|
|
337
|
+
const verificationHash = bytesToHex(sha256(encodedBytes));
|
|
338
|
+
return createJsonResponse({
|
|
339
|
+
success: true,
|
|
340
|
+
signature,
|
|
341
|
+
signatureFormat: "RSV (65 bytes hex)",
|
|
342
|
+
signer: account.address,
|
|
343
|
+
network: NETWORK,
|
|
344
|
+
chainId,
|
|
345
|
+
hashes: {
|
|
346
|
+
message: messageHash,
|
|
347
|
+
domain: domainHash,
|
|
348
|
+
encoded: encodedHex,
|
|
349
|
+
verification: verificationHash,
|
|
350
|
+
prefix: SIP018_MSG_PREFIX,
|
|
351
|
+
},
|
|
352
|
+
domain: {
|
|
353
|
+
name: domain.name,
|
|
354
|
+
version: domain.version,
|
|
355
|
+
chainId,
|
|
356
|
+
},
|
|
357
|
+
verificationNote: "Use sip018_verify with the 'verification' hash and signature to recover the signer. " +
|
|
358
|
+
"For on-chain verification, use secp256k1-recover? with sha256 of the 'encoded' hash.",
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
return createErrorResponse(error);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
// Verify SIP-018 signature
|
|
366
|
+
server.registerTool("sip018_verify", {
|
|
367
|
+
description: "Verify a SIP-018 signature and recover the signer's address. " +
|
|
368
|
+
"Takes the verification hash (from sip018_sign or sip018_hash 'verification' field) and the signature, " +
|
|
369
|
+
"then recovers the public key and derives the signer's Stacks address.",
|
|
370
|
+
inputSchema: {
|
|
371
|
+
messageHash: z
|
|
372
|
+
.string()
|
|
373
|
+
.describe("The SIP-018 verification hash (from sip018_sign/sip018_hash 'verification' field). " +
|
|
374
|
+
"This is sha256(prefix || domainHash || messageHash)."),
|
|
375
|
+
signature: z
|
|
376
|
+
.string()
|
|
377
|
+
.describe("The signature in RSV format (65 bytes hex from sip018_sign)"),
|
|
378
|
+
expectedSigner: z
|
|
379
|
+
.string()
|
|
380
|
+
.optional()
|
|
381
|
+
.describe("Optional: expected signer address to verify against. " +
|
|
382
|
+
"If provided, returns whether the signature is valid for this signer."),
|
|
383
|
+
},
|
|
384
|
+
}, async ({ messageHash, signature, expectedSigner }) => {
|
|
385
|
+
try {
|
|
386
|
+
// Recover public key from signature
|
|
387
|
+
// The signature is in RSV format, messageHash should be the full encoded hash
|
|
388
|
+
const recoveredPubKey = publicKeyFromSignatureRsv(messageHash, signature);
|
|
389
|
+
// Derive address from public key for current network
|
|
390
|
+
const recoveredAddress = getAddressFromPublicKey(recoveredPubKey, NETWORK);
|
|
391
|
+
// Check against expected signer if provided
|
|
392
|
+
const isValid = expectedSigner
|
|
393
|
+
? recoveredAddress === expectedSigner
|
|
394
|
+
: undefined;
|
|
395
|
+
return createJsonResponse({
|
|
396
|
+
success: true,
|
|
397
|
+
recoveredPublicKey: recoveredPubKey,
|
|
398
|
+
recoveredAddress,
|
|
399
|
+
network: NETWORK,
|
|
400
|
+
verification: expectedSigner
|
|
401
|
+
? {
|
|
402
|
+
expectedSigner,
|
|
403
|
+
isValid,
|
|
404
|
+
message: isValid
|
|
405
|
+
? "Signature is valid for the expected signer"
|
|
406
|
+
: "Signature does NOT match expected signer",
|
|
407
|
+
}
|
|
408
|
+
: undefined,
|
|
409
|
+
note: "The recovered address is derived from the public key recovered from the signature. " +
|
|
410
|
+
"For on-chain verification, use secp256k1-recover? and principal-of?.",
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
return createErrorResponse(error);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
// Hash structured data (SIP-018) without signing
|
|
418
|
+
server.registerTool("sip018_hash", {
|
|
419
|
+
description: "Compute the SIP-018 message hash without signing. " +
|
|
420
|
+
"Returns the full encoded hash, domain hash, and message hash. " +
|
|
421
|
+
"Useful for preparing data for on-chain verification or multi-sig coordination. " +
|
|
422
|
+
"Does not require an unlocked wallet.",
|
|
423
|
+
inputSchema: {
|
|
424
|
+
message: z
|
|
425
|
+
.record(z.string(), z.unknown())
|
|
426
|
+
.describe("The structured data as a JSON object. " +
|
|
427
|
+
"Use type hints for explicit types: {type: 'uint', value: 100}, " +
|
|
428
|
+
"{type: 'principal', value: 'SP...'}, etc."),
|
|
429
|
+
domain: z
|
|
430
|
+
.object({
|
|
431
|
+
name: z.string().describe("Application name"),
|
|
432
|
+
version: z.string().describe("Application version"),
|
|
433
|
+
chainId: z
|
|
434
|
+
.number()
|
|
435
|
+
.optional()
|
|
436
|
+
.describe("Optional chain ID. Defaults to current network (1 for mainnet, 2147483648 for testnet)"),
|
|
437
|
+
})
|
|
438
|
+
.describe("Domain binding for the hash"),
|
|
439
|
+
},
|
|
440
|
+
}, async ({ message, domain }) => {
|
|
441
|
+
try {
|
|
442
|
+
// Use provided chainId or default to current network
|
|
443
|
+
const chainId = domain.chainId ?? CHAIN_IDS[NETWORK];
|
|
444
|
+
const domainCV = buildDomainCV(domain.name, domain.version, chainId);
|
|
445
|
+
// Convert message to ClarityValue
|
|
446
|
+
const messageCV = jsonToClarityValue(message);
|
|
447
|
+
// Compute hashes
|
|
448
|
+
const messageHash = hashStructuredData(messageCV);
|
|
449
|
+
const domainHash = hashStructuredData(domainCV);
|
|
450
|
+
// Compute the full encoded bytes and its sha256 hash
|
|
451
|
+
const encodedBytes = encodeStructuredDataBytes({
|
|
452
|
+
message: messageCV,
|
|
453
|
+
domain: domainCV,
|
|
454
|
+
});
|
|
455
|
+
const encodedHex = bytesToHex(encodedBytes);
|
|
456
|
+
const verificationHash = bytesToHex(sha256(encodedBytes));
|
|
457
|
+
return createJsonResponse({
|
|
458
|
+
success: true,
|
|
459
|
+
hashes: {
|
|
460
|
+
message: messageHash,
|
|
461
|
+
domain: domainHash,
|
|
462
|
+
encoded: encodedHex,
|
|
463
|
+
verification: verificationHash,
|
|
464
|
+
},
|
|
465
|
+
hashConstruction: {
|
|
466
|
+
prefix: SIP018_MSG_PREFIX,
|
|
467
|
+
formula: "verification = sha256(prefix || domainHash || messageHash)",
|
|
468
|
+
note: "Use 'verification' hash with sip018_verify. Use 'encoded' with secp256k1-recover? on-chain.",
|
|
469
|
+
},
|
|
470
|
+
domain: {
|
|
471
|
+
name: domain.name,
|
|
472
|
+
version: domain.version,
|
|
473
|
+
chainId,
|
|
474
|
+
},
|
|
475
|
+
network: NETWORK,
|
|
476
|
+
clarityVerification: {
|
|
477
|
+
note: "For on-chain verification, use sha256 of 'encoded' with secp256k1-recover?",
|
|
478
|
+
example: "(secp256k1-recover? (sha256 encoded-data) signature)",
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
return createErrorResponse(error);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
// Sign plain text message (SIWS-compatible)
|
|
487
|
+
server.registerTool("stacks_sign_message", {
|
|
488
|
+
description: "Sign a plain text message using the Stacks message signing format. " +
|
|
489
|
+
"The message is prefixed with '\\x17Stacks Signed Message:\\n' before hashing (SIWS-compatible). " +
|
|
490
|
+
"Use cases: proving address ownership, authentication, sign-in flows. " +
|
|
491
|
+
"Requires an unlocked wallet.",
|
|
492
|
+
inputSchema: {
|
|
493
|
+
message: z
|
|
494
|
+
.string()
|
|
495
|
+
.describe("The plain text message to sign. Will be prefixed with Stacks message prefix before signing."),
|
|
496
|
+
},
|
|
497
|
+
}, async ({ message }) => {
|
|
498
|
+
try {
|
|
499
|
+
const account = requireUnlockedWallet();
|
|
500
|
+
// Hash the message with the Stacks prefix
|
|
501
|
+
const msgHash = hashMessage(message);
|
|
502
|
+
const msgHashHex = bytesToHex(msgHash);
|
|
503
|
+
// Sign the message hash
|
|
504
|
+
const signature = signMessageHashRsv({
|
|
505
|
+
messageHash: msgHashHex,
|
|
506
|
+
privateKey: account.privateKey,
|
|
507
|
+
});
|
|
508
|
+
return createJsonResponse({
|
|
509
|
+
success: true,
|
|
510
|
+
signature,
|
|
511
|
+
signatureFormat: "RSV (65 bytes hex)",
|
|
512
|
+
signer: account.address,
|
|
513
|
+
network: NETWORK,
|
|
514
|
+
message: {
|
|
515
|
+
original: message,
|
|
516
|
+
prefix: STACKS_MSG_PREFIX,
|
|
517
|
+
prefixHex: bytesToHex(new TextEncoder().encode(STACKS_MSG_PREFIX)),
|
|
518
|
+
hash: msgHashHex,
|
|
519
|
+
},
|
|
520
|
+
verificationNote: "Use stacks_verify_message with the original message and signature to verify. " +
|
|
521
|
+
"Compatible with SIWS (Sign In With Stacks) authentication flows.",
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
return createErrorResponse(error);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
// Verify plain text message signature (SIWS-compatible)
|
|
529
|
+
server.registerTool("stacks_verify_message", {
|
|
530
|
+
description: "Verify a Stacks message signature and recover the signer's address. " +
|
|
531
|
+
"Takes the original message and signature, applies the Stacks prefix, and verifies. " +
|
|
532
|
+
"Compatible with SIWS (Sign In With Stacks) authentication flows.",
|
|
533
|
+
inputSchema: {
|
|
534
|
+
message: z
|
|
535
|
+
.string()
|
|
536
|
+
.describe("The original plain text message that was signed."),
|
|
537
|
+
signature: z
|
|
538
|
+
.string()
|
|
539
|
+
.describe("The signature in RSV format (65 bytes hex from stacks_sign_message or wallet signature)."),
|
|
540
|
+
expectedSigner: z
|
|
541
|
+
.string()
|
|
542
|
+
.optional()
|
|
543
|
+
.describe("Optional: expected signer address to verify against. " +
|
|
544
|
+
"If provided, returns whether the signature is valid for this signer."),
|
|
545
|
+
},
|
|
546
|
+
}, async ({ message, signature, expectedSigner }) => {
|
|
547
|
+
try {
|
|
548
|
+
// Hash the message with the Stacks prefix
|
|
549
|
+
const messageHash = hashMessage(message);
|
|
550
|
+
const messageHashHex = bytesToHex(messageHash);
|
|
551
|
+
// Recover public key from signature
|
|
552
|
+
const recoveredPubKey = publicKeyFromSignatureRsv(messageHashHex, signature);
|
|
553
|
+
// Derive address from public key for current network
|
|
554
|
+
const recoveredAddress = getAddressFromPublicKey(recoveredPubKey, NETWORK);
|
|
555
|
+
// Verify the signature using the encryption library
|
|
556
|
+
const signatureValid = verifyMessageSignatureRsv({
|
|
557
|
+
signature,
|
|
558
|
+
message,
|
|
559
|
+
publicKey: recoveredPubKey,
|
|
560
|
+
});
|
|
561
|
+
// Check against expected signer if provided
|
|
562
|
+
const signerMatches = expectedSigner
|
|
563
|
+
? recoveredAddress === expectedSigner
|
|
564
|
+
: undefined;
|
|
565
|
+
const isFullyValid = signatureValid && (expectedSigner ? signerMatches : true);
|
|
566
|
+
return createJsonResponse({
|
|
567
|
+
success: true,
|
|
568
|
+
signatureValid,
|
|
569
|
+
recoveredPublicKey: recoveredPubKey,
|
|
570
|
+
recoveredAddress,
|
|
571
|
+
network: NETWORK,
|
|
572
|
+
message: {
|
|
573
|
+
original: message,
|
|
574
|
+
prefix: STACKS_MSG_PREFIX,
|
|
575
|
+
hash: messageHashHex,
|
|
576
|
+
},
|
|
577
|
+
verification: expectedSigner
|
|
578
|
+
? {
|
|
579
|
+
expectedSigner,
|
|
580
|
+
signerMatches,
|
|
581
|
+
isFullyValid,
|
|
582
|
+
message: isFullyValid
|
|
583
|
+
? "Signature is valid and matches expected signer"
|
|
584
|
+
: signatureValid
|
|
585
|
+
? "Signature is valid but does NOT match expected signer"
|
|
586
|
+
: "Signature is invalid",
|
|
587
|
+
}
|
|
588
|
+
: undefined,
|
|
589
|
+
note: "The recovered address is derived from the public key recovered from the signature. " +
|
|
590
|
+
"Compatible with SIWS (Sign In With Stacks) authentication flows.",
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
catch (error) {
|
|
594
|
+
return createErrorResponse(error);
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
// Sign Bitcoin message (BIP-137)
|
|
598
|
+
server.registerTool("btc_sign_message", {
|
|
599
|
+
description: "Sign a plain text message using the Bitcoin message signing format (BIP-137). " +
|
|
600
|
+
"Creates a 65-byte signature compatible with most Bitcoin wallets. " +
|
|
601
|
+
"Use cases: proving Bitcoin address ownership, authentication, off-chain verification. " +
|
|
602
|
+
"Requires an unlocked wallet with Bitcoin keys.",
|
|
603
|
+
inputSchema: {
|
|
604
|
+
message: z
|
|
605
|
+
.string()
|
|
606
|
+
.describe("The plain text message to sign. Will be formatted with Bitcoin message prefix before signing."),
|
|
607
|
+
},
|
|
608
|
+
}, async ({ message }) => {
|
|
609
|
+
try {
|
|
610
|
+
const account = requireUnlockedWallet();
|
|
611
|
+
if (!account.btcPrivateKey || !account.btcPublicKey) {
|
|
612
|
+
throw new Error("Bitcoin keys not available. Ensure the wallet has Bitcoin key derivation.");
|
|
613
|
+
}
|
|
614
|
+
// Format and hash the message according to BIP-137
|
|
615
|
+
const formattedMsg = formatBitcoinMessage(message);
|
|
616
|
+
const msgHash = doubleSha256(formattedMsg);
|
|
617
|
+
// Sign with recoverable signature
|
|
618
|
+
// format: 'recovered' returns 65 bytes: [recoveryId][32 r][32 s]
|
|
619
|
+
const sigWithRecovery = secp256k1.sign(msgHash, account.btcPrivateKey, {
|
|
620
|
+
prehash: false,
|
|
621
|
+
lowS: true,
|
|
622
|
+
format: "recovered",
|
|
623
|
+
});
|
|
624
|
+
// Build BIP-137 signature: [header][r][s]
|
|
625
|
+
// For P2WPKH (native SegWit), header = 39 + recoveryId
|
|
626
|
+
const recoveryId = sigWithRecovery[0];
|
|
627
|
+
const header = BIP137_HEADER_BASE.P2WPKH + recoveryId;
|
|
628
|
+
// Build the 65-byte BIP-137 signature: [header][r][s]
|
|
629
|
+
const rBytes = sigWithRecovery.slice(1, 33);
|
|
630
|
+
const sBytes = sigWithRecovery.slice(33, 65);
|
|
631
|
+
const bip137Sig = new Uint8Array(65);
|
|
632
|
+
bip137Sig[0] = header;
|
|
633
|
+
bip137Sig.set(rBytes, 1);
|
|
634
|
+
bip137Sig.set(sBytes, 33);
|
|
635
|
+
const signatureHex = hex.encode(bip137Sig);
|
|
636
|
+
const signatureBase64 = Buffer.from(bip137Sig).toString("base64");
|
|
637
|
+
return createJsonResponse({
|
|
638
|
+
success: true,
|
|
639
|
+
signature: signatureHex,
|
|
640
|
+
signatureBase64,
|
|
641
|
+
signatureFormat: "BIP-137 (65 bytes: 1 header + 32 r + 32 s)",
|
|
642
|
+
signer: account.btcAddress,
|
|
643
|
+
network: NETWORK,
|
|
644
|
+
addressType: "P2WPKH (native SegWit)",
|
|
645
|
+
message: {
|
|
646
|
+
original: message,
|
|
647
|
+
prefix: BITCOIN_MSG_PREFIX,
|
|
648
|
+
prefixHex: hex.encode(new TextEncoder().encode(BITCOIN_MSG_PREFIX)),
|
|
649
|
+
formattedHex: hex.encode(formattedMsg),
|
|
650
|
+
hash: hex.encode(msgHash),
|
|
651
|
+
},
|
|
652
|
+
header: {
|
|
653
|
+
value: header,
|
|
654
|
+
recoveryId,
|
|
655
|
+
addressType: getAddressTypeFromHeader(header),
|
|
656
|
+
},
|
|
657
|
+
verificationNote: "Use btc_verify_message with the original message and signature to verify. " +
|
|
658
|
+
"Base64 format is commonly used by wallets like Electrum and Bitcoin Core.",
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
catch (error) {
|
|
662
|
+
return createErrorResponse(error);
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
// Verify Bitcoin message signature (BIP-137)
|
|
666
|
+
server.registerTool("btc_verify_message", {
|
|
667
|
+
description: "Verify a BIP-137 Bitcoin message signature and recover the signer's address. " +
|
|
668
|
+
"Takes the original message and signature (hex or base64), recovers the public key, " +
|
|
669
|
+
"and derives the Bitcoin address. Compatible with signatures from most Bitcoin wallets.",
|
|
670
|
+
inputSchema: {
|
|
671
|
+
message: z
|
|
672
|
+
.string()
|
|
673
|
+
.describe("The original plain text message that was signed."),
|
|
674
|
+
signature: z
|
|
675
|
+
.string()
|
|
676
|
+
.describe("The BIP-137 signature (65 bytes as hex or base64). " +
|
|
677
|
+
"Hex format: 130 characters. Base64 format: 88 characters."),
|
|
678
|
+
expectedSigner: z
|
|
679
|
+
.string()
|
|
680
|
+
.optional()
|
|
681
|
+
.describe("Optional: expected signer Bitcoin address to verify against. " +
|
|
682
|
+
"If provided, returns whether the signature is valid for this address."),
|
|
683
|
+
},
|
|
684
|
+
}, async ({ message, signature, expectedSigner }) => {
|
|
685
|
+
try {
|
|
686
|
+
// Parse signature from hex or base64
|
|
687
|
+
let signatureBytes;
|
|
688
|
+
// Try to detect format
|
|
689
|
+
if (signature.length === 130 && /^[0-9a-fA-F]+$/.test(signature)) {
|
|
690
|
+
// Hex format (65 bytes = 130 hex chars)
|
|
691
|
+
signatureBytes = hex.decode(signature);
|
|
692
|
+
}
|
|
693
|
+
else if (signature.length === 88 && /^[A-Za-z0-9+/=]+$/.test(signature)) {
|
|
694
|
+
// Base64 format
|
|
695
|
+
signatureBytes = new Uint8Array(Buffer.from(signature, "base64"));
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
// Try hex first, then base64
|
|
699
|
+
try {
|
|
700
|
+
signatureBytes = hex.decode(signature);
|
|
701
|
+
}
|
|
702
|
+
catch {
|
|
703
|
+
signatureBytes = new Uint8Array(Buffer.from(signature, "base64"));
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (signatureBytes.length !== 65) {
|
|
707
|
+
throw new Error(`Invalid signature length: ${signatureBytes.length} bytes. Expected 65 bytes.`);
|
|
708
|
+
}
|
|
709
|
+
// Extract header and signature components
|
|
710
|
+
const header = signatureBytes[0];
|
|
711
|
+
const rBytes = signatureBytes.slice(1, 33);
|
|
712
|
+
const sBytes = signatureBytes.slice(33, 65);
|
|
713
|
+
// Get recovery ID and address type from header
|
|
714
|
+
const recoveryId = getRecoveryIdFromHeader(header);
|
|
715
|
+
const addressType = getAddressTypeFromHeader(header);
|
|
716
|
+
// Format the message and hash it
|
|
717
|
+
const formattedMessage = formatBitcoinMessage(message);
|
|
718
|
+
const messageHash = doubleSha256(formattedMessage);
|
|
719
|
+
// Recover public key from signature
|
|
720
|
+
// Create signature object from r, s, and recovery
|
|
721
|
+
const r = BigInt("0x" + hex.encode(rBytes));
|
|
722
|
+
const s = BigInt("0x" + hex.encode(sBytes));
|
|
723
|
+
const sig = new secp256k1.Signature(r, s, recoveryId);
|
|
724
|
+
const recoveredPoint = sig.recoverPublicKey(messageHash);
|
|
725
|
+
const recoveredPubKey = recoveredPoint.toBytes(true); // compressed
|
|
726
|
+
// Verify the signature
|
|
727
|
+
const isValidSig = secp256k1.verify(sig.toBytes(), messageHash, recoveredPubKey, { prehash: false });
|
|
728
|
+
// Derive Bitcoin address from public key
|
|
729
|
+
// Import btc-signer for address derivation
|
|
730
|
+
const btc = await import("@scure/btc-signer");
|
|
731
|
+
const btcNetwork = NETWORK === "testnet" ? btc.TEST_NETWORK : btc.NETWORK;
|
|
732
|
+
const p2wpkh = btc.p2wpkh(recoveredPubKey, btcNetwork);
|
|
733
|
+
const recoveredAddress = p2wpkh.address;
|
|
734
|
+
// Check against expected signer if provided
|
|
735
|
+
const signerMatches = expectedSigner
|
|
736
|
+
? recoveredAddress === expectedSigner
|
|
737
|
+
: undefined;
|
|
738
|
+
const isFullyValid = isValidSig && (expectedSigner ? signerMatches : true);
|
|
739
|
+
return createJsonResponse({
|
|
740
|
+
success: true,
|
|
741
|
+
signatureValid: isValidSig,
|
|
742
|
+
recoveredPublicKey: hex.encode(recoveredPubKey),
|
|
743
|
+
recoveredAddress,
|
|
744
|
+
network: NETWORK,
|
|
745
|
+
message: {
|
|
746
|
+
original: message,
|
|
747
|
+
prefix: BITCOIN_MSG_PREFIX,
|
|
748
|
+
hash: hex.encode(messageHash),
|
|
749
|
+
},
|
|
750
|
+
header: {
|
|
751
|
+
value: header,
|
|
752
|
+
recoveryId,
|
|
753
|
+
addressType,
|
|
754
|
+
},
|
|
755
|
+
verification: expectedSigner
|
|
756
|
+
? {
|
|
757
|
+
expectedSigner,
|
|
758
|
+
signerMatches,
|
|
759
|
+
isFullyValid,
|
|
760
|
+
message: isFullyValid
|
|
761
|
+
? "Signature is valid and matches expected signer"
|
|
762
|
+
: isValidSig
|
|
763
|
+
? "Signature is valid but does NOT match expected signer"
|
|
764
|
+
: "Signature is invalid",
|
|
765
|
+
}
|
|
766
|
+
: undefined,
|
|
767
|
+
note: "The recovered address is derived from the public key recovered from the signature. " +
|
|
768
|
+
"BIP-137 signatures are compatible with most Bitcoin wallets (Electrum, Bitcoin Core, etc.).",
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
catch (error) {
|
|
772
|
+
return createErrorResponse(error);
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
//# sourceMappingURL=signing.tools.js.map
|