@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.
- 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 +868 -545
- package/dist/browser.mjs +17 -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-Cwo3WhxX.d.mts → index-B_fGN4Fk.d.mts} +806 -597
- package/dist/{index-X8qPQdp6.d.ts → index-rqQpCeVM.d.ts} +806 -597
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +868 -545
- package/dist/index.mjs +17 -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 +16 -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
package/dist/index.mjs
CHANGED
|
@@ -448,7 +448,7 @@ import {
|
|
|
448
448
|
verifyOracleSignature,
|
|
449
449
|
verifyOwnership,
|
|
450
450
|
walletRegistry
|
|
451
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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,
|
package/dist/proofs/halo2.mjs
CHANGED
package/dist/proofs/kimchi.mjs
CHANGED
package/dist/proofs/noir.mjs
CHANGED
|
@@ -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-
|
|
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-
|
|
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,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sip-protocol/sdk",
|
|
3
|
-
"version": "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.
|
|
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.
|
|
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.
|
|
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": "^
|
|
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'
|
|
@@ -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
|
+
}
|