@sip-protocol/sdk 0.11.0 → 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 +868 -545
  5. package/dist/browser.mjs +17 -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-Cwo3WhxX.d.mts → index-B_fGN4Fk.d.mts} +806 -597
  16. package/dist/{index-X8qPQdp6.d.ts → index-rqQpCeVM.d.ts} +806 -597
  17. package/dist/index.d.mts +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.js +868 -545
  20. package/dist/index.mjs +17 -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 +16 -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
package/dist/index.mjs CHANGED
@@ -448,7 +448,7 @@ import {
448
448
  verifyOracleSignature,
449
449
  verifyOwnership,
450
450
  walletRegistry
451
- } from "./chunk-L4RKPNIJ.mjs";
451
+ } from "./chunk-4EHEBTKP.mjs";
452
452
  import {
453
453
  DEFAULT_NETWORK_CONFIG,
454
454
  DEFAULT_PROXY_TIMEOUT,
@@ -462,6 +462,7 @@ import {
462
462
  TOR_PORTS,
463
463
  addBlindings,
464
464
  addCommitments,
465
+ buildGaslessCashout,
465
466
  checkEd25519StealthAddress,
466
467
  checkEd25519StealthAddressV1,
467
468
  checkProxyAvailability,
@@ -470,6 +471,7 @@ import {
470
471
  claimStealthPayment,
471
472
  commit,
472
473
  commitZero,
474
+ computeRelayerFee,
473
475
  createAnnouncementMemo,
474
476
  createHeliusEnhanced,
475
477
  createNetworkPrivacyClient,
@@ -483,6 +485,7 @@ import {
483
485
  deriveSecp256k1StealthPrivateKeyV1,
484
486
  deriveStealthPrivateKey,
485
487
  deriveStealthPrivateKeyV1,
488
+ deriveStealthSigner,
486
489
  detectTorPort,
487
490
  ed25519PublicKeyToNearAddress,
488
491
  ed25519PublicKeyToSolanaAddress,
@@ -526,7 +529,9 @@ import {
526
529
  secureWipe,
527
530
  secureWipeAll,
528
531
  sendPrivateSPLTransfer,
532
+ signEd25519WithScalar,
529
533
  solanaAddressToEd25519PublicKey,
534
+ submitGaslessCashout,
530
535
  subtractBlindings,
531
536
  subtractCommitments,
532
537
  validateAsset,
@@ -540,7 +545,7 @@ import {
540
545
  verifyWebhookSignature,
541
546
  withSecureBuffer,
542
547
  withSecureBufferSync
543
- } from "./chunk-7IUKXWDN.mjs";
548
+ } from "./chunk-MKTCJPFH.mjs";
544
549
  import {
545
550
  Halo2Provider,
546
551
  KimchiProvider,
@@ -574,6 +579,8 @@ import {
574
579
  ETH_RPC_ENDPOINTS,
575
580
  MEMO_PROGRAM_ID,
576
581
  SIP_MEMO_PREFIX,
582
+ SIP_MEMO_PREFIX_ANY,
583
+ SIP_MEMO_PREFIX_V2,
577
584
  SOLANA_EXPLORER_URLS,
578
585
  SOLANA_RPC_ENDPOINTS,
579
586
  SOLANA_RPC_ENDPOINTS2,
@@ -585,7 +592,7 @@ import {
585
592
  getSolanaTokenDecimals,
586
593
  getTokenMint
587
594
  } from "./chunk-KXETSSKP.mjs";
588
- import "./chunk-XGB3TDIC.mjs";
595
+ import "./chunk-S3F4CPQ5.mjs";
589
596
  export {
590
597
  ATTESTATION_VERSION,
591
598
  AptosStealthService,
@@ -741,6 +748,8 @@ export {
741
748
  SIPError,
742
749
  SIPNativeBackend,
743
750
  SIP_MEMO_PREFIX,
751
+ SIP_MEMO_PREFIX_ANY,
752
+ SIP_MEMO_PREFIX_V2,
744
753
  SIP_VERSION,
745
754
  SOLANA_EXPLORER_URLS,
746
755
  SOLANA_RPC_ENDPOINTS2 as SOLANA_RPC_ENDPOINTS,
@@ -800,6 +809,7 @@ export {
800
809
  bpsToPercent,
801
810
  bytesToHex as browserBytesToHex,
802
811
  hexToBytes as browserHexToBytes,
812
+ buildGaslessCashout,
803
813
  cacheKeyGenerator,
804
814
  calculateFeeForSwap,
805
815
  calculatePrivacyScore,
@@ -821,6 +831,7 @@ export {
821
831
  computeAttestationHash,
822
832
  computeNEARViewingKeyHash,
823
833
  computeNEARViewingKeyHashFromPrivate,
834
+ computeRelayerFee,
824
835
  computeTweakedKey,
825
836
  configureLogger,
826
837
  convertFromSIP,
@@ -924,6 +935,7 @@ export {
924
935
  deriveSecp256k1StealthPrivateKeyV1,
925
936
  deriveStealthPrivateKey,
926
937
  deriveStealthPrivateKeyV1,
938
+ deriveStealthSigner,
927
939
  deriveSuiStealthPrivateKey,
928
940
  deriveTraditionalNullifier,
929
941
  deriveViewingKey,
@@ -1115,11 +1127,13 @@ export {
1115
1127
  serializePayment,
1116
1128
  setLogLevel,
1117
1129
  signAttestationMessage,
1130
+ signEd25519WithScalar,
1118
1131
  silenceLogger,
1119
1132
  solanaAddressToEd25519PublicKey,
1120
1133
  optimizations_exports as solanaOptimizations,
1121
1134
  solanaPublicKeyToHex,
1122
1135
  stealthKeyToCosmosAddress,
1136
+ submitGaslessCashout,
1123
1137
  subtractBlindings,
1124
1138
  subtractBlindingsNEAR,
1125
1139
  subtractCommitments,
@@ -3,7 +3,7 @@ import {
3
3
  createHalo2Provider,
4
4
  createOrchardProvider
5
5
  } from "../chunk-5D7A3L3W.mjs";
6
- import "../chunk-XGB3TDIC.mjs";
6
+ import "../chunk-S3F4CPQ5.mjs";
7
7
  export {
8
8
  Halo2Provider,
9
9
  createHalo2Provider,
@@ -4,7 +4,7 @@ import {
4
4
  createMinaMainnetProvider,
5
5
  createZkAppProvider
6
6
  } from "../chunk-5D7A3L3W.mjs";
7
- import "../chunk-XGB3TDIC.mjs";
7
+ import "../chunk-S3F4CPQ5.mjs";
8
8
  export {
9
9
  KimchiProvider,
10
10
  createKimchiProvider,
@@ -13,7 +13,7 @@ import {
13
13
  import {
14
14
  ProofError
15
15
  } from "../chunk-Z3K7W5S3.mjs";
16
- import "../chunk-XGB3TDIC.mjs";
16
+ import "../chunk-S3F4CPQ5.mjs";
17
17
 
18
18
  // src/proofs/noir.ts
19
19
  import { Noir } from "@noir-lang/noir_js";
@@ -32,10 +32,12 @@ import {
32
32
  batchGetTokenBalances,
33
33
  batchResolveTokenMetadata,
34
34
  batchScanForRecipients,
35
+ buildGaslessCashout,
35
36
  calculatePriorityFee,
36
37
  claimStealthPayment,
37
38
  commitSPLToken,
38
39
  commitSolana,
40
+ computeRelayerFee,
39
41
  computeViewingKeyHash,
40
42
  computeViewingKeyHashFromPrivate,
41
43
  createAddress,
@@ -56,6 +58,7 @@ import {
56
58
  decryptWithViewing,
57
59
  deriveChildViewingKey,
58
60
  deriveSolanaStealthKeys,
61
+ deriveStealthSigner,
59
62
  deriveViewingKeyFromSpending,
60
63
  disposeEphemeralKeypairs,
61
64
  encryptForViewing,
@@ -102,7 +105,9 @@ import {
102
105
  sendPrivateSPLTransfer,
103
106
  sendSOLTransfer,
104
107
  shieldedTransfer,
108
+ signEd25519WithScalar,
105
109
  solToLamports,
110
+ submitGaslessCashout,
106
111
  subtractBlindingsSolana,
107
112
  subtractCommitmentsSolana,
108
113
  toAddress,
@@ -122,7 +127,7 @@ import {
122
127
  verifySPLTokenCommitment,
123
128
  verifyWebhookSignature,
124
129
  wipeEphemeralPrivateKey
125
- } from "./chunk-7IUKXWDN.mjs";
130
+ } from "./chunk-MKTCJPFH.mjs";
126
131
  import "./chunk-Z3K7W5S3.mjs";
127
132
  import {
128
133
  ATA_RENT_LAMPORTS,
@@ -139,7 +144,7 @@ import {
139
144
  getSolanaTokenDecimals,
140
145
  getTokenMint
141
146
  } from "./chunk-KXETSSKP.mjs";
142
- import "./chunk-XGB3TDIC.mjs";
147
+ import "./chunk-S3F4CPQ5.mjs";
143
148
  export {
144
149
  ATA_RENT_LAMPORTS,
145
150
  CONFIG_PDA,
@@ -184,10 +189,12 @@ export {
184
189
  batchGetTokenBalances,
185
190
  batchResolveTokenMetadata,
186
191
  batchScanForRecipients,
192
+ buildGaslessCashout,
187
193
  calculatePriorityFee,
188
194
  claimStealthPayment,
189
195
  commitSPLToken,
190
196
  commitSolana,
197
+ computeRelayerFee,
191
198
  computeViewingKeyHash,
192
199
  computeViewingKeyHashFromPrivate,
193
200
  createAddress,
@@ -208,6 +215,7 @@ export {
208
215
  decryptWithViewing,
209
216
  deriveChildViewingKey,
210
217
  deriveSolanaStealthKeys,
218
+ deriveStealthSigner,
211
219
  deriveViewingKeyFromSpending,
212
220
  disposeEphemeralKeypairs,
213
221
  encryptForViewing,
@@ -257,7 +265,9 @@ export {
257
265
  sendPrivateSPLTransfer,
258
266
  sendSOLTransfer,
259
267
  shieldedTransfer,
268
+ signEd25519WithScalar,
260
269
  solToLamports,
270
+ submitGaslessCashout,
261
271
  subtractBlindingsSolana,
262
272
  subtractCommitmentsSolana,
263
273
  toAddress,
@@ -8,7 +8,7 @@ import {
8
8
  noir_version,
9
9
  validity_proof_default
10
10
  } from "./chunk-I534WKN7.mjs";
11
- import "./chunk-XGB3TDIC.mjs";
11
+ import "./chunk-S3F4CPQ5.mjs";
12
12
  export {
13
13
  abi,
14
14
  bytecode,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sip-protocol/sdk",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Core SDK for Shielded Intents Protocol - Privacy layer for cross-chain transactions",
5
5
  "author": "SIP Protocol <hello@sip-protocol.org>",
6
6
  "homepage": "https://sip-protocol.org",
@@ -56,12 +56,12 @@
56
56
  "@jup-ag/api": "^6.0.48",
57
57
  "@langchain/core": "^0.3.30",
58
58
  "@langchain/openai": "^0.4.2",
59
- "@magicblock-labs/ephemeral-rollups-sdk": "^0.14.4",
59
+ "@magicblock-labs/ephemeral-rollups-sdk": "^0.15.0",
60
60
  "@noble/ciphers": "^2.2.0",
61
61
  "@noble/curves": "^1.3.0",
62
62
  "@noble/hashes": "^1.3.3",
63
63
  "@noir-lang/noir_js": "1.0.0-beta.18",
64
- "@noir-lang/types": "1.0.0-beta.18",
64
+ "@noir-lang/types": "1.0.0-beta.22",
65
65
  "@radr/shadowwire": "^1.1.15",
66
66
  "@scure/base": "^2.2.0",
67
67
  "@scure/bip32": "^2.2.0",
@@ -74,19 +74,20 @@
74
74
  "@solana/spl-token": "^0.4.14",
75
75
  "@solana/web3.js": "^1.98.4",
76
76
  "@triton-one/yellowstone-grpc": "^4.0.2",
77
+ "bs58": "^6.0.0",
77
78
  "langchain": "^1.4.4",
78
79
  "pino": "^10.3.1",
79
80
  "zod": "^4.4.3"
80
81
  },
81
82
  "devDependencies": {
82
83
  "@grpc/grpc-js": "^1.14.4",
83
- "@types/node": "^25.9.1",
84
+ "@types/node": "^25.9.2",
84
85
  "@vitest/coverage-v8": "^4.1.0",
85
86
  "fast-check": "^4.8.0",
86
87
  "https-proxy-agent": "^7.0.0",
87
88
  "socks-proxy-agent": "^8.0.0",
88
89
  "tsup": "^8.0.0",
89
- "typescript": "^5.3.0",
90
+ "typescript": "^6.0.3",
90
91
  "vitest": "^4.1.0"
91
92
  },
92
93
  "keywords": [
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Gasless cash-out for stealth recipients.
3
+ *
4
+ * A stealth address that received SPL tokens typically holds ZERO SOL, so it cannot
5
+ * pay a transaction fee to move those tokens out. Here a relayer is the fee-payer; the
6
+ * stealth address signs only the token transfers (via its raw ed25519 scalar — see
7
+ * deriveStealthSigner). The relayer recovers its SOL cost as an SPL fee deducted from
8
+ * the tokens being moved (fee-from-claim). Direct submission is the primary path; a
9
+ * Jito bundle is an optional mainnet hardening layer (see submitGaslessCashout).
10
+ */
11
+
12
+ import {
13
+ Connection,
14
+ PublicKey,
15
+ Transaction,
16
+ Keypair,
17
+ } from '@solana/web3.js'
18
+ import {
19
+ getAssociatedTokenAddress,
20
+ createTransferInstruction,
21
+ createAssociatedTokenAccountIdempotentInstruction,
22
+ getAccount,
23
+ } from '@solana/spl-token'
24
+ import type { HexString } from '@sip-protocol/types'
25
+ import { deriveStealthSigner } from './stealth-signer'
26
+ import { computeRelayerFee, type RelayerFeeConfig } from './relayer-fee'
27
+ import { getExplorerUrl, type SolanaCluster } from './constants'
28
+ import type { JitoRelayer } from '../../solana/jito-relayer'
29
+
30
+ /** Parameters for building a gasless cash-out transaction. */
31
+ export interface GaslessCashoutParams {
32
+ /** Solana RPC connection */
33
+ connection: Connection
34
+ /** Stealth address holding the tokens (base58) */
35
+ stealthAddress: string
36
+ /** Ephemeral public key from the payment (base58) */
37
+ ephemeralPublicKey: string
38
+ /** Recipient's viewing private key (hex) */
39
+ viewingPrivateKey: HexString
40
+ /** Recipient's spending private key (hex) */
41
+ spendingPrivateKey: HexString
42
+ /** Final destination address (base58) */
43
+ destinationAddress: string
44
+ /** SPL token mint */
45
+ mint: PublicKey
46
+ /** Relayer public key — the transaction fee-payer (must hold SOL) */
47
+ relayerPublicKey: PublicKey
48
+ /** Relayer's token account that receives the fee (ATA for `mint`) */
49
+ relayerFeeAccount: PublicKey
50
+ /** Fee model */
51
+ feeConfig: RelayerFeeConfig
52
+ /** Announcement scheme version: '2' canonical (default) | '1' legacy */
53
+ version?: '1' | '2'
54
+ }
55
+
56
+ /** A stealth-signed gasless cash-out transaction, awaiting the relayer's fee-payer signature. */
57
+ export interface GaslessCashoutBuild {
58
+ /** Transaction signed by the stealth address; feePayer = relayer (relayer must still sign) */
59
+ transaction: Transaction
60
+ /** Stealth address (base58) */
61
+ stealthAddress: string
62
+ /** Final destination (base58) */
63
+ destinationAddress: string
64
+ /** Gross amount in the stealth token account (base units) */
65
+ grossAmount: bigint
66
+ /** Relayer fee deducted (base units) */
67
+ relayerFee: bigint
68
+ /** Net amount forwarded to the destination (base units) */
69
+ netAmount: bigint
70
+ /** Blockhash the transaction is bound to */
71
+ blockhash: string
72
+ /** Last valid block height for confirmation */
73
+ lastValidBlockHeight: number
74
+ }
75
+
76
+ /**
77
+ * Build a gasless cash-out transaction.
78
+ *
79
+ * Derives the stealth signer from the recipient's keys, computes the fee-from-claim,
80
+ * assembles a two- (or three-) instruction transaction with the relayer as fee-payer,
81
+ * and pre-signs with the stealth scalar. The relayer must add its own signature before
82
+ * broadcasting.
83
+ *
84
+ * @throws If the relayer fee >= the gross claim amount (nothing left for the recipient)
85
+ */
86
+ export async function buildGaslessCashout(
87
+ params: GaslessCashoutParams
88
+ ): Promise<GaslessCashoutBuild> {
89
+ const {
90
+ connection,
91
+ stealthAddress,
92
+ ephemeralPublicKey,
93
+ viewingPrivateKey,
94
+ spendingPrivateKey,
95
+ destinationAddress,
96
+ mint,
97
+ relayerPublicKey,
98
+ relayerFeeAccount,
99
+ feeConfig,
100
+ version = '2',
101
+ } = params
102
+
103
+ const stealthSigner = deriveStealthSigner({
104
+ stealthAddress,
105
+ ephemeralPublicKey,
106
+ viewingPrivateKey,
107
+ spendingPrivateKey,
108
+ version,
109
+ })
110
+ const stealthPubkey = stealthSigner.publicKey
111
+
112
+ const stealthATA = await getAssociatedTokenAddress(mint, stealthPubkey, true)
113
+ const destinationPubkey = new PublicKey(destinationAddress)
114
+ const destinationATA = await getAssociatedTokenAddress(mint, destinationPubkey)
115
+
116
+ if (destinationATA.equals(stealthATA)) {
117
+ throw new Error(
118
+ 'destinationAddress resolves to the stealth token account; choose a different destination'
119
+ )
120
+ }
121
+
122
+ // Validate the relayer fee account is a token account for `mint` — otherwise the fee
123
+ // transfer would fail (mint mismatch) or the fee would be unrecoverable.
124
+ let feeAccount
125
+ try {
126
+ feeAccount = await getAccount(connection, relayerFeeAccount)
127
+ } catch {
128
+ throw new Error('relayerFeeAccount does not exist or is not a token account')
129
+ }
130
+ if (!feeAccount.mint.equals(mint)) {
131
+ throw new Error('relayerFeeAccount is not an associated token account for the given mint')
132
+ }
133
+
134
+ // Gross balance held by the stealth ATA
135
+ let balanceResp
136
+ try {
137
+ balanceResp = await connection.getTokenAccountBalance(stealthATA)
138
+ } catch {
139
+ throw new Error(
140
+ `Stealth token account ${stealthATA.toBase58()} for mint ${mint.toBase58()} does not exist or holds no balance; nothing to cash out`
141
+ )
142
+ }
143
+ const grossAmount = BigInt(balanceResp.value.amount)
144
+
145
+ const relayerFee = computeRelayerFee(grossAmount, feeConfig)
146
+ if (relayerFee >= grossAmount) {
147
+ throw new Error(
148
+ `Relayer fee (${relayerFee}) equals or exceeds the claim amount (${grossAmount}); nothing left to forward`
149
+ )
150
+ }
151
+ const netAmount = grossAmount - relayerFee
152
+
153
+ const transaction = new Transaction()
154
+
155
+ // Idempotently create the destination ATA — relayer pays rent. Idempotent means a
156
+ // no-op if it already exists, so no separate existence read is needed (one fewer RPC,
157
+ // and no TOCTOU gap between the read and broadcast). The flat-floor fee is intended to
158
+ // cover this rent, but it is denominated in token base units (no price oracle in v1),
159
+ // so SOL-rent recovery is approximate, not guaranteed.
160
+ transaction.add(
161
+ createAssociatedTokenAccountIdempotentInstruction(
162
+ relayerPublicKey, // payer (relayer pays rent)
163
+ destinationATA,
164
+ destinationPubkey,
165
+ mint
166
+ )
167
+ )
168
+
169
+ // Fee: stealth -> relayer fee account
170
+ transaction.add(
171
+ createTransferInstruction(stealthATA, relayerFeeAccount, stealthPubkey, relayerFee)
172
+ )
173
+ // Net: stealth -> destination
174
+ transaction.add(
175
+ createTransferInstruction(stealthATA, destinationATA, stealthPubkey, netAmount)
176
+ )
177
+
178
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash()
179
+ transaction.recentBlockhash = blockhash
180
+ transaction.lastValidBlockHeight = lastValidBlockHeight
181
+ transaction.feePayer = relayerPublicKey
182
+
183
+ // Stealth signs the token transfers via its scalar; relayer (fee-payer) signs at submit time.
184
+ // Must be done AFTER feePayer + recentBlockhash + all instructions are set.
185
+ stealthSigner.signTransaction(transaction)
186
+
187
+ return {
188
+ transaction,
189
+ stealthAddress,
190
+ destinationAddress,
191
+ grossAmount,
192
+ relayerFee,
193
+ netAmount,
194
+ blockhash,
195
+ lastValidBlockHeight,
196
+ }
197
+ }
198
+
199
+ /** Detect the Solana cluster from an RPC endpoint URL. */
200
+ function detectCluster(endpoint: string): SolanaCluster {
201
+ if (endpoint.includes('devnet')) {
202
+ return 'devnet'
203
+ }
204
+ if (endpoint.includes('testnet')) {
205
+ return 'testnet'
206
+ }
207
+ if (endpoint.includes('localhost') || endpoint.includes('127.0.0.1')) {
208
+ return 'localnet'
209
+ }
210
+ return 'mainnet-beta'
211
+ }
212
+
213
+ /** Parameters for submitting a built gasless cash-out. */
214
+ export interface SubmitGaslessCashoutParams {
215
+ /** Solana RPC connection */
216
+ connection: Connection
217
+ /** Output of buildGaslessCashout (stealth already signed) */
218
+ build: GaslessCashoutBuild
219
+ /** Relayer keypair — must equal build.transaction.feePayer */
220
+ relayerKeypair: Keypair
221
+ /** Optional Jito relayer for the bundle path (mainnet hardening). Default: direct submit. */
222
+ jitoRelayer?: JitoRelayer
223
+ /** Tip in lamports for the Jito path (ignored on the direct path) */
224
+ tipLamports?: number
225
+ }
226
+
227
+ /** Result of a gasless cash-out. */
228
+ export interface GaslessCashoutResult {
229
+ /** Transaction signature (base58) */
230
+ txSignature: string
231
+ /** Destination that received the net amount (base58) */
232
+ destinationAddress: string
233
+ /** Net amount forwarded (base units) */
234
+ amount: bigint
235
+ /** Relayer fee charged (base units) */
236
+ relayerFee: bigint
237
+ /** Explorer URL */
238
+ explorerUrl: string
239
+ /** Whether the Jito bundle path was used */
240
+ viaJito: boolean
241
+ }
242
+
243
+ /**
244
+ * Sign as the relayer (fee-payer) and submit a gasless cash-out.
245
+ *
246
+ * Direct submission is the primary path (works on devnet + mainnet). Supplying a
247
+ * `jitoRelayer` routes through a Jito bundle instead (optional mainnet hardening;
248
+ * Jito has no devnet block engine).
249
+ *
250
+ * @throws If `relayerKeypair` is not the transaction's fee-payer
251
+ */
252
+ export async function submitGaslessCashout(
253
+ params: SubmitGaslessCashoutParams
254
+ ): Promise<GaslessCashoutResult> {
255
+ const { connection, build, relayerKeypair, jitoRelayer, tipLamports } = params
256
+ const { transaction, netAmount, relayerFee, destinationAddress } = build
257
+
258
+ if (!transaction.feePayer || !transaction.feePayer.equals(relayerKeypair.publicKey)) {
259
+ throw new Error('relayerKeypair does not match the transaction fee-payer')
260
+ }
261
+
262
+ // Relayer adds its fee-payer signature alongside the stealth signature. Guard against
263
+ // re-signing when the same build is retried after a transient send failure — a second
264
+ // partialSign is wasteful and could surprise callers inspecting the signature set.
265
+ const relayerAlreadySigned = transaction.signatures.some(
266
+ (s) => s.publicKey.equals(relayerKeypair.publicKey) && s.signature !== null
267
+ )
268
+ if (!relayerAlreadySigned) {
269
+ transaction.partialSign(relayerKeypair)
270
+ }
271
+
272
+ const cluster = detectCluster(connection.rpcEndpoint)
273
+
274
+ // Optional Jito path (mainnet only — Jito has no devnet block engine).
275
+ if (jitoRelayer) {
276
+ const relayed = await jitoRelayer.relayTransaction({
277
+ transaction,
278
+ tipLamports,
279
+ tipPayer: relayerKeypair,
280
+ waitForConfirmation: true,
281
+ })
282
+ // Only a confirmed bundle means the funds actually moved. A 'submitted'/'failed'
283
+ // status is NOT success — surfacing it as one would silently lose the cash-out.
284
+ if (relayed.status !== 'confirmed') {
285
+ throw new Error(
286
+ `Gasless cash-out via Jito did not confirm (status: ${relayed.status})` +
287
+ (relayed.error ? `: ${relayed.error}` : '')
288
+ )
289
+ }
290
+ if (!relayed.signature) {
291
+ throw new Error('Gasless cash-out via Jito returned an empty transaction signature')
292
+ }
293
+ return {
294
+ txSignature: relayed.signature,
295
+ destinationAddress,
296
+ amount: netAmount,
297
+ relayerFee,
298
+ explorerUrl: getExplorerUrl(relayed.signature, cluster),
299
+ // Report the TRUE path: the relayer may have fallen back to direct submission.
300
+ viaJito: relayed.relayed,
301
+ }
302
+ }
303
+
304
+ // Direct path (primary): relayer is fee-payer; sendRawTransaction returns a base58 sig.
305
+ const txSignature = await connection.sendRawTransaction(transaction.serialize(), {
306
+ skipPreflight: false,
307
+ preflightCommitment: 'confirmed',
308
+ })
309
+ const confirmation = await connection.confirmTransaction(
310
+ {
311
+ signature: txSignature,
312
+ blockhash: build.blockhash,
313
+ lastValidBlockHeight: build.lastValidBlockHeight,
314
+ },
315
+ 'confirmed'
316
+ )
317
+ if (confirmation.value.err) {
318
+ throw new Error(
319
+ `Gasless cash-out landed but failed on-chain: ${JSON.stringify(confirmation.value.err)}`
320
+ )
321
+ }
322
+
323
+ return {
324
+ txSignature,
325
+ destinationAddress,
326
+ amount: netAmount,
327
+ relayerFee,
328
+ explorerUrl: getExplorerUrl(txSignature, cluster),
329
+ viaJito: false,
330
+ }
331
+ }
@@ -359,3 +359,19 @@ export {
359
359
  type VerifyProofResult,
360
360
  type Groth16Proof,
361
361
  } from './sunspot-verifier'
362
+
363
+ // Gasless Cash-Out (relayer fee-payer for stealth recipients)
364
+ export {
365
+ buildGaslessCashout,
366
+ submitGaslessCashout,
367
+ } from './gasless-cashout'
368
+ export type {
369
+ GaslessCashoutParams,
370
+ GaslessCashoutBuild,
371
+ SubmitGaslessCashoutParams,
372
+ GaslessCashoutResult,
373
+ } from './gasless-cashout'
374
+ export { computeRelayerFee } from './relayer-fee'
375
+ export type { RelayerFeeConfig } from './relayer-fee'
376
+ export { signEd25519WithScalar, deriveStealthSigner } from './stealth-signer'
377
+ export type { StealthSigner, DeriveStealthSignerParams } from './stealth-signer'
@@ -520,6 +520,7 @@ export class SolanaPrivacyAdapter {
520
520
  spendingPrivateKey: params.spendingPrivateKey,
521
521
  destinationAddress: params.destinationAddress,
522
522
  mint: new PublicKey(params.payment.mint),
523
+ version: params.payment.version,
523
524
  })
524
525
  }
525
526
 
@@ -666,6 +666,7 @@ async function processRawTransaction(
666
666
  const payment: SolanaScanResult = {
667
667
  stealthAddress: announcement.stealthAddress || '',
668
668
  ephemeralPublicKey: announcement.ephemeralPublicKey,
669
+ version: announcement.version as '1' | '2',
669
670
  amount: transferInfo?.amount ?? 0n,
670
671
  mint: transferInfo?.mint ?? '',
671
672
  tokenSymbol: transferInfo?.mint ? getTokenSymbol(transferInfo.mint) : undefined,
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Relayer fee model for gasless cash-out.
3
+ *
4
+ * The relayer fronts the SOL transaction fee on behalf of a stealth recipient
5
+ * and recovers its cost from the SPL tokens being cashed out (fee-from-claim).
6
+ * The fee is `max(flatFloor, amount * bps / 10_000)`: the floor guarantees the
7
+ * fixed SOL gas cost is covered even on tiny claims; bps scales on large ones.
8
+ * No price oracle in v1 — the floor is set in the token's base units.
9
+ */
10
+
11
+ /** Relayer fee configuration (per token mint). */
12
+ export interface RelayerFeeConfig {
13
+ /** Flat floor fee in the token's base units. Guarantees gas coverage on small claims. */
14
+ flatFloor: bigint
15
+ /** Basis points of the claim amount (1 bps = 0.01%). e.g. 10 = 0.1%. */
16
+ bps: number
17
+ }
18
+
19
+ /**
20
+ * Compute the relayer fee for a claim.
21
+ *
22
+ * @param amount - Gross amount available in the stealth token account (base units)
23
+ * @param config - Fee model
24
+ * @returns Fee in the token's base units (always >= flatFloor)
25
+ * @throws If `bps` is negative or non-integer, or if `amount` is negative, or if flatFloor is negative
26
+ */
27
+ export function computeRelayerFee(amount: bigint, config: RelayerFeeConfig): bigint {
28
+ if (!Number.isInteger(config.bps) || config.bps < 0) {
29
+ throw new Error('bps must be a non-negative integer')
30
+ }
31
+ if (amount < 0n) {
32
+ throw new Error('amount must be a non-negative bigint')
33
+ }
34
+ if (typeof config.flatFloor !== 'bigint' || config.flatFloor < 0n) {
35
+ throw new Error('flatFloor must be a non-negative bigint')
36
+ }
37
+ const bpsFee = (amount * BigInt(config.bps)) / 10_000n
38
+ return bpsFee > config.flatFloor ? bpsFee : config.flatFloor
39
+ }