@sip-protocol/sdk 0.7.3 → 0.8.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/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 +44 -11
- 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,886 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced SPL Token Transfer with Privacy Wrapping
|
|
3
|
+
*
|
|
4
|
+
* Provides advanced SPL token transfer functionality with:
|
|
5
|
+
* - Token metadata resolution for UI display
|
|
6
|
+
* - Token balance validation before transfer
|
|
7
|
+
* - Batch transfer support
|
|
8
|
+
* - Enhanced error handling
|
|
9
|
+
*
|
|
10
|
+
* @module chains/solana/spl-transfer
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
Connection,
|
|
15
|
+
PublicKey,
|
|
16
|
+
Transaction,
|
|
17
|
+
TransactionInstruction,
|
|
18
|
+
type Commitment,
|
|
19
|
+
} from '@solana/web3.js'
|
|
20
|
+
import {
|
|
21
|
+
getAssociatedTokenAddress,
|
|
22
|
+
createAssociatedTokenAccountInstruction,
|
|
23
|
+
createTransferInstruction,
|
|
24
|
+
TOKEN_PROGRAM_ID,
|
|
25
|
+
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
26
|
+
getAccount,
|
|
27
|
+
getMint,
|
|
28
|
+
} from '@solana/spl-token'
|
|
29
|
+
import {
|
|
30
|
+
generateEd25519StealthAddress,
|
|
31
|
+
ed25519PublicKeyToSolanaAddress,
|
|
32
|
+
} from '../../stealth'
|
|
33
|
+
import { ValidationError } from '../../errors'
|
|
34
|
+
import type { StealthMetaAddress } from '@sip-protocol/types'
|
|
35
|
+
import { createAnnouncementMemo } from './types'
|
|
36
|
+
import {
|
|
37
|
+
MEMO_PROGRAM_ID,
|
|
38
|
+
getExplorerUrl,
|
|
39
|
+
ESTIMATED_TX_FEE_LAMPORTS,
|
|
40
|
+
type SolanaCluster,
|
|
41
|
+
} from './constants'
|
|
42
|
+
|
|
43
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Token metadata for UI display
|
|
47
|
+
*/
|
|
48
|
+
export interface TokenMetadata {
|
|
49
|
+
/** Token mint address */
|
|
50
|
+
mint: string
|
|
51
|
+
/** Token name (e.g., "USD Coin") */
|
|
52
|
+
name: string
|
|
53
|
+
/** Token symbol (e.g., "USDC") */
|
|
54
|
+
symbol: string
|
|
55
|
+
/** Number of decimals */
|
|
56
|
+
decimals: number
|
|
57
|
+
/** Token logo URI */
|
|
58
|
+
logoUri?: string
|
|
59
|
+
/** Token supply */
|
|
60
|
+
supply?: bigint
|
|
61
|
+
/** Is token frozen */
|
|
62
|
+
isFrozen?: boolean
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Token balance information
|
|
67
|
+
*/
|
|
68
|
+
export interface TokenBalance {
|
|
69
|
+
/** Token mint address */
|
|
70
|
+
mint: string
|
|
71
|
+
/** Balance in smallest unit (raw) */
|
|
72
|
+
amount: bigint
|
|
73
|
+
/** Balance formatted with decimals */
|
|
74
|
+
uiAmount: number
|
|
75
|
+
/** Token decimals */
|
|
76
|
+
decimals: number
|
|
77
|
+
/** Associated token account address */
|
|
78
|
+
tokenAccount: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Enhanced transfer parameters with validation options
|
|
83
|
+
*/
|
|
84
|
+
export interface EnhancedSPLTransferParams {
|
|
85
|
+
/** Solana RPC connection */
|
|
86
|
+
connection: Connection
|
|
87
|
+
/** Sender's public key */
|
|
88
|
+
sender: PublicKey
|
|
89
|
+
/** Sender's token account (ATA) - auto-detected if not provided */
|
|
90
|
+
senderTokenAccount?: PublicKey
|
|
91
|
+
/** Recipient's stealth meta-address */
|
|
92
|
+
recipientMetaAddress: StealthMetaAddress
|
|
93
|
+
/** SPL token mint address */
|
|
94
|
+
mint: PublicKey
|
|
95
|
+
/** Amount to transfer (in token's smallest unit) */
|
|
96
|
+
amount: bigint
|
|
97
|
+
/** Function to sign the transaction */
|
|
98
|
+
signTransaction: <T extends Transaction>(tx: T) => Promise<T>
|
|
99
|
+
/** Skip balance validation (default: false) */
|
|
100
|
+
skipBalanceCheck?: boolean
|
|
101
|
+
/** Transaction commitment level */
|
|
102
|
+
commitment?: Commitment
|
|
103
|
+
/** Custom memo to append (will be added after SIP announcement) */
|
|
104
|
+
customMemo?: string
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Enhanced transfer result with metadata
|
|
109
|
+
*/
|
|
110
|
+
export interface EnhancedSPLTransferResult {
|
|
111
|
+
/** Transaction signature */
|
|
112
|
+
txSignature: string
|
|
113
|
+
/** Stealth address (base58 Solana address) */
|
|
114
|
+
stealthAddress: string
|
|
115
|
+
/** Ephemeral public key (base58) for recipient scanning */
|
|
116
|
+
ephemeralPublicKey: string
|
|
117
|
+
/** View tag for efficient scanning */
|
|
118
|
+
viewTag: string
|
|
119
|
+
/** Explorer URL for the transaction */
|
|
120
|
+
explorerUrl: string
|
|
121
|
+
/** Cluster the transaction was sent on */
|
|
122
|
+
cluster: SolanaCluster
|
|
123
|
+
/** Token metadata */
|
|
124
|
+
tokenMetadata: TokenMetadata
|
|
125
|
+
/** Amount transferred */
|
|
126
|
+
amount: bigint
|
|
127
|
+
/** UI amount (formatted with decimals) */
|
|
128
|
+
uiAmount: number
|
|
129
|
+
/** Whether ATA was created */
|
|
130
|
+
ataCreated: boolean
|
|
131
|
+
/** Estimated fee paid */
|
|
132
|
+
estimatedFee: bigint
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Batch transfer item
|
|
137
|
+
*/
|
|
138
|
+
export interface BatchTransferItem {
|
|
139
|
+
/** Recipient's stealth meta-address */
|
|
140
|
+
recipientMetaAddress: StealthMetaAddress
|
|
141
|
+
/** Amount to transfer (in token's smallest unit) */
|
|
142
|
+
amount: bigint
|
|
143
|
+
/** Custom memo for this transfer */
|
|
144
|
+
customMemo?: string
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Batch transfer result
|
|
149
|
+
*/
|
|
150
|
+
export interface BatchTransferResult {
|
|
151
|
+
/** Transaction signature */
|
|
152
|
+
txSignature: string
|
|
153
|
+
/** Individual transfer results */
|
|
154
|
+
transfers: Array<{
|
|
155
|
+
stealthAddress: string
|
|
156
|
+
ephemeralPublicKey: string
|
|
157
|
+
viewTag: string
|
|
158
|
+
amount: bigint
|
|
159
|
+
uiAmount: number
|
|
160
|
+
}>
|
|
161
|
+
/** Explorer URL */
|
|
162
|
+
explorerUrl: string
|
|
163
|
+
/** Cluster */
|
|
164
|
+
cluster: SolanaCluster
|
|
165
|
+
/** Total amount transferred */
|
|
166
|
+
totalAmount: bigint
|
|
167
|
+
/** Token metadata */
|
|
168
|
+
tokenMetadata: TokenMetadata
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Transfer validation result
|
|
173
|
+
*/
|
|
174
|
+
export interface TransferValidation {
|
|
175
|
+
/** Whether transfer is valid */
|
|
176
|
+
isValid: boolean
|
|
177
|
+
/** Validation errors (if any) */
|
|
178
|
+
errors: string[]
|
|
179
|
+
/** Sender's token balance */
|
|
180
|
+
senderBalance?: TokenBalance
|
|
181
|
+
/** Estimated total fee */
|
|
182
|
+
estimatedFee: bigint
|
|
183
|
+
/** Whether ATA needs creation */
|
|
184
|
+
needsAtaCreation: boolean
|
|
185
|
+
/** Token metadata */
|
|
186
|
+
tokenMetadata?: TokenMetadata
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── Token Metadata ───────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Well-known token metadata (mainnet)
|
|
193
|
+
* Used as fallback when metadata cannot be fetched
|
|
194
|
+
*/
|
|
195
|
+
const KNOWN_TOKENS: Record<string, Omit<TokenMetadata, 'mint' | 'supply' | 'isFrozen'>> = {
|
|
196
|
+
// USDC
|
|
197
|
+
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v': {
|
|
198
|
+
name: 'USD Coin',
|
|
199
|
+
symbol: 'USDC',
|
|
200
|
+
decimals: 6,
|
|
201
|
+
logoUri: 'https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v/logo.png',
|
|
202
|
+
},
|
|
203
|
+
// USDT
|
|
204
|
+
'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB': {
|
|
205
|
+
name: 'Tether USD',
|
|
206
|
+
symbol: 'USDT',
|
|
207
|
+
decimals: 6,
|
|
208
|
+
logoUri: 'https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB/logo.png',
|
|
209
|
+
},
|
|
210
|
+
// SOL (wrapped)
|
|
211
|
+
'So11111111111111111111111111111111111111112': {
|
|
212
|
+
name: 'Wrapped SOL',
|
|
213
|
+
symbol: 'SOL',
|
|
214
|
+
decimals: 9,
|
|
215
|
+
logoUri: 'https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/So11111111111111111111111111111111111111112/logo.png',
|
|
216
|
+
},
|
|
217
|
+
// BONK
|
|
218
|
+
'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263': {
|
|
219
|
+
name: 'Bonk',
|
|
220
|
+
symbol: 'BONK',
|
|
221
|
+
decimals: 5,
|
|
222
|
+
logoUri: 'https://arweave.net/hQiPZOsRZXGXBJd_82PhVdlM_hACsT_q6wqwf5cSY7I',
|
|
223
|
+
},
|
|
224
|
+
// JUP
|
|
225
|
+
'JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN': {
|
|
226
|
+
name: 'Jupiter',
|
|
227
|
+
symbol: 'JUP',
|
|
228
|
+
decimals: 6,
|
|
229
|
+
logoUri: 'https://static.jup.ag/jup/icon.png',
|
|
230
|
+
},
|
|
231
|
+
// RAY
|
|
232
|
+
'4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R': {
|
|
233
|
+
name: 'Raydium',
|
|
234
|
+
symbol: 'RAY',
|
|
235
|
+
decimals: 6,
|
|
236
|
+
logoUri: 'https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R/logo.png',
|
|
237
|
+
},
|
|
238
|
+
// PYTH
|
|
239
|
+
'HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3': {
|
|
240
|
+
name: 'Pyth Network',
|
|
241
|
+
symbol: 'PYTH',
|
|
242
|
+
decimals: 6,
|
|
243
|
+
logoUri: 'https://pyth.network/token.svg',
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Resolve token metadata from mint address
|
|
249
|
+
*
|
|
250
|
+
* Attempts to fetch metadata from on-chain, falls back to known tokens.
|
|
251
|
+
*
|
|
252
|
+
* @param connection - Solana RPC connection
|
|
253
|
+
* @param mint - Token mint address
|
|
254
|
+
* @returns Token metadata
|
|
255
|
+
*/
|
|
256
|
+
export async function resolveTokenMetadata(
|
|
257
|
+
connection: Connection,
|
|
258
|
+
mint: PublicKey
|
|
259
|
+
): Promise<TokenMetadata> {
|
|
260
|
+
const mintAddress = mint.toBase58()
|
|
261
|
+
|
|
262
|
+
// Try known tokens first
|
|
263
|
+
const known = KNOWN_TOKENS[mintAddress]
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
// Fetch on-chain mint data
|
|
267
|
+
const mintInfo = await getMint(connection, mint)
|
|
268
|
+
|
|
269
|
+
if (known) {
|
|
270
|
+
return {
|
|
271
|
+
mint: mintAddress,
|
|
272
|
+
...known,
|
|
273
|
+
supply: mintInfo.supply,
|
|
274
|
+
isFrozen: mintInfo.freezeAuthority !== null,
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Unknown token - return basic info
|
|
279
|
+
return {
|
|
280
|
+
mint: mintAddress,
|
|
281
|
+
name: `Token ${mintAddress.slice(0, 8)}...`,
|
|
282
|
+
symbol: mintAddress.slice(0, 4).toUpperCase(),
|
|
283
|
+
decimals: mintInfo.decimals,
|
|
284
|
+
supply: mintInfo.supply,
|
|
285
|
+
isFrozen: mintInfo.freezeAuthority !== null,
|
|
286
|
+
}
|
|
287
|
+
} catch (error) {
|
|
288
|
+
// Fallback if mint fetch fails
|
|
289
|
+
if (known) {
|
|
290
|
+
return {
|
|
291
|
+
mint: mintAddress,
|
|
292
|
+
...known,
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
throw new ValidationError(
|
|
297
|
+
`Failed to resolve token metadata for ${mintAddress}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
298
|
+
'mint'
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Get multiple token metadata at once
|
|
305
|
+
*
|
|
306
|
+
* @param connection - Solana RPC connection
|
|
307
|
+
* @param mints - Array of token mint addresses
|
|
308
|
+
* @returns Array of token metadata
|
|
309
|
+
*/
|
|
310
|
+
export async function batchResolveTokenMetadata(
|
|
311
|
+
connection: Connection,
|
|
312
|
+
mints: PublicKey[]
|
|
313
|
+
): Promise<TokenMetadata[]> {
|
|
314
|
+
return Promise.all(mints.map(mint => resolveTokenMetadata(connection, mint)))
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ─── Token Balance ────────────────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Get token balance for an address
|
|
321
|
+
*
|
|
322
|
+
* @param connection - Solana RPC connection
|
|
323
|
+
* @param owner - Owner public key
|
|
324
|
+
* @param mint - Token mint address
|
|
325
|
+
* @returns Token balance or null if no account
|
|
326
|
+
*/
|
|
327
|
+
export async function getTokenBalance(
|
|
328
|
+
connection: Connection,
|
|
329
|
+
owner: PublicKey,
|
|
330
|
+
mint: PublicKey
|
|
331
|
+
): Promise<TokenBalance | null> {
|
|
332
|
+
try {
|
|
333
|
+
const ata = await getAssociatedTokenAddress(mint, owner, true)
|
|
334
|
+
const account = await getAccount(connection, ata)
|
|
335
|
+
const mintInfo = await getMint(connection, mint)
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
mint: mint.toBase58(),
|
|
339
|
+
amount: account.amount,
|
|
340
|
+
uiAmount: Number(account.amount) / Math.pow(10, mintInfo.decimals),
|
|
341
|
+
decimals: mintInfo.decimals,
|
|
342
|
+
tokenAccount: ata.toBase58(),
|
|
343
|
+
}
|
|
344
|
+
} catch {
|
|
345
|
+
return null
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Get multiple token balances for an address
|
|
351
|
+
*
|
|
352
|
+
* @param connection - Solana RPC connection
|
|
353
|
+
* @param owner - Owner public key
|
|
354
|
+
* @param mints - Array of token mint addresses
|
|
355
|
+
* @returns Array of token balances (null for missing accounts)
|
|
356
|
+
*/
|
|
357
|
+
export async function batchGetTokenBalances(
|
|
358
|
+
connection: Connection,
|
|
359
|
+
owner: PublicKey,
|
|
360
|
+
mints: PublicKey[]
|
|
361
|
+
): Promise<(TokenBalance | null)[]> {
|
|
362
|
+
return Promise.all(mints.map(mint => getTokenBalance(connection, owner, mint)))
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ─── Transfer Validation ──────────────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Validate a transfer before execution
|
|
369
|
+
*
|
|
370
|
+
* Checks:
|
|
371
|
+
* - Sender has sufficient balance
|
|
372
|
+
* - Meta-address is valid for Solana
|
|
373
|
+
* - Amount is valid
|
|
374
|
+
* - ATA creation requirements
|
|
375
|
+
*
|
|
376
|
+
* @param params - Transfer parameters to validate
|
|
377
|
+
* @returns Validation result
|
|
378
|
+
*/
|
|
379
|
+
export async function validateTransfer(
|
|
380
|
+
params: Omit<EnhancedSPLTransferParams, 'signTransaction'>
|
|
381
|
+
): Promise<TransferValidation> {
|
|
382
|
+
const errors: string[] = []
|
|
383
|
+
let senderBalance: TokenBalance | undefined
|
|
384
|
+
let tokenMetadata: TokenMetadata | undefined
|
|
385
|
+
let needsAtaCreation = false
|
|
386
|
+
let estimatedFee = ESTIMATED_TX_FEE_LAMPORTS
|
|
387
|
+
|
|
388
|
+
// Validate meta-address
|
|
389
|
+
if (!params.recipientMetaAddress) {
|
|
390
|
+
errors.push('Recipient meta-address is required')
|
|
391
|
+
} else if (params.recipientMetaAddress.chain !== 'solana') {
|
|
392
|
+
errors.push(`Invalid chain: expected 'solana', got '${params.recipientMetaAddress.chain}'`)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Validate amount
|
|
396
|
+
if (params.amount <= 0n) {
|
|
397
|
+
errors.push('Amount must be greater than 0')
|
|
398
|
+
}
|
|
399
|
+
if (params.amount > 2n ** 64n - 1n) {
|
|
400
|
+
errors.push('Amount exceeds maximum SPL token amount')
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Get sender balance
|
|
404
|
+
try {
|
|
405
|
+
const balance = await getTokenBalance(params.connection, params.sender, params.mint)
|
|
406
|
+
if (balance) {
|
|
407
|
+
senderBalance = balance
|
|
408
|
+
if (balance.amount < params.amount) {
|
|
409
|
+
errors.push(
|
|
410
|
+
`Insufficient balance: have ${balance.uiAmount}, need ${Number(params.amount) / Math.pow(10, balance.decimals)}`
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
} else {
|
|
414
|
+
errors.push('Sender does not have a token account for this mint')
|
|
415
|
+
}
|
|
416
|
+
} catch (error) {
|
|
417
|
+
errors.push(`Failed to check sender balance: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Get token metadata
|
|
421
|
+
try {
|
|
422
|
+
tokenMetadata = await resolveTokenMetadata(params.connection, params.mint)
|
|
423
|
+
} catch (error) {
|
|
424
|
+
errors.push(`Failed to resolve token metadata: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Check if stealth ATA needs creation
|
|
428
|
+
if (params.recipientMetaAddress && params.recipientMetaAddress.chain === 'solana') {
|
|
429
|
+
try {
|
|
430
|
+
const { stealthAddress } = generateEd25519StealthAddress(params.recipientMetaAddress)
|
|
431
|
+
const stealthAddressBase58 = ed25519PublicKeyToSolanaAddress(stealthAddress.address)
|
|
432
|
+
const stealthPubkey = new PublicKey(stealthAddressBase58)
|
|
433
|
+
const stealthATA = await getAssociatedTokenAddress(params.mint, stealthPubkey, true)
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
await getAccount(params.connection, stealthATA)
|
|
437
|
+
} catch {
|
|
438
|
+
needsAtaCreation = true
|
|
439
|
+
const rentExemption = await params.connection.getMinimumBalanceForRentExemption(165)
|
|
440
|
+
estimatedFee += BigInt(rentExemption)
|
|
441
|
+
}
|
|
442
|
+
} catch {
|
|
443
|
+
// Cannot generate stealth address - error already captured
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
isValid: errors.length === 0,
|
|
449
|
+
errors,
|
|
450
|
+
senderBalance,
|
|
451
|
+
estimatedFee,
|
|
452
|
+
needsAtaCreation,
|
|
453
|
+
tokenMetadata,
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ─── Enhanced Transfer ────────────────────────────────────────────────────────
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Send SPL tokens privately with enhanced features
|
|
461
|
+
*
|
|
462
|
+
* Improvements over base transfer:
|
|
463
|
+
* - Auto-detects sender token account if not provided
|
|
464
|
+
* - Validates balance before transfer
|
|
465
|
+
* - Includes token metadata in result
|
|
466
|
+
* - Better error messages
|
|
467
|
+
*
|
|
468
|
+
* @param params - Enhanced transfer parameters
|
|
469
|
+
* @returns Enhanced transfer result
|
|
470
|
+
*
|
|
471
|
+
* @example
|
|
472
|
+
* ```typescript
|
|
473
|
+
* const result = await sendEnhancedSPLTransfer({
|
|
474
|
+
* connection,
|
|
475
|
+
* sender: wallet.publicKey,
|
|
476
|
+
* recipientMetaAddress: recipientMeta,
|
|
477
|
+
* mint: USDC_MINT,
|
|
478
|
+
* amount: 5_000_000n,
|
|
479
|
+
* signTransaction: wallet.signTransaction,
|
|
480
|
+
* })
|
|
481
|
+
*
|
|
482
|
+
* console.log(`Sent ${result.uiAmount} ${result.tokenMetadata.symbol}`)
|
|
483
|
+
* console.log(`To stealth address: ${result.stealthAddress}`)
|
|
484
|
+
* ```
|
|
485
|
+
*/
|
|
486
|
+
export async function sendEnhancedSPLTransfer(
|
|
487
|
+
params: EnhancedSPLTransferParams
|
|
488
|
+
): Promise<EnhancedSPLTransferResult> {
|
|
489
|
+
const {
|
|
490
|
+
connection,
|
|
491
|
+
sender,
|
|
492
|
+
recipientMetaAddress,
|
|
493
|
+
mint,
|
|
494
|
+
amount,
|
|
495
|
+
signTransaction,
|
|
496
|
+
skipBalanceCheck = false,
|
|
497
|
+
commitment = 'confirmed',
|
|
498
|
+
customMemo,
|
|
499
|
+
} = params
|
|
500
|
+
|
|
501
|
+
// Validate meta-address
|
|
502
|
+
if (!recipientMetaAddress) {
|
|
503
|
+
throw new ValidationError('recipientMetaAddress is required', 'recipientMetaAddress')
|
|
504
|
+
}
|
|
505
|
+
if (recipientMetaAddress.chain !== 'solana') {
|
|
506
|
+
throw new ValidationError(
|
|
507
|
+
`Invalid chain: expected 'solana', got '${recipientMetaAddress.chain}'`,
|
|
508
|
+
'recipientMetaAddress.chain'
|
|
509
|
+
)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Validate amount
|
|
513
|
+
if (amount <= 0n) {
|
|
514
|
+
throw new ValidationError('amount must be greater than 0', 'amount')
|
|
515
|
+
}
|
|
516
|
+
if (amount > 2n ** 64n - 1n) {
|
|
517
|
+
throw new ValidationError('amount exceeds maximum SPL token amount', 'amount')
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Resolve token metadata
|
|
521
|
+
const tokenMetadata = await resolveTokenMetadata(connection, mint)
|
|
522
|
+
|
|
523
|
+
// Auto-detect sender token account if not provided
|
|
524
|
+
let senderTokenAccount = params.senderTokenAccount
|
|
525
|
+
if (!senderTokenAccount) {
|
|
526
|
+
senderTokenAccount = await getAssociatedTokenAddress(mint, sender, false)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Check balance (unless skipped)
|
|
530
|
+
if (!skipBalanceCheck) {
|
|
531
|
+
const balance = await getTokenBalance(connection, sender, mint)
|
|
532
|
+
if (!balance) {
|
|
533
|
+
throw new ValidationError(
|
|
534
|
+
`Sender does not have a ${tokenMetadata.symbol} token account`,
|
|
535
|
+
'sender'
|
|
536
|
+
)
|
|
537
|
+
}
|
|
538
|
+
if (balance.amount < amount) {
|
|
539
|
+
const uiAmount = Number(amount) / Math.pow(10, tokenMetadata.decimals)
|
|
540
|
+
throw new ValidationError(
|
|
541
|
+
`Insufficient ${tokenMetadata.symbol} balance: have ${balance.uiAmount}, need ${uiAmount}`,
|
|
542
|
+
'amount'
|
|
543
|
+
)
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Generate stealth address
|
|
548
|
+
const { stealthAddress } = generateEd25519StealthAddress(recipientMetaAddress)
|
|
549
|
+
const stealthAddressBase58 = ed25519PublicKeyToSolanaAddress(stealthAddress.address)
|
|
550
|
+
const stealthPubkey = new PublicKey(stealthAddressBase58)
|
|
551
|
+
const ephemeralPubkeyBase58 = ed25519PublicKeyToSolanaAddress(stealthAddress.ephemeralPublicKey)
|
|
552
|
+
|
|
553
|
+
// Get or create stealth ATA
|
|
554
|
+
const stealthATA = await getAssociatedTokenAddress(mint, stealthPubkey, true)
|
|
555
|
+
|
|
556
|
+
// Build transaction
|
|
557
|
+
const transaction = new Transaction()
|
|
558
|
+
let ataCreated = false
|
|
559
|
+
let estimatedFee = ESTIMATED_TX_FEE_LAMPORTS
|
|
560
|
+
|
|
561
|
+
// Check if stealth ATA exists
|
|
562
|
+
try {
|
|
563
|
+
await getAccount(connection, stealthATA)
|
|
564
|
+
} catch {
|
|
565
|
+
// Create ATA
|
|
566
|
+
transaction.add(
|
|
567
|
+
createAssociatedTokenAccountInstruction(
|
|
568
|
+
sender,
|
|
569
|
+
stealthATA,
|
|
570
|
+
stealthPubkey,
|
|
571
|
+
mint,
|
|
572
|
+
TOKEN_PROGRAM_ID,
|
|
573
|
+
ASSOCIATED_TOKEN_PROGRAM_ID
|
|
574
|
+
)
|
|
575
|
+
)
|
|
576
|
+
ataCreated = true
|
|
577
|
+
const rentExemption = await connection.getMinimumBalanceForRentExemption(165)
|
|
578
|
+
estimatedFee += BigInt(rentExemption)
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Add transfer instruction
|
|
582
|
+
transaction.add(
|
|
583
|
+
createTransferInstruction(
|
|
584
|
+
senderTokenAccount,
|
|
585
|
+
stealthATA,
|
|
586
|
+
sender,
|
|
587
|
+
amount
|
|
588
|
+
)
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
// Add SIP announcement memo
|
|
592
|
+
const viewTagHex = stealthAddress.viewTag.toString(16).padStart(2, '0')
|
|
593
|
+
const memoContent = createAnnouncementMemo(
|
|
594
|
+
ephemeralPubkeyBase58,
|
|
595
|
+
viewTagHex,
|
|
596
|
+
stealthAddressBase58
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
transaction.add(
|
|
600
|
+
new TransactionInstruction({
|
|
601
|
+
keys: [],
|
|
602
|
+
programId: new PublicKey(MEMO_PROGRAM_ID),
|
|
603
|
+
data: Buffer.from(memoContent, 'utf-8'),
|
|
604
|
+
})
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
// Add custom memo if provided
|
|
608
|
+
if (customMemo) {
|
|
609
|
+
transaction.add(
|
|
610
|
+
new TransactionInstruction({
|
|
611
|
+
keys: [],
|
|
612
|
+
programId: new PublicKey(MEMO_PROGRAM_ID),
|
|
613
|
+
data: Buffer.from(customMemo, 'utf-8'),
|
|
614
|
+
})
|
|
615
|
+
)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Get blockhash and sign
|
|
619
|
+
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(commitment)
|
|
620
|
+
transaction.recentBlockhash = blockhash
|
|
621
|
+
transaction.lastValidBlockHeight = lastValidBlockHeight
|
|
622
|
+
transaction.feePayer = sender
|
|
623
|
+
|
|
624
|
+
const signedTx = await signTransaction(transaction)
|
|
625
|
+
|
|
626
|
+
// Send and confirm
|
|
627
|
+
const txSignature = await connection.sendRawTransaction(signedTx.serialize(), {
|
|
628
|
+
skipPreflight: false,
|
|
629
|
+
preflightCommitment: commitment,
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
await connection.confirmTransaction(
|
|
633
|
+
{ signature: txSignature, blockhash, lastValidBlockHeight },
|
|
634
|
+
commitment
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
// Detect cluster
|
|
638
|
+
const cluster = detectCluster(connection.rpcEndpoint)
|
|
639
|
+
|
|
640
|
+
return {
|
|
641
|
+
txSignature,
|
|
642
|
+
stealthAddress: stealthAddressBase58,
|
|
643
|
+
ephemeralPublicKey: ephemeralPubkeyBase58,
|
|
644
|
+
viewTag: viewTagHex,
|
|
645
|
+
explorerUrl: getExplorerUrl(txSignature, cluster),
|
|
646
|
+
cluster,
|
|
647
|
+
tokenMetadata,
|
|
648
|
+
amount,
|
|
649
|
+
uiAmount: Number(amount) / Math.pow(10, tokenMetadata.decimals),
|
|
650
|
+
ataCreated,
|
|
651
|
+
estimatedFee,
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// ─── Batch Transfer ───────────────────────────────────────────────────────────
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Send SPL tokens to multiple stealth addresses in a single transaction
|
|
659
|
+
*
|
|
660
|
+
* Note: Solana transactions have size limits (~1232 bytes).
|
|
661
|
+
* Batch size is limited to prevent failures.
|
|
662
|
+
*
|
|
663
|
+
* @param connection - Solana RPC connection
|
|
664
|
+
* @param sender - Sender's public key
|
|
665
|
+
* @param senderTokenAccount - Sender's token account (optional, auto-detected)
|
|
666
|
+
* @param mint - Token mint address
|
|
667
|
+
* @param transfers - Array of transfer items
|
|
668
|
+
* @param signTransaction - Transaction signing function
|
|
669
|
+
* @returns Batch transfer result
|
|
670
|
+
*/
|
|
671
|
+
export async function sendBatchSPLTransfer(
|
|
672
|
+
connection: Connection,
|
|
673
|
+
sender: PublicKey,
|
|
674
|
+
senderTokenAccount: PublicKey | undefined,
|
|
675
|
+
mint: PublicKey,
|
|
676
|
+
transfers: BatchTransferItem[],
|
|
677
|
+
signTransaction: <T extends Transaction>(tx: T) => Promise<T>
|
|
678
|
+
): Promise<BatchTransferResult> {
|
|
679
|
+
// Validate batch size (max ~5-6 transfers per transaction due to size limits)
|
|
680
|
+
const MAX_BATCH_SIZE = 5
|
|
681
|
+
if (transfers.length > MAX_BATCH_SIZE) {
|
|
682
|
+
throw new ValidationError(
|
|
683
|
+
`Batch size ${transfers.length} exceeds maximum ${MAX_BATCH_SIZE}. Split into multiple transactions.`,
|
|
684
|
+
'transfers'
|
|
685
|
+
)
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (transfers.length === 0) {
|
|
689
|
+
throw new ValidationError('At least one transfer is required', 'transfers')
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Resolve token metadata
|
|
693
|
+
const tokenMetadata = await resolveTokenMetadata(connection, mint)
|
|
694
|
+
|
|
695
|
+
// Auto-detect sender token account
|
|
696
|
+
if (!senderTokenAccount) {
|
|
697
|
+
senderTokenAccount = await getAssociatedTokenAddress(mint, sender, false)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Calculate total amount
|
|
701
|
+
const totalAmount = transfers.reduce((sum, t) => sum + t.amount, 0n)
|
|
702
|
+
|
|
703
|
+
// Check balance
|
|
704
|
+
const balance = await getTokenBalance(connection, sender, mint)
|
|
705
|
+
if (!balance || balance.amount < totalAmount) {
|
|
706
|
+
const uiAmount = Number(totalAmount) / Math.pow(10, tokenMetadata.decimals)
|
|
707
|
+
throw new ValidationError(
|
|
708
|
+
`Insufficient ${tokenMetadata.symbol} balance for batch: need ${uiAmount}`,
|
|
709
|
+
'amount'
|
|
710
|
+
)
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Build transaction
|
|
714
|
+
const transaction = new Transaction()
|
|
715
|
+
const transferResults: BatchTransferResult['transfers'] = []
|
|
716
|
+
|
|
717
|
+
for (const transfer of transfers) {
|
|
718
|
+
// Validate meta-address
|
|
719
|
+
if (transfer.recipientMetaAddress.chain !== 'solana') {
|
|
720
|
+
throw new ValidationError(
|
|
721
|
+
`Invalid chain for recipient: expected 'solana', got '${transfer.recipientMetaAddress.chain}'`,
|
|
722
|
+
'recipientMetaAddress'
|
|
723
|
+
)
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Generate stealth address
|
|
727
|
+
const { stealthAddress } = generateEd25519StealthAddress(transfer.recipientMetaAddress)
|
|
728
|
+
const stealthAddressBase58 = ed25519PublicKeyToSolanaAddress(stealthAddress.address)
|
|
729
|
+
const stealthPubkey = new PublicKey(stealthAddressBase58)
|
|
730
|
+
const ephemeralPubkeyBase58 = ed25519PublicKeyToSolanaAddress(stealthAddress.ephemeralPublicKey)
|
|
731
|
+
|
|
732
|
+
// Get stealth ATA
|
|
733
|
+
const stealthATA = await getAssociatedTokenAddress(mint, stealthPubkey, true)
|
|
734
|
+
|
|
735
|
+
// Create ATA if needed
|
|
736
|
+
try {
|
|
737
|
+
await getAccount(connection, stealthATA)
|
|
738
|
+
} catch {
|
|
739
|
+
transaction.add(
|
|
740
|
+
createAssociatedTokenAccountInstruction(
|
|
741
|
+
sender,
|
|
742
|
+
stealthATA,
|
|
743
|
+
stealthPubkey,
|
|
744
|
+
mint,
|
|
745
|
+
TOKEN_PROGRAM_ID,
|
|
746
|
+
ASSOCIATED_TOKEN_PROGRAM_ID
|
|
747
|
+
)
|
|
748
|
+
)
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Add transfer
|
|
752
|
+
transaction.add(
|
|
753
|
+
createTransferInstruction(senderTokenAccount, stealthATA, sender, transfer.amount)
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
// Add announcement memo
|
|
757
|
+
const viewTagHex = stealthAddress.viewTag.toString(16).padStart(2, '0')
|
|
758
|
+
const memoContent = createAnnouncementMemo(
|
|
759
|
+
ephemeralPubkeyBase58,
|
|
760
|
+
viewTagHex,
|
|
761
|
+
stealthAddressBase58
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
transaction.add(
|
|
765
|
+
new TransactionInstruction({
|
|
766
|
+
keys: [],
|
|
767
|
+
programId: new PublicKey(MEMO_PROGRAM_ID),
|
|
768
|
+
data: Buffer.from(memoContent, 'utf-8'),
|
|
769
|
+
})
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
// Add custom memo if provided
|
|
773
|
+
if (transfer.customMemo) {
|
|
774
|
+
transaction.add(
|
|
775
|
+
new TransactionInstruction({
|
|
776
|
+
keys: [],
|
|
777
|
+
programId: new PublicKey(MEMO_PROGRAM_ID),
|
|
778
|
+
data: Buffer.from(transfer.customMemo, 'utf-8'),
|
|
779
|
+
})
|
|
780
|
+
)
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
transferResults.push({
|
|
784
|
+
stealthAddress: stealthAddressBase58,
|
|
785
|
+
ephemeralPublicKey: ephemeralPubkeyBase58,
|
|
786
|
+
viewTag: viewTagHex,
|
|
787
|
+
amount: transfer.amount,
|
|
788
|
+
uiAmount: Number(transfer.amount) / Math.pow(10, tokenMetadata.decimals),
|
|
789
|
+
})
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Get blockhash and sign
|
|
793
|
+
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash()
|
|
794
|
+
transaction.recentBlockhash = blockhash
|
|
795
|
+
transaction.lastValidBlockHeight = lastValidBlockHeight
|
|
796
|
+
transaction.feePayer = sender
|
|
797
|
+
|
|
798
|
+
const signedTx = await signTransaction(transaction)
|
|
799
|
+
|
|
800
|
+
// Send and confirm
|
|
801
|
+
const txSignature = await connection.sendRawTransaction(signedTx.serialize(), {
|
|
802
|
+
skipPreflight: false,
|
|
803
|
+
preflightCommitment: 'confirmed',
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
await connection.confirmTransaction(
|
|
807
|
+
{ signature: txSignature, blockhash, lastValidBlockHeight },
|
|
808
|
+
'confirmed'
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
const cluster = detectCluster(connection.rpcEndpoint)
|
|
812
|
+
|
|
813
|
+
return {
|
|
814
|
+
txSignature,
|
|
815
|
+
transfers: transferResults,
|
|
816
|
+
explorerUrl: getExplorerUrl(txSignature, cluster),
|
|
817
|
+
cluster,
|
|
818
|
+
totalAmount,
|
|
819
|
+
tokenMetadata,
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// ─── Utilities ────────────────────────────────────────────────────────────────
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Format token amount for display
|
|
827
|
+
*
|
|
828
|
+
* @param amount - Raw token amount
|
|
829
|
+
* @param decimals - Token decimals
|
|
830
|
+
* @param maxDecimals - Maximum decimal places to show (default: 4)
|
|
831
|
+
* @returns Formatted string
|
|
832
|
+
*/
|
|
833
|
+
export function formatTokenAmount(
|
|
834
|
+
amount: bigint,
|
|
835
|
+
decimals: number,
|
|
836
|
+
maxDecimals: number = 4
|
|
837
|
+
): string {
|
|
838
|
+
const value = Number(amount) / Math.pow(10, decimals)
|
|
839
|
+
|
|
840
|
+
if (value === 0) return '0'
|
|
841
|
+
|
|
842
|
+
// For very small amounts, use scientific notation
|
|
843
|
+
if (value < Math.pow(10, -maxDecimals) && value > 0) {
|
|
844
|
+
return value.toExponential(2)
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Format with appropriate decimals
|
|
848
|
+
const formatted = value.toFixed(maxDecimals)
|
|
849
|
+
|
|
850
|
+
// Remove trailing zeros
|
|
851
|
+
return formatted.replace(/\.?0+$/, '')
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Parse token amount from user input
|
|
856
|
+
*
|
|
857
|
+
* @param input - User input string (e.g., "1.5", "100")
|
|
858
|
+
* @param decimals - Token decimals
|
|
859
|
+
* @returns Raw token amount as bigint
|
|
860
|
+
*/
|
|
861
|
+
export function parseTokenAmount(input: string, decimals: number): bigint {
|
|
862
|
+
// Remove commas and whitespace
|
|
863
|
+
const cleaned = input.replace(/[,\s]/g, '')
|
|
864
|
+
|
|
865
|
+
// Parse as float
|
|
866
|
+
const value = parseFloat(cleaned)
|
|
867
|
+
if (isNaN(value) || value < 0) {
|
|
868
|
+
throw new ValidationError(`Invalid amount: ${input}`, 'amount')
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Convert to smallest unit
|
|
872
|
+
const multiplier = Math.pow(10, decimals)
|
|
873
|
+
const raw = Math.round(value * multiplier)
|
|
874
|
+
|
|
875
|
+
return BigInt(raw)
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Detect Solana cluster from RPC endpoint
|
|
880
|
+
*/
|
|
881
|
+
function detectCluster(endpoint: string): SolanaCluster {
|
|
882
|
+
if (endpoint.includes('devnet')) return 'devnet'
|
|
883
|
+
if (endpoint.includes('testnet')) return 'testnet'
|
|
884
|
+
if (endpoint.includes('localhost') || endpoint.includes('127.0.0.1')) return 'localnet'
|
|
885
|
+
return 'mainnet-beta'
|
|
886
|
+
}
|