@profullstack/coinpay 0.27.0 → 0.27.1

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/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to `@profullstack/coinpay` will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.27.1] - 2026-10-28
9
+
10
+ ### Fixed
11
+ - `coinpay wallet create`, `wallet import` and wallet sign-in failed on every call with
12
+ `400 INVALID_SIGNATURE`. The proof of ownership was signed over the raw message bytes; the server
13
+ (noble-curves v2) verifies a signature over sha256(message). `signProofMessage` (now exported) signs
14
+ that digest, so an agent can make its own non-custodial CoinPay wallet from a shell again.
15
+ - `WalletClient.authenticate()` POSTed to `/web-wallet/auth/challenge`, which only serves GET
16
+ (`?wallet_id=`); the 405 surfaced as "Unexpected end of JSON input".
17
+ - Wallet API errors printed as `Error: [object Object]`; they now show the server's message and code.
18
+ - `coinpay wallet addresses` was a stub. It now derives the receiving addresses locally from the
19
+ encrypted seed (`--chains SOL,USDC_POL,ETH`, `--index`, `--json`), ready to paste into a marketplace
20
+ payout wallet (e.g. `PUT https://ugig.net/api/profile/wallet-addresses`).
21
+
8
22
  ## [0.27.0] - 2026-10-28
9
23
 
10
24
  ### Added
package/bin/coinpay.js CHANGED
@@ -13,7 +13,9 @@ import {
13
13
  generateMnemonic,
14
14
  validateMnemonic,
15
15
  DEFAULT_CHAINS,
16
+ deriveAddress,
16
17
  } from '../src/wallet.js';
18
+ import { mnemonicToSeedSync } from '@scure/bip39';
17
19
  import { SwapClient, SwapCoins } from '../src/swap.js';
18
20
  import {
19
21
  createInvoice,
@@ -2831,8 +2833,41 @@ async function handleWallet(subcommand, args, flags) {
2831
2833
  return;
2832
2834
  }
2833
2835
 
2834
- print.info(`Wallet: ${config.walletId}`);
2835
- print.info('Note: Full address list requires API authentication.');
2836
+ // Derived locally from the encrypted seed: nothing leaves this machine.
2837
+ // These are the receiving addresses to hand a marketplace (ugig.net
2838
+ // profile payout wallets, invoices, bounties) so it pays you directly.
2839
+ if (!hasEncryptedWallet(flags)) {
2840
+ print.info(`Wallet: ${config.walletId}`);
2841
+ print.error('No encrypted wallet file found, so no addresses can be derived here.');
2842
+ return;
2843
+ }
2844
+ try {
2845
+ const mnemonic = await getDecryptedMnemonic(flags);
2846
+ const seed = mnemonicToSeedSync(mnemonic.trim());
2847
+ clearString(mnemonic);
2848
+ const chains = (flags.chains ? String(flags.chains).split(',') : DEFAULT_CHAINS)
2849
+ .map((c) => c.trim().toUpperCase())
2850
+ .filter(Boolean);
2851
+ const index = parseInt(flags.index || '0', 10);
2852
+ const addresses = [];
2853
+ for (const chain of chains) {
2854
+ try {
2855
+ addresses.push({ chain, index, address: deriveAddress(seed, chain, index) });
2856
+ } catch (err) {
2857
+ addresses.push({ chain, index, error: err.message });
2858
+ }
2859
+ }
2860
+ if (flags.json) {
2861
+ print.json({ walletId: config.walletId, addresses });
2862
+ } else {
2863
+ print.info(`Wallet: ${config.walletId}`);
2864
+ for (const a of addresses) {
2865
+ console.log(` ${a.chain.padEnd(9)} ${a.address || a.error}`);
2866
+ }
2867
+ }
2868
+ } catch (error) {
2869
+ print.error(error.message);
2870
+ }
2836
2871
  break;
2837
2872
  }
2838
2873
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@profullstack/coinpay",
3
- "version": "0.27.0",
3
+ "version": "0.27.1",
4
4
  "description": "CoinPay SDK & CLI — Accept cryptocurrency and card payments (BTC, ETH, SOL, POL, BCH, USDC) with wallet, swap and a live finances dashboard",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/wallet.js CHANGED
@@ -403,34 +403,36 @@ function getSecp256k1PublicKey(seed) {
403
403
  }
404
404
 
