@lerna-labs/hydra-sdk 1.0.0-beta.13 → 1.0.0-beta.14

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.
@@ -1,10 +1,22 @@
1
- import { HydraInstance, HydraProvider } from "@meshsdk/hydra";
2
- import { BlockfrostProvider } from "@meshsdk/core";
3
- const BLOCKFROST_KEY = process.env.BLOCKFROST_API_KEY;
4
- if (!BLOCKFROST_KEY)
5
- throw new Error("BLOCKFROST_API_KEY not set");
1
+ import { BlockfrostProvider } from '@meshsdk/core';
2
+ import { HydraInstance, HydraProvider } from '@meshsdk/hydra';
3
+ import { requireEnv } from '../config.js';
4
+ /**
5
+ * High-level controller for Hydra head lifecycle operations.
6
+ *
7
+ * Wraps `HydraProvider` and `HydraInstance` to provide a simplified API
8
+ * for initializing, opening, and closing a Hydra head.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const wrangler = new Wrangler("http://localhost:4001");
13
+ * await wrangler.waitForHeadOpen({
14
+ * utxos: [{ txHash: "abc...", outputIndex: 0 }],
15
+ * blueprintTx: { type: "Tx ConwayEra", cborHex: "...", description: "" },
16
+ * });
17
+ * ```
18
+ */
6
19
  export class Wrangler {
7
- BLOCKFROST_KEY;
8
20
  mode;
9
21
  provider;
10
22
  instance;
@@ -12,10 +24,9 @@ export class Wrangler {
12
24
  url;
13
25
  wsUrl;
14
26
  constructor(url, wsUrl) {
15
- this.url = url || process.env.HYDRA_API_URL;
16
- this.wsUrl = wsUrl || process.env.HYDRA_WS_URL;
17
- this.BLOCKFROST_KEY = process.env.BLOCKFROST_API_KEY;
18
- this.blockfrost = new BlockfrostProvider(this.BLOCKFROST_KEY);
27
+ this.url = url || requireEnv('HYDRA_API_URL');
28
+ this.wsUrl = wsUrl || requireEnv('HYDRA_WS_URL');
29
+ this.blockfrost = new BlockfrostProvider(requireEnv('BLOCKFROST_API_KEY'));
19
30
  this.provider = this.createHydraProvider();
20
31
  this.instance = this.createHydraInstance();
21
32
  }
@@ -29,207 +40,243 @@ export class Wrangler {
29
40
  submitter: this.provider,
30
41
  });
31
42
  }
