@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,876 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NEAR Viewing Key Management
|
|
3
|
+
*
|
|
4
|
+
* Provides viewing key generation, export/import, encryption, and storage
|
|
5
|
+
* for selective disclosure and compliance on NEAR.
|
|
6
|
+
*
|
|
7
|
+
* ## Architecture
|
|
8
|
+
*
|
|
9
|
+
* ```
|
|
10
|
+
* Spending Private Key
|
|
11
|
+
* │
|
|
12
|
+
* ▼ HMAC-SHA256(context)
|
|
13
|
+
* Viewing Private Key (32 bytes)
|
|
14
|
+
* │
|
|
15
|
+
* ▼ ed25519.getPublicKey()
|
|
16
|
+
* Viewing Public Key (32 bytes)
|
|
17
|
+
* │
|
|
18
|
+
* ▼ sha256()
|
|
19
|
+
* Viewing Key Hash (32 bytes) ← Used for announcement matching
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* ## Security Properties
|
|
23
|
+
*
|
|
24
|
+
* - Viewing keys can decrypt but NOT spend funds
|
|
25
|
+
* - Hash is safe to publish on-chain for announcement matching
|
|
26
|
+
* - XChaCha20-Poly1305 provides authenticated encryption
|
|
27
|
+
*
|
|
28
|
+
* @module chains/near/viewing-key
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { ed25519 } from '@noble/curves/ed25519'
|
|
32
|
+
import { sha256 } from '@noble/hashes/sha256'
|
|
33
|
+
import { hmac } from '@noble/hashes/hmac'
|
|
34
|
+
import { hkdf } from '@noble/hashes/hkdf'
|
|
35
|
+
import { bytesToHex, hexToBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils'
|
|
36
|
+
import { xchacha20poly1305 } from '@noble/ciphers/chacha.js'
|
|
37
|
+
import type { HexString, Hash } from '@sip-protocol/types'
|
|
38
|
+
import { ValidationError, CryptoError, ErrorCode } from '../../errors'
|
|
39
|
+
import { isValidHex } from '../../validation'
|
|
40
|
+
import { secureWipe } from '../../secure-memory'
|
|
41
|
+
|
|
42
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Domain separation for viewing key derivation from spending key
|
|
46
|
+
*/
|
|
47
|
+
const VIEWING_KEY_CONTEXT = 'SIP-NEAR-viewing-key-v1'
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Domain separation for encryption key derivation
|
|
51
|
+
*/
|
|
52
|
+
const ENCRYPTION_DOMAIN = 'SIP-NEAR-VIEWING-KEY-ENCRYPTION-V1'
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* XChaCha20-Poly1305 nonce size (24 bytes)
|
|
56
|
+
*/
|
|
57
|
+
const NONCE_SIZE = 24
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Standard export format version for viewing keys
|
|
61
|
+
*/
|
|
62
|
+
const EXPORT_VERSION = 1
|
|
63
|
+
|
|
64
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A NEAR viewing key with associated metadata
|
|
68
|
+
*/
|
|
69
|
+
export interface NEARViewingKey {
|
|
70
|
+
/**
|
|
71
|
+
* The viewing private key (32 bytes)
|
|
72
|
+
* Used for decryption and stealth address scanning
|
|
73
|
+
*/
|
|
74
|
+
privateKey: HexString
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The viewing public key (32 bytes)
|
|
78
|
+
* Can be shared for encryption
|
|
79
|
+
*/
|
|
80
|
+
publicKey: HexString
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Hash of the viewing public key for announcement matching
|
|
84
|
+
* This is published on-chain to enable efficient scanning
|
|
85
|
+
*/
|
|
86
|
+
hash: Hash
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Optional label for this viewing key
|
|
90
|
+
*/
|
|
91
|
+
label?: string
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Timestamp when this key was created
|
|
95
|
+
*/
|
|
96
|
+
createdAt: number
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Standard export format for NEAR viewing keys
|
|
101
|
+
*/
|
|
102
|
+
export interface NEARViewingKeyExport {
|
|
103
|
+
/**
|
|
104
|
+
* Export format version
|
|
105
|
+
*/
|
|
106
|
+
version: number
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The chain this key is for
|
|
110
|
+
*/
|
|
111
|
+
chain: 'near'
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The viewing private key (hex encoded)
|
|
115
|
+
*/
|
|
116
|
+
privateKey: HexString
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The viewing public key (hex encoded)
|
|
120
|
+
*/
|
|
121
|
+
publicKey: HexString
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Hash for announcement matching
|
|
125
|
+
*/
|
|
126
|
+
hash: Hash
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Optional label
|
|
130
|
+
*/
|
|
131
|
+
label?: string
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Creation timestamp
|
|
135
|
+
*/
|
|
136
|
+
createdAt: number
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Export timestamp
|
|
140
|
+
*/
|
|
141
|
+
exportedAt: number
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Encrypted data structure for viewing key operations
|
|
146
|
+
*/
|
|
147
|
+
export interface NEAREncryptedPayload {
|
|
148
|
+
/**
|
|
149
|
+
* The encrypted ciphertext (hex encoded)
|
|
150
|
+
*/
|
|
151
|
+
ciphertext: HexString
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The nonce used for encryption (hex encoded, 24 bytes)
|
|
155
|
+
*/
|
|
156
|
+
nonce: HexString
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Hash of the viewing key that can decrypt this
|
|
160
|
+
*/
|
|
161
|
+
viewingKeyHash: Hash
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Transaction data that can be encrypted for viewing
|
|
166
|
+
*/
|
|
167
|
+
export interface NEARTransactionData {
|
|
168
|
+
/**
|
|
169
|
+
* Sender's account ID
|
|
170
|
+
*/
|
|
171
|
+
sender: string
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Recipient's stealth address (implicit account)
|
|
175
|
+
*/
|
|
176
|
+
recipient: string
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Amount in yoctoNEAR (string for bigint serialization)
|
|
180
|
+
*/
|
|
181
|
+
amount: string
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Token contract address (null for native NEAR)
|
|
185
|
+
*/
|
|
186
|
+
tokenContract: string | null
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Token decimals (24 for NEAR, 6 for USDC, etc.)
|
|
190
|
+
*/
|
|
191
|
+
decimals: number
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Transaction timestamp
|
|
195
|
+
*/
|
|
196
|
+
timestamp: number
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Optional memo
|
|
200
|
+
*/
|
|
201
|
+
memo?: string
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Interface for viewing key storage providers
|
|
206
|
+
*/
|
|
207
|
+
export interface NEARViewingKeyStorage {
|
|
208
|
+
/**
|
|
209
|
+
* Store a viewing key
|
|
210
|
+
* @param key - The viewing key to store
|
|
211
|
+
* @returns Promise resolving to the key's hash (for identification)
|
|
212
|
+
*/
|
|
213
|
+
save(key: NEARViewingKey): Promise<Hash>
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Retrieve a viewing key by its hash
|
|
217
|
+
* @param hash - The viewing key hash
|
|
218
|
+
* @returns Promise resolving to the key or null if not found
|
|
219
|
+
*/
|
|
220
|
+
load(hash: Hash): Promise<NEARViewingKey | null>
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* List all stored viewing keys
|
|
224
|
+
* @returns Promise resolving to array of keys
|
|
225
|
+
*/
|
|
226
|
+
list(): Promise<NEARViewingKey[]>
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Delete a viewing key by its hash
|
|
230
|
+
* @param hash - The viewing key hash
|
|
231
|
+
* @returns Promise resolving to true if deleted, false if not found
|
|
232
|
+
*/
|
|
233
|
+
delete(hash: Hash): Promise<boolean>
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ─── Viewing Key Generation ───────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Generate a viewing key from a spending private key
|
|
240
|
+
*
|
|
241
|
+
* The viewing key is derived deterministically using HMAC-SHA256 with domain
|
|
242
|
+
* separation, ensuring it cannot be used to derive the spending key.
|
|
243
|
+
*
|
|
244
|
+
* @param spendingPrivateKey - The spending private key (32 bytes, hex)
|
|
245
|
+
* @param label - Optional label for the viewing key
|
|
246
|
+
* @returns The generated viewing key with public key and hash
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```typescript
|
|
250
|
+
* const viewingKey = generateNEARViewingKeyFromSpending(
|
|
251
|
+
* spendingPrivateKey,
|
|
252
|
+
* 'My NEAR Wallet'
|
|
253
|
+
* )
|
|
254
|
+
*
|
|
255
|
+
* // Share the public key for encryption
|
|
256
|
+
* console.log('Public key:', viewingKey.publicKey)
|
|
257
|
+
*
|
|
258
|
+
* // Use hash for on-chain announcement matching
|
|
259
|
+
* console.log('Hash:', viewingKey.hash)
|
|
260
|
+
* ```
|
|
261
|
+
*/
|
|
262
|
+
export function generateNEARViewingKeyFromSpending(
|
|
263
|
+
spendingPrivateKey: HexString,
|
|
264
|
+
label?: string
|
|
265
|
+
): NEARViewingKey {
|
|
266
|
+
// Validate input
|
|
267
|
+
if (!spendingPrivateKey || !spendingPrivateKey.startsWith('0x')) {
|
|
268
|
+
throw new ValidationError(
|
|
269
|
+
'spendingPrivateKey must be a hex string with 0x prefix',
|
|
270
|
+
'spendingPrivateKey'
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const spendingBytes = hexToBytes(spendingPrivateKey.slice(2))
|
|
275
|
+
|
|
276
|
+
if (spendingBytes.length !== 32) {
|
|
277
|
+
throw new ValidationError(
|
|
278
|
+
'spendingPrivateKey must be 32 bytes',
|
|
279
|
+
'spendingPrivateKey'
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let viewingPrivateBytes: Uint8Array | null = null
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
// Derive viewing key using HMAC-SHA256 with domain separation
|
|
287
|
+
viewingPrivateBytes = hmac(
|
|
288
|
+
sha256,
|
|
289
|
+
utf8ToBytes(VIEWING_KEY_CONTEXT),
|
|
290
|
+
spendingBytes
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
// Derive public key
|
|
294
|
+
const viewingPublicBytes = ed25519.getPublicKey(viewingPrivateBytes)
|
|
295
|
+
|
|
296
|
+
// Compute hash for announcement matching
|
|
297
|
+
const hashBytes = sha256(viewingPublicBytes)
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
privateKey: `0x${bytesToHex(viewingPrivateBytes)}` as HexString,
|
|
301
|
+
publicKey: `0x${bytesToHex(viewingPublicBytes)}` as HexString,
|
|
302
|
+
hash: `0x${bytesToHex(hashBytes)}` as Hash,
|
|
303
|
+
label,
|
|
304
|
+
createdAt: Date.now(),
|
|
305
|
+
}
|
|
306
|
+
} finally {
|
|
307
|
+
// Secure wipe sensitive data
|
|
308
|
+
secureWipe(spendingBytes)
|
|
309
|
+
if (viewingPrivateBytes) secureWipe(viewingPrivateBytes)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Generate a new random viewing key
|
|
315
|
+
*
|
|
316
|
+
* Creates a cryptographically random viewing key that is NOT derived from
|
|
317
|
+
* a spending key. Use this for standalone viewing keys or testing.
|
|
318
|
+
*
|
|
319
|
+
* @param label - Optional label for the viewing key
|
|
320
|
+
* @returns The generated viewing key
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* ```typescript
|
|
324
|
+
* const viewingKey = generateRandomNEARViewingKey('Audit Key')
|
|
325
|
+
* ```
|
|
326
|
+
*/
|
|
327
|
+
export function generateRandomNEARViewingKey(label?: string): NEARViewingKey {
|
|
328
|
+
const privateBytes = randomBytes(32)
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
const publicBytes = ed25519.getPublicKey(privateBytes)
|
|
332
|
+
const hashBytes = sha256(publicBytes)
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
privateKey: `0x${bytesToHex(privateBytes)}` as HexString,
|
|
336
|
+
publicKey: `0x${bytesToHex(publicBytes)}` as HexString,
|
|
337
|
+
hash: `0x${bytesToHex(hashBytes)}` as Hash,
|
|
338
|
+
label,
|
|
339
|
+
createdAt: Date.now(),
|
|
340
|
+
}
|
|
341
|
+
} finally {
|
|
342
|
+
secureWipe(privateBytes)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ─── Viewing Key Hash ─────────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Compute the viewing key hash from a public key
|
|
350
|
+
*
|
|
351
|
+
* The hash is used for announcement matching on-chain. Recipients publish
|
|
352
|
+
* their viewing key hash, and senders include it in transaction announcements.
|
|
353
|
+
*
|
|
354
|
+
* @param viewingPublicKey - The viewing public key (32 bytes, hex)
|
|
355
|
+
* @returns The hash for announcement matching
|
|
356
|
+
*
|
|
357
|
+
* @example
|
|
358
|
+
* ```typescript
|
|
359
|
+
* const hash = computeNEARViewingKeyHash(viewingKey.publicKey)
|
|
360
|
+
* // Use hash in transaction announcements
|
|
361
|
+
* ```
|
|
362
|
+
*/
|
|
363
|
+
export function computeNEARViewingKeyHash(viewingPublicKey: HexString): Hash {
|
|
364
|
+
if (!viewingPublicKey || !viewingPublicKey.startsWith('0x')) {
|
|
365
|
+
throw new ValidationError(
|
|
366
|
+
'viewingPublicKey must be a hex string with 0x prefix',
|
|
367
|
+
'viewingPublicKey'
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const publicBytes = hexToBytes(viewingPublicKey.slice(2))
|
|
372
|
+
|
|
373
|
+
if (publicBytes.length !== 32) {
|
|
374
|
+
throw new ValidationError(
|
|
375
|
+
'viewingPublicKey must be 32 bytes',
|
|
376
|
+
'viewingPublicKey'
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const hashBytes = sha256(publicBytes)
|
|
381
|
+
return `0x${bytesToHex(hashBytes)}` as Hash
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Compute viewing key hash from a private key
|
|
386
|
+
*
|
|
387
|
+
* Derives the public key and computes its hash.
|
|
388
|
+
*
|
|
389
|
+
* @param viewingPrivateKey - The viewing private key (32 bytes, hex)
|
|
390
|
+
* @returns The hash for announcement matching
|
|
391
|
+
*/
|
|
392
|
+
export function computeNEARViewingKeyHashFromPrivate(
|
|
393
|
+
viewingPrivateKey: HexString
|
|
394
|
+
): Hash {
|
|
395
|
+
if (!viewingPrivateKey || !viewingPrivateKey.startsWith('0x')) {
|
|
396
|
+
throw new ValidationError(
|
|
397
|
+
'viewingPrivateKey must be a hex string with 0x prefix',
|
|
398
|
+
'viewingPrivateKey'
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const privateBytes = hexToBytes(viewingPrivateKey.slice(2))
|
|
403
|
+
|
|
404
|
+
if (privateBytes.length !== 32) {
|
|
405
|
+
throw new ValidationError(
|
|
406
|
+
'viewingPrivateKey must be 32 bytes',
|
|
407
|
+
'viewingPrivateKey'
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
const publicBytes = ed25519.getPublicKey(privateBytes)
|
|
413
|
+
const hashBytes = sha256(publicBytes)
|
|
414
|
+
return `0x${bytesToHex(hashBytes)}` as Hash
|
|
415
|
+
} finally {
|
|
416
|
+
secureWipe(privateBytes)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ─── Export/Import ────────────────────────────────────────────────────────────
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Export a viewing key in standard JSON format
|
|
424
|
+
*
|
|
425
|
+
* The export format includes version information for forward compatibility
|
|
426
|
+
* and can be safely serialized to JSON.
|
|
427
|
+
*
|
|
428
|
+
* @param viewingKey - The viewing key to export
|
|
429
|
+
* @returns The export object (serialize with JSON.stringify)
|
|
430
|
+
*
|
|
431
|
+
* @example
|
|
432
|
+
* ```typescript
|
|
433
|
+
* const exported = exportNEARViewingKey(viewingKey)
|
|
434
|
+
* const json = JSON.stringify(exported)
|
|
435
|
+
*
|
|
436
|
+
* // Save to file or send to auditor
|
|
437
|
+
* ```
|
|
438
|
+
*/
|
|
439
|
+
export function exportNEARViewingKey(viewingKey: NEARViewingKey): NEARViewingKeyExport {
|
|
440
|
+
return {
|
|
441
|
+
version: EXPORT_VERSION,
|
|
442
|
+
chain: 'near',
|
|
443
|
+
privateKey: viewingKey.privateKey,
|
|
444
|
+
publicKey: viewingKey.publicKey,
|
|
445
|
+
hash: viewingKey.hash,
|
|
446
|
+
label: viewingKey.label,
|
|
447
|
+
createdAt: viewingKey.createdAt,
|
|
448
|
+
exportedAt: Date.now(),
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Import a viewing key from standard JSON format
|
|
454
|
+
*
|
|
455
|
+
* Validates the export format and reconstructs the viewing key object.
|
|
456
|
+
*
|
|
457
|
+
* @param exported - The exported viewing key data
|
|
458
|
+
* @returns The imported viewing key
|
|
459
|
+
* @throws {ValidationError} If the export format is invalid
|
|
460
|
+
*
|
|
461
|
+
* @example
|
|
462
|
+
* ```typescript
|
|
463
|
+
* const json = await readFile('near-viewing-key.json')
|
|
464
|
+
* const exported = JSON.parse(json)
|
|
465
|
+
* const viewingKey = importNEARViewingKey(exported)
|
|
466
|
+
* ```
|
|
467
|
+
*/
|
|
468
|
+
export function importNEARViewingKey(exported: NEARViewingKeyExport): NEARViewingKey {
|
|
469
|
+
// Validate version
|
|
470
|
+
if (exported.version !== EXPORT_VERSION) {
|
|
471
|
+
throw new ValidationError(
|
|
472
|
+
`Unsupported export version: ${exported.version}. Expected: ${EXPORT_VERSION}`,
|
|
473
|
+
'version'
|
|
474
|
+
)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Validate chain
|
|
478
|
+
if (exported.chain !== 'near') {
|
|
479
|
+
throw new ValidationError(
|
|
480
|
+
`Invalid chain: ${exported.chain}. Expected: near`,
|
|
481
|
+
'chain'
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Validate keys
|
|
486
|
+
if (!isValidHex(exported.privateKey) || exported.privateKey.length !== 66) {
|
|
487
|
+
throw new ValidationError('Invalid private key format', 'privateKey')
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (!isValidHex(exported.publicKey) || exported.publicKey.length !== 66) {
|
|
491
|
+
throw new ValidationError('Invalid public key format', 'publicKey')
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (!isValidHex(exported.hash) || exported.hash.length !== 66) {
|
|
495
|
+
throw new ValidationError('Invalid hash format', 'hash')
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Verify the hash matches the public key
|
|
499
|
+
const computedHash = computeNEARViewingKeyHash(exported.publicKey)
|
|
500
|
+
if (computedHash !== exported.hash) {
|
|
501
|
+
throw new ValidationError(
|
|
502
|
+
'Hash does not match public key',
|
|
503
|
+
'hash',
|
|
504
|
+
{ expected: computedHash, received: exported.hash }
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Verify public key matches private key
|
|
509
|
+
const privateBytes = hexToBytes(exported.privateKey.slice(2))
|
|
510
|
+
try {
|
|
511
|
+
const derivedPublic = `0x${bytesToHex(ed25519.getPublicKey(privateBytes))}` as HexString
|
|
512
|
+
if (derivedPublic !== exported.publicKey) {
|
|
513
|
+
throw new ValidationError(
|
|
514
|
+
'Public key does not match private key',
|
|
515
|
+
'publicKey'
|
|
516
|
+
)
|
|
517
|
+
}
|
|
518
|
+
} finally {
|
|
519
|
+
secureWipe(privateBytes)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
privateKey: exported.privateKey,
|
|
524
|
+
publicKey: exported.publicKey,
|
|
525
|
+
hash: exported.hash,
|
|
526
|
+
label: exported.label,
|
|
527
|
+
createdAt: exported.createdAt,
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ─── Encryption/Decryption ────────────────────────────────────────────────────
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Derive an encryption key from a viewing key using HKDF
|
|
535
|
+
*
|
|
536
|
+
* @param key - The viewing key (private or public depending on operation)
|
|
537
|
+
* @param salt - Optional salt for HKDF
|
|
538
|
+
* @returns 32-byte encryption key (caller must wipe after use)
|
|
539
|
+
*/
|
|
540
|
+
function deriveEncryptionKey(key: HexString, salt?: Uint8Array): Uint8Array {
|
|
541
|
+
const keyBytes = hexToBytes(key.slice(2))
|
|
542
|
+
|
|
543
|
+
try {
|
|
544
|
+
// Use HKDF to derive a proper encryption key
|
|
545
|
+
const hkdfSalt = salt ?? utf8ToBytes(ENCRYPTION_DOMAIN)
|
|
546
|
+
return hkdf(sha256, keyBytes, hkdfSalt, utf8ToBytes('encryption'), 32)
|
|
547
|
+
} finally {
|
|
548
|
+
secureWipe(keyBytes)
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Encrypt transaction data for viewing key holders
|
|
554
|
+
*
|
|
555
|
+
* Uses XChaCha20-Poly1305 authenticated encryption with a random nonce.
|
|
556
|
+
* The encryption key is derived from the viewing private key using HKDF.
|
|
557
|
+
*
|
|
558
|
+
* @param data - Transaction data to encrypt
|
|
559
|
+
* @param viewingKey - The viewing key for encryption
|
|
560
|
+
* @returns Encrypted payload with nonce and key hash
|
|
561
|
+
*
|
|
562
|
+
* @example
|
|
563
|
+
* ```typescript
|
|
564
|
+
* const encrypted = encryptForNEARViewing({
|
|
565
|
+
* sender: 'alice.near',
|
|
566
|
+
* recipient: stealthAccountId,
|
|
567
|
+
* amount: '1000000000000000000000000', // 1 NEAR
|
|
568
|
+
* tokenContract: null, // native NEAR
|
|
569
|
+
* decimals: 24,
|
|
570
|
+
* timestamp: Date.now(),
|
|
571
|
+
* }, viewingKey)
|
|
572
|
+
*
|
|
573
|
+
* // Store encrypted.ciphertext on-chain or off-chain
|
|
574
|
+
* ```
|
|
575
|
+
*/
|
|
576
|
+
export function encryptForNEARViewing(
|
|
577
|
+
data: NEARTransactionData,
|
|
578
|
+
viewingKey: NEARViewingKey
|
|
579
|
+
): NEAREncryptedPayload {
|
|
580
|
+
// Derive encryption key from viewing private key
|
|
581
|
+
const encKey = deriveEncryptionKey(viewingKey.privateKey)
|
|
582
|
+
|
|
583
|
+
try {
|
|
584
|
+
// Generate random nonce
|
|
585
|
+
const nonce = randomBytes(NONCE_SIZE)
|
|
586
|
+
|
|
587
|
+
// Serialize data to JSON
|
|
588
|
+
const plaintext = utf8ToBytes(JSON.stringify(data))
|
|
589
|
+
|
|
590
|
+
// Encrypt with XChaCha20-Poly1305
|
|
591
|
+
const cipher = xchacha20poly1305(encKey, nonce)
|
|
592
|
+
const ciphertext = cipher.encrypt(plaintext)
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
ciphertext: `0x${bytesToHex(ciphertext)}` as HexString,
|
|
596
|
+
nonce: `0x${bytesToHex(nonce)}` as HexString,
|
|
597
|
+
viewingKeyHash: viewingKey.hash,
|
|
598
|
+
}
|
|
599
|
+
} finally {
|
|
600
|
+
secureWipe(encKey)
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Decrypt transaction data with a viewing key
|
|
606
|
+
*
|
|
607
|
+
* @param encrypted - The encrypted payload
|
|
608
|
+
* @param viewingKey - The viewing key for decryption
|
|
609
|
+
* @returns The decrypted transaction data
|
|
610
|
+
* @throws {CryptoError} If decryption fails (wrong key or tampered data)
|
|
611
|
+
*
|
|
612
|
+
* @example
|
|
613
|
+
* ```typescript
|
|
614
|
+
* const data = decryptWithNEARViewing(encrypted, viewingKey)
|
|
615
|
+
* console.log('Amount:', data.amount)
|
|
616
|
+
* console.log('Sender:', data.sender)
|
|
617
|
+
* ```
|
|
618
|
+
*/
|
|
619
|
+
export function decryptWithNEARViewing(
|
|
620
|
+
encrypted: NEAREncryptedPayload,
|
|
621
|
+
viewingKey: NEARViewingKey
|
|
622
|
+
): NEARTransactionData {
|
|
623
|
+
// Verify the viewing key can decrypt this
|
|
624
|
+
if (encrypted.viewingKeyHash !== viewingKey.hash) {
|
|
625
|
+
throw new CryptoError(
|
|
626
|
+
'Viewing key hash does not match encrypted payload',
|
|
627
|
+
ErrorCode.DECRYPTION_FAILED,
|
|
628
|
+
{ context: { expected: encrypted.viewingKeyHash, received: viewingKey.hash } }
|
|
629
|
+
)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Derive encryption key
|
|
633
|
+
const encKey = deriveEncryptionKey(viewingKey.privateKey)
|
|
634
|
+
|
|
635
|
+
try {
|
|
636
|
+
// Parse ciphertext and nonce
|
|
637
|
+
const ciphertext = hexToBytes(encrypted.ciphertext.slice(2))
|
|
638
|
+
const nonce = hexToBytes(encrypted.nonce.slice(2))
|
|
639
|
+
|
|
640
|
+
if (nonce.length !== NONCE_SIZE) {
|
|
641
|
+
throw new ValidationError(
|
|
642
|
+
`Invalid nonce length: ${nonce.length}. Expected: ${NONCE_SIZE}`,
|
|
643
|
+
'nonce'
|
|
644
|
+
)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Decrypt with XChaCha20-Poly1305
|
|
648
|
+
const cipher = xchacha20poly1305(encKey, nonce)
|
|
649
|
+
const plaintext = cipher.decrypt(ciphertext)
|
|
650
|
+
|
|
651
|
+
// Parse JSON
|
|
652
|
+
const json = new TextDecoder().decode(plaintext)
|
|
653
|
+
return JSON.parse(json) as NEARTransactionData
|
|
654
|
+
} catch (error) {
|
|
655
|
+
if (error instanceof ValidationError || error instanceof CryptoError) {
|
|
656
|
+
throw error
|
|
657
|
+
}
|
|
658
|
+
throw new CryptoError(
|
|
659
|
+
'Failed to decrypt: authentication failed or data corrupted',
|
|
660
|
+
ErrorCode.DECRYPTION_FAILED,
|
|
661
|
+
{ cause: error instanceof Error ? error : undefined }
|
|
662
|
+
)
|
|
663
|
+
} finally {
|
|
664
|
+
secureWipe(encKey)
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// ─── In-Memory Storage ────────────────────────────────────────────────────────
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Simple in-memory viewing key storage
|
|
672
|
+
*
|
|
673
|
+
* Useful for testing and temporary storage. For production use,
|
|
674
|
+
* implement a persistent storage provider.
|
|
675
|
+
*
|
|
676
|
+
* @example
|
|
677
|
+
* ```typescript
|
|
678
|
+
* const storage = createNEARMemoryStorage()
|
|
679
|
+
*
|
|
680
|
+
* // Store a key
|
|
681
|
+
* await storage.save(viewingKey)
|
|
682
|
+
*
|
|
683
|
+
* // List all keys
|
|
684
|
+
* const keys = await storage.list()
|
|
685
|
+
*
|
|
686
|
+
* // Load a specific key
|
|
687
|
+
* const key = await storage.load(hash)
|
|
688
|
+
* ```
|
|
689
|
+
*/
|
|
690
|
+
export function createNEARMemoryStorage(): NEARViewingKeyStorage {
|
|
691
|
+
const keys = new Map<Hash, NEARViewingKey>()
|
|
692
|
+
|
|
693
|
+
return {
|
|
694
|
+
async save(key: NEARViewingKey): Promise<Hash> {
|
|
695
|
+
keys.set(key.hash, { ...key })
|
|
696
|
+
return key.hash
|
|
697
|
+
},
|
|
698
|
+
|
|
699
|
+
async load(hash: Hash): Promise<NEARViewingKey | null> {
|
|
700
|
+
const key = keys.get(hash)
|
|
701
|
+
return key ? { ...key } : null
|
|
702
|
+
},
|
|
703
|
+
|
|
704
|
+
async list(): Promise<NEARViewingKey[]> {
|
|
705
|
+
return Array.from(keys.values()).map(k => ({ ...k }))
|
|
706
|
+
},
|
|
707
|
+
|
|
708
|
+
async delete(hash: Hash): Promise<boolean> {
|
|
709
|
+
return keys.delete(hash)
|
|
710
|
+
},
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// ─── Utilities ────────────────────────────────────────────────────────────────
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Check if an announcement hash matches a viewing key
|
|
718
|
+
*
|
|
719
|
+
* Used during scanning to efficiently filter announcements that belong
|
|
720
|
+
* to this viewing key.
|
|
721
|
+
*
|
|
722
|
+
* @param announcementHash - Hash from the on-chain announcement
|
|
723
|
+
* @param viewingKey - The viewing key to check against
|
|
724
|
+
* @returns true if the announcement is for this viewing key
|
|
725
|
+
*/
|
|
726
|
+
export function isNEARAnnouncementForViewingKey(
|
|
727
|
+
announcementHash: Hash,
|
|
728
|
+
viewingKey: NEARViewingKey
|
|
729
|
+
): boolean {
|
|
730
|
+
return announcementHash === viewingKey.hash
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Derive a child viewing key for hierarchical key management
|
|
735
|
+
*
|
|
736
|
+
* Uses HMAC-SHA256 with the parent key and child path to derive
|
|
737
|
+
* a new independent viewing key.
|
|
738
|
+
*
|
|
739
|
+
* @param parentKey - The parent viewing key
|
|
740
|
+
* @param childPath - A path string for derivation (e.g., "audit/2024")
|
|
741
|
+
* @param label - Optional label for the child key
|
|
742
|
+
* @returns The derived child viewing key
|
|
743
|
+
*
|
|
744
|
+
* @example
|
|
745
|
+
* ```typescript
|
|
746
|
+
* const auditKey = deriveNEARChildViewingKey(masterKey, 'audit/2024', 'Audit 2024')
|
|
747
|
+
* const accountingKey = deriveNEARChildViewingKey(masterKey, 'accounting', 'Accounting')
|
|
748
|
+
* ```
|
|
749
|
+
*/
|
|
750
|
+
export function deriveNEARChildViewingKey(
|
|
751
|
+
parentKey: NEARViewingKey,
|
|
752
|
+
childPath: string,
|
|
753
|
+
label?: string
|
|
754
|
+
): NEARViewingKey {
|
|
755
|
+
if (!childPath || typeof childPath !== 'string') {
|
|
756
|
+
throw new ValidationError('childPath must be a non-empty string', 'childPath')
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const parentBytes = hexToBytes(parentKey.privateKey.slice(2))
|
|
760
|
+
|
|
761
|
+
try {
|
|
762
|
+
// Derive child key using HMAC-SHA256
|
|
763
|
+
const childBytes = hmac(sha256, utf8ToBytes(childPath), parentBytes)
|
|
764
|
+
const publicBytes = ed25519.getPublicKey(childBytes)
|
|
765
|
+
const hashBytes = sha256(publicBytes)
|
|
766
|
+
|
|
767
|
+
const result: NEARViewingKey = {
|
|
768
|
+
privateKey: `0x${bytesToHex(childBytes)}` as HexString,
|
|
769
|
+
publicKey: `0x${bytesToHex(publicBytes)}` as HexString,
|
|
770
|
+
hash: `0x${bytesToHex(hashBytes)}` as Hash,
|
|
771
|
+
label: label ?? `${parentKey.label ?? 'Key'}/${childPath}`,
|
|
772
|
+
createdAt: Date.now(),
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Wipe child bytes after creating hex
|
|
776
|
+
secureWipe(childBytes)
|
|
777
|
+
|
|
778
|
+
return result
|
|
779
|
+
} finally {
|
|
780
|
+
secureWipe(parentBytes)
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Get the public key from a viewing private key
|
|
786
|
+
*
|
|
787
|
+
* @param viewingPrivateKey - The viewing private key
|
|
788
|
+
* @returns The corresponding public key
|
|
789
|
+
*/
|
|
790
|
+
export function getNEARViewingPublicKey(viewingPrivateKey: HexString): HexString {
|
|
791
|
+
if (!viewingPrivateKey || !viewingPrivateKey.startsWith('0x')) {
|
|
792
|
+
throw new ValidationError(
|
|
793
|
+
'viewingPrivateKey must be a hex string with 0x prefix',
|
|
794
|
+
'viewingPrivateKey'
|
|
795
|
+
)
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const privateBytes = hexToBytes(viewingPrivateKey.slice(2))
|
|
799
|
+
|
|
800
|
+
if (privateBytes.length !== 32) {
|
|
801
|
+
throw new ValidationError(
|
|
802
|
+
'viewingPrivateKey must be 32 bytes',
|
|
803
|
+
'viewingPrivateKey'
|
|
804
|
+
)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
try {
|
|
808
|
+
const publicBytes = ed25519.getPublicKey(privateBytes)
|
|
809
|
+
return `0x${bytesToHex(publicBytes)}` as HexString
|
|
810
|
+
} finally {
|
|
811
|
+
secureWipe(privateBytes)
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Validate a NEAR viewing key object
|
|
817
|
+
*
|
|
818
|
+
* @param viewingKey - The viewing key to validate
|
|
819
|
+
* @returns true if the viewing key is valid
|
|
820
|
+
* @throws {ValidationError} If the viewing key is invalid
|
|
821
|
+
*/
|
|
822
|
+
export function validateNEARViewingKey(viewingKey: NEARViewingKey): boolean {
|
|
823
|
+
if (!viewingKey) {
|
|
824
|
+
throw new ValidationError('viewingKey is required', 'viewingKey')
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Check privateKey
|
|
828
|
+
if (!viewingKey.privateKey || !viewingKey.privateKey.startsWith('0x')) {
|
|
829
|
+
throw new ValidationError('privateKey must be a hex string with 0x prefix', 'privateKey')
|
|
830
|
+
}
|
|
831
|
+
if (viewingKey.privateKey.length !== 66) {
|
|
832
|
+
throw new ValidationError('privateKey must be 32 bytes (66 chars with 0x)', 'privateKey')
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Check publicKey
|
|
836
|
+
if (!viewingKey.publicKey || !viewingKey.publicKey.startsWith('0x')) {
|
|
837
|
+
throw new ValidationError('publicKey must be a hex string with 0x prefix', 'publicKey')
|
|
838
|
+
}
|
|
839
|
+
if (viewingKey.publicKey.length !== 66) {
|
|
840
|
+
throw new ValidationError('publicKey must be 32 bytes (66 chars with 0x)', 'publicKey')
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Check hash
|
|
844
|
+
if (!viewingKey.hash || !viewingKey.hash.startsWith('0x')) {
|
|
845
|
+
throw new ValidationError('hash must be a hex string with 0x prefix', 'hash')
|
|
846
|
+
}
|
|
847
|
+
if (viewingKey.hash.length !== 66) {
|
|
848
|
+
throw new ValidationError('hash must be 32 bytes (66 chars with 0x)', 'hash')
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Verify hash matches public key
|
|
852
|
+
const computedHash = computeNEARViewingKeyHash(viewingKey.publicKey)
|
|
853
|
+
if (computedHash !== viewingKey.hash) {
|
|
854
|
+
throw new ValidationError(
|
|
855
|
+
'hash does not match publicKey',
|
|
856
|
+
'hash',
|
|
857
|
+
{ expected: computedHash, received: viewingKey.hash }
|
|
858
|
+
)
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Verify public key matches private key
|
|
862
|
+
const privateBytes = hexToBytes(viewingKey.privateKey.slice(2))
|
|
863
|
+
try {
|
|
864
|
+
const derivedPublic = `0x${bytesToHex(ed25519.getPublicKey(privateBytes))}` as HexString
|
|
865
|
+
if (derivedPublic !== viewingKey.publicKey) {
|
|
866
|
+
throw new ValidationError(
|
|
867
|
+
'publicKey does not match privateKey',
|
|
868
|
+
'publicKey'
|
|
869
|
+
)
|
|
870
|
+
}
|
|
871
|
+
} finally {
|
|
872
|
+
secureWipe(privateBytes)
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
return true
|
|
876
|
+
}
|