405
405
  /**
406
- * Sign a message with secp256k1 private key
407
- * @private
406
+ * Sign a proof-of-ownership / auth-challenge message with a secp256k1 key,
407
+ * the way the CoinPay server verifies it.
408
+ *
409
+ * The server (src/lib/web-wallet/auth.ts::verifySecp256k1Signature) runs
410
+ * noble-curves v2, whose `verify(sig, message, pub)` sha256-hashes the message
411
+ * first (prehash defaults to true). This SDK pins noble-curves v1, whose
412
+ * `sign(x, key)` treats `x` as an already-hashed digest. So the SDK must hash
413
+ * explicitly: sign(sha256(message), { prehash: false }) is byte-for-byte what
414
+ * the server checks, on either major version.
415
+ *
416
+ * History: 0.x signed sha256(message) and was right while the server ran v1
417
+ * (raw verify); a later "fix" switched to raw bytes; the server then moved to
418
+ * v2 and every `wallet create`, `wallet import` and wallet sign-in from the
419
+ * SDK/CLI failed with 400 INVALID_SIGNATURE (2026-10).
408
420
  */
409
- function signMessage(message, privateKey) {
410
- // Signed over the RAW message bytes, not sha256(message).
411
- //
412
- // This used to hash first. The server verifies proofs by passing the raw
413
- // encoded message straight to `secp256k1.verify` (see
414
- // web-wallet/auth.ts::verifySecp256k1Signature), and noble treats that
415
- // argument as the thing that was signed — so a signature over the digest
416
- // verifies false against the message. Every proof of ownership this SDK
417
- // produced was rejected, which meant `WalletClient.fromSeed()` could not
418
- // import a wallet at all. Found while wiring the same proof into
419
- // `create()` for V-04.
420
- const signature = secp256k1.sign(new TextEncoder().encode(message), privateKey);
421
- // Handle different noble-curves versions:
422
- // v1.x returns Signature object with toCompactHex()
423
- // v2.x returns raw Uint8Array directly
421
+ export function signProofMessage(message, privateKey) {
422
+ const digest = sha256(new TextEncoder().encode(message));
423
+ const signature = secp256k1.sign(digest, privateKey, { prehash: false });
424
+ // v1.x returns a Signature object, v2.x a compact Uint8Array.
424
425
  if (signature instanceof Uint8Array) {
425
426
  return bytesToHex(signature);
426
427
  }
427
428
  if (typeof signature.toCompactHex === 'function') {
428
429
  return signature.toCompactHex();
429
430
  }
430
- // Fallback: try toCompactRawBytes
431
431
  return bytesToHex(signature.toCompactRawBytes());
432
432
  }
433
433
 
434
+ const signMessage = signProofMessage;
435
+
434
436
  // ================================================================
435
437
  // Arbitrary tx / message signing
436
438
  // ----------------------------------------------------------------
@@ -795,9 +797,13 @@ export class WalletClient {
795
797
  const data = await response.json();
796
798
 
797
799
  if (!response.ok) {
798
- const error = new Error(data.error || `HTTP ${response.status}`);
800
+ // Web-wallet routes answer { error: { code, message } }; older ones a string.
801
+ const apiError = data && typeof data.error === 'object' && data.error ? data.error : null;
802
+ const error = new Error(
803
+ (apiError ? apiError.message || apiError.code : data.error) || `HTTP ${response.status}`
804
+ );
799
805
  error.status = response.status;
800
- error.code = data.code;
806
+ error.code = apiError?.code ?? data.code;
801
807
  error.response = data;
802
808
  throw error;
803
809
  }
@@ -823,10 +829,11 @@ export class WalletClient {
823
829
  }
824
830
 
825
831
  // Get challenge
826
- const challengeRes = await this.#request(`/web-wallet/auth/challenge`, {
827
- method: 'POST',
828
- body: JSON.stringify({ wallet_id: this.#walletId }),
829
- });
832
+ // The server serves challenges on GET ?wallet_id= (a POST is a 405 with
833
+ // an empty body, which surfaced as "Unexpected end of JSON input").
834
+ const challengeRes = await this.#request(
835
+ `/web-wallet/auth/challenge?wallet_id=${encodeURIComponent(this.#walletId)}`
836
+ );
830
837
  const challenge = challengeRes.data?.challenge || challengeRes.challenge;
831
838
  const challengeId = challengeRes.data?.challenge_id || challengeRes.challenge_id;
832
839