32
- async connect() {
33
- return await this.provider.connect();
34
- }
35
- async startHead(txHash, txIndex) {
36
- this.mode = "start";
37
- this.provider.onMessage(msg => this.handleIncoming(msg, { txHash, txIndex }));
38
- await this.provider.connect();
39
- }
40
- async shutdownHead() {
41
- this.mode = "shutdown";
42
- this.provider.onMessage(msg => this.handleIncoming(msg));
43
- await this.provider.connect();
44
- }
45
- async waitForHeadClose(timeoutMs) {
46
- this.mode = "shutdown";
47
- return new Promise(async (resolve, reject) => {
48
- let settled = false;
49
- const handle = async (message) => {
50
- try {
51
- console.log("Message received: ", message.tag, message);
52
- switch (message.tag) {
53
- case "HeadIsClosed":
54
- case "HeadIsFinalized":
55
- if (settled)
56
- return;
57
- settled = true;
58
- resolve();
59
- break;
60
- case "ReadyToFanout":
61
- if (settled)
62
- return;
63
- await this.provider.fanout();
64
- break;
65
- case "Greetings":
66
- await this.onGreetings(message.headStatus);
67
- break;
68
- }
69
- }
70
- catch (err) {
71
- if (!settled) {
72
- settled = true;
73
- reject(err);
74
- }
75
- }
76
- };
77
- this.provider.onMessage(handle);
43
+ /**
44
+ * Connect to the Hydra node with exponential-backoff retry.
45
+ *
46
+ * Uses `HydraProvider.isConnected()` which establishes the WebSocket
47
+ * **and** waits for the Hydra `Greetings` handshake, unlike the raw
48
+ * `connect()` which only opens the socket.
49
+ *
50
+ * @param maxAttempts - Maximum number of connection attempts (default 5).
51
+ * @param baseDelayMs - Initial retry delay in milliseconds (default 1000). Doubles each attempt, capped at 30 s.
52
+ */
53
+ async connectWithRetry(maxAttempts = 5, baseDelayMs = 1000) {
54
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
78
55
  try {
79
- await this.provider.connect();
56
+ const connected = await this.provider.isConnected();
57
+ if (connected)
58
+ return;
59
+ throw new Error('isConnected() returned false');
80
60
  }
81
61
  catch (err) {
82
- if (!settled) {
83
- settled = true;
84
- return reject(new Error("Failed to connect to Hydra provider: " + String(err)));
62
+ if (attempt === maxAttempts - 1) {
63
+ throw new Error(`Failed to connect after ${maxAttempts} attempts: ${String(err)}`);
85
64
  }
65
+ const delay = Math.min(baseDelayMs * 2 ** attempt, 30_000);
66
+ await new Promise((r) => setTimeout(r, delay));
86
67
  }
87
- const timer = setTimeout(() => {
88
- if (!settled) {
89
- settled = true;
90
- reject(new Error("Timeout waiting for head to close!"));
91
- }
92
- }, timeoutMs);
93
- const finalizer = () => clearTimeout(timer);
94
- const origResolve = resolve;
95
- const origReject = reject;
96
- resolve = (v) => {
97
- finalizer();
98
- origResolve(v);
99
- };
100
- reject = (e) => {
101
- finalizer();
102
- origReject(e);
68
+ }
69
+ }
70
+ /**
71
+ * Shared helper for promise-based methods that wait for a specific
72
+ * Hydra message. Handles connection, timeout, and settlement in one place.
73
+ */
74
+ awaitMessage(handler, timeoutMs, timeoutMessage) {
75
+ return new Promise((resolve, reject) => {
76
+ let settled = false;
77
+ const settle = (fn, value) => {
78
+ if (settled)
79
+ return;
80
+ settled = true;
81
+ clearTimeout(timer);
82
+ fn(value);
103
83
  };
84
+ this.provider.onMessage((message) => {
85
+ handler(message, (value) => settle(resolve, value), (reason) => settle(reject, reason));
86
+ });
87
+ const timer = setTimeout(() => settle(reject, new Error(timeoutMessage)), timeoutMs);
88
+ this.connectWithRetry().catch((err) => settle(reject, new Error(`Failed to connect: ${String(err)}`)));
104
89
  });
105
90
  }
91
+ /** Connect the underlying HydraProvider WebSocket with retry logic. */
92
+ async connect() {
93
+ return await this.connectWithRetry();
94
+ }
95
+ /** Disconnect the underlying HydraProvider WebSocket. */
96
+ async disconnect(timeout) {
97
+ return this.provider.disconnect(timeout);
98
+ }
99
+ /** Return the current HydraProvider connection/head status. */
100
+ getStatus() {
101
+ return this.provider.getStatus();
102
+ }
103
+ /** Register a callback for HydraProvider status changes. */
104
+ onStatusChange(callback) {
105
+ return this.provider.onStatusChange(callback);
106
+ }
107
+ /** Begin the head-opening sequence: init, commit, and listen for state changes. */
108
+ async startHead(commitArgs) {
109
+ this.mode = 'start';
110
+ this.provider.onMessage((msg) => this.handleIncoming(msg, commitArgs));
111
+ await this.connectWithRetry();
112
+ }
113
+ /** Begin the head-closing sequence: close, fanout, and finalize. */
114
+ async shutdownHead() {
115
+ this.mode = 'shutdown';
116
+ this.provider.onMessage((msg) => this.handleIncoming(msg));
117
+ await this.connectWithRetry();
118
+ }
119
+ /**
120
+ * Wait for the Hydra head to fully close and finalize.
121
+ * @param timeoutMs - Maximum time to wait in milliseconds.
122
+ */
123
+ async waitForHeadClose(timeoutMs = 180000) {
124
+ this.mode = 'shutdown';
125
+ return this.awaitMessage((message, resolve, reject) => {
126
+ switch (message.tag) {
127
+ case 'HeadIsClosed':
128
+ case 'HeadIsFinalized':
129
+ resolve();
130
+ break;
131
+ case 'ReadyToFanout':
132
+ this.provider.fanout();
133
+ break;
134
+ case 'Greetings':
135
+ this.onGreetings(message.headStatus).catch((err) => reject(new Error(`Greetings handler failed: ${String(err)}`)));
136
+ break;
137
+ }
138
+ }, timeoutMs, 'Timeout waiting for head to close!');
139
+ }
140
+ /**
141
+ * Wait for the Hydra head to reach the `Open` state.
142
+ * @param commitArgs - UTxO to commit into the head during initialization.
143
+ * @param timeoutMs - Maximum time to wait in milliseconds.
144
+ */
106
145
  async waitForHeadOpen(commitArgs, timeoutMs = 180000) {
107
- this.mode = "start";
108
- return new Promise(async (resolve, reject) => {
109
- let settled = false;
110
- const handle = async (message) => {
111
- try {
112
- if (message.tag === "HeadIsOpen") {
113
- if (settled)
114
- return;
115
- settled = true;
116
- resolve();
117
- }
118
- else if (message.tag === "HeadIsInitializing") {
119
- if (!commitArgs)
120
- return;
121
- await this.doCommit(commitArgs);
122
- }
123
- else if (message.tag === "Greetings") {
124
- await this.onGreetings(message.headStatus, commitArgs);
125
- }
126
- }
127
- catch (err) {
128
- if (!settled) {
129
- settled = true;
130
- reject(err);
131
- }
132
- }
133
- };
134
- this.provider.onMessage(handle);
135
- try {
136
- await this.provider.connect();
146
+ this.mode = 'start';
147
+ return this.awaitMessage((message, resolve, reject) => {
148
+ if (message.tag === 'HeadIsOpen') {
149
+ resolve();
137
150
  }
138
- catch (err) {
139
- if (!settled) {
140
- settled = true;
141
- return reject(new Error("Failed to connect to Hydra provider: " + String(err)));
142
- }
151
+ else if (message.tag === 'HeadIsInitializing') {
152
+ if (!commitArgs)
153
+ return;
154
+ this.doCommit(commitArgs).catch((err) => reject(new Error(`Commit failed: ${String(err)}`)));
143
155
  }
144
- const timer = setTimeout(() => {
145
- if (!settled) {
146
- settled = true;
147
- reject(new Error("Timeout waiting for head to open"));
148
- }
149
- }, timeoutMs);
150
- const finalizer = () => clearTimeout(timer);
151
- const origResolve = resolve;
152
- const origReject = reject;
153
- resolve = (v) => {
154
- finalizer();
155
- origResolve(v);
156
- };
157
- reject = (e) => {
158
- finalizer();
159
- origReject(e);
160
- };
161
- });
156
+ else if (message.tag === 'Greetings') {
157
+ this.onGreetings(message.headStatus, commitArgs).catch((err) => reject(new Error(`Greetings handler failed: ${String(err)}`)));
158
+ }
159
+ }, timeoutMs, 'Timeout waiting for head to open');
162
160
  }
161
+ /**
162
+ * Query the current Hydra head status via a `Greetings` message.
163
+ * @param timeoutMs - Maximum time to wait for the status response.
164
+ * @returns The head status string (e.g. `"Idle"`, `"Open"`, `"Closed"`).
165
+ */
163
166
  async getHeadStatus(timeoutMs = 5000) {
164
- return new Promise(async (resolve, reject) => {
165
- let settled = false;
166
- const handle = (message) => {
167
- if (settled)
168
- return;
169
- if (message.tag === "Greetings") {
170
- settled = true;
171
- resolve(message.headStatus);
172
- }
173
- };
174
- this.provider.onMessage(handle);
175
- try {
176
- await this.provider.connect();
167
+ return this.awaitMessage((message, resolve, _reject) => {
168
+ if (message.tag === 'Greetings') {
169
+ resolve(message.headStatus);
177
170
  }
178
- catch (err) {
179
- if (!settled) {
180
- settled = true;
181
- return reject(new Error("Failed to connect to Hydra provider: " + String(err)));
182
- }
183
- }
184
- const timer = setTimeout(() => {
185
- if (!settled) {
186
- settled = true;
187
- reject(new Error("Timeout waiting for head to open"));
188
- }
189
- }, timeoutMs);
190
- const finalizer = () => clearTimeout(timer);
191
- const origResolve = resolve;
192
- const origReject = reject;
193
- resolve = (v) => {
194
- finalizer();
195
- origResolve(v);
196
- };
197
- reject = (e) => {
198
- finalizer();
199
- origReject(e);
200
- };
201
- });
171
+ }, timeoutMs, 'Timeout waiting for head status');
202
172
  }
203
173
  async doCommit(commitArgs) {
204
- try {
205
- const rawTx = await this.instance.commitFunds(commitArgs.txHash, commitArgs.txIndex);
206
- return await this.blockfrost.submitTx(rawTx);
174
+ let rawTx;
175
+ if (commitArgs.blueprintTx) {
176
+ rawTx = await this.instance.commitBlueprintUTxOs(commitArgs.utxos, commitArgs.blueprintTx);
177
+ }
178
+ else if (commitArgs.utxos.length === 0) {
179
+ rawTx = await this.instance.commitEmpty();
180
+ }
181
+ else if (commitArgs.utxos.length === 1) {
182
+ const { txHash, outputIndex } = commitArgs.utxos[0];
183
+ rawTx = await this.instance.commitFunds(txHash, outputIndex);
184
+ }
185
+ else {
186
+ throw new Error('Multiple UTxOs without a blueprintTx require a blueprint transaction');
187
+ }
188
+ return await this.blockfrost.submitTx(rawTx);
189
+ }
190
+ /**
191
+ * Decommit funds from an open Hydra head back to L1.
192
+ *
193
+ * Posts the decommit transaction via `provider.publishDecommit()` (HTTP POST)
194
+ * instead of `provider.decommit()` to avoid overwriting the Wrangler's
195
+ * `onMessage` handler (single-callback replacement pattern).
196
+ *
197
+ * Resolves on `DecommitApproved` — L1 settlement happens asynchronously.
198
+ *
199
+ * @param transaction - The decommit transaction (CBOR-encoded).
200
+ * @param timeoutMs - Maximum time to wait for approval (default 60s).
201
+ */
202
+ async decommit(transaction, timeoutMs = 60000) {
203
+ const status = await this.getHeadStatus();
204
+ if (status !== 'Open') {
205
+ throw new Error(`Cannot decommit: head is "${status}", expected "Open"`);
206
+ }
207
+ const result = this.awaitMessage((message, resolve, reject) => {
208
+ if (message.tag === 'DecommitApproved') {
209
+ resolve();
210
+ }
211
+ else if (message.tag === 'DecommitInvalid') {
212
+ reject(new Error(`Decommit invalid: ${JSON.stringify(message.decommitInvalidReason)}`));
213
+ }
214
+ }, timeoutMs, 'Timeout waiting for decommit approval');
215
+ await this.provider.publishDecommit(transaction);
216
+ return result;
217
+ }
218
+ /**
219
+ * Incrementally commit funds into an already-open Hydra head.
220
+ *
221
+ * Only single-UTxO commits are supported (MeshJS limitation).
222
+ * The raw L1 transaction is submitted to Blockfrost automatically.
223
+ *
224
+ * Resolves on `CommitFinalized`.
225
+ *
226
+ * @param commitArgs - Single UTxO (with optional blueprint) to commit.
227
+ * @param timeoutMs - Maximum time to wait for finalization (default 120s).
228
+ */
229
+ async incrementalCommit(commitArgs, timeoutMs = 120000) {
230
+ const status = await this.getHeadStatus();
231
+ if (status !== 'Open') {
232
+ throw new Error(`Cannot incrementally commit: head is "${status}", expected "Open"`);
233
+ }
234
+ await this.doIncrementalCommit(commitArgs);
235
+ return this.awaitMessage((message, resolve, _reject) => {
236
+ if (message.tag === 'CommitFinalized') {
237
+ resolve();
238
+ }
239
+ }, timeoutMs, 'Timeout waiting for incremental commit finalization');
240
+ }
241
+ async doIncrementalCommit(commitArgs) {
242
+ if (commitArgs.utxos.length !== 1) {
243
+ throw new Error('Incremental commit requires exactly one UTxO');
207
244
  }
208
- catch (err) {
209
- console.error(`Commit error`, err);
210
- return false;
245
+ const { txHash, outputIndex } = commitArgs.utxos[0];
246
+ let rawTx;
247
+ if (commitArgs.blueprintTx) {
248
+ rawTx = await this.instance.incrementalBlueprintCommit(txHash, outputIndex, commitArgs.blueprintTx);
211
249
  }
250
+ else {
251
+ rawTx = await this.instance.incrementalCommitFunds(txHash, outputIndex);
252
+ }
253
+ return await this.blockfrost.submitTx(rawTx);
212
254
  }
213
255
  async handleIncoming(message, commitArgs) {
214
- if (message.tag === "Greetings") {
256
+ if (message.tag === 'Greetings') {
215
257
  await this.onGreetings(message.headStatus, commitArgs);
216
258
  }
217
259
  else {
218
260
  switch (this.mode) {
219
- case "start":
220
- if (message.tag === "HeadIsInitializing") {
261
+ case 'start':
262
+ if (message.tag === 'HeadIsInitializing') {
221
263
  if (commitArgs === undefined) {
222
- console.error("No commit arguments specified... aborting commit!");
264
+ console.error('No commit arguments specified... aborting commit!');
223
265
  return;
224
266
  }
225
- await this.doCommit(commitArgs);
267
+ try {
268
+ await this.doCommit(commitArgs);
269
+ }
270
+ catch (err) {
271
+ console.error('Commit failed during startHead:', err);
272
+ }
226
273
  }
227
- if (message.tag === "HeadIsOpen") {
274
+ if (message.tag === 'HeadIsOpen') {
228
275
  // Successfully started the head here... close gracefully?
229
276
  }
230
277
  break;
231
- case "shutdown":
232
- if (message.tag === "ReadyToFanout") {
278
+ case 'shutdown':
279
+ if (message.tag === 'ReadyToFanout') {
233
280
  await this.provider.fanout();
234
281
  }
235
282
  break;
@@ -238,35 +285,35 @@ export class Wrangler {
238
285
  }
239
286
  async onGreetings(status, commitArgs) {
240
287
  switch (this.mode) {
241
- case "start":
288
+ case 'start':
242
289
  switch (status) {
243
- case "Idle":
244
- console.log("Idle → init()");
290
+ case 'Idle':
291
+ console.log('Idle → init()');
245
292
  await this.provider.init();
246
293
  break;
247
- case "Initializing":
248
- console.log("Initializing -> commit()");
294
+ case 'Initializing':
295
+ console.log('Initializing -> commit()');
249
296
  if (commitArgs === undefined) {
250
- console.error("No commit arguments specified... aborting commit!");
297
+ console.error('No commit arguments specified... aborting commit!');
251
298
  return;
252
299
  }
253
300
  await this.doCommit(commitArgs);
254
301
  break;
255
- case "Open":
256
- console.log("Open → already ready, proceeding");
302
+ case 'Open':
303
+ console.log('Open → already ready, proceeding');
257
304
  break;
258
305
  default:
259
306
  console.log(`Greetings in start mode, ignoring status: ${status}`);
260
307
  }
261
308
  break;
262
- case "shutdown":
309
+ case 'shutdown':
263
310
  switch (status) {
264
- case "Open":
265
- console.log("Shutting down: closing head…");
311
+ case 'Open':
312
+ console.log('Shutting down: closing head…');
266
313
  await this.provider.close();
267
314
  break;
268
- case "FanoutPossible":
269
- console.log("Fanout now possible: fanning out…");
315
+ case 'FanoutPossible':
316
+ console.log('Fanout now possible: fanning out…');
270
317
  await this.provider.fanout();
271
318
  break;
272
319
  default:
package/dist/test.js CHANGED
@@ -14,8 +14,8 @@ import { Wrangler } from './mesh/wrangler';
14
14
  const admin_address = admin_wallet.addresses.enterpriseAddressBech32;
15
15
  console.log(`Admin address: ${admin_address}`);
16
16
  const blockfrostProvider = new BlockfrostProvider(process.env.BLOCKFROST_API_KEY);
17
- const txHash = 'a000003f633d9b2efcc18dedefaf60623e3132c8b05a5751ac08d3bf6f505d54';
18
- const txIndex = 3;
17
+ const _txHash = 'a000003f633d9b2efcc18dedefaf60623e3132c8b05a5751ac08d3bf6f505d54';
18
+ const _txIndex = 3;
19
19
  const utxo = await blockfrostProvider.fetchAddressUTxOs(admin_address);
20
20
  if (utxo.length < 3) {
21
21
  console.log(utxo);
@@ -1 +1,9 @@
1
+ /**
2
+ * Submit a transaction to a TRP endpoint via JSON-RPC.
3
+ *
4
+ * @param submit_endpoint - URL of the TRP submit endpoint.
5
+ * @param payload - Hex-encoded transaction payload.
6
+ * @param id - JSON-RPC request identifier.
7
+ * @returns The fetch Response from the TRP endpoint.
8
+ */
1
9
  export declare function submitTx(submit_endpoint: string, payload: string, id: string): Promise<Response>;
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Submit a transaction to a TRP endpoint via JSON-RPC.
3
+ *
4
+ * @param submit_endpoint - URL of the TRP submit endpoint.
5
+ * @param payload - Hex-encoded transaction payload.
6
+ * @param id - JSON-RPC request identifier.
7
+ * @returns The fetch Response from the TRP endpoint.
8
+ */
1
9
  export async function submitTx(submit_endpoint, payload, id) {
2
10
  return await fetch(submit_endpoint, {
3
11
  method: 'POST',
@@ -1 +1,8 @@
1
+ /**
2
+ * Split a string into fixed-size chunks.
3
+ *
4
+ * @param str - The string to split.
5
+ * @param size - Maximum character count per chunk.
6
+ * @returns Array of string chunks.
7
+ */
1
8
  export declare function chunkString(str: string, size: number): string[];
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Split a string into fixed-size chunks.
3
+ *
4
+ * @param str - The string to split.
5
+ * @param size - Maximum character count per chunk.
6
+ * @returns Array of string chunks.
7
+ */
1
8
  export function chunkString(str, size) {
2
9
  const chunks = [];
3
10
  for (let i = 0; i < str.length; i += size) {
@@ -1,10 +1,18 @@
1
- export declare const bufferToHex: (buffer: any) => string;
2
- export declare const bufferToAscii: (buffer: any) => string;
1
+ /** Convert a buffer-like value to a hex string. */
2
+ export declare const bufferToHex: (buffer: Uint8Array | string) => string;
3
+ /** Convert a buffer-like value to an ASCII string. */
4
+ export declare const bufferToAscii: (buffer: Uint8Array | string) => string;
5
+ /**
6
+ * Verify a CIP-30 COSE_Sign1 signature against an expected message and address.
7
+ *
8
+ * @param signature - Hex-encoded COSE_Sign1 signature bytes.
9
+ * @param message - The original plaintext message that was signed.
10
+ * @param signingAddress - Bech32 address of the expected signer.
11
+ * @param signatureKey - Hex-encoded COSE key containing the public key.
12
+ * @returns Validation result with `isValid`, chunked signature metadata, and public key hex.
13
+ */
3
14
  export declare function verifySignature(signature: string, message: string, signingAddress: string, signatureKey: string): {
4
15
  isValid: boolean;
5
16
  sigMeta: string[];
6
17
  pubKeyHex: string;
7
18
  };
8
- /**
9
- * End Signature Validation Stuff
10
- */
@@ -1,17 +1,25 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { COSESign1 } from '@emurgo/cardano-message-signing-nodejs';
3
+ import * as CSL from '@emurgo/cardano-serialization-lib-nodejs';
4
+ import { bech32 } from 'bech32';
5
+ import { default as cbor } from 'cbor';
6
+ import { chunkString } from './chunk-string.js';
7
+ /** Convert a buffer-like value to a hex string. */
8
+ export const bufferToHex = (buffer) => Buffer.from(buffer).toString('hex');
9
+ /** Convert a buffer-like value to an ASCII string. */
10
+ export const bufferToAscii = (buffer) => Buffer.from(buffer).toString('ascii');
1
11
  /**
2
- * Signature Validation Stuff
12
+ * Verify a CIP-30 COSE_Sign1 signature against an expected message and address.
13
+ *
14
+ * @param signature - Hex-encoded COSE_Sign1 signature bytes.
15
+ * @param message - The original plaintext message that was signed.
16
+ * @param signingAddress - Bech32 address of the expected signer.
17
+ * @param signatureKey - Hex-encoded COSE key containing the public key.
18
+ * @returns Validation result with `isValid`, chunked signature metadata, and public key hex.
3
19
  */
4
- import * as CSL from "@emurgo/cardano-serialization-lib-nodejs";
5
- import { COSESign1 } from "@emurgo/cardano-message-signing-nodejs";
6
- import { Buffer } from "buffer";
7
- import { default as cbor } from "cbor";
8
- import { chunkString } from "./chunk-string.js";
9
- import { bech32 } from "bech32";
10
- export const bufferToHex = (buffer) => Buffer.from(buffer).toString("hex");
11
- export const bufferToAscii = (buffer) => Buffer.from(buffer).toString("ascii");
12
20
  export function verifySignature(signature, message, signingAddress, signatureKey) {
13
21
  try {
14
- const coseSign1 = COSESign1.from_bytes(Buffer.from(signature, "hex"));
22
+ const coseSign1 = COSESign1.from_bytes(Buffer.from(signature, 'hex'));
15
23
  const signatureBytes = coseSign1.signature();
16
24
  const [, , , payload1] = cbor.decode(bufferToHex(coseSign1.signed_data().to_bytes()));
17
25
  const signaturePayloadAscii = bufferToAscii(payload1);
@@ -21,23 +29,20 @@ export function verifySignature(signature, message, signingAddress, signatureKey
21
29
  const cosePublicKey = coseSigKey.get(-2);
22
30
  const sigKey = CSL.PublicKey.from_bytes(cosePublicKey);
23
31
  const publicKeyHash = sigKey.hash();
24
- const address_matches = addressBytes.toString("hex").slice(2) === publicKeyHash.to_hex();
32
+ const address_matches = addressBytes.toString('hex').slice(2) === publicKeyHash.to_hex();
25
33
  const sig = CSL.Ed25519Signature.from_bytes(signatureBytes);
26
34
  const validates = sigKey.verify(coseSign1.signed_data().to_bytes(), sig);
27
35
  const message_matches = signaturePayloadAscii === message;
28
36
  const isValid = validates && message_matches && address_matches;
29
37
  const sigMeta = chunkString(sig.to_hex(), 64);
30
38
  if (!isValid) {
31
- console.log("Failed to validate signature!");
39
+ console.log('Failed to validate signature!');
32
40
  console.log(isValid, validates, message_matches, address_matches);
33
41
  }
34
42
  return { isValid, sigMeta, pubKeyHex: sigKey.to_hex() };
35
43
  }
36
44
  catch (error) {
37
- console.error(`Error during signature validation:`, error);
45
+ console.error(`Error during signature validation:`, String(error));
38
46
  return { isValid: false, sigMeta: [], pubKeyHex: '' };
39
47
  }
40
48
  }
41
- /**
42
- * End Signature Validation Stuff
43
- */