@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,703 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Solana Stealth Address Scanner
|
|
3
|
+
*
|
|
4
|
+
* Advanced scanner for detecting incoming stealth payments with support for:
|
|
5
|
+
* - Real-time WebSocket subscriptions
|
|
6
|
+
* - Historical scanning with pagination
|
|
7
|
+
* - Batch scanning for multiple viewing keys
|
|
8
|
+
* - Efficient view tag filtering
|
|
9
|
+
*
|
|
10
|
+
* @module chains/solana/stealth-scanner
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { PublicKey, type Connection } from '@solana/web3.js'
|
|
14
|
+
// hexToBytes available but unused - kept for potential future view tag computation
|
|
15
|
+
// import { hexToBytes } from '@noble/hashes/utils'
|
|
16
|
+
import type { StealthAddress, HexString } from '@sip-protocol/types'
|
|
17
|
+
import { checkEd25519StealthAddress, solanaAddressToEd25519PublicKey } from '../../stealth'
|
|
18
|
+
import { parseAnnouncement, type SolanaAnnouncement } from './types'
|
|
19
|
+
import {
|
|
20
|
+
SIP_MEMO_PREFIX,
|
|
21
|
+
MEMO_PROGRAM_ID,
|
|
22
|
+
DEFAULT_SCAN_LIMIT,
|
|
23
|
+
VIEW_TAG_MAX,
|
|
24
|
+
} from './constants'
|
|
25
|
+
import { getTokenSymbol, parseTokenTransferFromBalances } from './utils'
|
|
26
|
+
import type { SolanaRPCProvider } from './providers/interface'
|
|
27
|
+
|
|
28
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A recipient to scan for (viewing + spending key pair)
|
|
32
|
+
*/
|
|
33
|
+
export interface ScanRecipient {
|
|
34
|
+
/**
|
|
35
|
+
* Viewing private key (hex)
|
|
36
|
+
* @security SENSITIVE - enables scanning for payments
|
|
37
|
+
*/
|
|
38
|
+
viewingPrivateKey: HexString
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Spending public key (hex)
|
|
42
|
+
*/
|
|
43
|
+
spendingPublicKey: HexString
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Optional label for this recipient
|
|
47
|
+
*/
|
|
48
|
+
label?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Options for the stealth scanner
|
|
53
|
+
*/
|
|
54
|
+
export interface StealthScannerOptions {
|
|
55
|
+
/**
|
|
56
|
+
* Solana RPC connection
|
|
57
|
+
*/
|
|
58
|
+
connection: Connection
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Optional RPC provider for efficient queries
|
|
62
|
+
*/
|
|
63
|
+
provider?: SolanaRPCProvider
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Maximum results per scan batch
|
|
67
|
+
* @default 100
|
|
68
|
+
*/
|
|
69
|
+
batchSize?: number
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Enable view tag filtering for efficient scanning
|
|
73
|
+
* @default true
|
|
74
|
+
*/
|
|
75
|
+
useViewTagFilter?: boolean
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Options for historical scanning
|
|
80
|
+
*/
|
|
81
|
+
export interface HistoricalScanOptions {
|
|
82
|
+
/**
|
|
83
|
+
* Start slot for scanning
|
|
84
|
+
*/
|
|
85
|
+
fromSlot?: number
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* End slot for scanning
|
|
89
|
+
*/
|
|
90
|
+
toSlot?: number
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Maximum number of transactions to scan
|
|
94
|
+
* @default 1000
|
|
95
|
+
*/
|
|
96
|
+
limit?: number
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Cursor for pagination (signature of last scanned tx)
|
|
100
|
+
*/
|
|
101
|
+
beforeSignature?: string
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A detected stealth payment
|
|
106
|
+
*/
|
|
107
|
+
export interface DetectedPayment {
|
|
108
|
+
/**
|
|
109
|
+
* Stealth address that received the payment (base58)
|
|
110
|
+
*/
|
|
111
|
+
stealthAddress: string
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Ephemeral public key from the sender (base58)
|
|
115
|
+
*/
|
|
116
|
+
ephemeralPublicKey: string
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* View tag for efficient scanning
|
|
120
|
+
*/
|
|
121
|
+
viewTag: number
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Token amount (in smallest unit)
|
|
125
|
+
*/
|
|
126
|
+
amount: bigint
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Token mint address (base58)
|
|
130
|
+
*/
|
|
131
|
+
mint: string
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Human-readable token symbol
|
|
135
|
+
*/
|
|
136
|
+
tokenSymbol: string
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Transaction signature
|
|
140
|
+
*/
|
|
141
|
+
txSignature: string
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Slot number
|
|
145
|
+
*/
|
|
146
|
+
slot: number
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Unix timestamp
|
|
150
|
+
*/
|
|
151
|
+
timestamp: number
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Label of the recipient this payment was detected for
|
|
155
|
+
*/
|
|
156
|
+
recipientLabel?: string
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Result of a historical scan
|
|
161
|
+
*/
|
|
162
|
+
export interface HistoricalScanResult {
|
|
163
|
+
/**
|
|
164
|
+
* Detected payments
|
|
165
|
+
*/
|
|
166
|
+
payments: DetectedPayment[]
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Total transactions scanned
|
|
170
|
+
*/
|
|
171
|
+
scannedCount: number
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Whether more results are available
|
|
175
|
+
*/
|
|
176
|
+
hasMore: boolean
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Cursor for next page (last signature scanned)
|
|
180
|
+
*/
|
|
181
|
+
nextCursor?: string
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Last slot scanned
|
|
185
|
+
*/
|
|
186
|
+
lastSlot?: number
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Callback for real-time payment detection
|
|
191
|
+
*/
|
|
192
|
+
export type PaymentCallback = (payment: DetectedPayment) => void
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Callback for scan errors
|
|
196
|
+
*/
|
|
197
|
+
export type ErrorCallback = (error: Error) => void
|
|
198
|
+
|
|
199
|
+
// ─── View Tag Computation ─────────────────────────────────────────────────────
|
|
200
|
+
// NOTE: View tag filtering optimization is planned for future implementation.
|
|
201
|
+
// The view tag is derived from the shared secret between ephemeral and viewing keys,
|
|
202
|
+
// which allows for efficient filtering without full ECDH computation.
|
|
203
|
+
// Currently all announcements are checked against all recipients.
|
|
204
|
+
|
|
205
|
+
// ─── StealthScanner Class ─────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Advanced stealth address scanner
|
|
209
|
+
*
|
|
210
|
+
* Provides efficient scanning for incoming stealth payments with support for
|
|
211
|
+
* multiple recipients, real-time subscriptions, and pagination.
|
|
212
|
+
*
|
|
213
|
+
* @example Basic usage
|
|
214
|
+
* ```typescript
|
|
215
|
+
* const scanner = new StealthScanner({
|
|
216
|
+
* connection,
|
|
217
|
+
* provider: heliusProvider,
|
|
218
|
+
* })
|
|
219
|
+
*
|
|
220
|
+
* // Add recipients to scan for
|
|
221
|
+
* scanner.addRecipient({
|
|
222
|
+
* viewingPrivateKey: '0x...',
|
|
223
|
+
* spendingPublicKey: '0x...',
|
|
224
|
+
* label: 'Wallet 1',
|
|
225
|
+
* })
|
|
226
|
+
*
|
|
227
|
+
* // Historical scan
|
|
228
|
+
* const result = await scanner.scanHistorical({
|
|
229
|
+
* fromSlot: 250000000,
|
|
230
|
+
* limit: 1000,
|
|
231
|
+
* })
|
|
232
|
+
*
|
|
233
|
+
* console.log(`Found ${result.payments.length} payments`)
|
|
234
|
+
* ```
|
|
235
|
+
*
|
|
236
|
+
* @example Real-time scanning
|
|
237
|
+
* ```typescript
|
|
238
|
+
* scanner.subscribe(
|
|
239
|
+
* (payment) => console.log('New payment:', payment),
|
|
240
|
+
* (error) => console.error('Scan error:', error)
|
|
241
|
+
* )
|
|
242
|
+
*
|
|
243
|
+
* // Later: stop subscription
|
|
244
|
+
* scanner.unsubscribe()
|
|
245
|
+
* ```
|
|
246
|
+
*/
|
|
247
|
+
export class StealthScanner {
|
|
248
|
+
private connection: Connection
|
|
249
|
+
private provider?: SolanaRPCProvider
|
|
250
|
+
private recipients: ScanRecipient[] = []
|
|
251
|
+
private batchSize: number
|
|
252
|
+
private subscriptionId: number | null = null
|
|
253
|
+
private paymentCallback: PaymentCallback | null = null
|
|
254
|
+
private errorCallback: ErrorCallback | null = null
|
|
255
|
+
|
|
256
|
+
constructor(options: StealthScannerOptions) {
|
|
257
|
+
this.connection = options.connection
|
|
258
|
+
this.provider = options.provider
|
|
259
|
+
this.batchSize = options.batchSize ?? DEFAULT_SCAN_LIMIT
|
|
260
|
+
// Note: useViewTagFilter option is accepted for future optimization
|
|
261
|
+
// Currently all announcements are checked against all recipients
|
|
262
|
+
void options.useViewTagFilter
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Add a recipient to scan for
|
|
267
|
+
*
|
|
268
|
+
* @param recipient - Recipient with viewing/spending keys
|
|
269
|
+
*/
|
|
270
|
+
addRecipient(recipient: ScanRecipient): void {
|
|
271
|
+
this.recipients.push(recipient)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Remove a recipient by label
|
|
276
|
+
*
|
|
277
|
+
* @param label - Recipient label to remove
|
|
278
|
+
*/
|
|
279
|
+
removeRecipient(label: string): void {
|
|
280
|
+
this.recipients = this.recipients.filter(r => r.label !== label)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Clear all recipients
|
|
285
|
+
*/
|
|
286
|
+
clearRecipients(): void {
|
|
287
|
+
this.recipients = []
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Get current recipients
|
|
292
|
+
*/
|
|
293
|
+
getRecipients(): ScanRecipient[] {
|
|
294
|
+
return [...this.recipients]
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Scan historical transactions for stealth payments
|
|
299
|
+
*
|
|
300
|
+
* @param options - Scan options
|
|
301
|
+
* @returns Scan result with detected payments
|
|
302
|
+
*/
|
|
303
|
+
async scanHistorical(options: HistoricalScanOptions = {}): Promise<HistoricalScanResult> {
|
|
304
|
+
if (this.recipients.length === 0) {
|
|
305
|
+
return {
|
|
306
|
+
payments: [],
|
|
307
|
+
scannedCount: 0,
|
|
308
|
+
hasMore: false,
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const {
|
|
313
|
+
fromSlot,
|
|
314
|
+
toSlot,
|
|
315
|
+
limit = 1000,
|
|
316
|
+
beforeSignature,
|
|
317
|
+
} = options
|
|
318
|
+
|
|
319
|
+
const payments: DetectedPayment[] = []
|
|
320
|
+
let scannedCount = 0
|
|
321
|
+
let lastSignature: string | undefined
|
|
322
|
+
let lastSlot: number | undefined
|
|
323
|
+
|
|
324
|
+
const memoProgram = new PublicKey(MEMO_PROGRAM_ID)
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
// Get transaction signatures
|
|
328
|
+
const signatures = await this.connection.getSignaturesForAddress(
|
|
329
|
+
memoProgram,
|
|
330
|
+
{
|
|
331
|
+
limit: Math.min(limit, this.batchSize),
|
|
332
|
+
before: beforeSignature,
|
|
333
|
+
minContextSlot: fromSlot,
|
|
334
|
+
}
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
// Filter by slot range
|
|
338
|
+
const filteredSignatures = toSlot
|
|
339
|
+
? signatures.filter(s => s.slot <= toSlot)
|
|
340
|
+
: signatures
|
|
341
|
+
|
|
342
|
+
// Process each transaction
|
|
343
|
+
for (const sigInfo of filteredSignatures) {
|
|
344
|
+
scannedCount++
|
|
345
|
+
lastSignature = sigInfo.signature
|
|
346
|
+
lastSlot = sigInfo.slot
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
const tx = await this.connection.getTransaction(sigInfo.signature, {
|
|
350
|
+
maxSupportedTransactionVersion: 0,
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
if (!tx?.meta?.logMessages) continue
|
|
354
|
+
|
|
355
|
+
// Look for SIP announcements in logs
|
|
356
|
+
for (const log of tx.meta.logMessages) {
|
|
357
|
+
if (!log.includes(SIP_MEMO_PREFIX)) continue
|
|
358
|
+
|
|
359
|
+
const memoMatch = log.match(/Program log: (.+)/)
|
|
360
|
+
if (!memoMatch) continue
|
|
361
|
+
|
|
362
|
+
const announcement = parseAnnouncement(memoMatch[1])
|
|
363
|
+
if (!announcement) continue
|
|
364
|
+
|
|
365
|
+
// Check against all recipients
|
|
366
|
+
const detectedPayment = await this.checkAnnouncementAgainstRecipients(
|
|
367
|
+
announcement,
|
|
368
|
+
{ signature: sigInfo.signature, slot: sigInfo.slot, blockTime: sigInfo.blockTime ?? null },
|
|
369
|
+
tx.meta.preTokenBalances as any,
|
|
370
|
+
tx.meta.postTokenBalances as any
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
if (detectedPayment) {
|
|
374
|
+
payments.push(detectedPayment)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
} catch {
|
|
378
|
+
// Skip failed transaction parsing
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
payments,
|
|
384
|
+
scannedCount,
|
|
385
|
+
hasMore: signatures.length >= Math.min(limit, this.batchSize),
|
|
386
|
+
nextCursor: lastSignature,
|
|
387
|
+
lastSlot,
|
|
388
|
+
}
|
|
389
|
+
} catch (err) {
|
|
390
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
391
|
+
throw new Error(`Historical scan failed: ${message}`)
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Subscribe to real-time stealth payments
|
|
397
|
+
*
|
|
398
|
+
* Uses WebSocket to monitor new transactions for stealth payments.
|
|
399
|
+
*
|
|
400
|
+
* @param onPayment - Callback when a payment is detected
|
|
401
|
+
* @param onError - Callback when an error occurs
|
|
402
|
+
*/
|
|
403
|
+
subscribe(onPayment: PaymentCallback, onError?: ErrorCallback): void {
|
|
404
|
+
if (this.subscriptionId !== null) {
|
|
405
|
+
throw new Error('Already subscribed. Call unsubscribe() first.')
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (this.recipients.length === 0) {
|
|
409
|
+
throw new Error('No recipients configured. Call addRecipient() first.')
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
this.paymentCallback = onPayment
|
|
413
|
+
this.errorCallback = onError ?? null
|
|
414
|
+
|
|
415
|
+
const memoProgram = new PublicKey(MEMO_PROGRAM_ID)
|
|
416
|
+
|
|
417
|
+
// Subscribe to logs mentioning the memo program
|
|
418
|
+
this.subscriptionId = this.connection.onLogs(
|
|
419
|
+
memoProgram,
|
|
420
|
+
async (logs) => {
|
|
421
|
+
try {
|
|
422
|
+
// Look for SIP announcements
|
|
423
|
+
for (const log of logs.logs) {
|
|
424
|
+
if (!log.includes(SIP_MEMO_PREFIX)) continue
|
|
425
|
+
|
|
426
|
+
const memoMatch = log.match(/Program log: (.+)/)
|
|
427
|
+
if (!memoMatch) continue
|
|
428
|
+
|
|
429
|
+
const announcement = parseAnnouncement(memoMatch[1])
|
|
430
|
+
if (!announcement) continue
|
|
431
|
+
|
|
432
|
+
// Get full transaction for balance info
|
|
433
|
+
const tx = await this.connection.getTransaction(logs.signature, {
|
|
434
|
+
maxSupportedTransactionVersion: 0,
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
if (!tx?.meta) continue
|
|
438
|
+
|
|
439
|
+
const payment = await this.checkAnnouncementAgainstRecipients(
|
|
440
|
+
announcement,
|
|
441
|
+
{ signature: logs.signature, slot: tx.slot ?? 0, blockTime: tx.blockTime ?? null },
|
|
442
|
+
tx.meta.preTokenBalances as any,
|
|
443
|
+
tx.meta.postTokenBalances as any
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
if (payment && this.paymentCallback) {
|
|
447
|
+
this.paymentCallback(payment)
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
} catch (err) {
|
|
451
|
+
if (this.errorCallback) {
|
|
452
|
+
this.errorCallback(err instanceof Error ? err : new Error(String(err)))
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
'confirmed'
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Unsubscribe from real-time payments
|
|
462
|
+
*/
|
|
463
|
+
async unsubscribe(): Promise<void> {
|
|
464
|
+
if (this.subscriptionId !== null) {
|
|
465
|
+
await this.connection.removeOnLogsListener(this.subscriptionId)
|
|
466
|
+
this.subscriptionId = null
|
|
467
|
+
this.paymentCallback = null
|
|
468
|
+
this.errorCallback = null
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Check if currently subscribed
|
|
474
|
+
*/
|
|
475
|
+
isSubscribed(): boolean {
|
|
476
|
+
return this.subscriptionId !== null
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Check an announcement against all recipients
|
|
481
|
+
*/
|
|
482
|
+
private async checkAnnouncementAgainstRecipients(
|
|
483
|
+
announcement: SolanaAnnouncement,
|
|
484
|
+
sigInfo: { signature: string; slot: number; blockTime: number | null },
|
|
485
|
+
preBalances: Parameters<typeof parseTokenTransferFromBalances>[0],
|
|
486
|
+
postBalances: Parameters<typeof parseTokenTransferFromBalances>[1]
|
|
487
|
+
): Promise<DetectedPayment | null> {
|
|
488
|
+
// Parse view tag
|
|
489
|
+
const viewTagNumber = parseInt(announcement.viewTag, 16)
|
|
490
|
+
if (!Number.isInteger(viewTagNumber) || viewTagNumber < 0 || viewTagNumber > VIEW_TAG_MAX) {
|
|
491
|
+
return null
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Convert ephemeral key
|
|
495
|
+
let ephemeralPubKeyHex: HexString
|
|
496
|
+
try {
|
|
497
|
+
ephemeralPubKeyHex = solanaAddressToEd25519PublicKey(announcement.ephemeralPublicKey)
|
|
498
|
+
} catch {
|
|
499
|
+
return null
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Convert stealth address
|
|
503
|
+
let stealthAddressHex: HexString
|
|
504
|
+
try {
|
|
505
|
+
stealthAddressHex = announcement.stealthAddress
|
|
506
|
+
? solanaAddressToEd25519PublicKey(announcement.stealthAddress)
|
|
507
|
+
: ('0x' + '00'.repeat(32)) as HexString
|
|
508
|
+
} catch {
|
|
509
|
+
return null
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const stealthAddressToCheck: StealthAddress = {
|
|
513
|
+
address: stealthAddressHex,
|
|
514
|
+
ephemeralPublicKey: ephemeralPubKeyHex,
|
|
515
|
+
viewTag: viewTagNumber,
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Check against each recipient
|
|
519
|
+
for (const recipient of this.recipients) {
|
|
520
|
+
try {
|
|
521
|
+
const isMatch = checkEd25519StealthAddress(
|
|
522
|
+
stealthAddressToCheck,
|
|
523
|
+
recipient.viewingPrivateKey,
|
|
524
|
+
recipient.spendingPublicKey
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
if (isMatch) {
|
|
528
|
+
// Parse token transfer
|
|
529
|
+
const transferInfo = parseTokenTransferFromBalances(preBalances, postBalances)
|
|
530
|
+
if (!transferInfo) continue
|
|
531
|
+
|
|
532
|
+
// Get current balance if provider available
|
|
533
|
+
let amount = transferInfo.amount
|
|
534
|
+
if (this.provider && announcement.stealthAddress) {
|
|
535
|
+
try {
|
|
536
|
+
const balance = await this.provider.getTokenBalance(
|
|
537
|
+
announcement.stealthAddress,
|
|
538
|
+
transferInfo.mint
|
|
539
|
+
)
|
|
540
|
+
if (balance > 0n) {
|
|
541
|
+
amount = balance
|
|
542
|
+
}
|
|
543
|
+
} catch {
|
|
544
|
+
// Use parsed amount
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return {
|
|
549
|
+
stealthAddress: announcement.stealthAddress || '',
|
|
550
|
+
ephemeralPublicKey: announcement.ephemeralPublicKey,
|
|
551
|
+
viewTag: viewTagNumber,
|
|
552
|
+
amount,
|
|
553
|
+
mint: transferInfo.mint,
|
|
554
|
+
tokenSymbol: getTokenSymbol(transferInfo.mint) || 'UNKNOWN',
|
|
555
|
+
txSignature: sigInfo.signature,
|
|
556
|
+
slot: sigInfo.slot,
|
|
557
|
+
timestamp: sigInfo.blockTime || 0,
|
|
558
|
+
recipientLabel: recipient.label,
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
} catch {
|
|
562
|
+
// Invalid keys or malformed data, try next recipient
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return null
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ─── Factory Function ─────────────────────────────────────────────────────────
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Create a new stealth scanner
|
|
574
|
+
*
|
|
575
|
+
* @param options - Scanner options
|
|
576
|
+
* @returns Configured stealth scanner
|
|
577
|
+
*
|
|
578
|
+
* @example
|
|
579
|
+
* ```typescript
|
|
580
|
+
* const scanner = createStealthScanner({
|
|
581
|
+
* connection,
|
|
582
|
+
* provider: heliusProvider,
|
|
583
|
+
* })
|
|
584
|
+
* ```
|
|
585
|
+
*/
|
|
586
|
+
export function createStealthScanner(options: StealthScannerOptions): StealthScanner {
|
|
587
|
+
return new StealthScanner(options)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ─── Batch Scanning Utilities ─────────────────────────────────────────────────
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Batch scan for multiple recipients across a slot range
|
|
594
|
+
*
|
|
595
|
+
* Efficiently scans a range of slots for payments to multiple recipients.
|
|
596
|
+
* Use this for initial wallet sync or periodic historical scans.
|
|
597
|
+
*
|
|
598
|
+
* @param options - Scanner options
|
|
599
|
+
* @param recipients - Recipients to scan for
|
|
600
|
+
* @param scanOptions - Historical scan options
|
|
601
|
+
* @returns All detected payments grouped by recipient
|
|
602
|
+
*
|
|
603
|
+
* @example
|
|
604
|
+
* ```typescript
|
|
605
|
+
* const results = await batchScanForRecipients(
|
|
606
|
+
* { connection, provider },
|
|
607
|
+
* [
|
|
608
|
+
* { viewingPrivateKey: '0x...', spendingPublicKey: '0x...', label: 'Wallet 1' },
|
|
609
|
+
* { viewingPrivateKey: '0x...', spendingPublicKey: '0x...', label: 'Wallet 2' },
|
|
610
|
+
* ],
|
|
611
|
+
* { fromSlot: 250000000, limit: 5000 }
|
|
612
|
+
* )
|
|
613
|
+
*
|
|
614
|
+
* for (const [label, payments] of Object.entries(results)) {
|
|
615
|
+
* console.log(`${label}: ${payments.length} payments`)
|
|
616
|
+
* }
|
|
617
|
+
* ```
|
|
618
|
+
*/
|
|
619
|
+
export async function batchScanForRecipients(
|
|
620
|
+
options: StealthScannerOptions,
|
|
621
|
+
recipients: ScanRecipient[],
|
|
622
|
+
scanOptions: HistoricalScanOptions = {}
|
|
623
|
+
): Promise<Record<string, DetectedPayment[]>> {
|
|
624
|
+
const scanner = createStealthScanner(options)
|
|
625
|
+
|
|
626
|
+
for (const recipient of recipients) {
|
|
627
|
+
scanner.addRecipient(recipient)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const result = await scanner.scanHistorical(scanOptions)
|
|
631
|
+
|
|
632
|
+
// Group by recipient label
|
|
633
|
+
const grouped: Record<string, DetectedPayment[]> = {}
|
|
634
|
+
|
|
635
|
+
for (const recipient of recipients) {
|
|
636
|
+
const label = recipient.label || 'unknown'
|
|
637
|
+
grouped[label] = result.payments.filter(p => p.recipientLabel === label)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return grouped
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Full historical scan with automatic pagination
|
|
645
|
+
*
|
|
646
|
+
* Scans the entire history (or specified range) with automatic pagination.
|
|
647
|
+
* Useful for complete wallet sync.
|
|
648
|
+
*
|
|
649
|
+
* @param options - Scanner options
|
|
650
|
+
* @param recipients - Recipients to scan for
|
|
651
|
+
* @param scanOptions - Historical scan options
|
|
652
|
+
* @param onProgress - Optional progress callback
|
|
653
|
+
* @returns All detected payments
|
|
654
|
+
*
|
|
655
|
+
* @example
|
|
656
|
+
* ```typescript
|
|
657
|
+
* const payments = await fullHistoricalScan(
|
|
658
|
+
* { connection },
|
|
659
|
+
* [{ viewingPrivateKey, spendingPublicKey }],
|
|
660
|
+
* { fromSlot: 250000000 },
|
|
661
|
+
* (scanned, found) => console.log(`Scanned ${scanned}, found ${found}`)
|
|
662
|
+
* )
|
|
663
|
+
* ```
|
|
664
|
+
*/
|
|
665
|
+
export async function fullHistoricalScan(
|
|
666
|
+
options: StealthScannerOptions,
|
|
667
|
+
recipients: ScanRecipient[],
|
|
668
|
+
scanOptions: HistoricalScanOptions = {},
|
|
669
|
+
onProgress?: (scannedCount: number, foundCount: number) => void
|
|
670
|
+
): Promise<DetectedPayment[]> {
|
|
671
|
+
const scanner = createStealthScanner(options)
|
|
672
|
+
|
|
673
|
+
for (const recipient of recipients) {
|
|
674
|
+
scanner.addRecipient(recipient)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const allPayments: DetectedPayment[] = []
|
|
678
|
+
let cursor: string | undefined = scanOptions.beforeSignature
|
|
679
|
+
let totalScanned = 0
|
|
680
|
+
const maxIterations = 100 // Safety limit
|
|
681
|
+
|
|
682
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
683
|
+
const result = await scanner.scanHistorical({
|
|
684
|
+
...scanOptions,
|
|
685
|
+
beforeSignature: cursor,
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
allPayments.push(...result.payments)
|
|
689
|
+
totalScanned += result.scannedCount
|
|
690
|
+
|
|
691
|
+
if (onProgress) {
|
|
692
|
+
onProgress(totalScanned, allPayments.length)
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (!result.hasMore || !result.nextCursor) {
|
|
696
|
+
break
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
cursor = result.nextCursor
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return allPayments
|
|
703
|
+
}
|