@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,971 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NEAR Stealth Address Resolver
|
|
3
|
+
*
|
|
4
|
+
* Scans NEAR blockchain for stealth address announcements and identifies
|
|
5
|
+
* addresses belonging to a user's viewing key for wallet balance discovery.
|
|
6
|
+
*
|
|
7
|
+
* ## Architecture
|
|
8
|
+
*
|
|
9
|
+
* ```
|
|
10
|
+
* NEAR Blockchain
|
|
11
|
+
* │
|
|
12
|
+
* ▼ RPC / Indexer
|
|
13
|
+
* Transaction Logs
|
|
14
|
+
* │
|
|
15
|
+
* ▼ Parse SIP: prefixed memos
|
|
16
|
+
* Announcements
|
|
17
|
+
* │
|
|
18
|
+
* ▼ Check against viewing keys
|
|
19
|
+
* Detected Payments
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* ## Features
|
|
23
|
+
*
|
|
24
|
+
* - Historical scanning with pagination
|
|
25
|
+
* - Real-time scanning placeholder (WebSocket planned)
|
|
26
|
+
* - Batch scanning for multiple recipients
|
|
27
|
+
* - View tag filtering for efficient scanning
|
|
28
|
+
* - Announcement caching layer
|
|
29
|
+
*
|
|
30
|
+
* @module chains/near/resolver
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
|
|
34
|
+
import type { HexString, StealthAddress } from '@sip-protocol/types'
|
|
35
|
+
import { ValidationError } from '../../errors'
|
|
36
|
+
import { isValidHex } from '../../validation'
|
|
37
|
+
import { checkNEARStealthAddress, implicitAccountToEd25519PublicKey } from './stealth'
|
|
38
|
+
import { parseAnnouncement, type NEARAnnouncement } from './types'
|
|
39
|
+
import {
|
|
40
|
+
SIP_MEMO_PREFIX,
|
|
41
|
+
VIEW_TAG_MIN,
|
|
42
|
+
VIEW_TAG_MAX,
|
|
43
|
+
isImplicitAccount,
|
|
44
|
+
} from './constants'
|
|
45
|
+
import type { NEARViewingKey } from './viewing-key'
|
|
46
|
+
|
|
47
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A recipient to scan for (viewing + spending key pair)
|
|
51
|
+
*/
|
|
52
|
+
export interface NEARScanRecipient {
|
|
53
|
+
/**
|
|
54
|
+
* Viewing private key (hex)
|
|
55
|
+
* @security SENSITIVE - enables scanning for payments
|
|
56
|
+
*/
|
|
57
|
+
viewingPrivateKey: HexString
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Spending private key (hex)
|
|
61
|
+
* @security SENSITIVE - required for DKSAP shared secret computation
|
|
62
|
+
*/
|
|
63
|
+
spendingPrivateKey: HexString
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Optional label for this recipient
|
|
67
|
+
*/
|
|
68
|
+
label?: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Options for the NEAR stealth scanner
|
|
73
|
+
*/
|
|
74
|
+
export interface NEARStealthScannerOptions {
|
|
75
|
+
/**
|
|
76
|
+
* NEAR RPC URL
|
|
77
|
+
*/
|
|
78
|
+
rpcUrl: string
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Optional network for explorer links
|
|
82
|
+
* @default 'mainnet'
|
|
83
|
+
*/
|
|
84
|
+
network?: 'mainnet' | 'testnet'
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Maximum results per scan batch
|
|
88
|
+
* @default 100
|
|
89
|
+
*/
|
|
90
|
+
batchSize?: number
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Enable view tag filtering for efficient scanning
|
|
94
|
+
* @default true
|
|
95
|
+
*/
|
|
96
|
+
useViewTagFilter?: boolean
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Request timeout in milliseconds
|
|
100
|
+
* @default 30000
|
|
101
|
+
*/
|
|
102
|
+
timeout?: number
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Options for historical scanning
|
|
107
|
+
*/
|
|
108
|
+
export interface NEARHistoricalScanOptions {
|
|
109
|
+
/**
|
|
110
|
+
* Account ID to scan for announcements
|
|
111
|
+
* This is typically the SIP registry contract or a specific stealth address
|
|
112
|
+
*/
|
|
113
|
+
accountId?: string
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Start block height for scanning
|
|
117
|
+
*/
|
|
118
|
+
fromBlock?: number
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* End block height for scanning
|
|
122
|
+
*/
|
|
123
|
+
toBlock?: number
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Maximum number of transactions to scan
|
|
127
|
+
* @default 1000
|
|
128
|
+
*/
|
|
129
|
+
limit?: number
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Cursor for pagination
|
|
133
|
+
*/
|
|
134
|
+
cursor?: string
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* A detected stealth payment on NEAR
|
|
139
|
+
*/
|
|
140
|
+
export interface NEARDetectedPaymentResult {
|
|
141
|
+
/**
|
|
142
|
+
* Stealth address (implicit account ID - 64 hex chars)
|
|
143
|
+
*/
|
|
144
|
+
stealthAddress: string
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Ed25519 public key for the stealth address (hex)
|
|
148
|
+
*/
|
|
149
|
+
stealthPublicKey: HexString
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Ephemeral public key from the sender (hex)
|
|
153
|
+
*/
|
|
154
|
+
ephemeralPublicKey: HexString
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* View tag for efficient scanning
|
|
158
|
+
*/
|
|
159
|
+
viewTag: number
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Amount in yoctoNEAR
|
|
163
|
+
*/
|
|
164
|
+
amount: bigint
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Token contract (null for native NEAR)
|
|
168
|
+
*/
|
|
169
|
+
tokenContract: string | null
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Token decimals
|
|
173
|
+
*/
|
|
174
|
+
decimals: number
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Transaction hash
|
|
178
|
+
*/
|
|
179
|
+
txHash: string
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Block height
|
|
183
|
+
*/
|
|
184
|
+
blockHeight: number
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Block timestamp (nanoseconds)
|
|
188
|
+
*/
|
|
189
|
+
timestamp: number
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Label of the recipient this payment was detected for
|
|
193
|
+
*/
|
|
194
|
+
recipientLabel?: string
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Result of a historical scan
|
|
199
|
+
*/
|
|
200
|
+
export interface NEARHistoricalScanResult {
|
|
201
|
+
/**
|
|
202
|
+
* Detected payments
|
|
203
|
+
*/
|
|
204
|
+
payments: NEARDetectedPaymentResult[]
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Total transactions scanned
|
|
208
|
+
*/
|
|
209
|
+
scannedCount: number
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Whether more results are available
|
|
213
|
+
*/
|
|
214
|
+
hasMore: boolean
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Cursor for next page
|
|
218
|
+
*/
|
|
219
|
+
nextCursor?: string
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Last block height scanned
|
|
223
|
+
*/
|
|
224
|
+
lastBlockHeight?: number
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Callback for real-time payment detection
|
|
229
|
+
*/
|
|
230
|
+
export type NEARPaymentCallback = (payment: NEARDetectedPaymentResult) => void
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Callback for scan errors
|
|
234
|
+
*/
|
|
235
|
+
export type NEARErrorCallback = (error: Error) => void
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Announcement cache entry
|
|
239
|
+
*/
|
|
240
|
+
interface CacheEntry {
|
|
241
|
+
announcement: NEARAnnouncement
|
|
242
|
+
txHash: string
|
|
243
|
+
blockHeight: number
|
|
244
|
+
timestamp: number
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Announcement cache interface
|
|
249
|
+
*/
|
|
250
|
+
export interface NEARAnnouncementCache {
|
|
251
|
+
/**
|
|
252
|
+
* Get cached announcements for a block range
|
|
253
|
+
*/
|
|
254
|
+
get(fromBlock: number, toBlock: number): CacheEntry[]
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Add announcements to cache
|
|
258
|
+
*/
|
|
259
|
+
add(entries: CacheEntry[]): void
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Get the highest cached block
|
|
263
|
+
*/
|
|
264
|
+
getHighestBlock(): number | null
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Clear cache for reorg handling
|
|
268
|
+
*/
|
|
269
|
+
clearFrom(blockHeight: number): void
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Get total cached count
|
|
273
|
+
*/
|
|
274
|
+
size(): number
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ─── In-Memory Cache Implementation ───────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Create an in-memory announcement cache
|
|
281
|
+
*/
|
|
282
|
+
export function createNEARAnnouncementCache(): NEARAnnouncementCache {
|
|
283
|
+
const entries: CacheEntry[] = []
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
get(fromBlock: number, toBlock: number): CacheEntry[] {
|
|
287
|
+
return entries.filter(
|
|
288
|
+
e => e.blockHeight >= fromBlock && e.blockHeight <= toBlock
|
|
289
|
+
)
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
add(newEntries: CacheEntry[]): void {
|
|
293
|
+
for (const entry of newEntries) {
|
|
294
|
+
// Avoid duplicates
|
|
295
|
+
const exists = entries.some(
|
|
296
|
+
e => e.txHash === entry.txHash
|
|
297
|
+
)
|
|
298
|
+
if (!exists) {
|
|
299
|
+
entries.push(entry)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Sort by block height
|
|
303
|
+
entries.sort((a, b) => a.blockHeight - b.blockHeight)
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
getHighestBlock(): number | null {
|
|
307
|
+
if (entries.length === 0) return null
|
|
308
|
+
return entries[entries.length - 1].blockHeight
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
clearFrom(blockHeight: number): void {
|
|
312
|
+
const idx = entries.findIndex(e => e.blockHeight >= blockHeight)
|
|
313
|
+
if (idx !== -1) {
|
|
314
|
+
entries.splice(idx)
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
size(): number {
|
|
319
|
+
return entries.length
|
|
320
|
+
},
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ─── NEAR RPC Helper ──────────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Simple NEAR RPC client for scanning
|
|
328
|
+
*/
|
|
329
|
+
class NEARRpcClient {
|
|
330
|
+
constructor(
|
|
331
|
+
private rpcUrl: string,
|
|
332
|
+
private timeout: number = 30000
|
|
333
|
+
) {}
|
|
334
|
+
|
|
335
|
+
async call<T>(method: string, params: unknown[]): Promise<T> {
|
|
336
|
+
const controller = new AbortController()
|
|
337
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout)
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
const response = await fetch(this.rpcUrl, {
|
|
341
|
+
method: 'POST',
|
|
342
|
+
headers: { 'Content-Type': 'application/json' },
|
|
343
|
+
body: JSON.stringify({
|
|
344
|
+
jsonrpc: '2.0',
|
|
345
|
+
id: Date.now(),
|
|
346
|
+
method,
|
|
347
|
+
params,
|
|
348
|
+
}),
|
|
349
|
+
signal: controller.signal,
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
if (!response.ok) {
|
|
353
|
+
throw new Error(`RPC request failed: ${response.status} ${response.statusText}`)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const json = await response.json() as { result?: T; error?: { message: string } }
|
|
357
|
+
|
|
358
|
+
if (json.error) {
|
|
359
|
+
throw new Error(`RPC error: ${json.error.message}`)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return json.result as T
|
|
363
|
+
} finally {
|
|
364
|
+
clearTimeout(timeoutId)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Get account balance
|
|
370
|
+
*/
|
|
371
|
+
async getBalance(accountId: string): Promise<bigint> {
|
|
372
|
+
interface AccountView {
|
|
373
|
+
amount: string
|
|
374
|
+
locked: string
|
|
375
|
+
code_hash: string
|
|
376
|
+
storage_usage: number
|
|
377
|
+
storage_paid_at: number
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const result = await this.call<AccountView>('query', [{
|
|
381
|
+
request_type: 'view_account',
|
|
382
|
+
finality: 'final',
|
|
383
|
+
account_id: accountId,
|
|
384
|
+
}])
|
|
385
|
+
|
|
386
|
+
return BigInt(result.amount)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Get block info
|
|
391
|
+
*/
|
|
392
|
+
async getBlock(blockId: number | string = 'final'): Promise<{
|
|
393
|
+
header: {
|
|
394
|
+
height: number
|
|
395
|
+
timestamp: number
|
|
396
|
+
hash: string
|
|
397
|
+
}
|
|
398
|
+
}> {
|
|
399
|
+
if (typeof blockId === 'number') {
|
|
400
|
+
return this.call('block', [{ block_id: blockId }])
|
|
401
|
+
}
|
|
402
|
+
return this.call('block', [{ finality: blockId }])
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Get transaction status
|
|
407
|
+
*/
|
|
408
|
+
async getTxStatus(txHash: string, senderId: string): Promise<{
|
|
409
|
+
transaction: {
|
|
410
|
+
hash: string
|
|
411
|
+
signer_id: string
|
|
412
|
+
receiver_id: string
|
|
413
|
+
actions: Array<{
|
|
414
|
+
FunctionCall?: {
|
|
415
|
+
method_name: string
|
|
416
|
+
args: string
|
|
417
|
+
}
|
|
418
|
+
Transfer?: {
|
|
419
|
+
deposit: string
|
|
420
|
+
}
|
|
421
|
+
}>
|
|
422
|
+
}
|
|
423
|
+
receipts_outcome: Array<{
|
|
424
|
+
outcome: {
|
|
425
|
+
logs: string[]
|
|
426
|
+
status: unknown
|
|
427
|
+
}
|
|
428
|
+
}>
|
|
429
|
+
}> {
|
|
430
|
+
return this.call('tx', [txHash, senderId])
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ─── NEARStealthScanner Class ─────────────────────────────────────────────────
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* NEAR Stealth Address Scanner/Resolver
|
|
438
|
+
*
|
|
439
|
+
* Scans NEAR blockchain for stealth address announcements and identifies
|
|
440
|
+
* which addresses belong to a user's viewing key.
|
|
441
|
+
*
|
|
442
|
+
* @example Basic usage
|
|
443
|
+
* ```typescript
|
|
444
|
+
* const scanner = new NEARStealthScanner({
|
|
445
|
+
* rpcUrl: 'https://rpc.mainnet.near.org',
|
|
446
|
+
* })
|
|
447
|
+
*
|
|
448
|
+
* // Add recipients to scan for
|
|
449
|
+
* scanner.addRecipient({
|
|
450
|
+
* viewingPrivateKey: '0x...',
|
|
451
|
+
* spendingPublicKey: '0x...',
|
|
452
|
+
* label: 'Wallet 1',
|
|
453
|
+
* })
|
|
454
|
+
*
|
|
455
|
+
* // Scan announcements
|
|
456
|
+
* const result = await scanner.scanAnnouncements([
|
|
457
|
+
* { stealthAddress: '...', ephemeralPublicKey: '0x...', viewTag: 42 }
|
|
458
|
+
* ])
|
|
459
|
+
*
|
|
460
|
+
* console.log(`Found ${result.payments.length} payments`)
|
|
461
|
+
* ```
|
|
462
|
+
*/
|
|
463
|
+
export class NEARStealthScanner {
|
|
464
|
+
private rpc: NEARRpcClient
|
|
465
|
+
private recipients: NEARScanRecipient[] = []
|
|
466
|
+
private _batchSize: number
|
|
467
|
+
private cache: NEARAnnouncementCache | null = null
|
|
468
|
+
private network: 'mainnet' | 'testnet'
|
|
469
|
+
|
|
470
|
+
constructor(options: NEARStealthScannerOptions) {
|
|
471
|
+
this.rpc = new NEARRpcClient(options.rpcUrl, options.timeout)
|
|
472
|
+
this._batchSize = options.batchSize ?? 100
|
|
473
|
+
this.network = options.network ?? 'mainnet'
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Get the batch size for scanning
|
|
478
|
+
*/
|
|
479
|
+
get batchSize(): number {
|
|
480
|
+
return this._batchSize
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Get the network
|
|
485
|
+
*/
|
|
486
|
+
getNetwork(): 'mainnet' | 'testnet' {
|
|
487
|
+
return this.network
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Enable caching for announcements
|
|
492
|
+
*/
|
|
493
|
+
enableCache(cache?: NEARAnnouncementCache): void {
|
|
494
|
+
this.cache = cache ?? createNEARAnnouncementCache()
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Disable caching
|
|
499
|
+
*/
|
|
500
|
+
disableCache(): void {
|
|
501
|
+
this.cache = null
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Get the current cache
|
|
506
|
+
*/
|
|
507
|
+
getCache(): NEARAnnouncementCache | null {
|
|
508
|
+
return this.cache
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Add a recipient to scan for
|
|
513
|
+
*
|
|
514
|
+
* @param recipient - Recipient with viewing/spending keys
|
|
515
|
+
*/
|
|
516
|
+
addRecipient(recipient: NEARScanRecipient): void {
|
|
517
|
+
// Validate keys
|
|
518
|
+
if (!isValidHex(recipient.viewingPrivateKey)) {
|
|
519
|
+
throw new ValidationError('Invalid viewingPrivateKey', 'viewingPrivateKey')
|
|
520
|
+
}
|
|
521
|
+
if (!isValidHex(recipient.spendingPrivateKey)) {
|
|
522
|
+
throw new ValidationError('Invalid spendingPrivateKey', 'spendingPrivateKey')
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
this.recipients.push(recipient)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Add recipient from a NEARViewingKey
|
|
530
|
+
*/
|
|
531
|
+
addRecipientFromViewingKey(
|
|
532
|
+
viewingKey: NEARViewingKey,
|
|
533
|
+
spendingPrivateKey: HexString
|
|
534
|
+
): void {
|
|
535
|
+
this.addRecipient({
|
|
536
|
+
viewingPrivateKey: viewingKey.privateKey,
|
|
537
|
+
spendingPrivateKey,
|
|
538
|
+
label: viewingKey.label,
|
|
539
|
+
})
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Remove a recipient by label
|
|
544
|
+
*
|
|
545
|
+
* @param label - Recipient label to remove
|
|
546
|
+
*/
|
|
547
|
+
removeRecipient(label: string): void {
|
|
548
|
+
this.recipients = this.recipients.filter(r => r.label !== label)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Clear all recipients
|
|
553
|
+
*/
|
|
554
|
+
clearRecipients(): void {
|
|
555
|
+
this.recipients = []
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Get current recipients (labels only, keys are sensitive)
|
|
560
|
+
*/
|
|
561
|
+
getRecipients(): Array<{ label?: string }> {
|
|
562
|
+
return this.recipients.map(r => ({
|
|
563
|
+
label: r.label,
|
|
564
|
+
}))
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Get the current block height
|
|
569
|
+
*/
|
|
570
|
+
async getCurrentBlockHeight(): Promise<number> {
|
|
571
|
+
const block = await this.rpc.getBlock('final')
|
|
572
|
+
return block.header.height
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Get balance of a stealth address
|
|
577
|
+
*/
|
|
578
|
+
async getStealthAddressBalance(stealthAddress: string): Promise<bigint> {
|
|
579
|
+
if (!isImplicitAccount(stealthAddress)) {
|
|
580
|
+
throw new ValidationError(
|
|
581
|
+
'stealthAddress must be a valid implicit account (64 hex chars)',
|
|
582
|
+
'stealthAddress'
|
|
583
|
+
)
|
|
584
|
+
}
|
|
585
|
+
return this.rpc.getBalance(stealthAddress)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Scan a list of announcements against configured recipients
|
|
590
|
+
*
|
|
591
|
+
* This is the core scanning function. Use this when you have already
|
|
592
|
+
* fetched announcements from the chain or an indexer.
|
|
593
|
+
*
|
|
594
|
+
* @param announcements - Announcements to check
|
|
595
|
+
* @param txMetadata - Optional transaction metadata for each announcement
|
|
596
|
+
* @returns Detected payments
|
|
597
|
+
*/
|
|
598
|
+
async scanAnnouncements(
|
|
599
|
+
announcements: NEARAnnouncement[],
|
|
600
|
+
txMetadata?: Array<{
|
|
601
|
+
txHash: string
|
|
602
|
+
blockHeight: number
|
|
603
|
+
timestamp: number
|
|
604
|
+
amount?: bigint
|
|
605
|
+
tokenContract?: string
|
|
606
|
+
decimals?: number
|
|
607
|
+
}>
|
|
608
|
+
): Promise<NEARDetectedPaymentResult[]> {
|
|
609
|
+
if (this.recipients.length === 0) {
|
|
610
|
+
return []
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const payments: NEARDetectedPaymentResult[] = []
|
|
614
|
+
|
|
615
|
+
for (let i = 0; i < announcements.length; i++) {
|
|
616
|
+
const announcement = announcements[i]
|
|
617
|
+
const metadata = txMetadata?.[i]
|
|
618
|
+
|
|
619
|
+
// Validate announcement - viewTag is already a number from parseAnnouncement
|
|
620
|
+
const viewTag = announcement.viewTag
|
|
621
|
+
if (!Number.isInteger(viewTag) || viewTag < VIEW_TAG_MIN || viewTag > VIEW_TAG_MAX) {
|
|
622
|
+
continue
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Get stealth address as ed25519 public key
|
|
626
|
+
let stealthPublicKey: HexString
|
|
627
|
+
try {
|
|
628
|
+
if (isImplicitAccount(announcement.stealthAddress)) {
|
|
629
|
+
stealthPublicKey = implicitAccountToEd25519PublicKey(announcement.stealthAddress)
|
|
630
|
+
} else if (announcement.stealthAddress.startsWith('0x')) {
|
|
631
|
+
stealthPublicKey = announcement.stealthAddress as HexString
|
|
632
|
+
} else {
|
|
633
|
+
continue
|
|
634
|
+
}
|
|
635
|
+
} catch {
|
|
636
|
+
continue
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Validate ephemeral public key
|
|
640
|
+
if (!isValidHex(announcement.ephemeralPublicKey)) {
|
|
641
|
+
continue
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const stealthAddressToCheck: StealthAddress = {
|
|
645
|
+
address: stealthPublicKey,
|
|
646
|
+
ephemeralPublicKey: announcement.ephemeralPublicKey,
|
|
647
|
+
viewTag,
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Check against each recipient
|
|
651
|
+
for (const recipient of this.recipients) {
|
|
652
|
+
try {
|
|
653
|
+
const isMatch = checkNEARStealthAddress(
|
|
654
|
+
stealthAddressToCheck,
|
|
655
|
+
recipient.spendingPrivateKey,
|
|
656
|
+
recipient.viewingPrivateKey
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
if (isMatch) {
|
|
660
|
+
// Get stealth address as implicit account
|
|
661
|
+
const stealthAddress = isImplicitAccount(announcement.stealthAddress)
|
|
662
|
+
? announcement.stealthAddress
|
|
663
|
+
: bytesToHex(hexToBytes(stealthPublicKey.slice(2)))
|
|
664
|
+
|
|
665
|
+
// Get balance if not provided
|
|
666
|
+
let amount = metadata?.amount ?? 0n
|
|
667
|
+
if (amount === 0n && isImplicitAccount(stealthAddress)) {
|
|
668
|
+
try {
|
|
669
|
+
amount = await this.getStealthAddressBalance(stealthAddress)
|
|
670
|
+
} catch {
|
|
671
|
+
// Account might not exist yet
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
payments.push({
|
|
676
|
+
stealthAddress,
|
|
677
|
+
stealthPublicKey,
|
|
678
|
+
ephemeralPublicKey: announcement.ephemeralPublicKey,
|
|
679
|
+
viewTag,
|
|
680
|
+
amount,
|
|
681
|
+
tokenContract: metadata?.tokenContract ?? null,
|
|
682
|
+
decimals: metadata?.decimals ?? 24,
|
|
683
|
+
txHash: metadata?.txHash ?? '',
|
|
684
|
+
blockHeight: metadata?.blockHeight ?? 0,
|
|
685
|
+
timestamp: metadata?.timestamp ?? Date.now(),
|
|
686
|
+
recipientLabel: recipient.label,
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
// Only one recipient can match per announcement
|
|
690
|
+
break
|
|
691
|
+
}
|
|
692
|
+
} catch {
|
|
693
|
+
// Invalid keys or malformed data, try next recipient
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return payments
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Parse announcements from transaction logs
|
|
703
|
+
*
|
|
704
|
+
* @param logs - Transaction log strings
|
|
705
|
+
* @returns Parsed announcements
|
|
706
|
+
*/
|
|
707
|
+
parseAnnouncementsFromLogs(logs: string[]): NEARAnnouncement[] {
|
|
708
|
+
const announcements: NEARAnnouncement[] = []
|
|
709
|
+
|
|
710
|
+
for (const log of logs) {
|
|
711
|
+
if (!log.includes(SIP_MEMO_PREFIX)) {
|
|
712
|
+
continue
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const parsed = parseAnnouncement(log)
|
|
716
|
+
// parseAnnouncement only returns ephemeralPublicKey and viewTag from the memo
|
|
717
|
+
// stealthAddress must be added separately from the transaction receiver
|
|
718
|
+
if (parsed && parsed.ephemeralPublicKey && typeof parsed.viewTag === 'number') {
|
|
719
|
+
announcements.push({
|
|
720
|
+
ephemeralPublicKey: parsed.ephemeralPublicKey,
|
|
721
|
+
viewTag: parsed.viewTag,
|
|
722
|
+
stealthAddress: '' as HexString, // Must be filled in by caller
|
|
723
|
+
stealthAccountId: '', // Must be filled in by caller
|
|
724
|
+
})
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return announcements
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Verify a specific stealth address belongs to a recipient
|
|
733
|
+
*
|
|
734
|
+
* @param stealthAddress - Stealth address (implicit account or hex)
|
|
735
|
+
* @param ephemeralPublicKey - Ephemeral public key from sender
|
|
736
|
+
* @param viewTag - View tag for filtering
|
|
737
|
+
* @param viewingPrivateKey - Viewing private key to check
|
|
738
|
+
* @param spendingPrivateKey - Spending private key to check
|
|
739
|
+
* @returns True if the stealth address belongs to the recipient
|
|
740
|
+
*/
|
|
741
|
+
verifyStealthAddressOwnership(
|
|
742
|
+
stealthAddress: string,
|
|
743
|
+
ephemeralPublicKey: HexString,
|
|
744
|
+
viewTag: number,
|
|
745
|
+
viewingPrivateKey: HexString,
|
|
746
|
+
spendingPrivateKey: HexString
|
|
747
|
+
): boolean {
|
|
748
|
+
// Get stealth address as ed25519 public key
|
|
749
|
+
let stealthPublicKey: HexString
|
|
750
|
+
if (isImplicitAccount(stealthAddress)) {
|
|
751
|
+
stealthPublicKey = implicitAccountToEd25519PublicKey(stealthAddress)
|
|
752
|
+
} else if (stealthAddress.startsWith('0x')) {
|
|
753
|
+
stealthPublicKey = stealthAddress as HexString
|
|
754
|
+
} else {
|
|
755
|
+
return false
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const stealthAddressToCheck: StealthAddress = {
|
|
759
|
+
address: stealthPublicKey,
|
|
760
|
+
ephemeralPublicKey,
|
|
761
|
+
viewTag,
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
try {
|
|
765
|
+
return checkNEARStealthAddress(
|
|
766
|
+
stealthAddressToCheck,
|
|
767
|
+
spendingPrivateKey,
|
|
768
|
+
viewingPrivateKey
|
|
769
|
+
)
|
|
770
|
+
} catch {
|
|
771
|
+
return false
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Batch check multiple announcements efficiently
|
|
777
|
+
*
|
|
778
|
+
* @param announcements - Announcements to check
|
|
779
|
+
* @returns Map of stealth address to recipient label for matches
|
|
780
|
+
*/
|
|
781
|
+
batchCheckAnnouncements(
|
|
782
|
+
announcements: NEARAnnouncement[]
|
|
783
|
+
): Map<string, string | undefined> {
|
|
784
|
+
const matches = new Map<string, string | undefined>()
|
|
785
|
+
|
|
786
|
+
for (const announcement of announcements) {
|
|
787
|
+
// viewTag is already a number from parseAnnouncement
|
|
788
|
+
const viewTag = announcement.viewTag
|
|
789
|
+
if (!Number.isInteger(viewTag) || viewTag < VIEW_TAG_MIN || viewTag > VIEW_TAG_MAX) {
|
|
790
|
+
continue
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Get stealth public key
|
|
794
|
+
let stealthPublicKey: HexString
|
|
795
|
+
try {
|
|
796
|
+
if (isImplicitAccount(announcement.stealthAddress)) {
|
|
797
|
+
stealthPublicKey = implicitAccountToEd25519PublicKey(announcement.stealthAddress)
|
|
798
|
+
} else if (announcement.stealthAddress.startsWith('0x')) {
|
|
799
|
+
stealthPublicKey = announcement.stealthAddress as HexString
|
|
800
|
+
} else {
|
|
801
|
+
continue
|
|
802
|
+
}
|
|
803
|
+
} catch {
|
|
804
|
+
continue
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const stealthAddressToCheck: StealthAddress = {
|
|
808
|
+
address: stealthPublicKey,
|
|
809
|
+
ephemeralPublicKey: announcement.ephemeralPublicKey,
|
|
810
|
+
viewTag,
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
for (const recipient of this.recipients) {
|
|
814
|
+
try {
|
|
815
|
+
const isMatch = checkNEARStealthAddress(
|
|
816
|
+
stealthAddressToCheck,
|
|
817
|
+
recipient.spendingPrivateKey,
|
|
818
|
+
recipient.viewingPrivateKey
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
if (isMatch) {
|
|
822
|
+
matches.set(announcement.stealthAddress, recipient.label)
|
|
823
|
+
break
|
|
824
|
+
}
|
|
825
|
+
} catch {
|
|
826
|
+
continue
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
return matches
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// ─── Factory Function ─────────────────────────────────────────────────────────
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Create a new NEAR stealth scanner
|
|
839
|
+
*
|
|
840
|
+
* @param options - Scanner options
|
|
841
|
+
* @returns Configured stealth scanner
|
|
842
|
+
*
|
|
843
|
+
* @example
|
|
844
|
+
* ```typescript
|
|
845
|
+
* const scanner = createNEARStealthScanner({
|
|
846
|
+
* rpcUrl: 'https://rpc.mainnet.near.org',
|
|
847
|
+
* })
|
|
848
|
+
* ```
|
|
849
|
+
*/
|
|
850
|
+
export function createNEARStealthScanner(
|
|
851
|
+
options: NEARStealthScannerOptions
|
|
852
|
+
): NEARStealthScanner {
|
|
853
|
+
return new NEARStealthScanner(options)
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// ─── Batch Scanning Utilities ─────────────────────────────────────────────────
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Batch scan announcements for multiple recipients
|
|
860
|
+
*
|
|
861
|
+
* @param options - Scanner options
|
|
862
|
+
* @param recipients - Recipients to scan for
|
|
863
|
+
* @param announcements - Announcements to check
|
|
864
|
+
* @returns All detected payments grouped by recipient
|
|
865
|
+
*
|
|
866
|
+
* @example
|
|
867
|
+
* ```typescript
|
|
868
|
+
* const results = await batchScanNEARAnnouncements(
|
|
869
|
+
* { rpcUrl: 'https://rpc.mainnet.near.org' },
|
|
870
|
+
* [
|
|
871
|
+
* { viewingPrivateKey: '0x...', spendingPublicKey: '0x...', label: 'Wallet 1' },
|
|
872
|
+
* { viewingPrivateKey: '0x...', spendingPublicKey: '0x...', label: 'Wallet 2' },
|
|
873
|
+
* ],
|
|
874
|
+
* announcements
|
|
875
|
+
* )
|
|
876
|
+
*
|
|
877
|
+
* for (const [label, payments] of Object.entries(results)) {
|
|
878
|
+
* console.log(`${label}: ${payments.length} payments`)
|
|
879
|
+
* }
|
|
880
|
+
* ```
|
|
881
|
+
*/
|
|
882
|
+
export async function batchScanNEARAnnouncements(
|
|
883
|
+
options: NEARStealthScannerOptions,
|
|
884
|
+
recipients: NEARScanRecipient[],
|
|
885
|
+
announcements: NEARAnnouncement[],
|
|
886
|
+
txMetadata?: Array<{
|
|
887
|
+
txHash: string
|
|
888
|
+
blockHeight: number
|
|
889
|
+
timestamp: number
|
|
890
|
+
amount?: bigint
|
|
891
|
+
tokenContract?: string
|
|
892
|
+
decimals?: number
|
|
893
|
+
}>
|
|
894
|
+
): Promise<Record<string, NEARDetectedPaymentResult[]>> {
|
|
895
|
+
const scanner = createNEARStealthScanner(options)
|
|
896
|
+
|
|
897
|
+
for (const recipient of recipients) {
|
|
898
|
+
scanner.addRecipient(recipient)
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const payments = await scanner.scanAnnouncements(announcements, txMetadata)
|
|
902
|
+
|
|
903
|
+
// Group by recipient label
|
|
904
|
+
const grouped: Record<string, NEARDetectedPaymentResult[]> = {}
|
|
905
|
+
|
|
906
|
+
for (const recipient of recipients) {
|
|
907
|
+
const label = recipient.label || 'unknown'
|
|
908
|
+
grouped[label] = payments.filter(p => p.recipientLabel === label)
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
return grouped
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Quick check if any announcement matches any recipient
|
|
916
|
+
*
|
|
917
|
+
* Useful for efficient initial filtering before detailed processing.
|
|
918
|
+
*
|
|
919
|
+
* @param recipients - Recipients to check
|
|
920
|
+
* @param announcements - Announcements to check
|
|
921
|
+
* @returns True if any announcement matches any recipient
|
|
922
|
+
*/
|
|
923
|
+
export function hasNEARAnnouncementMatch(
|
|
924
|
+
recipients: NEARScanRecipient[],
|
|
925
|
+
announcements: NEARAnnouncement[]
|
|
926
|
+
): boolean {
|
|
927
|
+
for (const announcement of announcements) {
|
|
928
|
+
// viewTag is already a number from parseAnnouncement
|
|
929
|
+
const viewTag = announcement.viewTag
|
|
930
|
+
if (!Number.isInteger(viewTag) || viewTag < VIEW_TAG_MIN || viewTag > VIEW_TAG_MAX) {
|
|
931
|
+
continue
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
let stealthPublicKey: HexString
|
|
935
|
+
try {
|
|
936
|
+
if (isImplicitAccount(announcement.stealthAddress)) {
|
|
937
|
+
stealthPublicKey = implicitAccountToEd25519PublicKey(announcement.stealthAddress)
|
|
938
|
+
} else if (announcement.stealthAddress.startsWith('0x')) {
|
|
939
|
+
stealthPublicKey = announcement.stealthAddress as HexString
|
|
940
|
+
} else {
|
|
941
|
+
continue
|
|
942
|
+
}
|
|
943
|
+
} catch {
|
|
944
|
+
continue
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const stealthAddressToCheck: StealthAddress = {
|
|
948
|
+
address: stealthPublicKey,
|
|
949
|
+
ephemeralPublicKey: announcement.ephemeralPublicKey,
|
|
950
|
+
viewTag,
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
for (const recipient of recipients) {
|
|
954
|
+
try {
|
|
955
|
+
const isMatch = checkNEARStealthAddress(
|
|
956
|
+
stealthAddressToCheck,
|
|
957
|
+
recipient.spendingPrivateKey,
|
|
958
|
+
recipient.viewingPrivateKey
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
if (isMatch) {
|
|
962
|
+
return true
|
|
963
|
+
}
|
|
964
|
+
} catch {
|
|
965
|
+
continue
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
return false
|
|
971
|
+
}
|