@sip-protocol/sdk 0.11.1 → 0.12.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.
Files changed (39) hide show
  1. package/dist/{TransportWebUSB-2KITI5HD.mjs → TransportWebUSB-TXDZJBGS.mjs} +12 -12
  2. package/dist/browser.d.mts +1 -1
  3. package/dist/browser.d.ts +1 -1
  4. package/dist/browser.js +864 -545
  5. package/dist/browser.mjs +13 -3
  6. package/dist/{chunk-L4RKPNIJ.mjs → chunk-4EHEBTKP.mjs} +132 -87
  7. package/dist/{chunk-7IUKXWDN.mjs → chunk-MKTCJPFH.mjs} +331 -94
  8. package/dist/{chunk-IBZVA5Y7.mjs → chunk-NMC5RNMV.mjs} +2 -2
  9. package/dist/{chunk-XGB3TDIC.mjs → chunk-S3F4CPQ5.mjs} +5 -1
  10. package/dist/{constants-DCJYTIU3.mjs → constants-NCGOQF7S.mjs} +1 -1
  11. package/dist/{dist-PYEXZNFD.mjs → dist-4KSUM2PU.mjs} +2 -2
  12. package/dist/{dist-IFHPYLDX.mjs → dist-4O5YILSN.mjs} +2 -2
  13. package/dist/{fulfillment_proof-ANHVPKTB.mjs → fulfillment_proof-WCEE5GGO.mjs} +1 -1
  14. package/dist/{funding_proof-ICFZ5LHY.mjs → funding_proof-X5IP4SG5.mjs} +1 -1
  15. package/dist/{index-DH5XRHYV.d.mts → index-B_fGN4Fk.d.mts} +796 -597
  16. package/dist/{index-mw7KGX5M.d.ts → index-rqQpCeVM.d.ts} +796 -597
  17. package/dist/index.d.mts +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.js +864 -545
  20. package/dist/index.mjs +13 -3
  21. package/dist/proofs/halo2.mjs +1 -1
  22. package/dist/proofs/kimchi.mjs +1 -1
  23. package/dist/proofs/noir.mjs +1 -1
  24. package/dist/{solana-7QOA3HBZ.mjs → solana-VKZI66MK.mjs} +12 -2
  25. package/dist/{validity_proof-3POXLPNY.mjs → validity_proof-2GVV6GA6.mjs} +1 -1
  26. package/package.json +6 -5
  27. package/src/chains/solana/gasless-cashout.ts +331 -0
  28. package/src/chains/solana/index.ts +16 -0
  29. package/src/chains/solana/privacy-adapter.ts +1 -0
  30. package/src/chains/solana/providers/webhook.ts +1 -0
  31. package/src/chains/solana/relayer-fee.ts +39 -0
  32. package/src/chains/solana/scan.ts +11 -70
  33. package/src/chains/solana/stealth-scanner.ts +8 -0
  34. package/src/chains/solana/stealth-signer.ts +145 -0
  35. package/src/chains/solana/types.ts +2 -0
  36. package/src/index.ts +14 -0
  37. package/src/proofs/parallel/concurrency.ts +2 -2
  38. package/src/solana/jito-relayer.ts +40 -8
  39. package/src/wallet/solana/privacy-adapter.ts +29 -66
@@ -7,7 +7,6 @@
7
7
  import {
8
8
  PublicKey,
9
9
  Transaction,
10
- Keypair,
11
10
  } from '@solana/web3.js'
