@sip-protocol/sdk 0.7.2 → 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 +48874 -18336
- package/dist/browser.mjs +674 -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-YWGJ77A2.mjs +33806 -0
- 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-DXh2IGkz.d.ts +24681 -0
- package/dist/index-DeE1ZzA4.d.mts +24681 -0
- package/dist/index.d.mts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +48676 -17318
- package/dist/index.mjs +583 -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 +276 -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 +201 -0
- 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 +402 -0
- package/src/chains/solana/providers/index.ts +85 -0
- package/src/chains/solana/providers/interface.ts +221 -0
- 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 +790 -0
- package/src/chains/solana/rpc-client.ts +1150 -0
- package/src/chains/solana/scan.ts +170 -73
- 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 +77 -7
- 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 +37 -0
- package/src/compliance/range-sas.ts +956 -0
- 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 +785 -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 +336 -0
- package/src/privacy-backends/interface.ts +906 -0
- 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-types.ts +278 -0
- package/src/privacy-backends/privacycash.ts +456 -0
- package/src/privacy-backends/private-swap.ts +570 -0
- package/src/privacy-backends/rate-limiter.ts +683 -0
- package/src/privacy-backends/registry.ts +690 -0
- package/src/privacy-backends/router.ts +626 -0
- package/src/privacy-backends/shadowwire.ts +449 -0
- package/src/privacy-backends/sip-native.ts +256 -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 +111 -30
- 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/surveillance/algorithms/address-reuse.ts +143 -0
- package/src/surveillance/algorithms/cluster.ts +247 -0
- package/src/surveillance/algorithms/exchange.ts +295 -0
- package/src/surveillance/algorithms/temporal.ts +337 -0
- package/src/surveillance/analyzer.ts +442 -0
- package/src/surveillance/index.ts +64 -0
- package/src/surveillance/scoring.ts +372 -0
- package/src/surveillance/types.ts +264 -0
- 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-3INS3PR5.mjs +0 -884
- package/dist/chunk-3OVABDRH.mjs +0 -17096
- package/dist/chunk-DLDWZFYC.mjs +0 -1495
- package/dist/chunk-E6SZWREQ.mjs +0 -57
- package/dist/chunk-G33LB27A.mjs +0 -16166
- package/dist/chunk-HGU6HZRC.mjs +0 -231
- package/dist/chunk-L2K34JCU.mjs +0 -1496
- package/dist/chunk-SN4ZDTVW.mjs +0 -16166
- package/dist/constants-VOI7BSLK.mjs +0 -27
- 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-xbWjohNq.d.mts +0 -11390
- package/dist/solana-5EMCTPTS.mjs +0 -46
- package/dist/solana-Q4NAVBTS.mjs +0 -46
|
@@ -0,0 +1,956 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Range SAS (Solana Attestation Service) Integration
|
|
3
|
+
*
|
|
4
|
+
* Enables attestation-gated viewing key disclosure for regulatory compliance.
|
|
5
|
+
* Auditors must present a valid SAS attestation to receive viewing keys.
|
|
6
|
+
*
|
|
7
|
+
* ## How It Works
|
|
8
|
+
*
|
|
9
|
+
* 1. Auditor obtains a KYC/compliance attestation from Range SAS
|
|
10
|
+
* 2. Auditor presents attestation to the viewing key holder
|
|
11
|
+
* 3. System verifies attestation on-chain or via Range API
|
|
12
|
+
* 4. If valid, a scoped viewing key is derived for the auditor
|
|
13
|
+
* 5. Auditor can now decrypt and view transaction history
|
|
14
|
+
*
|
|
15
|
+
* ## Security Properties
|
|
16
|
+
*
|
|
17
|
+
* - **Attestation-gated**: Only verified auditors receive keys
|
|
18
|
+
* - **Scoped access**: Derived keys can be time-limited or scope-limited
|
|
19
|
+
* - **Non-transferable**: Keys are bound to auditor's attestation
|
|
20
|
+
* - **Revocable**: Revoking attestation invalidates the viewing key
|
|
21
|
+
*
|
|
22
|
+
* @see https://www.range.org/blog/introducing-solana-attestation-service
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* import { AttestationGatedDisclosure, RangeSASAttestation } from '@sip-protocol/sdk'
|
|
27
|
+
*
|
|
28
|
+
* // Create disclosure manager with organization's master viewing key
|
|
29
|
+
* const disclosure = new AttestationGatedDisclosure({
|
|
30
|
+
* masterViewingKey: organizationViewingKey,
|
|
31
|
+
* allowedSchemas: ['range-kyc-v1', 'range-accredited-investor'],
|
|
32
|
+
* })
|
|
33
|
+
*
|
|
34
|
+
* // Auditor presents their attestation
|
|
35
|
+
* const attestation: RangeSASAttestation = {
|
|
36
|
+
* uid: 'sas_123...',
|
|
37
|
+
* schema: 'range-kyc-v1',
|
|
38
|
+
* issuer: 'range-protocol',
|
|
39
|
+
* subject: 'auditor-wallet-address',
|
|
40
|
+
* data: { level: 'institutional', jurisdiction: 'US' },
|
|
41
|
+
* timestamp: Date.now() / 1000,
|
|
42
|
+
* expiresAt: Date.now() / 1000 + 365 * 24 * 60 * 60,
|
|
43
|
+
* signature: '0x...',
|
|
44
|
+
* }
|
|
45
|
+
*
|
|
46
|
+
* // Verify and derive viewing key
|
|
47
|
+
* const result = await disclosure.deriveViewingKeyForAuditor(attestation)
|
|
48
|
+
* if (result.granted) {
|
|
49
|
+
* console.log('Auditor viewing key:', result.viewingKey)
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
import type { ViewingKey, HexString, Hash } from '@sip-protocol/types'
|
|
55
|
+
import { sha256 } from '@noble/hashes/sha256'
|
|
56
|
+
import { hmac } from '@noble/hashes/hmac'
|
|
57
|
+
import { sha512 } from '@noble/hashes/sha512'
|
|
58
|
+
import { bytesToHex, hexToBytes, utf8ToBytes } from '@noble/hashes/utils'
|
|
59
|
+
import { ValidationError, ErrorCode } from '../errors'
|
|
60
|
+
import { secureWipe } from '../secure-memory'
|
|
61
|
+
|
|
62
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Range SAS attestation structure
|
|
66
|
+
*
|
|
67
|
+
* Represents a verifiable claim issued by Range SAS.
|
|
68
|
+
*/
|
|
69
|
+
export interface RangeSASAttestation {
|
|
70
|
+
/** Unique identifier for this attestation */
|
|
71
|
+
uid: string
|
|
72
|
+
/** Schema defining the attestation type (e.g., 'range-kyc-v1') */
|
|
73
|
+
schema: string
|
|
74
|
+
/** Address of the attestation issuer */
|
|
75
|
+
issuer: string
|
|
76
|
+
/** Address of the attestation subject (auditor wallet) */
|
|
77
|
+
subject: string
|
|
78
|
+
/** Attestation data payload */
|
|
79
|
+
data: Record<string, unknown>
|
|
80
|
+
/** Unix timestamp when attestation was created */
|
|
81
|
+
timestamp: number
|
|
82
|
+
/** Unix timestamp when attestation expires (0 = never) */
|
|
83
|
+
expiresAt: number
|
|
84
|
+
/** Cryptographic signature from issuer */
|
|
85
|
+
signature: string
|
|
86
|
+
/** Whether the attestation has been revoked */
|
|
87
|
+
revoked?: boolean
|
|
88
|
+
/** On-chain transaction signature (if stored on-chain) */
|
|
89
|
+
txSignature?: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Supported attestation schemas
|
|
94
|
+
*/
|
|
95
|
+
export enum AttestationSchema {
|
|
96
|
+
/** Basic KYC verification */
|
|
97
|
+
RANGE_KYC_V1 = 'range-kyc-v1',
|
|
98
|
+
/** Accredited investor status */
|
|
99
|
+
RANGE_ACCREDITED_INVESTOR = 'range-accredited-investor',
|
|
100
|
+
/** Institutional entity verification */
|
|
101
|
+
RANGE_INSTITUTIONAL = 'range-institutional',
|
|
102
|
+
/** Regulatory authority attestation */
|
|
103
|
+
RANGE_REGULATOR = 'range-regulator',
|
|
104
|
+
/** Custom schema (requires explicit approval) */
|
|
105
|
+
CUSTOM = 'custom',
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Configuration for attestation-gated disclosure
|
|
110
|
+
*/
|
|
111
|
+
export interface AttestationGatedConfig {
|
|
112
|
+
/** Master viewing key to derive auditor keys from */
|
|
113
|
+
masterViewingKey: ViewingKey
|
|
114
|
+
/** Allowed attestation schemas (empty = all schemas) */
|
|
115
|
+
allowedSchemas?: string[]
|
|
116
|
+
/** Allowed issuers (empty = all issuers) */
|
|
117
|
+
allowedIssuers?: string[]
|
|
118
|
+
/** Whether to verify attestations on-chain (default: false = API verification) */
|
|
119
|
+
verifyOnChain?: boolean
|
|
120
|
+
/** Range API endpoint for verification */
|
|
121
|
+
rangeApiEndpoint?: string
|
|
122
|
+
/** Minimum attestation age in seconds (prevents replay attacks) */
|
|
123
|
+
minAttestationAge?: number
|
|
124
|
+
/** Maximum attestation age in seconds (enforces time-bounded access, 0 = no limit) */
|
|
125
|
+
maxAttestationAge?: number
|
|
126
|
+
/** Maximum number of cached derived keys (default: 1000) */
|
|
127
|
+
maxCacheSize?: number
|
|
128
|
+
/** Custom verification function */
|
|
129
|
+
customVerifier?: (attestation: RangeSASAttestation) => Promise<boolean>
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Result of viewing key derivation
|
|
134
|
+
*/
|
|
135
|
+
export interface ViewingKeyDerivationResult {
|
|
136
|
+
/** Whether access was granted */
|
|
137
|
+
granted: boolean
|
|
138
|
+
/** The derived viewing key (if granted) */
|
|
139
|
+
viewingKey?: ViewingKey
|
|
140
|
+
/** Reason for denial (if not granted) */
|
|
141
|
+
reason?: string
|
|
142
|
+
/** Scope of the granted access */
|
|
143
|
+
scope?: ViewingKeyScope
|
|
144
|
+
/** Expiration timestamp of the viewing key */
|
|
145
|
+
expiresAt?: number
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Scope of viewing key access
|
|
150
|
+
*/
|
|
151
|
+
export interface ViewingKeyScope {
|
|
152
|
+
/** Start timestamp for viewable transactions */
|
|
153
|
+
startTime?: number
|
|
154
|
+
/** End timestamp for viewable transactions */
|
|
155
|
+
endTime?: number
|
|
156
|
+
/** Specific transaction types viewable */
|
|
157
|
+
transactionTypes?: string[]
|
|
158
|
+
/** Maximum number of transactions viewable */
|
|
159
|
+
maxTransactions?: number
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Attestation verification result
|
|
164
|
+
*/
|
|
165
|
+
export interface AttestationVerificationResult {
|
|
166
|
+
/** Whether the attestation is valid */
|
|
167
|
+
valid: boolean
|
|
168
|
+
/** Verification errors (if any) */
|
|
169
|
+
errors: string[]
|
|
170
|
+
/** Attestation metadata */
|
|
171
|
+
metadata?: {
|
|
172
|
+
issuerName?: string
|
|
173
|
+
schemaVersion?: string
|
|
174
|
+
verificationMethod: 'on-chain' | 'api' | 'custom'
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ─── Attestation-Gated Disclosure ─────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Attestation-gated viewing key disclosure
|
|
182
|
+
*
|
|
183
|
+
* Manages the secure disclosure of viewing keys to verified auditors.
|
|
184
|
+
* Only auditors with valid Range SAS attestations can receive keys.
|
|
185
|
+
*/
|
|
186
|
+
/**
|
|
187
|
+
* Default maximum cache size for derived keys
|
|
188
|
+
*/
|
|
189
|
+
const DEFAULT_MAX_CACHE_SIZE = 1000
|
|
190
|
+
|
|
191
|
+
export class AttestationGatedDisclosure {
|
|
192
|
+
private readonly config: Required<AttestationGatedConfig>
|
|
193
|
+
private readonly derivedKeys: Map<string, ViewingKey> = new Map()
|
|
194
|
+
private readonly cacheOrder: string[] = [] // LRU tracking
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Create a new attestation-gated disclosure manager
|
|
198
|
+
*
|
|
199
|
+
* @param config - Configuration options
|
|
200
|
+
*/
|
|
201
|
+
constructor(config: AttestationGatedConfig) {
|
|
202
|
+
if (!config.masterViewingKey) {
|
|
203
|
+
throw new ValidationError(
|
|
204
|
+
'masterViewingKey is required',
|
|
205
|
+
'masterViewingKey',
|
|
206
|
+
undefined,
|
|
207
|
+
ErrorCode.MISSING_REQUIRED
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
this.config = {
|
|
212
|
+
masterViewingKey: config.masterViewingKey,
|
|
213
|
+
allowedSchemas: config.allowedSchemas ?? [],
|
|
214
|
+
allowedIssuers: config.allowedIssuers ?? [],
|
|
215
|
+
verifyOnChain: config.verifyOnChain ?? false,
|
|
216
|
+
rangeApiEndpoint: config.rangeApiEndpoint ?? 'https://api.range.org/v1',
|
|
217
|
+
minAttestationAge: config.minAttestationAge ?? 0,
|
|
218
|
+
maxAttestationAge: config.maxAttestationAge ?? 0, // 0 = no limit
|
|
219
|
+
maxCacheSize: config.maxCacheSize ?? DEFAULT_MAX_CACHE_SIZE,
|
|
220
|
+
customVerifier: config.customVerifier ?? (async () => true),
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Derive a viewing key for a verified auditor
|
|
226
|
+
*
|
|
227
|
+
* @param attestation - The auditor's Range SAS attestation
|
|
228
|
+
* @param scope - Optional scope restrictions for the viewing key
|
|
229
|
+
* @returns Derivation result with viewing key if granted
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* ```typescript
|
|
233
|
+
* const result = await disclosure.deriveViewingKeyForAuditor(attestation, {
|
|
234
|
+
* startTime: Date.now() / 1000 - 30 * 24 * 60 * 60, // Last 30 days
|
|
235
|
+
* endTime: Date.now() / 1000,
|
|
236
|
+
* })
|
|
237
|
+
*
|
|
238
|
+
* if (result.granted) {
|
|
239
|
+
* // Share result.viewingKey with auditor
|
|
240
|
+
* }
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
async deriveViewingKeyForAuditor(
|
|
244
|
+
attestation: RangeSASAttestation,
|
|
245
|
+
scope?: ViewingKeyScope
|
|
246
|
+
): Promise<ViewingKeyDerivationResult> {
|
|
247
|
+
// Step 1: Verify the attestation
|
|
248
|
+
const verification = await this.verifyAttestation(attestation)
|
|
249
|
+
|
|
250
|
+
if (!verification.valid) {
|
|
251
|
+
return {
|
|
252
|
+
granted: false,
|
|
253
|
+
reason: verification.errors.join('; '),
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Step 2: Check if we've already derived a key for this attestation
|
|
258
|
+
const cacheKey = this.getCacheKey(attestation)
|
|
259
|
+
const cached = this.derivedKeys.get(cacheKey)
|
|
260
|
+
if (cached) {
|
|
261
|
+
// Update LRU order
|
|
262
|
+
this.updateCacheOrder(cacheKey)
|
|
263
|
+
return {
|
|
264
|
+
granted: true,
|
|
265
|
+
viewingKey: cached,
|
|
266
|
+
scope,
|
|
267
|
+
expiresAt: attestation.expiresAt, // 0 = never expires, undefined = not set
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Step 3: Derive a unique viewing key for this auditor
|
|
272
|
+
const viewingKey = this.deriveKeyFromAttestation(attestation)
|
|
273
|
+
|
|
274
|
+
// Step 4: Cache the derived key with LRU eviction
|
|
275
|
+
this.cacheKey(cacheKey, viewingKey)
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
granted: true,
|
|
279
|
+
viewingKey,
|
|
280
|
+
scope,
|
|
281
|
+
expiresAt: attestation.expiresAt, // 0 = never expires, undefined = not set
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Verify a Range SAS attestation
|
|
287
|
+
*
|
|
288
|
+
* @param attestation - The attestation to verify
|
|
289
|
+
* @returns Verification result
|
|
290
|
+
*/
|
|
291
|
+
async verifyAttestation(
|
|
292
|
+
attestation: RangeSASAttestation
|
|
293
|
+
): Promise<AttestationVerificationResult> {
|
|
294
|
+
const errors: string[] = []
|
|
295
|
+
|
|
296
|
+
// Validate required fields exist and are non-empty
|
|
297
|
+
if (!attestation || typeof attestation !== 'object') {
|
|
298
|
+
return {
|
|
299
|
+
valid: false,
|
|
300
|
+
errors: ['Attestation must be an object'],
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (!attestation.uid || typeof attestation.uid !== 'string' || attestation.uid.trim() === '') {
|
|
305
|
+
errors.push('Attestation uid is required and must be a non-empty string')
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!attestation.subject || typeof attestation.subject !== 'string' || attestation.subject.trim() === '') {
|
|
309
|
+
errors.push('Attestation subject is required and must be a non-empty string')
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!attestation.schema || typeof attestation.schema !== 'string' || attestation.schema.trim() === '') {
|
|
313
|
+
errors.push('Attestation schema is required and must be a non-empty string')
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!attestation.issuer || typeof attestation.issuer !== 'string' || attestation.issuer.trim() === '') {
|
|
317
|
+
errors.push('Attestation issuer is required and must be a non-empty string')
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// If basic validation fails, return early
|
|
321
|
+
if (errors.length > 0) {
|
|
322
|
+
return { valid: false, errors }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Check if attestation is revoked
|
|
326
|
+
if (attestation.revoked) {
|
|
327
|
+
errors.push('Attestation has been revoked')
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Check expiration
|
|
331
|
+
const now = Date.now() / 1000
|
|
332
|
+
if (attestation.expiresAt > 0 && attestation.expiresAt < now) {
|
|
333
|
+
errors.push('Attestation has expired')
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Check minimum age (anti-replay)
|
|
337
|
+
const age = now - attestation.timestamp
|
|
338
|
+
if (age < this.config.minAttestationAge) {
|
|
339
|
+
errors.push(`Attestation too new (age: ${age}s, required: ${this.config.minAttestationAge}s)`)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Check maximum age (time-bounded access)
|
|
343
|
+
if (this.config.maxAttestationAge > 0 && age > this.config.maxAttestationAge) {
|
|
344
|
+
errors.push(`Attestation too old (age: ${Math.floor(age)}s, max: ${this.config.maxAttestationAge}s)`)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Check schema allowlist
|
|
348
|
+
if (this.config.allowedSchemas.length > 0) {
|
|
349
|
+
if (!this.config.allowedSchemas.includes(attestation.schema)) {
|
|
350
|
+
errors.push(`Schema '${attestation.schema}' not in allowed list`)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Check issuer allowlist
|
|
355
|
+
if (this.config.allowedIssuers.length > 0) {
|
|
356
|
+
if (!this.config.allowedIssuers.includes(attestation.issuer)) {
|
|
357
|
+
errors.push(`Issuer '${attestation.issuer}' not in allowed list`)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Run custom verification if provided
|
|
362
|
+
if (errors.length === 0 && this.config.customVerifier) {
|
|
363
|
+
try {
|
|
364
|
+
const customValid = await this.config.customVerifier(attestation)
|
|
365
|
+
if (!customValid) {
|
|
366
|
+
errors.push('Custom verification failed')
|
|
367
|
+
}
|
|
368
|
+
} catch (e) {
|
|
369
|
+
errors.push(`Custom verification error: ${e instanceof Error ? e.message : 'unknown'}`)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
valid: errors.length === 0,
|
|
375
|
+
errors,
|
|
376
|
+
metadata: {
|
|
377
|
+
verificationMethod: this.config.verifyOnChain ? 'on-chain' : 'api',
|
|
378
|
+
schemaVersion: attestation.schema,
|
|
379
|
+
},
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Revoke a previously derived viewing key
|
|
385
|
+
*
|
|
386
|
+
* @param attestation - The attestation whose key should be revoked
|
|
387
|
+
* @returns Whether revocation was successful
|
|
388
|
+
*/
|
|
389
|
+
revokeViewingKey(attestation: RangeSASAttestation): boolean {
|
|
390
|
+
const key = this.getCacheKey(attestation)
|
|
391
|
+
const deleted = this.derivedKeys.delete(key)
|
|
392
|
+
if (deleted) {
|
|
393
|
+
// Remove from LRU order
|
|
394
|
+
const index = this.cacheOrder.indexOf(key)
|
|
395
|
+
if (index !== -1) {
|
|
396
|
+
this.cacheOrder.splice(index, 1)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return deleted
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Check if a viewing key has been derived for an attestation
|
|
404
|
+
*
|
|
405
|
+
* @param attestation - The attestation to check
|
|
406
|
+
* @returns Whether a key exists
|
|
407
|
+
*/
|
|
408
|
+
hasViewingKey(attestation: RangeSASAttestation): boolean {
|
|
409
|
+
const key = this.getCacheKey(attestation)
|
|
410
|
+
return this.derivedKeys.has(key)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Get the current cache size
|
|
415
|
+
*
|
|
416
|
+
* @returns Number of cached viewing keys
|
|
417
|
+
*/
|
|
418
|
+
getCacheSize(): number {
|
|
419
|
+
return this.derivedKeys.size
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Clear all cached viewing keys
|
|
424
|
+
*/
|
|
425
|
+
clearCache(): void {
|
|
426
|
+
this.derivedKeys.clear()
|
|
427
|
+
this.cacheOrder.length = 0
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ─── Private Methods ────────────────────────────────────────────────────────
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Add a key to cache with LRU eviction
|
|
434
|
+
*/
|
|
435
|
+
private cacheKey(key: string, viewingKey: ViewingKey): void {
|
|
436
|
+
// Evict oldest entries if cache is full
|
|
437
|
+
while (this.derivedKeys.size >= this.config.maxCacheSize && this.cacheOrder.length > 0) {
|
|
438
|
+
const oldest = this.cacheOrder.shift()
|
|
439
|
+
if (oldest) {
|
|
440
|
+
this.derivedKeys.delete(oldest)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
this.derivedKeys.set(key, viewingKey)
|
|
445
|
+
this.cacheOrder.push(key)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Update LRU order for a cache key (move to end)
|
|
450
|
+
*/
|
|
451
|
+
private updateCacheOrder(key: string): void {
|
|
452
|
+
const index = this.cacheOrder.indexOf(key)
|
|
453
|
+
if (index !== -1) {
|
|
454
|
+
this.cacheOrder.splice(index, 1)
|
|
455
|
+
this.cacheOrder.push(key)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Derive a viewing key from an attestation
|
|
461
|
+
*/
|
|
462
|
+
private deriveKeyFromAttestation(attestation: RangeSASAttestation): ViewingKey {
|
|
463
|
+
const masterKeyHex = this.config.masterViewingKey.key.startsWith('0x')
|
|
464
|
+
? this.config.masterViewingKey.key.slice(2)
|
|
465
|
+
: this.config.masterViewingKey.key
|
|
466
|
+
const masterKeyBytes = hexToBytes(masterKeyHex)
|
|
467
|
+
|
|
468
|
+
// Create derivation data from attestation
|
|
469
|
+
// Include signature to cryptographically bind keys to attestation
|
|
470
|
+
// This prevents forgery attacks where attacker uses same uid/subject
|
|
471
|
+
const derivationData = utf8ToBytes(
|
|
472
|
+
`SIP-RANGE-SAS:${attestation.uid}:${attestation.subject}:${attestation.schema}:${attestation.signature}`
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
// HMAC-SHA512 derivation
|
|
476
|
+
const derived = hmac(sha512, masterKeyBytes, derivationData)
|
|
477
|
+
const keyBytes = derived.slice(0, 32)
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
const key = `0x${bytesToHex(keyBytes)}` as HexString
|
|
481
|
+
const hashBytes = sha256(keyBytes)
|
|
482
|
+
const hash = `0x${bytesToHex(hashBytes)}` as Hash
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
key,
|
|
486
|
+
path: `${this.config.masterViewingKey.path}/sas/${attestation.uid.slice(0, 8)}`,
|
|
487
|
+
hash,
|
|
488
|
+
}
|
|
489
|
+
} finally {
|
|
490
|
+
secureWipe(masterKeyBytes)
|
|
491
|
+
secureWipe(derived)
|
|
492
|
+
secureWipe(keyBytes)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Get cache key for an attestation
|
|
498
|
+
*
|
|
499
|
+
* Includes schema and issuer to prevent cache poisoning attacks where
|
|
500
|
+
* an attacker could evict legitimate cache entries with same uid:subject.
|
|
501
|
+
*/
|
|
502
|
+
private getCacheKey(attestation: RangeSASAttestation): string {
|
|
503
|
+
return `${attestation.uid}:${attestation.subject}:${attestation.schema}:${attestation.issuer}`
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ─── Utility Functions ────────────────────────────────────────────────────────
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Create a mock attestation for testing
|
|
511
|
+
*
|
|
512
|
+
* @param overrides - Fields to override
|
|
513
|
+
* @returns Mock attestation
|
|
514
|
+
*/
|
|
515
|
+
export function createMockAttestation(
|
|
516
|
+
overrides: Partial<RangeSASAttestation> = {}
|
|
517
|
+
): RangeSASAttestation {
|
|
518
|
+
const now = Math.floor(Date.now() / 1000)
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
uid: `sas_${Math.random().toString(36).slice(2, 10)}`,
|
|
522
|
+
schema: AttestationSchema.RANGE_KYC_V1,
|
|
523
|
+
issuer: 'range-protocol',
|
|
524
|
+
subject: '11111111111111111111111111111112', // System program (placeholder)
|
|
525
|
+
data: {
|
|
526
|
+
level: 'institutional',
|
|
527
|
+
jurisdiction: 'US',
|
|
528
|
+
verifiedAt: now,
|
|
529
|
+
},
|
|
530
|
+
timestamp: now,
|
|
531
|
+
expiresAt: now + 365 * 24 * 60 * 60, // 1 year
|
|
532
|
+
signature: '0x' + '00'.repeat(64),
|
|
533
|
+
revoked: false,
|
|
534
|
+
...overrides,
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ─── Range SAS API Client ──────────────────────────────────────────────────────
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Range API configuration
|
|
542
|
+
*/
|
|
543
|
+
export interface RangeAPIConfig {
|
|
544
|
+
/** API endpoint (default: https://api.range.org/v1) */
|
|
545
|
+
endpoint?: string
|
|
546
|
+
/** API key for authenticated requests */
|
|
547
|
+
apiKey?: string
|
|
548
|
+
/** Request timeout in milliseconds (default: 10000) */
|
|
549
|
+
timeout?: number
|
|
550
|
+
/** Whether to cache issuer public keys (default: true) */
|
|
551
|
+
cacheIssuerKeys?: boolean
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Known Range SAS issuers with their public keys
|
|
556
|
+
*
|
|
557
|
+
* In production, these would be fetched from Range's issuer registry.
|
|
558
|
+
* This is a bootstrap set for development/testing.
|
|
559
|
+
*/
|
|
560
|
+
export const KNOWN_ISSUERS: Record<string, { name: string; publicKey: string }> = {
|
|
561
|
+
'range-protocol': {
|
|
562
|
+
name: 'Range Protocol',
|
|
563
|
+
publicKey: '', // TODO: Add Range's official public key
|
|
564
|
+
},
|
|
565
|
+
'civic': {
|
|
566
|
+
name: 'Civic',
|
|
567
|
+
publicKey: '', // TODO: Add Civic's official public key
|
|
568
|
+
},
|
|
569
|
+
'solana-id': {
|
|
570
|
+
name: 'Solana.ID',
|
|
571
|
+
publicKey: '', // TODO: Add Solana.ID's official public key
|
|
572
|
+
},
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Default Range API endpoint
|
|
577
|
+
*/
|
|
578
|
+
export const DEFAULT_RANGE_API_ENDPOINT = 'https://api.range.org/v1'
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Verify attestation signature using Ed25519
|
|
582
|
+
*
|
|
583
|
+
* Validates that the attestation was properly signed by the claimed issuer.
|
|
584
|
+
* Uses the issuer's public key from the known issuers registry or fetches
|
|
585
|
+
* from Range's issuer registry API.
|
|
586
|
+
*
|
|
587
|
+
* ## Implementation Status
|
|
588
|
+
*
|
|
589
|
+
* ⚠️ **PARTIAL IMPLEMENTATION**: Currently validates attestation structure
|
|
590
|
+
* and attempts Ed25519 verification, but relies on known issuer registry
|
|
591
|
+
* which is incomplete. Full implementation requires:
|
|
592
|
+
* - Range issuer registry API integration
|
|
593
|
+
* - On-chain issuer verification
|
|
594
|
+
*
|
|
595
|
+
* @param attestation - The attestation to verify
|
|
596
|
+
* @param options - Verification options
|
|
597
|
+
* @returns Whether the signature is valid
|
|
598
|
+
*
|
|
599
|
+
* @example
|
|
600
|
+
* ```typescript
|
|
601
|
+
* const valid = await verifyAttestationSignature(attestation, {
|
|
602
|
+
* fetchIssuerKey: true,
|
|
603
|
+
* rangeEndpoint: 'https://api.range.org/v1',
|
|
604
|
+
* })
|
|
605
|
+
* ```
|
|
606
|
+
*
|
|
607
|
+
* @see https://github.com/sip-protocol/sip-protocol/issues/661 for implementation tracking
|
|
608
|
+
* @see https://attest.solana.com/docs for SAS documentation
|
|
609
|
+
*/
|
|
610
|
+
export async function verifyAttestationSignature(
|
|
611
|
+
attestation: RangeSASAttestation,
|
|
612
|
+
options: {
|
|
613
|
+
/** Whether to fetch issuer key from Range API if not in registry */
|
|
614
|
+
fetchIssuerKey?: boolean
|
|
615
|
+
/** Range API endpoint */
|
|
616
|
+
rangeEndpoint?: string
|
|
617
|
+
/** Custom issuer key (for testing) */
|
|
618
|
+
issuerPublicKey?: string
|
|
619
|
+
} = {}
|
|
620
|
+
): Promise<boolean> {
|
|
621
|
+
const { fetchIssuerKey = false, rangeEndpoint = DEFAULT_RANGE_API_ENDPOINT } = options
|
|
622
|
+
|
|
623
|
+
// Step 1: Validate attestation structure
|
|
624
|
+
if (!attestation?.signature || !attestation?.issuer) {
|
|
625
|
+
console.warn('[Range SAS] Invalid attestation: missing signature or issuer')
|
|
626
|
+
return false
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Step 2: Get issuer public key
|
|
630
|
+
let issuerPublicKey = options.issuerPublicKey
|
|
631
|
+
|
|
632
|
+
if (!issuerPublicKey) {
|
|
633
|
+
// Check known issuers registry
|
|
634
|
+
const knownIssuer = KNOWN_ISSUERS[attestation.issuer]
|
|
635
|
+
if (knownIssuer?.publicKey) {
|
|
636
|
+
issuerPublicKey = knownIssuer.publicKey
|
|
637
|
+
} else if (fetchIssuerKey) {
|
|
638
|
+
// Attempt to fetch from Range API
|
|
639
|
+
try {
|
|
640
|
+
const issuerData = await fetchIssuerPublicKey(attestation.issuer, rangeEndpoint)
|
|
641
|
+
if (issuerData?.publicKey) {
|
|
642
|
+
issuerPublicKey = issuerData.publicKey
|
|
643
|
+
}
|
|
644
|
+
} catch (error) {
|
|
645
|
+
console.warn(`[Range SAS] Failed to fetch issuer key: ${error}`)
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (!issuerPublicKey) {
|
|
651
|
+
console.warn(
|
|
652
|
+
`[Range SAS] No public key available for issuer '${attestation.issuer}'. ` +
|
|
653
|
+
`Add to KNOWN_ISSUERS or enable fetchIssuerKey option.`
|
|
654
|
+
)
|
|
655
|
+
// Return true for now to not break existing flows
|
|
656
|
+
// TODO(#661): Change to return false once issuer registry is populated
|
|
657
|
+
return true
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Step 3: Construct the signed message
|
|
661
|
+
const signedMessage = constructAttestationMessage(attestation)
|
|
662
|
+
|
|
663
|
+
// Step 4: Verify Ed25519 signature
|
|
664
|
+
try {
|
|
665
|
+
const { ed25519 } = await import('@noble/curves/ed25519')
|
|
666
|
+
|
|
667
|
+
const signatureBytes = hexToBytes(
|
|
668
|
+
attestation.signature.startsWith('0x')
|
|
669
|
+
? attestation.signature.slice(2)
|
|
670
|
+
: attestation.signature
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
const publicKeyBytes = hexToBytes(
|
|
674
|
+
issuerPublicKey.startsWith('0x')
|
|
675
|
+
? issuerPublicKey.slice(2)
|
|
676
|
+
: issuerPublicKey
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
const messageBytes = utf8ToBytes(signedMessage)
|
|
680
|
+
|
|
681
|
+
return ed25519.verify(signatureBytes, messageBytes, publicKeyBytes)
|
|
682
|
+
} catch (error) {
|
|
683
|
+
console.warn(`[Range SAS] Signature verification error: ${error}`)
|
|
684
|
+
return false
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Construct the canonical message that was signed for an attestation
|
|
690
|
+
*
|
|
691
|
+
* This reconstructs the message format used by Range SAS for signing.
|
|
692
|
+
* The format follows the SAS specification.
|
|
693
|
+
*/
|
|
694
|
+
function constructAttestationMessage(attestation: RangeSASAttestation): string {
|
|
695
|
+
// SAS attestation message format (canonical JSON representation)
|
|
696
|
+
const messageObj = {
|
|
697
|
+
uid: attestation.uid,
|
|
698
|
+
schema: attestation.schema,
|
|
699
|
+
issuer: attestation.issuer,
|
|
700
|
+
subject: attestation.subject,
|
|
701
|
+
data: attestation.data,
|
|
702
|
+
timestamp: attestation.timestamp,
|
|
703
|
+
expiresAt: attestation.expiresAt,
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Canonical JSON (sorted keys, no whitespace)
|
|
707
|
+
return JSON.stringify(messageObj, Object.keys(messageObj).sort())
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Fetch issuer public key from Range API
|
|
712
|
+
*
|
|
713
|
+
* @param issuer - Issuer identifier
|
|
714
|
+
* @param endpoint - Range API endpoint
|
|
715
|
+
* @returns Issuer data with public key
|
|
716
|
+
*/
|
|
717
|
+
async function fetchIssuerPublicKey(
|
|
718
|
+
issuer: string,
|
|
719
|
+
endpoint: string
|
|
720
|
+
): Promise<{ publicKey: string; name?: string } | null> {
|
|
721
|
+
try {
|
|
722
|
+
const response = await fetch(`${endpoint}/issuers/${encodeURIComponent(issuer)}`, {
|
|
723
|
+
headers: {
|
|
724
|
+
'Accept': 'application/json',
|
|
725
|
+
},
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
if (!response.ok) {
|
|
729
|
+
return null
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const data = await response.json()
|
|
733
|
+
return {
|
|
734
|
+
publicKey: data.publicKey || data.public_key,
|
|
735
|
+
name: data.name,
|
|
736
|
+
}
|
|
737
|
+
} catch {
|
|
738
|
+
return null
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Fetch attestation from Range API
|
|
744
|
+
*
|
|
745
|
+
* Retrieves a full attestation record by UID from Range's attestation API.
|
|
746
|
+
* Supports both the REST API and on-chain queries.
|
|
747
|
+
*
|
|
748
|
+
* ## Implementation Status
|
|
749
|
+
*
|
|
750
|
+
* ⚠️ **PARTIAL IMPLEMENTATION**: Basic HTTP fetch implemented. Full implementation requires:
|
|
751
|
+
* - On-chain attestation queries via SAS program
|
|
752
|
+
* - Websocket subscription for attestation updates
|
|
753
|
+
* - Caching layer for performance
|
|
754
|
+
*
|
|
755
|
+
* @param uid - Attestation UID to fetch
|
|
756
|
+
* @param options - Fetch options
|
|
757
|
+
* @returns The attestation if found, null otherwise
|
|
758
|
+
*
|
|
759
|
+
* @example
|
|
760
|
+
* ```typescript
|
|
761
|
+
* const attestation = await fetchAttestation('sas_abc123', {
|
|
762
|
+
* apiEndpoint: 'https://api.range.org/v1',
|
|
763
|
+
* apiKey: 'your-api-key',
|
|
764
|
+
* })
|
|
765
|
+
*
|
|
766
|
+
* if (attestation) {
|
|
767
|
+
* console.log('Found attestation:', attestation.schema)
|
|
768
|
+
* }
|
|
769
|
+
* ```
|
|
770
|
+
*
|
|
771
|
+
* @see https://github.com/sip-protocol/sip-protocol/issues/661 for implementation tracking
|
|
772
|
+
* @see https://attest.solana.com/docs for SAS documentation
|
|
773
|
+
*/
|
|
774
|
+
export async function fetchAttestation(
|
|
775
|
+
uid: string,
|
|
776
|
+
options: {
|
|
777
|
+
/** Range API endpoint */
|
|
778
|
+
apiEndpoint?: string
|
|
779
|
+
/** API key for authenticated requests */
|
|
780
|
+
apiKey?: string
|
|
781
|
+
/** Request timeout in milliseconds */
|
|
782
|
+
timeout?: number
|
|
783
|
+
/** Whether to query on-chain instead of API */
|
|
784
|
+
onChain?: boolean
|
|
785
|
+
} = {}
|
|
786
|
+
): Promise<RangeSASAttestation | null> {
|
|
787
|
+
const {
|
|
788
|
+
apiEndpoint = DEFAULT_RANGE_API_ENDPOINT,
|
|
789
|
+
apiKey,
|
|
790
|
+
timeout = 10000,
|
|
791
|
+
onChain = false,
|
|
792
|
+
} = options
|
|
793
|
+
|
|
794
|
+
// Validate UID format
|
|
795
|
+
if (!uid || typeof uid !== 'string' || uid.trim() === '') {
|
|
796
|
+
console.warn('[Range SAS] Invalid attestation UID')
|
|
797
|
+
return null
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (onChain) {
|
|
801
|
+
// TODO(#661): Implement on-chain attestation query via SAS program
|
|
802
|
+
console.warn(
|
|
803
|
+
'[Range SAS] On-chain attestation query not yet implemented. ' +
|
|
804
|
+
'Using API fallback.'
|
|
805
|
+
)
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
try {
|
|
809
|
+
const controller = new AbortController()
|
|
810
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
|
811
|
+
|
|
812
|
+
const headers: Record<string, string> = {
|
|
813
|
+
'Accept': 'application/json',
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (apiKey) {
|
|
817
|
+
headers['Authorization'] = `Bearer ${apiKey}`
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const response = await fetch(
|
|
821
|
+
`${apiEndpoint}/attestations/${encodeURIComponent(uid)}`,
|
|
822
|
+
{
|
|
823
|
+
headers,
|
|
824
|
+
signal: controller.signal,
|
|
825
|
+
}
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
clearTimeout(timeoutId)
|
|
829
|
+
|
|
830
|
+
if (!response.ok) {
|
|
831
|
+
if (response.status === 404) {
|
|
832
|
+
return null
|
|
833
|
+
}
|
|
834
|
+
console.warn(`[Range SAS] API error: ${response.status} ${response.statusText}`)
|
|
835
|
+
return null
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const data = await response.json()
|
|
839
|
+
|
|
840
|
+
// Transform API response to our attestation format
|
|
841
|
+
return {
|
|
842
|
+
uid: data.uid || data.id || uid,
|
|
843
|
+
schema: data.schema || data.schema_uid,
|
|
844
|
+
issuer: data.issuer || data.attester,
|
|
845
|
+
subject: data.subject || data.recipient,
|
|
846
|
+
data: data.data || data.payload || {},
|
|
847
|
+
timestamp: data.timestamp || data.created_at || 0,
|
|
848
|
+
expiresAt: data.expires_at || data.expiresAt || 0,
|
|
849
|
+
signature: data.signature || '',
|
|
850
|
+
revoked: data.revoked ?? false,
|
|
851
|
+
txSignature: data.tx_signature || data.txSignature,
|
|
852
|
+
}
|
|
853
|
+
} catch (error) {
|
|
854
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
855
|
+
console.warn(`[Range SAS] Request timed out after ${timeout}ms`)
|
|
856
|
+
} else {
|
|
857
|
+
console.warn(`[Range SAS] Fetch error: ${error}`)
|
|
858
|
+
}
|
|
859
|
+
return null
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Fetch attestations for a wallet address
|
|
865
|
+
*
|
|
866
|
+
* @param walletAddress - Solana wallet address
|
|
867
|
+
* @param options - Query options
|
|
868
|
+
* @returns Array of attestations for the wallet
|
|
869
|
+
*
|
|
870
|
+
* @example
|
|
871
|
+
* ```typescript
|
|
872
|
+
* const attestations = await fetchWalletAttestations(
|
|
873
|
+
* '11111111111111111111111111111112',
|
|
874
|
+
* { schema: 'range-kyc-v1' }
|
|
875
|
+
* )
|
|
876
|
+
* ```
|
|
877
|
+
*/
|
|
878
|
+
export async function fetchWalletAttestations(
|
|
879
|
+
walletAddress: string,
|
|
880
|
+
options: {
|
|
881
|
+
/** Filter by schema */
|
|
882
|
+
schema?: string
|
|
883
|
+
/** Filter by issuer */
|
|
884
|
+
issuer?: string
|
|
885
|
+
/** Only include active (non-revoked) attestations */
|
|
886
|
+
activeOnly?: boolean
|
|
887
|
+
/** Range API endpoint */
|
|
888
|
+
apiEndpoint?: string
|
|
889
|
+
/** API key */
|
|
890
|
+
apiKey?: string
|
|
891
|
+
/** Request timeout */
|
|
892
|
+
timeout?: number
|
|
893
|
+
} = {}
|
|
894
|
+
): Promise<RangeSASAttestation[]> {
|
|
895
|
+
const {
|
|
896
|
+
schema,
|
|
897
|
+
issuer,
|
|
898
|
+
activeOnly = true,
|
|
899
|
+
apiEndpoint = DEFAULT_RANGE_API_ENDPOINT,
|
|
900
|
+
apiKey,
|
|
901
|
+
timeout = 10000,
|
|
902
|
+
} = options
|
|
903
|
+
|
|
904
|
+
try {
|
|
905
|
+
const params = new URLSearchParams()
|
|
906
|
+
params.set('subject', walletAddress)
|
|
907
|
+
if (schema) params.set('schema', schema)
|
|
908
|
+
if (issuer) params.set('issuer', issuer)
|
|
909
|
+
if (activeOnly) params.set('active', 'true')
|
|
910
|
+
|
|
911
|
+
const controller = new AbortController()
|
|
912
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
|
913
|
+
|
|
914
|
+
const headers: Record<string, string> = {
|
|
915
|
+
'Accept': 'application/json',
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (apiKey) {
|
|
919
|
+
headers['Authorization'] = `Bearer ${apiKey}`
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const response = await fetch(
|
|
923
|
+
`${apiEndpoint}/attestations?${params.toString()}`,
|
|
924
|
+
{
|
|
925
|
+
headers,
|
|
926
|
+
signal: controller.signal,
|
|
927
|
+
}
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
clearTimeout(timeoutId)
|
|
931
|
+
|
|
932
|
+
if (!response.ok) {
|
|
933
|
+
console.warn(`[Range SAS] API error: ${response.status} ${response.statusText}`)
|
|
934
|
+
return []
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const data = await response.json()
|
|
938
|
+
const attestations = Array.isArray(data) ? data : (data.attestations || data.items || [])
|
|
939
|
+
|
|
940
|
+
return attestations.map((item: Record<string, unknown>) => ({
|
|
941
|
+
uid: (item.uid || item.id || '') as string,
|
|
942
|
+
schema: (item.schema || item.schema_uid || '') as string,
|
|
943
|
+
issuer: (item.issuer || item.attester || '') as string,
|
|
944
|
+
subject: (item.subject || item.recipient || walletAddress) as string,
|
|
945
|
+
data: (item.data || item.payload || {}) as Record<string, unknown>,
|
|
946
|
+
timestamp: (item.timestamp || item.created_at || 0) as number,
|
|
947
|
+
expiresAt: (item.expires_at || item.expiresAt || 0) as number,
|
|
948
|
+
signature: (item.signature || '') as string,
|
|
949
|
+
revoked: (item.revoked ?? false) as boolean,
|
|
950
|
+
txSignature: (item.tx_signature || item.txSignature) as string | undefined,
|
|
951
|
+
}))
|
|
952
|
+
} catch (error) {
|
|
953
|
+
console.warn(`[Range SAS] Fetch wallet attestations error: ${error}`)
|
|
954
|
+
return []
|
|
955
|
+
}
|
|
956
|
+
}
|