@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.
- package/dist/{TransportWebUSB-2KITI5HD.mjs → TransportWebUSB-TXDZJBGS.mjs} +12 -12
- package/dist/browser.d.mts +1 -1
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +864 -545
- package/dist/browser.mjs +13 -3
- package/dist/{chunk-L4RKPNIJ.mjs → chunk-4EHEBTKP.mjs} +132 -87
- package/dist/{chunk-7IUKXWDN.mjs → chunk-MKTCJPFH.mjs} +331 -94
- package/dist/{chunk-IBZVA5Y7.mjs → chunk-NMC5RNMV.mjs} +2 -2
- package/dist/{chunk-XGB3TDIC.mjs → chunk-S3F4CPQ5.mjs} +5 -1
- package/dist/{constants-DCJYTIU3.mjs → constants-NCGOQF7S.mjs} +1 -1
- package/dist/{dist-PYEXZNFD.mjs → dist-4KSUM2PU.mjs} +2 -2
- package/dist/{dist-IFHPYLDX.mjs → dist-4O5YILSN.mjs} +2 -2
- package/dist/{fulfillment_proof-ANHVPKTB.mjs → fulfillment_proof-WCEE5GGO.mjs} +1 -1
- package/dist/{funding_proof-ICFZ5LHY.mjs → funding_proof-X5IP4SG5.mjs} +1 -1
- package/dist/{index-DH5XRHYV.d.mts → index-B_fGN4Fk.d.mts} +796 -597
- package/dist/{index-mw7KGX5M.d.ts → index-rqQpCeVM.d.ts} +796 -597
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +864 -545
- package/dist/index.mjs +13 -3
- package/dist/proofs/halo2.mjs +1 -1
- package/dist/proofs/kimchi.mjs +1 -1
- package/dist/proofs/noir.mjs +1 -1
- package/dist/{solana-7QOA3HBZ.mjs → solana-VKZI66MK.mjs} +12 -2
- package/dist/{validity_proof-3POXLPNY.mjs → validity_proof-2GVV6GA6.mjs} +1 -1
- package/package.json +6 -5
- package/src/chains/solana/gasless-cashout.ts +331 -0
- package/src/chains/solana/index.ts +16 -0
- package/src/chains/solana/privacy-adapter.ts +1 -0
- package/src/chains/solana/providers/webhook.ts +1 -0
- package/src/chains/solana/relayer-fee.ts +39 -0
- package/src/chains/solana/scan.ts +11 -70
- package/src/chains/solana/stealth-scanner.ts +8 -0
- package/src/chains/solana/stealth-signer.ts +145 -0
- package/src/chains/solana/types.ts +2 -0
- package/src/index.ts +14 -0
- package/src/proofs/parallel/concurrency.ts +2 -2
- package/src/solana/jito-relayer.ts +40 -8
- 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 {
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
364
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
47
|
+
Keypair,
|
|
48
48
|
type TransactionInstruction,
|
|
49
49
|
} from '@solana/web3.js'
|
|
50
|
-
import
|
|
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
|
|
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 =
|
|
419
|
+
signature = JitoRelayer.encodeSignature(request.transaction.signatures[0])
|
|
389
420
|
} else {
|
|
390
|
-
|
|
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
|
-
/**
|
|
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
|
-
* //
|
|
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
|
-
|
|
363
|
-
|
|
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
|
/**
|