12
11
  import {
13
12
  getAssociatedTokenAddress,
@@ -16,8 +15,6 @@ import {
16
15
  } from '@solana/spl-token'
17
16
  import {
18
17
  checkEd25519StealthAddress,
19
- deriveEd25519StealthPrivateKey,
20
- deriveEd25519StealthPrivateKeyV1,
21
18
  solanaAddressToEd25519PublicKey,
22
19
  } from '../../stealth'
23
20
  import type { StealthAddress, HexString } from '@sip-protocol/types'
@@ -38,8 +35,7 @@ import {
38
35
  } from './constants'
39
36
  import { getTokenSymbol, parseTokenTransferFromBalances } from './utils'
40
37
  import type { SolanaRPCProvider } from './providers/interface'
41
- import { hexToBytes } from '@noble/hashes/utils'
42
- import { ed25519 } from '@noble/curves/ed25519'
38
+ import { deriveStealthSigner } from './stealth-signer'
43
39
 
44
40
  /**
45
41
  * Scan for incoming stealth payments
@@ -201,6 +197,7 @@ export async function scanForPayments(
201
197
  results.push({
202
198
  stealthAddress: announcement.stealthAddress || '',
203
199
  ephemeralPublicKey: announcement.ephemeralPublicKey,
200
+ version: announcement.version as '1' | '2',
204
201
  amount,
205
202
  mint: transferInfo.mint,
206
203
  tokenSymbol,
@@ -276,54 +273,14 @@ export async function claimStealthPayment(
276
273
  )
277
274
  }
278
275
 
279
- // Convert addresses to hex for SDK functions
280
- const stealthAddressHex = solanaAddressToEd25519PublicKey(stealthAddress)
281
- const ephemeralPubKeyHex = solanaAddressToEd25519PublicKey(ephemeralPublicKey)
282
-
283
- // Construct stealth address object
284
- const stealthAddressObj: StealthAddress = {
285
- address: stealthAddressHex,
286
- ephemeralPublicKey: ephemeralPubKeyHex,
287
- viewTag: 0, // Not needed for derivation
288
- }
289
-
290
- // Derive stealth private key — route by announcement version:
291
- // legacy SIP:1 (swapped scheme) vs canonical SIP:2 (EIP-5564).
292
- const recovery = version === '1'
293
- ? deriveEd25519StealthPrivateKeyV1(stealthAddressObj, spendingPrivateKey, viewingPrivateKey)
294
- : deriveEd25519StealthPrivateKey(stealthAddressObj, spendingPrivateKey, viewingPrivateKey)
295
-
296
- // Create Solana keypair from derived private key
297
- // Note: ed25519 private keys in Solana are seeds, not raw scalars
298
- // The SDK returns a scalar, so we need to handle this carefully
299
- const stealthPrivKeyBytes = hexToBytes(recovery.privateKey.slice(2))
300
-
301
- // Validate that the derived private key (scalar) produces the expected public key
302
- // Note: SIP derives a scalar, not a seed. We use scalar multiplication to verify.
276
+ const stealthSigner = deriveStealthSigner({
277
+ stealthAddress,
278
+ ephemeralPublicKey,
279
+ viewingPrivateKey,
280
+ spendingPrivateKey,
281
+ version,
282
+ })
303
283
  const stealthPubkey = new PublicKey(stealthAddress)
304
- const expectedPubKeyBytes = stealthPubkey.toBytes()
305
-
306
- // Convert scalar bytes to bigint (little-endian for ed25519)
307
- const scalarBigInt = bytesToBigIntLE(stealthPrivKeyBytes)
308
- const ED25519_ORDER = 2n ** 252n + 27742317777372353535851937790883648493n
309
- let validScalar = scalarBigInt % ED25519_ORDER
310
- if (validScalar === 0n) validScalar = 1n
311
-
312
- // Derive public key via scalar multiplication
313
- const derivedPubKeyBytes = ed25519.ExtendedPoint.BASE.multiply(validScalar).toRawBytes()
314
-
315
- if (!derivedPubKeyBytes.every((b, i) => b === expectedPubKeyBytes[i])) {
316
- throw new Error(
317
- 'Stealth key derivation failed: derived private key does not produce expected public key. ' +
318
- 'This may indicate incorrect spending/viewing keys or corrupted announcement data.'
319
- )
320
- }
321
-
322
- // Solana keypairs expect 64 bytes (32 byte seed + 32 byte public key)
323
- // We construct this from the derived scalar (now validated)
324
- const stealthKeypair = Keypair.fromSecretKey(
325
- new Uint8Array([...stealthPrivKeyBytes, ...expectedPubKeyBytes])
326
- )
327
284
 
328
285
  // Get token accounts
329
286
  const stealthATA = await getAssociatedTokenAddress(
@@ -360,8 +317,8 @@ export async function claimStealthPayment(
360
317
  transaction.lastValidBlockHeight = lastValidBlockHeight
361
318
  transaction.feePayer = stealthPubkey // Stealth address pays fee
362
319
 
363
- // Sign with stealth keypair
364
- transaction.sign(stealthKeypair)
320
+ // Sign with stealth scalar signer (Keypair cannot sign a raw-scalar stealth address)
321
+ stealthSigner.signTransaction(transaction)
365
322
 
366
323
  // Send transaction
367
324
  const txSignature = await connection.sendRawTransaction(
@@ -462,19 +419,3 @@ function detectCluster(endpoint: string): SolanaCluster {
462
419
  return 'mainnet-beta'
463
420
  }
464
421
 
465
- /**
466
- * Convert bytes to bigint in little-endian format
467
- *
468
- * Used for ed25519 scalar conversion where bytes are in little-endian order.
469
- *
470
- * @param bytes - Byte array to convert
471
- * @returns BigInt representation of the bytes
472
- * @internal
473
- */
474
- function bytesToBigIntLE(bytes: Uint8Array): bigint {
475
- let result = 0n
476
- for (let i = bytes.length - 1; i >= 0; i--) {
477
- result = (result << 8n) | BigInt(bytes[i])
478
- }
479
- return result
480
- }
@@ -115,6 +115,13 @@ export interface DetectedPayment {
115
115
  */
116
116
  ephemeralPublicKey: string
117
117
 
118
+ /**
119
+ * Announcement scheme version ('1' legacy | '2' canonical).
120
+ * Pass to the claim path so it routes to the matching derivation.
121
+ * Optional for back-compat with payments detected before version threading.
122
+ */
123
+ version?: '1' | '2'
124
+
118
125
  /**
119
126
  * View tag for efficient scanning
120
127
  */
@@ -548,6 +555,7 @@ export class StealthScanner {
548
555
  return {
549
556
  stealthAddress: announcement.stealthAddress || '',
550
557
  ephemeralPublicKey: announcement.ephemeralPublicKey,
558
+ version: announcement.version as '1' | '2',
551
559
  viewTag: viewTagNumber,
552
560
  amount,
553
561
  mint: transferInfo.mint,
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Stealth address signing for Solana.
3
+ *
4
+ * SIP's ed25519 stealth derivation yields a RAW SCALAR (s = s_spend + H(S) mod L),
5
+ * not a standard ed25519 seed. Solana's `Keypair` (tweetnacl) signs by treating the
6
+ * first 32 bytes as a seed (SHA-512 + clamp), so it cannot sign for a stealth address
7
+ * whose private key is a raw scalar. This module signs directly with the scalar using
8
+ * RFC 8032 Ed25519 — producing signatures any standard verifier and the Solana runtime
9
+ * accept — and attaches them to a transaction via `Transaction.addSignature`.
10
+ */
11
+
12
+ import { PublicKey, Transaction } from '@solana/web3.js'
13
+ import { ed25519 } from '@noble/curves/ed25519'
14
+ import { sha512 } from '@noble/hashes/sha512'
15
+ import { hexToBytes } from '@noble/hashes/utils'
16
+ import type { StealthAddress, HexString } from '@sip-protocol/types'
17
+ import {
18
+ deriveEd25519StealthPrivateKey,
19
+ deriveEd25519StealthPrivateKeyV1,
20
+ solanaAddressToEd25519PublicKey,
21
+ } from '../../stealth'
22
+ import { bytesToBigIntLE, bigIntToBytesLE, ED25519_ORDER } from '../../stealth/utils'
23
+
24
+ /** Reduce a bigint into [0, L). */
25
+ function modL(value: bigint): bigint {
26
+ const reduced = value % ED25519_ORDER
27
+ return reduced >= 0n ? reduced : reduced + ED25519_ORDER
28
+ }
29
+
30
+ /**
31
+ * Produce an RFC 8032 Ed25519 signature from a raw little-endian scalar.
32
+ *
33
+ * Unlike `Keypair`/tweetnacl signing (which expects a 32-byte seed and re-derives the
34
+ * scalar via SHA-512 + clamp), this signs directly with the provided scalar `a` whose
35
+ * public key is `A = a·G`. The per-signature nonce is derived deterministically from a
36
+ * hash of the scalar and the message (RFC 8032 structure): unique per message, never
37
+ * reused across distinct messages.
38
+ *
39
+ * @param message - Exact bytes to sign (e.g. a compiled transaction message)
40
+ * @param scalar - 32-byte little-endian ed25519 scalar (the stealth private key)
41
+ * @returns 64-byte signature (R ‖ S)
42
+ * @throws If the scalar reduces to zero
43
+ */
44
+ export function signEd25519WithScalar(message: Uint8Array, scalar: Uint8Array): Uint8Array {
45
+ const a = modL(bytesToBigIntLE(scalar))
46
+ if (a === 0n) {
47
+ throw new Error('Invalid stealth scalar: reduces to zero')
48
+ }
49
+ const A = ed25519.ExtendedPoint.BASE.multiply(a).toRawBytes()
50
+
51
+ const prefix = sha512(scalar).slice(32, 64)
52
+ const r = modL(bytesToBigIntLE(sha512(new Uint8Array([...prefix, ...message]))))
53
+ if (r === 0n) {
54
+ throw new Error('Invalid nonce: reduces to zero')
55
+ }
56
+ const R = ed25519.ExtendedPoint.BASE.multiply(r).toRawBytes()
57
+
58
+ const k = modL(bytesToBigIntLE(sha512(new Uint8Array([...R, ...A, ...message]))))
59
+ const S = modL(r + k * a)
60
+
61
+ return new Uint8Array([...R, ...bigIntToBytesLE(S, 32)])
62
+ }
63
+
64
+ /** Parameters needed to derive a stealth address's signer. */
65
+ export interface DeriveStealthSignerParams {
66
+ /** Stealth address (base58) */
67
+ stealthAddress: string
68
+ /** Ephemeral public key from the payment (base58) */
69
+ ephemeralPublicKey: string
70
+ /** Recipient's viewing private key (hex) */
71
+ viewingPrivateKey: HexString
72
+ /** Recipient's spending private key (hex) */
73
+ spendingPrivateKey: HexString
74
+ /** Announcement scheme version: '2' canonical (default) | '1' legacy */
75
+ version?: '1' | '2'
76
+ }
77
+
78
+ /** Signs transactions/messages as a stealth address using its raw ed25519 scalar. */
79
+ export interface StealthSigner {
80
+ /** The stealth address this signer controls */
81
+ readonly publicKey: PublicKey
82
+ /** Sign arbitrary bytes, returning a 64-byte ed25519 signature */
83
+ signMessage(message: Uint8Array): Uint8Array
84
+ /** Attach this stealth address's signature to a transaction. Call LAST — after feePayer, recentBlockhash, and all instructions are finalized (the signature covers the serialized message at call time). */
85
+ signTransaction(transaction: Transaction): void
86
+ }
87
+
88
+ /**
89
+ * Re-derive the signer that controls a stealth address.
90
+ *
91
+ * Routes derivation by announcement version (canonical SIP:2 vs legacy SIP:1) and
92
+ * validates the derived scalar reproduces the on-chain stealth public key before
93
+ * returning a signer. The signer signs with the raw scalar (see
94
+ * {@link signEd25519WithScalar}); it does NOT construct a `Keypair`, which cannot sign
95
+ * for a scalar-derived stealth address.
96
+ *
97
+ * @throws If the derived scalar does not produce the expected stealth public key
98
+ */
99
+ export function deriveStealthSigner(params: DeriveStealthSignerParams): StealthSigner {
100
+ const {
101
+ stealthAddress,
102
+ ephemeralPublicKey,
103
+ viewingPrivateKey,
104
+ spendingPrivateKey,
105
+ version = '2',
106
+ } = params
107
+
108
+ const stealthAddressHex = solanaAddressToEd25519PublicKey(stealthAddress)
109
+ const ephemeralPubKeyHex = solanaAddressToEd25519PublicKey(ephemeralPublicKey)
110
+
111
+ const stealthAddressObj: StealthAddress = {
112
+ address: stealthAddressHex,
113
+ ephemeralPublicKey: ephemeralPubKeyHex,
114
+ viewTag: 0,
115
+ }
116
+
117
+ const recovery = version === '1'
118
+ ? deriveEd25519StealthPrivateKeyV1(stealthAddressObj, spendingPrivateKey, viewingPrivateKey)
119
+ : deriveEd25519StealthPrivateKey(stealthAddressObj, spendingPrivateKey, viewingPrivateKey)
120
+
121
+ const scalar = hexToBytes(recovery.privateKey.slice(2))
122
+
123
+ const publicKey = new PublicKey(stealthAddress)
124
+ const expectedPubKeyBytes = publicKey.toBytes()
125
+ const derivedPubKeyBytes = ed25519.ExtendedPoint.BASE.multiply(modL(bytesToBigIntLE(scalar))).toRawBytes()
126
+
127
+ if (!derivedPubKeyBytes.every((b, i) => b === expectedPubKeyBytes[i])) {
128
+ throw new Error(
129
+ 'Stealth key derivation failed: derived scalar does not produce expected public key. ' +
130
+ 'This may indicate incorrect spending/viewing keys or corrupted announcement data.'
131
+ )
132
+ }
133
+
134
+ return {
135
+ publicKey,
136
+ signMessage(message: Uint8Array): Uint8Array {
137
+ return signEd25519WithScalar(message, scalar)
138
+ },
139
+ signTransaction(transaction: Transaction): void {
140
+ const message = transaction.serializeMessage()
141
+ const signature = signEd25519WithScalar(message, scalar)
142
+ transaction.addSignature(publicKey, Buffer.from(signature))
143
+ },
144
+ }
145
+ }
@@ -99,6 +99,8 @@ export interface SolanaScanResult {
99
99
  stealthAddress: string
100
100
  /** Ephemeral public key from the sender (base58) */
101
101
  ephemeralPublicKey: string
102
+ /** Announcement scheme version ('1' legacy | '2' canonical) — pass to claim */
103
+ version: '1' | '2'
102
104
  /** Amount received (in token's smallest unit) */
103
105
  amount: bigint
104
106
  /** Token mint address */
package/src/index.ts CHANGED
@@ -1036,6 +1036,12 @@ export {
1036
1036
  // Helius Enhanced Transactions (Human-readable TX data)
1037
1037
  HeliusEnhanced,
1038
1038
  createHeliusEnhanced,
1039
+ // Gasless Cash-Out (relayer fee-payer for stealth recipients)
1040
+ buildGaslessCashout,
1041
+ submitGaslessCashout,
1042
+ computeRelayerFee,
1043
+ signEd25519WithScalar,
1044
+ deriveStealthSigner,
1039
1045
  } from './chains/solana'
1040
1046
 
1041
1047
  // Solana Noir Verification (Aztec/Noir bounty)
@@ -1132,6 +1138,14 @@ export type {
1132
1138
  SIPTransactionMetadata,
1133
1139
  SIPEnhancedTransaction,
1134
1140
  TransactionSummary,
1141
+ // Gasless Cash-Out types (relayer fee-payer for stealth recipients)
1142
+ GaslessCashoutParams,
1143
+ GaslessCashoutBuild,
1144
+ SubmitGaslessCashoutParams,
1145
+ GaslessCashoutResult,
1146
+ RelayerFeeConfig,
1147
+ StealthSigner,
1148
+ DeriveStealthSignerParams,
1135
1149
  } from './chains/solana'
1136
1150
 
1137
1151
  // NEAR Same-Chain Privacy (M17-NEAR)
@@ -47,7 +47,7 @@ function getCpuCores(): number {
47
47
  }
48
48
  // Node.js
49
49
  try {
50
- // eslint-disable-next-line @typescript-eslint/no-var-requires
50
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
51
51
  const os = require('os')
52
52
  return os.cpus().length
53
53
  } catch {
@@ -73,7 +73,7 @@ function getMemoryInfo(): { total: number; available: number } {
73
73
  }
74
74
  // Node.js
75
75
  try {
76
- // eslint-disable-next-line @typescript-eslint/no-var-requires
76
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
77
77
  const os = require('os')
78
78
  return {
79
79
  total: os.totalmem(),
@@ -44,10 +44,10 @@ import {
44
44
  VersionedTransaction,
45
45
  Transaction,
46
46
  SystemProgram,
47
- type Keypair,
47
+ Keypair,
48
48
  type TransactionInstruction,
49
49
  } from '@solana/web3.js'
50
- import { bytesToHex } from '@noble/hashes/utils'
50
+ import bs58 from 'bs58'
51
51
 
52
52
  // ─── Constants ────────────────────────────────────────────────────────────────
53
53
 
@@ -156,6 +156,8 @@ export interface RelayedTransactionRequest {
156
156
  transaction: Transaction | VersionedTransaction
157
157
  /** Tip amount in lamports (paid by relayer, recovered from user) */
158
158
  tipLamports?: number
159
+ /** Keypair that pays the Jito tip. Required for the Jito-bundle path to land. */
160
+ tipPayer?: Keypair
159
161
  /** Whether to wait for confirmation */
160
162
  waitForConfirmation?: boolean
161
163
  }
@@ -233,10 +235,11 @@ export class JitoRelayerError extends Error {
233
235
  * blockEngineUrl: 'https://ny.mainnet.block-engine.jito.wtf/api/v1',
234
236
  * })
235
237
  *
236
- * // Submit a signed transaction via relayer
238
+ * // Submit a signed transaction via the relayer (tipPayer is required for the bundle to land)
237
239
  * const result = await relayer.relayTransaction({
238
240
  * transaction: signedTx,
239
241
  * tipLamports: 10_000, // 0.00001 SOL tip
242
+ * tipPayer: relayerKeypair,
240
243
  * })
241
244
  *
242
245
  * console.log('Transaction relayed:', result.signature)
@@ -262,6 +265,13 @@ export class JitoRelayer {
262
265
  this.submissionTimeout = config.submissionTimeout ?? JITO_DEFAULTS.submissionTimeout
263
266
  }
264
267
 
268
+ // ─── Static Helpers ─────────────────────────────────────────────────────────
269
+
270
+ /** Encode a 64-byte ed25519 signature as a base58 string (Solana canonical form). */
271
+ static encodeSignature(sig: Uint8Array): string {
272
+ return bs58.encode(sig)
273
+ }
274
+
265
275
  // ─── Public Methods ─────────────────────────────────────────────────────────
266
276
 
267
277
  /**
@@ -331,10 +341,10 @@ export class JitoRelayer {
331
341
  // Extract signatures
332
342
  const signatures = bundleTransactions.map(tx => {
333
343
  if (tx instanceof VersionedTransaction) {
334
- return bytesToHex(tx.signatures[0])
335
- } else {
336
- return tx.signature?.toString() ?? ''
344
+ return JitoRelayer.encodeSignature(tx.signatures[0])
337
345
  }
346
+ const sig = tx.signature
347
+ return sig ? JitoRelayer.encodeSignature(sig) : ''
338
348
  })
339
349
 
340
350
  // Wait for confirmation if requested
@@ -373,6 +383,27 @@ export class JitoRelayer {
373
383
  this.log('Relaying transaction')
374
384
 
375
385
  try {
386
+ // Jito bundles require a tip to land. If a tipPayer is supplied, use the
387
+ // bundle path (which prepends a tip tx); otherwise fall through to the
388
+ // existing single-tx submission.
389
+ if (request.tipPayer) {
390
+ const bundle = await this.submitBundle({
391
+ transactions: [request.transaction],
392
+ tipLamports: request.tipLamports,
393
+ tipPayer: request.tipPayer,
394
+ waitForConfirmation: request.waitForConfirmation,
395
+ })
396
+ return {
397
+ signature: bundle.signatures[bundle.signatures.length - 1] ?? '',
398
+ bundleId: bundle.bundleId,
399
+ status: bundle.status === 'landed' ? 'confirmed'
400
+ : bundle.status === 'submitted' ? 'submitted' : 'failed',
401
+ slot: bundle.slot,
402
+ error: bundle.error,
403
+ relayed: true,
404
+ }
405
+ }
406
+
376
407
  // For single transaction relay, we need to handle it differently
377
408
  // The transaction should already be signed by the user
378
409
  // We add it to a bundle with a tip transaction
@@ -385,9 +416,10 @@ export class JitoRelayer {
385
416
  // Get signature
386
417
  let signature: string
387
418
  if (request.transaction instanceof VersionedTransaction) {
388
- signature = bytesToHex(request.transaction.signatures[0])
419
+ signature = JitoRelayer.encodeSignature(request.transaction.signatures[0])
389
420
  } else {
390
- signature = request.transaction.signature?.toString() ?? ''
421
+ const sig = request.transaction.signature
422
+ signature = sig ? JitoRelayer.encodeSignature(sig) : ''
391
423
  }
392
424
 
393
425
  // Wait for confirmation if requested
@@ -23,6 +23,20 @@ import {
23
23
  checkEd25519StealthAddress,
24
24
  ed25519PublicKeyToSolanaAddress,
25
25
  } from '../../stealth'
26
+ // Imported from the source module (not the barrel) to avoid a circular-import
27
+ // cycle that leaves the barrel re-export undefined at call time — matching the
28
+ // pattern in chains/solana/stealth-signer.ts and chains/ethereum/stealth.ts.
29
+ // Using the canonical primitives here (rather than local copies) guarantees the
30
+ // stealth address this adapter reconstructs is byte-for-byte consistent with the
31
+ // key deriveEd25519StealthPrivateKey returns — they must share the SAME
32
+ // bytesToBigInt (big-endian) and curve order.
33
+ import {
34
+ getEd25519Scalar,
35
+ bytesToBigInt,
36
+ bytesToHex,
37
+ hexToBytes,
38
+ ED25519_ORDER,
39
+ } from '../../stealth/utils'
26
40
 
27
41
  // ─── Types ──────────────────────────────────────────────────────────────────
28
42
 
@@ -87,7 +101,12 @@ export interface ScannedPayment {
87
101
  * Result of claiming a stealth payment
88
102
  */
89
103
  export interface ClaimResult {
90
- /** Derived private key for spending */
104
+ /**
105
+ * Derived spending key — a RAW ed25519 scalar (hex), NOT a 32-byte Keypair seed.
106
+ * Do NOT pass to `new Keypair(...)` / `Keypair.fromSecretKey(...)` — it produces
107
+ * INVALID signatures. Sign with `signEd25519WithScalar` (or `deriveStealthSigner`)
108
+ * from `@sip-protocol/sdk` instead.
109
+ */
91
110
  privateKey: HexString
92
111
  /** Public key (stealth address) */
93
112
  publicKey: HexString
@@ -134,7 +153,8 @@ export interface ClaimResult {
134
153
  * const payments = wallet.scanPayments([announcement1, announcement2])
135
154
  * for (const payment of payments.filter(p => p.isOwned)) {
136
155
  * const claim = wallet.deriveClaimKey(payment.ephemeralPublicKey, payment.viewTag)
137
- * // Use claim.privateKey to sign transactions
156
+ * // claim.privateKey is a RAW scalar — sign via signEd25519WithScalar, NOT a Keypair:
157
+ * const sig = signEd25519WithScalar(message, hexToBytes(claim.privateKey.slice(2)))
138
158
  * }
139
159
  * ```
140
160
  */
@@ -358,10 +378,13 @@ export class PrivacySolanaWalletAdapter extends SolanaWalletAdapter {
358
378
  // Compute stealth address from ephemeral key
359
379
  const ephemeralPubBytes = hexToBytes(ephemeralPublicKey.slice(2))
360
380
 
361
- // Compute shared secret: S = viewing_scalar * R (canonical EIP-5564)
362
- const viewingScalar = getEd25519ScalarFromPrivate(
363
- hexToBytes(this.stealthKeys.viewingPrivateKey.slice(2))
364
- )
381
+ // Compute shared secret: S = viewing_scalar * R (canonical EIP-5564).
382
+ // Must use the canonical ed25519 scalar (SHA-512 + little-endian +
383
+ // reduction mod L) so the address this reconstructs matches the key
384
+ // deriveEd25519StealthPrivateKey returns below.
385
+ const viewingScalar =
386
+ getEd25519Scalar(hexToBytes(this.stealthKeys.viewingPrivateKey.slice(2))) %
387
+ ED25519_ORDER
365
388
 
366
389
  const ephemeralPoint = ed25519.ExtendedPoint.fromHex(ephemeralPubBytes)
367
390
  const sharedSecretPoint = ephemeralPoint.multiply(viewingScalar)
@@ -478,66 +501,6 @@ export class PrivacySolanaWalletAdapter extends SolanaWalletAdapter {
478
501
  }
479
502
  }
480
503
 
481
- // ─── Utilities ──────────────────────────────────────────────────────────────
482
-
483
- /** ed25519 curve order */
484
- const ED25519_ORDER = BigInt('0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed')
485
-
486
- /**
487
- * Convert bytes to BigInt (little-endian)
488
- */
489
- function bytesToBigInt(bytes: Uint8Array): bigint {
490
- let result = 0n
491
- for (let i = bytes.length - 1; i >= 0; i--) {
492
- result = (result << 8n) | BigInt(bytes[i])
493
- }
494
- return result
495
- }
496
-
497
- /**
498
- * Convert hex string to bytes
499
- */
500
- function hexToBytes(hex: string): Uint8Array {
501
- const bytes = new Uint8Array(hex.length / 2)
502
- for (let i = 0; i < hex.length; i += 2) {
503
- bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16)
504
- }
505
- return bytes
506
- }
507
-
508
- /**
509
- * Convert bytes to hex string
510
- */
511
- function bytesToHex(bytes: Uint8Array): string {
512
- return Array.from(bytes)
513
- .map(b => b.toString(16).padStart(2, '0'))
514
- .join('')
515
- }
516
-
517
- /**
518
- * Get ed25519 scalar from private key seed
519
- *
520
- * ed25519 derives the actual scalar by hashing the seed and taking
521
- * the lower 32 bytes with specific bit manipulations.
522
- */
523
- function getEd25519ScalarFromPrivate(seed: Uint8Array): bigint {
524
- // Hash the seed to get 64 bytes
525
- const h = sha256(seed)
526
-
527
- // Take lower 32 bytes and apply ed25519 clamping
528
- const scalar = new Uint8Array(32)
529
- for (let i = 0; i < 32; i++) {
530
- scalar[i] = h[i]
531
- }
532
-
533
- // Clamp: clear low 3 bits, clear high bit, set second-high bit
534
- scalar[0] &= 248
535
- scalar[31] &= 127
536
- scalar[31] |= 64
537
-
538
- return bytesToBigInt(scalar)
539
- }
540
-
541
504
  // ─── Factory ────────────────────────────────────────────────────────────────
542
505
 
543
506
  /**