@sip-protocol/sdk 0.7.3 → 0.7.4
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/LICENSE +21 -0
- package/README.md +267 -0
- package/dist/{TransportWebUSB-TQ7WZ4LE.mjs → TransportWebUSB-YQMAGJAJ.mjs} +12 -9
- package/dist/browser.d.mts +10 -4
- package/dist/browser.d.ts +10 -4
- package/dist/browser.js +47556 -19603
- package/dist/browser.mjs +628 -48
- package/dist/chunk-4GRJ5MAW.mjs +152 -0
- package/dist/chunk-5D7A3L3W.mjs +717 -0
- package/dist/chunk-64AYA5F5.mjs +7834 -0
- package/dist/chunk-GMDGB22A.mjs +379 -0
- package/dist/chunk-I534WKN7.mjs +328 -0
- package/dist/chunk-IBZVA5Y7.mjs +1003 -0
- package/dist/chunk-PRRZAWJE.mjs +223 -0
- package/dist/{chunk-UJCSKKID.mjs → chunk-XGB3TDIC.mjs} +13 -1
- package/dist/{chunk-3M3HNQCW.mjs → chunk-YWGJ77A2.mjs} +28656 -13103
- package/dist/{chunk-6WGN57S2.mjs → chunk-Z3K7W5S3.mjs} +48 -0
- package/dist/constants-LHAAUC2T.mjs +51 -0
- package/dist/dist-2OGQ7FED.mjs +3957 -0
- package/dist/dist-IFHPYLDX.mjs +254 -0
- package/dist/fulfillment_proof-ANHVPKTB.mjs +21 -0
- package/dist/funding_proof-ICFZ5LHY.mjs +21 -0
- package/dist/{index-DIBZHOOQ.d.ts → index-DXh2IGkz.d.ts} +21239 -10304
- package/dist/{index-8MQz13eJ.d.mts → index-DeE1ZzA4.d.mts} +21239 -10304
- package/dist/index.d.mts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +48396 -19623
- package/dist/index.mjs +537 -19
- package/dist/interface-Bf7w1PLW.d.mts +679 -0
- package/dist/interface-Bf7w1PLW.d.ts +679 -0
- package/dist/{noir-DKfEzWy9.d.mts → noir-kzbLVTei.d.mts} +31 -21
- package/dist/{noir-DKfEzWy9.d.ts → noir-kzbLVTei.d.ts} +31 -21
- package/dist/proofs/halo2.d.mts +151 -0
- package/dist/proofs/halo2.d.ts +151 -0
- package/dist/proofs/halo2.js +350 -0
- package/dist/proofs/halo2.mjs +11 -0
- package/dist/proofs/kimchi.d.mts +160 -0
- package/dist/proofs/kimchi.d.ts +160 -0
- package/dist/proofs/kimchi.js +431 -0
- package/dist/proofs/kimchi.mjs +13 -0
- package/dist/proofs/noir.d.mts +1 -1
- package/dist/proofs/noir.d.ts +1 -1
- package/dist/proofs/noir.js +74 -18
- package/dist/proofs/noir.mjs +84 -24
- package/dist/solana-U3MEGU7W.mjs +280 -0
- package/dist/validity_proof-3POXLPNY.mjs +21 -0
- package/package.json +54 -21
- package/src/adapters/index.ts +41 -0
- package/src/adapters/jupiter.ts +571 -0
- package/src/adapters/near-intents.ts +135 -0
- package/src/advisor/advisor.ts +653 -0
- package/src/advisor/index.ts +54 -0
- package/src/advisor/tools.ts +303 -0
- package/src/advisor/types.ts +164 -0
- package/src/chains/ethereum/announcement.ts +536 -0
- package/src/chains/ethereum/bnb-optimizations.ts +474 -0
- package/src/chains/ethereum/commitment.ts +522 -0
- package/src/chains/ethereum/constants.ts +462 -0
- package/src/chains/ethereum/deployment.ts +596 -0
- package/src/chains/ethereum/gas-estimation.ts +538 -0
- package/src/chains/ethereum/index.ts +268 -0
- package/src/chains/ethereum/optimizations.ts +614 -0
- package/src/chains/ethereum/privacy-adapter.ts +855 -0
- package/src/chains/ethereum/registry.ts +584 -0
- package/src/chains/ethereum/rpc.ts +905 -0
- package/src/chains/ethereum/stealth.ts +491 -0
- package/src/chains/ethereum/token.ts +790 -0
- package/src/chains/ethereum/transfer.ts +637 -0
- package/src/chains/ethereum/types.ts +456 -0
- package/src/chains/ethereum/viewing-key.ts +455 -0
- package/src/chains/near/commitment.ts +608 -0
- package/src/chains/near/constants.ts +284 -0
- package/src/chains/near/function-call.ts +871 -0
- package/src/chains/near/history.ts +654 -0
- package/src/chains/near/implicit-account.ts +840 -0
- package/src/chains/near/index.ts +393 -0
- package/src/chains/near/native-transfer.ts +658 -0
- package/src/chains/near/nep141.ts +775 -0
- package/src/chains/near/privacy-adapter.ts +889 -0
- package/src/chains/near/resolver.ts +971 -0
- package/src/chains/near/rpc.ts +1016 -0
- package/src/chains/near/stealth.ts +419 -0
- package/src/chains/near/types.ts +317 -0
- package/src/chains/near/viewing-key.ts +876 -0
- package/src/chains/solana/anchor-transfer.ts +386 -0
- package/src/chains/solana/commitment.ts +577 -0
- package/src/chains/solana/constants.ts +126 -12
- package/src/chains/solana/ephemeral-keys.ts +543 -0
- package/src/chains/solana/index.ts +252 -1
- package/src/chains/solana/key-derivation.ts +418 -0
- package/src/chains/solana/kit-compat.ts +334 -0
- package/src/chains/solana/optimizations.ts +560 -0
- package/src/chains/solana/privacy-adapter.ts +605 -0
- package/src/chains/solana/providers/generic.ts +47 -6
- package/src/chains/solana/providers/helius-enhanced-types.ts +336 -0
- package/src/chains/solana/providers/helius-enhanced.ts +623 -0
- package/src/chains/solana/providers/helius.ts +186 -33
- package/src/chains/solana/providers/index.ts +31 -0
- package/src/chains/solana/providers/interface.ts +61 -18
- package/src/chains/solana/providers/quicknode.ts +409 -0
- package/src/chains/solana/providers/triton.ts +426 -0
- package/src/chains/solana/providers/webhook.ts +338 -67
- package/src/chains/solana/rpc-client.ts +1150 -0
- package/src/chains/solana/scan.ts +83 -66
- package/src/chains/solana/sol-transfer.ts +732 -0
- package/src/chains/solana/spl-transfer.ts +886 -0
- package/src/chains/solana/stealth-scanner.ts +703 -0
- package/src/chains/solana/sunspot-verifier.ts +453 -0
- package/src/chains/solana/transaction-builder.ts +755 -0
- package/src/chains/solana/transfer.ts +74 -5
- package/src/chains/solana/types.ts +57 -6
- package/src/chains/solana/utils.ts +110 -0
- package/src/chains/solana/viewing-key.ts +807 -0
- package/src/compliance/fireblocks.ts +921 -0
- package/src/compliance/index.ts +23 -0
- package/src/compliance/range-sas.ts +398 -33
- package/src/config/endpoints.ts +100 -0
- package/src/crypto.ts +11 -8
- package/src/errors.ts +82 -0
- package/src/evm/erc4337-relayer.ts +830 -0
- package/src/evm/index.ts +47 -0
- package/src/fees/calculator.ts +396 -0
- package/src/fees/index.ts +87 -0
- package/src/fees/near-contract.ts +429 -0
- package/src/fees/types.ts +268 -0
- package/src/index.ts +686 -1
- package/src/intent.ts +6 -3
- package/src/logger.ts +324 -0
- package/src/network/index.ts +80 -0
- package/src/network/proxy.ts +691 -0
- package/src/optimizations/index.ts +541 -0
- package/src/oracle/types.ts +1 -0
- package/src/privacy-backends/arcium-types.ts +727 -0
- package/src/privacy-backends/arcium.ts +719 -0
- package/src/privacy-backends/combined-privacy.ts +866 -0
- package/src/privacy-backends/cspl-token.ts +595 -0
- package/src/privacy-backends/cspl-types.ts +512 -0
- package/src/privacy-backends/cspl.ts +907 -0
- package/src/privacy-backends/health.ts +488 -0
- package/src/privacy-backends/inco-types.ts +323 -0
- package/src/privacy-backends/inco.ts +616 -0
- package/src/privacy-backends/index.ts +254 -4
- package/src/privacy-backends/interface.ts +649 -6
- package/src/privacy-backends/lru-cache.ts +343 -0
- package/src/privacy-backends/magicblock.ts +458 -0
- package/src/privacy-backends/mock.ts +258 -0
- package/src/privacy-backends/privacycash.ts +13 -17
- package/src/privacy-backends/private-swap.ts +570 -0
- package/src/privacy-backends/rate-limiter.ts +683 -0
- package/src/privacy-backends/registry.ts +414 -2
- package/src/privacy-backends/router.ts +283 -3
- package/src/privacy-backends/shadowwire.ts +449 -0
- package/src/privacy-backends/sip-native.ts +3 -0
- package/src/privacy-logger.ts +191 -0
- package/src/production-safety.ts +373 -0
- package/src/proofs/aggregator.ts +1029 -0
- package/src/proofs/browser-composer.ts +1150 -0
- package/src/proofs/browser.ts +113 -25
- package/src/proofs/cache/index.ts +127 -0
- package/src/proofs/cache/interface.ts +545 -0
- package/src/proofs/cache/key-generator.ts +188 -0
- package/src/proofs/cache/lru-cache.ts +481 -0
- package/src/proofs/cache/multi-tier-cache.ts +575 -0
- package/src/proofs/cache/persistent-cache.ts +788 -0
- package/src/proofs/compliance-proof.ts +872 -0
- package/src/proofs/composer/base.ts +923 -0
- package/src/proofs/composer/index.ts +25 -0
- package/src/proofs/composer/interface.ts +518 -0
- package/src/proofs/composer/types.ts +383 -0
- package/src/proofs/converters/halo2.ts +452 -0
- package/src/proofs/converters/index.ts +208 -0
- package/src/proofs/converters/interface.ts +363 -0
- package/src/proofs/converters/kimchi.ts +462 -0
- package/src/proofs/converters/noir.ts +451 -0
- package/src/proofs/fallback.ts +888 -0
- package/src/proofs/halo2.ts +42 -0
- package/src/proofs/index.ts +471 -0
- package/src/proofs/interface.ts +13 -0
- package/src/proofs/kimchi.ts +42 -0
- package/src/proofs/lazy.ts +1004 -0
- package/src/proofs/mock.ts +25 -1
- package/src/proofs/noir.ts +110 -29
- package/src/proofs/orchestrator.ts +960 -0
- package/src/proofs/parallel/concurrency.ts +297 -0
- package/src/proofs/parallel/dependency-graph.ts +602 -0
- package/src/proofs/parallel/executor.ts +420 -0
- package/src/proofs/parallel/index.ts +131 -0
- package/src/proofs/parallel/interface.ts +685 -0
- package/src/proofs/parallel/worker-pool.ts +644 -0
- package/src/proofs/providers/halo2.ts +560 -0
- package/src/proofs/providers/index.ts +34 -0
- package/src/proofs/providers/kimchi.ts +641 -0
- package/src/proofs/validator.ts +881 -0
- package/src/proofs/verifier.ts +867 -0
- package/src/quantum/index.ts +112 -0
- package/src/quantum/winternitz-vault.ts +639 -0
- package/src/quantum/wots.ts +611 -0
- package/src/settlement/backends/direct-chain.ts +1 -0
- package/src/settlement/index.ts +9 -0
- package/src/settlement/router.ts +732 -46
- package/src/solana/index.ts +72 -0
- package/src/solana/jito-relayer.ts +687 -0
- package/src/solana/noir-verifier-types.ts +430 -0
- package/src/solana/noir-verifier.ts +816 -0
- package/src/stealth/address-derivation.ts +193 -0
- package/src/stealth/ed25519.ts +431 -0
- package/src/stealth/index.ts +233 -0
- package/src/stealth/meta-address.ts +221 -0
- package/src/stealth/secp256k1.ts +368 -0
- package/src/stealth/utils.ts +194 -0
- package/src/stealth.ts +50 -1504
- package/src/sync/index.ts +106 -0
- package/src/sync/manager.ts +504 -0
- package/src/sync/mock-provider.ts +318 -0
- package/src/sync/oblivious.ts +625 -0
- package/src/tokens/index.ts +15 -0
- package/src/tokens/registry.ts +301 -0
- package/src/utils/deprecation.ts +94 -0
- package/src/utils/index.ts +9 -0
- package/src/wallet/ethereum/index.ts +68 -0
- package/src/wallet/ethereum/metamask-privacy.ts +420 -0
- package/src/wallet/ethereum/multi-wallet.ts +646 -0
- package/src/wallet/ethereum/privacy-adapter.ts +700 -0
- package/src/wallet/ethereum/types.ts +3 -1
- package/src/wallet/ethereum/walletconnect-adapter.ts +675 -0
- package/src/wallet/hardware/index.ts +10 -0
- package/src/wallet/hardware/ledger-privacy.ts +414 -0
- package/src/wallet/index.ts +71 -0
- package/src/wallet/near/adapter.ts +626 -0
- package/src/wallet/near/index.ts +86 -0
- package/src/wallet/near/meteor-wallet.ts +1153 -0
- package/src/wallet/near/my-near-wallet.ts +790 -0
- package/src/wallet/near/wallet-selector.ts +702 -0
- package/src/wallet/solana/adapter.ts +6 -4
- package/src/wallet/solana/index.ts +13 -0
- package/src/wallet/solana/privacy-adapter.ts +567 -0
- package/src/wallet/sui/types.ts +6 -4
- package/src/zcash/rpc-client.ts +13 -6
- package/dist/chunk-2XIVXWHA.mjs +0 -1930
- package/dist/chunk-3INS3PR5.mjs +0 -884
- package/dist/chunk-3OVABDRH.mjs +0 -17096
- package/dist/chunk-7RFRWDCW.mjs +0 -1504
- package/dist/chunk-DLDWZFYC.mjs +0 -1495
- package/dist/chunk-E6SZWREQ.mjs +0 -57
- package/dist/chunk-F6F73W35.mjs +0 -16166
- package/dist/chunk-G33LB27A.mjs +0 -16166
- package/dist/chunk-HGU6HZRC.mjs +0 -231
- package/dist/chunk-L2K34JCU.mjs +0 -1496
- package/dist/chunk-OFDBEIEK.mjs +0 -16166
- package/dist/chunk-SF7YSLF5.mjs +0 -1515
- package/dist/chunk-SN4ZDTVW.mjs +0 -16166
- package/dist/chunk-WWUSGOXE.mjs +0 -17129
- package/dist/constants-VOI7BSLK.mjs +0 -27
- package/dist/index-B71aXVzk.d.ts +0 -13264
- package/dist/index-BYZbDjal.d.ts +0 -11390
- package/dist/index-CHB3KuOB.d.mts +0 -11859
- package/dist/index-CzWPI6Le.d.ts +0 -11859
- package/dist/index-pOIIuwfV.d.mts +0 -13264
- package/dist/index-xbWjohNq.d.mts +0 -11390
- package/dist/solana-4O4K45VU.mjs +0 -46
- package/dist/solana-5EMCTPTS.mjs +0 -46
- package/dist/solana-NDABAZ6P.mjs +0 -56
- package/dist/solana-Q4NAVBTS.mjs +0 -46
- package/dist/solana-ZYO63LY5.mjs +0 -46
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native SOL Transfer with Stealth Addresses
|
|
3
|
+
*
|
|
4
|
+
* Provides private SOL transfers using stealth addresses.
|
|
5
|
+
* Simpler than SPL transfers - no ATA required.
|
|
6
|
+
*
|
|
7
|
+
* @module chains/solana/sol-transfer
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
Connection,
|
|
12
|
+
PublicKey,
|
|
13
|
+
Transaction,
|
|
14
|
+
TransactionInstruction,
|
|
15
|
+
SystemProgram,
|
|
16
|
+
LAMPORTS_PER_SOL,
|
|
17
|
+
type Commitment,
|
|
18
|
+
} from '@solana/web3.js'
|
|
19
|
+
import {
|
|
20
|
+
generateEd25519StealthAddress,
|
|
21
|
+
ed25519PublicKeyToSolanaAddress,
|
|
22
|
+
} from '../../stealth'
|
|
23
|
+
import { ValidationError } from '../../errors'
|
|
24
|
+
import type { StealthMetaAddress } from '@sip-protocol/types'
|
|
25
|
+
import { createAnnouncementMemo } from './types'
|
|
26
|
+
import {
|
|
27
|
+
MEMO_PROGRAM_ID,
|
|
28
|
+
getExplorerUrl,
|
|
29
|
+
ESTIMATED_TX_FEE_LAMPORTS,
|
|
30
|
+
type SolanaCluster,
|
|
31
|
+
} from './constants'
|
|
32
|
+
|
|
33
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Minimum rent-exempt balance for a Solana account (in lamports)
|
|
37
|
+
* This is approximately 0.00089 SOL
|
|
38
|
+
*/
|
|
39
|
+
export const RENT_EXEMPT_MINIMUM = 890_880n
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Recommended buffer for new stealth accounts
|
|
43
|
+
* Slightly more than minimum to cover edge cases
|
|
44
|
+
*/
|
|
45
|
+
export const STEALTH_ACCOUNT_BUFFER = 1_000_000n // ~0.001 SOL
|
|
46
|
+
|
|
47
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parameters for native SOL transfer to stealth address
|
|
51
|
+
*/
|
|
52
|
+
export interface SOLTransferParams {
|
|
53
|
+
/** Solana RPC connection */
|
|
54
|
+
connection: Connection
|
|
55
|
+
/** Sender's public key */
|
|
56
|
+
sender: PublicKey
|
|
57
|
+
/** Recipient's stealth meta-address */
|
|
58
|
+
recipientMetaAddress: StealthMetaAddress
|
|
59
|
+
/** Amount to transfer (in lamports) */
|
|
60
|
+
amount: bigint
|
|
61
|
+
/** Function to sign the transaction */
|
|
62
|
+
signTransaction: <T extends Transaction>(tx: T) => Promise<T>
|
|
63
|
+
/** Include rent-exempt buffer (default: true for new accounts) */
|
|
64
|
+
includeRentBuffer?: boolean
|
|
65
|
+
/** Transaction commitment level */
|
|
66
|
+
commitment?: Commitment
|
|
67
|
+
/** Custom memo to append */
|
|
68
|
+
customMemo?: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parameters for max SOL transfer (send all available balance)
|
|
73
|
+
*/
|
|
74
|
+
export interface MaxSOLTransferParams extends Omit<SOLTransferParams, 'amount'> {
|
|
75
|
+
/** Minimum to leave in sender account (default: 0) */
|
|
76
|
+
keepMinimum?: bigint
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Result of a native SOL transfer
|
|
81
|
+
*/
|
|
82
|
+
export interface SOLTransferResult {
|
|
83
|
+
/** Transaction signature */
|
|
84
|
+
txSignature: string
|
|
85
|
+
/** Stealth address (base58) */
|
|
86
|
+
stealthAddress: string
|
|
87
|
+
/** Ephemeral public key (base58) */
|
|
88
|
+
ephemeralPublicKey: string
|
|
89
|
+
/** View tag for scanning */
|
|
90
|
+
viewTag: string
|
|
91
|
+
/** Explorer URL */
|
|
92
|
+
explorerUrl: string
|
|
93
|
+
/** Cluster */
|
|
94
|
+
cluster: SolanaCluster
|
|
95
|
+
/** Amount transferred (lamports) */
|
|
96
|
+
amount: bigint
|
|
97
|
+
/** Amount in SOL */
|
|
98
|
+
amountSol: number
|
|
99
|
+
/** Whether rent buffer was included */
|
|
100
|
+
rentBufferIncluded: boolean
|
|
101
|
+
/** Estimated fee paid */
|
|
102
|
+
estimatedFee: bigint
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* SOL transfer validation result
|
|
107
|
+
*/
|
|
108
|
+
export interface SOLTransferValidation {
|
|
109
|
+
/** Whether transfer is valid */
|
|
110
|
+
isValid: boolean
|
|
111
|
+
/** Validation errors */
|
|
112
|
+
errors: string[]
|
|
113
|
+
/** Sender's SOL balance (lamports) */
|
|
114
|
+
senderBalance?: bigint
|
|
115
|
+
/** Sender's SOL balance in SOL */
|
|
116
|
+
senderBalanceSol?: number
|
|
117
|
+
/** Whether stealth account exists */
|
|
118
|
+
stealthAccountExists?: boolean
|
|
119
|
+
/** Recommended rent buffer */
|
|
120
|
+
recommendedRentBuffer: bigint
|
|
121
|
+
/** Estimated total fee */
|
|
122
|
+
estimatedFee: bigint
|
|
123
|
+
/** Maximum transferable amount */
|
|
124
|
+
maxTransferable?: bigint
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Gas estimation for SOL transfer
|
|
129
|
+
*/
|
|
130
|
+
export interface SOLTransferEstimate {
|
|
131
|
+
/** Base transaction fee (lamports) */
|
|
132
|
+
baseFee: bigint
|
|
133
|
+
/** Rent buffer for new account (if needed) */
|
|
134
|
+
rentBuffer: bigint
|
|
135
|
+
/** Total estimated cost (fee + rent) */
|
|
136
|
+
totalCost: bigint
|
|
137
|
+
/** Whether stealth account exists */
|
|
138
|
+
stealthAccountExists: boolean
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ─── Validation ───────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Validate a SOL transfer before execution
|
|
145
|
+
*
|
|
146
|
+
* Checks:
|
|
147
|
+
* - Sender has sufficient balance
|
|
148
|
+
* - Meta-address is valid for Solana
|
|
149
|
+
* - Amount is valid and meets minimums
|
|
150
|
+
* - Stealth account requirements
|
|
151
|
+
*
|
|
152
|
+
* @param params - Transfer parameters to validate
|
|
153
|
+
* @returns Validation result
|
|
154
|
+
*/
|
|
155
|
+
export async function validateSOLTransfer(
|
|
156
|
+
params: Omit<SOLTransferParams, 'signTransaction'>
|
|
157
|
+
): Promise<SOLTransferValidation> {
|
|
158
|
+
const errors: string[] = []
|
|
159
|
+
let senderBalance: bigint | undefined
|
|
160
|
+
let senderBalanceSol: number | undefined
|
|
161
|
+
let stealthAccountExists = false
|
|
162
|
+
let maxTransferable: bigint | undefined
|
|
163
|
+
|
|
164
|
+
const estimatedFee = ESTIMATED_TX_FEE_LAMPORTS
|
|
165
|
+
let recommendedRentBuffer = 0n
|
|
166
|
+
|
|
167
|
+
// Validate meta-address
|
|
168
|
+
if (!params.recipientMetaAddress) {
|
|
169
|
+
errors.push('Recipient meta-address is required')
|
|
170
|
+
} else if (params.recipientMetaAddress.chain !== 'solana') {
|
|
171
|
+
errors.push(`Invalid chain: expected 'solana', got '${params.recipientMetaAddress.chain}'`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Validate amount
|
|
175
|
+
if (params.amount <= 0n) {
|
|
176
|
+
errors.push('Amount must be greater than 0')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Get sender balance
|
|
180
|
+
try {
|
|
181
|
+
const balance = await params.connection.getBalance(params.sender)
|
|
182
|
+
senderBalance = BigInt(balance)
|
|
183
|
+
senderBalanceSol = balance / LAMPORTS_PER_SOL
|
|
184
|
+
|
|
185
|
+
// Calculate max transferable (balance - fee)
|
|
186
|
+
maxTransferable = senderBalance > estimatedFee ? senderBalance - estimatedFee : 0n
|
|
187
|
+
} catch (error) {
|
|
188
|
+
errors.push(`Failed to check sender balance: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check if stealth account exists and calculate rent buffer
|
|
192
|
+
if (params.recipientMetaAddress && params.recipientMetaAddress.chain === 'solana') {
|
|
193
|
+
try {
|
|
194
|
+
const { stealthAddress } = generateEd25519StealthAddress(params.recipientMetaAddress)
|
|
195
|
+
const stealthAddressBase58 = ed25519PublicKeyToSolanaAddress(stealthAddress.address)
|
|
196
|
+
const stealthPubkey = new PublicKey(stealthAddressBase58)
|
|
197
|
+
|
|
198
|
+
const accountInfo = await params.connection.getAccountInfo(stealthPubkey)
|
|
199
|
+
stealthAccountExists = accountInfo !== null
|
|
200
|
+
|
|
201
|
+
if (!stealthAccountExists) {
|
|
202
|
+
// Account doesn't exist, recommend rent buffer
|
|
203
|
+
recommendedRentBuffer = STEALTH_ACCOUNT_BUFFER
|
|
204
|
+
|
|
205
|
+
// Amount must be >= rent-exempt minimum
|
|
206
|
+
if (params.amount < RENT_EXEMPT_MINIMUM) {
|
|
207
|
+
errors.push(
|
|
208
|
+
`Amount ${formatLamports(params.amount)} SOL is below rent-exempt minimum ${formatLamports(RENT_EXEMPT_MINIMUM)} SOL`
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
// Cannot generate stealth address - error already captured
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check sufficient balance
|
|
218
|
+
if (senderBalance !== undefined) {
|
|
219
|
+
const totalNeeded = params.amount + estimatedFee
|
|
220
|
+
if (senderBalance < totalNeeded) {
|
|
221
|
+
errors.push(
|
|
222
|
+
`Insufficient balance: have ${formatLamports(senderBalance)} SOL, need ${formatLamports(totalNeeded)} SOL (including fee)`
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
isValid: errors.length === 0,
|
|
229
|
+
errors,
|
|
230
|
+
senderBalance,
|
|
231
|
+
senderBalanceSol,
|
|
232
|
+
stealthAccountExists,
|
|
233
|
+
recommendedRentBuffer,
|
|
234
|
+
estimatedFee,
|
|
235
|
+
maxTransferable,
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Estimate gas and costs for SOL transfer
|
|
241
|
+
*
|
|
242
|
+
* @param connection - Solana RPC connection
|
|
243
|
+
* @param recipientMetaAddress - Recipient's meta-address
|
|
244
|
+
* @returns Gas estimation
|
|
245
|
+
*/
|
|
246
|
+
export async function estimateSOLTransfer(
|
|
247
|
+
connection: Connection,
|
|
248
|
+
recipientMetaAddress: StealthMetaAddress
|
|
249
|
+
): Promise<SOLTransferEstimate> {
|
|
250
|
+
const baseFee = ESTIMATED_TX_FEE_LAMPORTS
|
|
251
|
+
let rentBuffer = 0n
|
|
252
|
+
let stealthAccountExists = false
|
|
253
|
+
|
|
254
|
+
// Check if stealth account exists
|
|
255
|
+
const { stealthAddress } = generateEd25519StealthAddress(recipientMetaAddress)
|
|
256
|
+
const stealthAddressBase58 = ed25519PublicKeyToSolanaAddress(stealthAddress.address)
|
|
257
|
+
const stealthPubkey = new PublicKey(stealthAddressBase58)
|
|
258
|
+
|
|
259
|
+
const accountInfo = await connection.getAccountInfo(stealthPubkey)
|
|
260
|
+
stealthAccountExists = accountInfo !== null
|
|
261
|
+
|
|
262
|
+
if (!stealthAccountExists) {
|
|
263
|
+
rentBuffer = STEALTH_ACCOUNT_BUFFER
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
baseFee,
|
|
268
|
+
rentBuffer,
|
|
269
|
+
totalCost: baseFee + rentBuffer,
|
|
270
|
+
stealthAccountExists,
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ─── Transfer Functions ───────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Send native SOL privately to a stealth address
|
|
278
|
+
*
|
|
279
|
+
* @param params - Transfer parameters
|
|
280
|
+
* @returns Transfer result
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* ```typescript
|
|
284
|
+
* const result = await sendSOLTransfer({
|
|
285
|
+
* connection,
|
|
286
|
+
* sender: wallet.publicKey,
|
|
287
|
+
* recipientMetaAddress: recipientMeta,
|
|
288
|
+
* amount: 1_000_000_000n, // 1 SOL
|
|
289
|
+
* signTransaction: wallet.signTransaction,
|
|
290
|
+
* })
|
|
291
|
+
*
|
|
292
|
+
* console.log(`Sent ${result.amountSol} SOL to ${result.stealthAddress}`)
|
|
293
|
+
* ```
|
|
294
|
+
*/
|
|
295
|
+
export async function sendSOLTransfer(
|
|
296
|
+
params: SOLTransferParams
|
|
297
|
+
): Promise<SOLTransferResult> {
|
|
298
|
+
const {
|
|
299
|
+
connection,
|
|
300
|
+
sender,
|
|
301
|
+
recipientMetaAddress,
|
|
302
|
+
amount,
|
|
303
|
+
signTransaction,
|
|
304
|
+
includeRentBuffer = true,
|
|
305
|
+
commitment = 'confirmed',
|
|
306
|
+
customMemo,
|
|
307
|
+
} = params
|
|
308
|
+
|
|
309
|
+
// Validate meta-address
|
|
310
|
+
if (!recipientMetaAddress) {
|
|
311
|
+
throw new ValidationError('recipientMetaAddress is required', 'recipientMetaAddress')
|
|
312
|
+
}
|
|
313
|
+
if (recipientMetaAddress.chain !== 'solana') {
|
|
314
|
+
throw new ValidationError(
|
|
315
|
+
`Invalid chain: expected 'solana', got '${recipientMetaAddress.chain}'`,
|
|
316
|
+
'recipientMetaAddress.chain'
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Validate amount
|
|
321
|
+
if (amount <= 0n) {
|
|
322
|
+
throw new ValidationError('amount must be greater than 0', 'amount')
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Generate stealth address
|
|
326
|
+
const { stealthAddress } = generateEd25519StealthAddress(recipientMetaAddress)
|
|
327
|
+
const stealthAddressBase58 = ed25519PublicKeyToSolanaAddress(stealthAddress.address)
|
|
328
|
+
const stealthPubkey = new PublicKey(stealthAddressBase58)
|
|
329
|
+
const ephemeralPubkeyBase58 = ed25519PublicKeyToSolanaAddress(stealthAddress.ephemeralPublicKey)
|
|
330
|
+
|
|
331
|
+
// Check if stealth account exists
|
|
332
|
+
const accountInfo = await connection.getAccountInfo(stealthPubkey)
|
|
333
|
+
const stealthAccountExists = accountInfo !== null
|
|
334
|
+
|
|
335
|
+
// Calculate actual transfer amount
|
|
336
|
+
const transferAmount = amount
|
|
337
|
+
let rentBufferIncluded = false
|
|
338
|
+
|
|
339
|
+
if (!stealthAccountExists && includeRentBuffer) {
|
|
340
|
+
// Check if amount is below rent-exempt minimum
|
|
341
|
+
if (amount < RENT_EXEMPT_MINIMUM) {
|
|
342
|
+
throw new ValidationError(
|
|
343
|
+
`Amount ${formatLamports(amount)} SOL is below rent-exempt minimum ${formatLamports(RENT_EXEMPT_MINIMUM)} SOL for new accounts`,
|
|
344
|
+
'amount'
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
rentBufferIncluded = true
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Build transaction
|
|
351
|
+
const transaction = new Transaction()
|
|
352
|
+
|
|
353
|
+
// Add SOL transfer instruction
|
|
354
|
+
transaction.add(
|
|
355
|
+
SystemProgram.transfer({
|
|
356
|
+
fromPubkey: sender,
|
|
357
|
+
toPubkey: stealthPubkey,
|
|
358
|
+
lamports: transferAmount,
|
|
359
|
+
})
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
// Add SIP announcement memo
|
|
363
|
+
const viewTagHex = stealthAddress.viewTag.toString(16).padStart(2, '0')
|
|
364
|
+
const memoContent = createAnnouncementMemo(
|
|
365
|
+
ephemeralPubkeyBase58,
|
|
366
|
+
viewTagHex,
|
|
367
|
+
stealthAddressBase58
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
transaction.add(
|
|
371
|
+
new TransactionInstruction({
|
|
372
|
+
keys: [],
|
|
373
|
+
programId: new PublicKey(MEMO_PROGRAM_ID),
|
|
374
|
+
data: Buffer.from(memoContent, 'utf-8'),
|
|
375
|
+
})
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
// Add custom memo if provided
|
|
379
|
+
if (customMemo) {
|
|
380
|
+
transaction.add(
|
|
381
|
+
new TransactionInstruction({
|
|
382
|
+
keys: [],
|
|
383
|
+
programId: new PublicKey(MEMO_PROGRAM_ID),
|
|
384
|
+
data: Buffer.from(customMemo, 'utf-8'),
|
|
385
|
+
})
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Get blockhash and sign
|
|
390
|
+
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(commitment)
|
|
391
|
+
transaction.recentBlockhash = blockhash
|
|
392
|
+
transaction.lastValidBlockHeight = lastValidBlockHeight
|
|
393
|
+
transaction.feePayer = sender
|
|
394
|
+
|
|
395
|
+
const signedTx = await signTransaction(transaction)
|
|
396
|
+
|
|
397
|
+
// Send and confirm
|
|
398
|
+
const txSignature = await connection.sendRawTransaction(signedTx.serialize(), {
|
|
399
|
+
skipPreflight: false,
|
|
400
|
+
preflightCommitment: commitment,
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
await connection.confirmTransaction(
|
|
404
|
+
{ signature: txSignature, blockhash, lastValidBlockHeight },
|
|
405
|
+
commitment
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
const cluster = detectCluster(connection.rpcEndpoint)
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
txSignature,
|
|
412
|
+
stealthAddress: stealthAddressBase58,
|
|
413
|
+
ephemeralPublicKey: ephemeralPubkeyBase58,
|
|
414
|
+
viewTag: viewTagHex,
|
|
415
|
+
explorerUrl: getExplorerUrl(txSignature, cluster),
|
|
416
|
+
cluster,
|
|
417
|
+
amount: transferAmount,
|
|
418
|
+
amountSol: Number(transferAmount) / LAMPORTS_PER_SOL,
|
|
419
|
+
rentBufferIncluded,
|
|
420
|
+
estimatedFee: ESTIMATED_TX_FEE_LAMPORTS,
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Send maximum available SOL privately to a stealth address
|
|
426
|
+
*
|
|
427
|
+
* Calculates maximum transferable amount (balance - fee - keepMinimum)
|
|
428
|
+
* and sends that amount.
|
|
429
|
+
*
|
|
430
|
+
* @param params - Transfer parameters
|
|
431
|
+
* @returns Transfer result
|
|
432
|
+
*
|
|
433
|
+
* @example
|
|
434
|
+
* ```typescript
|
|
435
|
+
* // Send all SOL
|
|
436
|
+
* const result = await sendMaxSOLTransfer({
|
|
437
|
+
* connection,
|
|
438
|
+
* sender: wallet.publicKey,
|
|
439
|
+
* recipientMetaAddress: recipientMeta,
|
|
440
|
+
* signTransaction: wallet.signTransaction,
|
|
441
|
+
* })
|
|
442
|
+
*
|
|
443
|
+
* // Keep 0.1 SOL
|
|
444
|
+
* const result = await sendMaxSOLTransfer({
|
|
445
|
+
* connection,
|
|
446
|
+
* sender: wallet.publicKey,
|
|
447
|
+
* recipientMetaAddress: recipientMeta,
|
|
448
|
+
* keepMinimum: 100_000_000n, // 0.1 SOL
|
|
449
|
+
* signTransaction: wallet.signTransaction,
|
|
450
|
+
* })
|
|
451
|
+
* ```
|
|
452
|
+
*/
|
|
453
|
+
export async function sendMaxSOLTransfer(
|
|
454
|
+
params: MaxSOLTransferParams
|
|
455
|
+
): Promise<SOLTransferResult> {
|
|
456
|
+
const {
|
|
457
|
+
connection,
|
|
458
|
+
sender,
|
|
459
|
+
recipientMetaAddress,
|
|
460
|
+
signTransaction,
|
|
461
|
+
keepMinimum = 0n,
|
|
462
|
+
includeRentBuffer = true,
|
|
463
|
+
commitment = 'confirmed',
|
|
464
|
+
customMemo,
|
|
465
|
+
} = params
|
|
466
|
+
|
|
467
|
+
// Get sender balance
|
|
468
|
+
const balance = await connection.getBalance(sender)
|
|
469
|
+
const balanceLamports = BigInt(balance)
|
|
470
|
+
|
|
471
|
+
// Calculate max transferable
|
|
472
|
+
const estimatedFee = ESTIMATED_TX_FEE_LAMPORTS
|
|
473
|
+
const reserved = estimatedFee + keepMinimum
|
|
474
|
+
|
|
475
|
+
if (balanceLamports <= reserved) {
|
|
476
|
+
throw new ValidationError(
|
|
477
|
+
`Insufficient balance for max transfer: have ${formatLamports(balanceLamports)} SOL, need at least ${formatLamports(reserved + 1n)} SOL`,
|
|
478
|
+
'amount'
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const maxAmount = balanceLamports - reserved
|
|
483
|
+
|
|
484
|
+
// Check rent requirement for new accounts
|
|
485
|
+
const { stealthAddress } = generateEd25519StealthAddress(recipientMetaAddress)
|
|
486
|
+
const stealthAddressBase58 = ed25519PublicKeyToSolanaAddress(stealthAddress.address)
|
|
487
|
+
const stealthPubkey = new PublicKey(stealthAddressBase58)
|
|
488
|
+
|
|
489
|
+
const accountInfo = await connection.getAccountInfo(stealthPubkey)
|
|
490
|
+
const stealthAccountExists = accountInfo !== null
|
|
491
|
+
|
|
492
|
+
if (!stealthAccountExists && maxAmount < RENT_EXEMPT_MINIMUM) {
|
|
493
|
+
throw new ValidationError(
|
|
494
|
+
`Insufficient balance for max transfer to new account: need at least ${formatLamports(RENT_EXEMPT_MINIMUM)} SOL after fees`,
|
|
495
|
+
'amount'
|
|
496
|
+
)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return sendSOLTransfer({
|
|
500
|
+
connection,
|
|
501
|
+
sender,
|
|
502
|
+
recipientMetaAddress,
|
|
503
|
+
amount: maxAmount,
|
|
504
|
+
signTransaction,
|
|
505
|
+
includeRentBuffer,
|
|
506
|
+
commitment,
|
|
507
|
+
customMemo,
|
|
508
|
+
})
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ─── Batch Transfer ───────────────────────────────────────────────────────────
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Batch SOL transfer item
|
|
515
|
+
*/
|
|
516
|
+
export interface BatchSOLTransferItem {
|
|
517
|
+
/** Recipient's stealth meta-address */
|
|
518
|
+
recipientMetaAddress: StealthMetaAddress
|
|
519
|
+
/** Amount in lamports */
|
|
520
|
+
amount: bigint
|
|
521
|
+
/** Custom memo */
|
|
522
|
+
customMemo?: string
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Batch SOL transfer result
|
|
527
|
+
*/
|
|
528
|
+
export interface BatchSOLTransferResult {
|
|
529
|
+
/** Transaction signature */
|
|
530
|
+
txSignature: string
|
|
531
|
+
/** Individual transfer results */
|
|
532
|
+
transfers: Array<{
|
|
533
|
+
stealthAddress: string
|
|
534
|
+
ephemeralPublicKey: string
|
|
535
|
+
viewTag: string
|
|
536
|
+
amount: bigint
|
|
537
|
+
amountSol: number
|
|
538
|
+
}>
|
|
539
|
+
/** Explorer URL */
|
|
540
|
+
explorerUrl: string
|
|
541
|
+
/** Cluster */
|
|
542
|
+
cluster: SolanaCluster
|
|
543
|
+
/** Total amount transferred */
|
|
544
|
+
totalAmount: bigint
|
|
545
|
+
/** Total amount in SOL */
|
|
546
|
+
totalAmountSol: number
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Send SOL to multiple stealth addresses in a single transaction
|
|
551
|
+
*
|
|
552
|
+
* @param connection - Solana RPC connection
|
|
553
|
+
* @param sender - Sender's public key
|
|
554
|
+
* @param transfers - Array of transfer items
|
|
555
|
+
* @param signTransaction - Transaction signing function
|
|
556
|
+
* @returns Batch transfer result
|
|
557
|
+
*/
|
|
558
|
+
export async function sendBatchSOLTransfer(
|
|
559
|
+
connection: Connection,
|
|
560
|
+
sender: PublicKey,
|
|
561
|
+
transfers: BatchSOLTransferItem[],
|
|
562
|
+
signTransaction: <T extends Transaction>(tx: T) => Promise<T>
|
|
563
|
+
): Promise<BatchSOLTransferResult> {
|
|
564
|
+
// Validate batch size
|
|
565
|
+
const MAX_BATCH_SIZE = 8 // SOL transfers are smaller than SPL
|
|
566
|
+
if (transfers.length > MAX_BATCH_SIZE) {
|
|
567
|
+
throw new ValidationError(
|
|
568
|
+
`Batch size ${transfers.length} exceeds maximum ${MAX_BATCH_SIZE}`,
|
|
569
|
+
'transfers'
|
|
570
|
+
)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (transfers.length === 0) {
|
|
574
|
+
throw new ValidationError('At least one transfer is required', 'transfers')
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Calculate total amount
|
|
578
|
+
const totalAmount = transfers.reduce((sum, t) => sum + t.amount, 0n)
|
|
579
|
+
|
|
580
|
+
// Check balance
|
|
581
|
+
const balance = await connection.getBalance(sender)
|
|
582
|
+
const balanceLamports = BigInt(balance)
|
|
583
|
+
const estimatedFee = ESTIMATED_TX_FEE_LAMPORTS
|
|
584
|
+
|
|
585
|
+
if (balanceLamports < totalAmount + estimatedFee) {
|
|
586
|
+
throw new ValidationError(
|
|
587
|
+
`Insufficient balance for batch: have ${formatLamports(balanceLamports)} SOL, need ${formatLamports(totalAmount + estimatedFee)} SOL`,
|
|
588
|
+
'amount'
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Build transaction
|
|
593
|
+
const transaction = new Transaction()
|
|
594
|
+
const transferResults: BatchSOLTransferResult['transfers'] = []
|
|
595
|
+
|
|
596
|
+
for (const transfer of transfers) {
|
|
597
|
+
// Validate meta-address
|
|
598
|
+
if (transfer.recipientMetaAddress.chain !== 'solana') {
|
|
599
|
+
throw new ValidationError(
|
|
600
|
+
`Invalid chain for recipient: expected 'solana', got '${transfer.recipientMetaAddress.chain}'`,
|
|
601
|
+
'recipientMetaAddress'
|
|
602
|
+
)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Generate stealth address
|
|
606
|
+
const { stealthAddress } = generateEd25519StealthAddress(transfer.recipientMetaAddress)
|
|
607
|
+
const stealthAddressBase58 = ed25519PublicKeyToSolanaAddress(stealthAddress.address)
|
|
608
|
+
const stealthPubkey = new PublicKey(stealthAddressBase58)
|
|
609
|
+
const ephemeralPubkeyBase58 = ed25519PublicKeyToSolanaAddress(stealthAddress.ephemeralPublicKey)
|
|
610
|
+
|
|
611
|
+
// Add transfer
|
|
612
|
+
transaction.add(
|
|
613
|
+
SystemProgram.transfer({
|
|
614
|
+
fromPubkey: sender,
|
|
615
|
+
toPubkey: stealthPubkey,
|
|
616
|
+
lamports: transfer.amount,
|
|
617
|
+
})
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
// Add announcement memo
|
|
621
|
+
const viewTagHex = stealthAddress.viewTag.toString(16).padStart(2, '0')
|
|
622
|
+
const memoContent = createAnnouncementMemo(
|
|
623
|
+
ephemeralPubkeyBase58,
|
|
624
|
+
viewTagHex,
|
|
625
|
+
stealthAddressBase58
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
transaction.add(
|
|
629
|
+
new TransactionInstruction({
|
|
630
|
+
keys: [],
|
|
631
|
+
programId: new PublicKey(MEMO_PROGRAM_ID),
|
|
632
|
+
data: Buffer.from(memoContent, 'utf-8'),
|
|
633
|
+
})
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
// Add custom memo if provided
|
|
637
|
+
if (transfer.customMemo) {
|
|
638
|
+
transaction.add(
|
|
639
|
+
new TransactionInstruction({
|
|
640
|
+
keys: [],
|
|
641
|
+
programId: new PublicKey(MEMO_PROGRAM_ID),
|
|
642
|
+
data: Buffer.from(transfer.customMemo, 'utf-8'),
|
|
643
|
+
})
|
|
644
|
+
)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
transferResults.push({
|
|
648
|
+
stealthAddress: stealthAddressBase58,
|
|
649
|
+
ephemeralPublicKey: ephemeralPubkeyBase58,
|
|
650
|
+
viewTag: viewTagHex,
|
|
651
|
+
amount: transfer.amount,
|
|
652
|
+
amountSol: Number(transfer.amount) / LAMPORTS_PER_SOL,
|
|
653
|
+
})
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Get blockhash and sign
|
|
657
|
+
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash()
|
|
658
|
+
transaction.recentBlockhash = blockhash
|
|
659
|
+
transaction.lastValidBlockHeight = lastValidBlockHeight
|
|
660
|
+
transaction.feePayer = sender
|
|
661
|
+
|
|
662
|
+
const signedTx = await signTransaction(transaction)
|
|
663
|
+
|
|
664
|
+
// Send and confirm
|
|
665
|
+
const txSignature = await connection.sendRawTransaction(signedTx.serialize(), {
|
|
666
|
+
skipPreflight: false,
|
|
667
|
+
preflightCommitment: 'confirmed',
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
await connection.confirmTransaction(
|
|
671
|
+
{ signature: txSignature, blockhash, lastValidBlockHeight },
|
|
672
|
+
'confirmed'
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
const cluster = detectCluster(connection.rpcEndpoint)
|
|
676
|
+
|
|
677
|
+
return {
|
|
678
|
+
txSignature,
|
|
679
|
+
transfers: transferResults,
|
|
680
|
+
explorerUrl: getExplorerUrl(txSignature, cluster),
|
|
681
|
+
cluster,
|
|
682
|
+
totalAmount,
|
|
683
|
+
totalAmountSol: Number(totalAmount) / LAMPORTS_PER_SOL,
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ─── Utilities ────────────────────────────────────────────────────────────────
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Format lamports as SOL string
|
|
691
|
+
*/
|
|
692
|
+
export function formatLamports(lamports: bigint): string {
|
|
693
|
+
const sol = Number(lamports) / LAMPORTS_PER_SOL
|
|
694
|
+
if (sol === 0) return '0'
|
|
695
|
+
if (sol < 0.0001) return sol.toExponential(2)
|
|
696
|
+
return sol.toFixed(sol < 1 ? 4 : 2).replace(/\.?0+$/, '')
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Parse SOL amount to lamports
|
|
701
|
+
*/
|
|
702
|
+
export function parseSOLToLamports(sol: string | number): bigint {
|
|
703
|
+
const value = typeof sol === 'string' ? parseFloat(sol.replace(/[,\s]/g, '')) : sol
|
|
704
|
+
if (isNaN(value) || value < 0) {
|
|
705
|
+
throw new ValidationError(`Invalid SOL amount: ${sol}`, 'amount')
|
|
706
|
+
}
|
|
707
|
+
return BigInt(Math.round(value * LAMPORTS_PER_SOL))
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Get sender's SOL balance
|
|
712
|
+
*/
|
|
713
|
+
export async function getSOLBalance(
|
|
714
|
+
connection: Connection,
|
|
715
|
+
address: PublicKey
|
|
716
|
+
): Promise<{ lamports: bigint; sol: number }> {
|
|
717
|
+
const balance = await connection.getBalance(address)
|
|
718
|
+
return {
|
|
719
|
+
lamports: BigInt(balance),
|
|
720
|
+
sol: balance / LAMPORTS_PER_SOL,
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Detect Solana cluster from RPC endpoint
|
|
726
|
+
*/
|
|
727
|
+
function detectCluster(endpoint: string): SolanaCluster {
|
|
728
|
+
if (endpoint.includes('devnet')) return 'devnet'
|
|
729
|
+
if (endpoint.includes('testnet')) return 'testnet'
|
|
730
|
+
if (endpoint.includes('localhost') || endpoint.includes('127.0.0.1')) return 'localnet'
|
|
731
|
+
return 'mainnet-beta'
|
|
732
|
+
}
|