@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.
Files changed (262) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +267 -0
  3. package/dist/{TransportWebUSB-TQ7WZ4LE.mjs → TransportWebUSB-YQMAGJAJ.mjs} +12 -9
  4. package/dist/browser.d.mts +10 -4
  5. package/dist/browser.d.ts +10 -4
  6. package/dist/browser.js +48874 -18336
  7. package/dist/browser.mjs +674 -48
  8. package/dist/chunk-4GRJ5MAW.mjs +152 -0
  9. package/dist/chunk-5D7A3L3W.mjs +717 -0
  10. package/dist/chunk-64AYA5F5.mjs +7834 -0
  11. package/dist/chunk-GMDGB22A.mjs +379 -0
  12. package/dist/chunk-I534WKN7.mjs +328 -0
  13. package/dist/chunk-IBZVA5Y7.mjs +1003 -0
  14. package/dist/chunk-PRRZAWJE.mjs +223 -0
  15. package/dist/{chunk-UJCSKKID.mjs → chunk-XGB3TDIC.mjs} +13 -1
  16. package/dist/chunk-YWGJ77A2.mjs +33806 -0
  17. package/dist/{chunk-6WGN57S2.mjs → chunk-Z3K7W5S3.mjs} +48 -0
  18. package/dist/constants-LHAAUC2T.mjs +51 -0
  19. package/dist/dist-2OGQ7FED.mjs +3957 -0
  20. package/dist/dist-IFHPYLDX.mjs +254 -0
  21. package/dist/fulfillment_proof-ANHVPKTB.mjs +21 -0
  22. package/dist/funding_proof-ICFZ5LHY.mjs +21 -0
  23. package/dist/index-DXh2IGkz.d.ts +24681 -0
  24. package/dist/index-DeE1ZzA4.d.mts +24681 -0
  25. package/dist/index.d.mts +9 -3
  26. package/dist/index.d.ts +9 -3
  27. package/dist/index.js +48676 -17318
  28. package/dist/index.mjs +583 -19
  29. package/dist/interface-Bf7w1PLW.d.mts +679 -0
  30. package/dist/interface-Bf7w1PLW.d.ts +679 -0
  31. package/dist/{noir-DKfEzWy9.d.mts → noir-kzbLVTei.d.mts} +31 -21
  32. package/dist/{noir-DKfEzWy9.d.ts → noir-kzbLVTei.d.ts} +31 -21
  33. package/dist/proofs/halo2.d.mts +151 -0
  34. package/dist/proofs/halo2.d.ts +151 -0
  35. package/dist/proofs/halo2.js +350 -0
  36. package/dist/proofs/halo2.mjs +11 -0
  37. package/dist/proofs/kimchi.d.mts +160 -0
  38. package/dist/proofs/kimchi.d.ts +160 -0
  39. package/dist/proofs/kimchi.js +431 -0
  40. package/dist/proofs/kimchi.mjs +13 -0
  41. package/dist/proofs/noir.d.mts +1 -1
  42. package/dist/proofs/noir.d.ts +1 -1
  43. package/dist/proofs/noir.js +74 -18
  44. package/dist/proofs/noir.mjs +84 -24
  45. package/dist/solana-U3MEGU7W.mjs +280 -0
  46. package/dist/validity_proof-3POXLPNY.mjs +21 -0
  47. package/package.json +54 -21
  48. package/src/adapters/index.ts +41 -0
  49. package/src/adapters/jupiter.ts +571 -0
  50. package/src/adapters/near-intents.ts +135 -0
  51. package/src/advisor/advisor.ts +653 -0
  52. package/src/advisor/index.ts +54 -0
  53. package/src/advisor/tools.ts +303 -0
  54. package/src/advisor/types.ts +164 -0
  55. package/src/chains/ethereum/announcement.ts +536 -0
  56. package/src/chains/ethereum/bnb-optimizations.ts +474 -0
  57. package/src/chains/ethereum/commitment.ts +522 -0
  58. package/src/chains/ethereum/constants.ts +462 -0
  59. package/src/chains/ethereum/deployment.ts +596 -0
  60. package/src/chains/ethereum/gas-estimation.ts +538 -0
  61. package/src/chains/ethereum/index.ts +268 -0
  62. package/src/chains/ethereum/optimizations.ts +614 -0
  63. package/src/chains/ethereum/privacy-adapter.ts +855 -0
  64. package/src/chains/ethereum/registry.ts +584 -0
  65. package/src/chains/ethereum/rpc.ts +905 -0
  66. package/src/chains/ethereum/stealth.ts +491 -0
  67. package/src/chains/ethereum/token.ts +790 -0
  68. package/src/chains/ethereum/transfer.ts +637 -0
  69. package/src/chains/ethereum/types.ts +456 -0
  70. package/src/chains/ethereum/viewing-key.ts +455 -0
  71. package/src/chains/near/commitment.ts +608 -0
  72. package/src/chains/near/constants.ts +284 -0
  73. package/src/chains/near/function-call.ts +871 -0
  74. package/src/chains/near/history.ts +654 -0
  75. package/src/chains/near/implicit-account.ts +840 -0
  76. package/src/chains/near/index.ts +393 -0
  77. package/src/chains/near/native-transfer.ts +658 -0
  78. package/src/chains/near/nep141.ts +775 -0
  79. package/src/chains/near/privacy-adapter.ts +889 -0
  80. package/src/chains/near/resolver.ts +971 -0
  81. package/src/chains/near/rpc.ts +1016 -0
  82. package/src/chains/near/stealth.ts +419 -0
  83. package/src/chains/near/types.ts +317 -0
  84. package/src/chains/near/viewing-key.ts +876 -0
  85. package/src/chains/solana/anchor-transfer.ts +386 -0
  86. package/src/chains/solana/commitment.ts +577 -0
  87. package/src/chains/solana/constants.ts +126 -12
  88. package/src/chains/solana/ephemeral-keys.ts +543 -0
  89. package/src/chains/solana/index.ts +276 -1
  90. package/src/chains/solana/key-derivation.ts +418 -0
  91. package/src/chains/solana/kit-compat.ts +334 -0
  92. package/src/chains/solana/optimizations.ts +560 -0
  93. package/src/chains/solana/privacy-adapter.ts +605 -0
  94. package/src/chains/solana/providers/generic.ts +201 -0
  95. package/src/chains/solana/providers/helius-enhanced-types.ts +336 -0
  96. package/src/chains/solana/providers/helius-enhanced.ts +623 -0
  97. package/src/chains/solana/providers/helius.ts +402 -0
  98. package/src/chains/solana/providers/index.ts +85 -0
  99. package/src/chains/solana/providers/interface.ts +221 -0
  100. package/src/chains/solana/providers/quicknode.ts +409 -0
  101. package/src/chains/solana/providers/triton.ts +426 -0
  102. package/src/chains/solana/providers/webhook.ts +790 -0
  103. package/src/chains/solana/rpc-client.ts +1150 -0
  104. package/src/chains/solana/scan.ts +170 -73
  105. package/src/chains/solana/sol-transfer.ts +732 -0
  106. package/src/chains/solana/spl-transfer.ts +886 -0
  107. package/src/chains/solana/stealth-scanner.ts +703 -0
  108. package/src/chains/solana/sunspot-verifier.ts +453 -0
  109. package/src/chains/solana/transaction-builder.ts +755 -0
  110. package/src/chains/solana/transfer.ts +74 -5
  111. package/src/chains/solana/types.ts +77 -7
  112. package/src/chains/solana/utils.ts +110 -0
  113. package/src/chains/solana/viewing-key.ts +807 -0
  114. package/src/compliance/fireblocks.ts +921 -0
  115. package/src/compliance/index.ts +37 -0
  116. package/src/compliance/range-sas.ts +956 -0
  117. package/src/config/endpoints.ts +100 -0
  118. package/src/crypto.ts +11 -8
  119. package/src/errors.ts +82 -0
  120. package/src/evm/erc4337-relayer.ts +830 -0
  121. package/src/evm/index.ts +47 -0
  122. package/src/fees/calculator.ts +396 -0
  123. package/src/fees/index.ts +87 -0
  124. package/src/fees/near-contract.ts +429 -0
  125. package/src/fees/types.ts +268 -0
  126. package/src/index.ts +785 -1
  127. package/src/intent.ts +6 -3
  128. package/src/logger.ts +324 -0
  129. package/src/network/index.ts +80 -0
  130. package/src/network/proxy.ts +691 -0
  131. package/src/optimizations/index.ts +541 -0
  132. package/src/oracle/types.ts +1 -0
  133. package/src/privacy-backends/arcium-types.ts +727 -0
  134. package/src/privacy-backends/arcium.ts +719 -0
  135. package/src/privacy-backends/combined-privacy.ts +866 -0
  136. package/src/privacy-backends/cspl-token.ts +595 -0
  137. package/src/privacy-backends/cspl-types.ts +512 -0
  138. package/src/privacy-backends/cspl.ts +907 -0
  139. package/src/privacy-backends/health.ts +488 -0
  140. package/src/privacy-backends/inco-types.ts +323 -0
  141. package/src/privacy-backends/inco.ts +616 -0
  142. package/src/privacy-backends/index.ts +336 -0
  143. package/src/privacy-backends/interface.ts +906 -0
  144. package/src/privacy-backends/lru-cache.ts +343 -0
  145. package/src/privacy-backends/magicblock.ts +458 -0
  146. package/src/privacy-backends/mock.ts +258 -0
  147. package/src/privacy-backends/privacycash-types.ts +278 -0
  148. package/src/privacy-backends/privacycash.ts +456 -0
  149. package/src/privacy-backends/private-swap.ts +570 -0
  150. package/src/privacy-backends/rate-limiter.ts +683 -0
  151. package/src/privacy-backends/registry.ts +690 -0
  152. package/src/privacy-backends/router.ts +626 -0
  153. package/src/privacy-backends/shadowwire.ts +449 -0
  154. package/src/privacy-backends/sip-native.ts +256 -0
  155. package/src/privacy-logger.ts +191 -0
  156. package/src/production-safety.ts +373 -0
  157. package/src/proofs/aggregator.ts +1029 -0
  158. package/src/proofs/browser-composer.ts +1150 -0
  159. package/src/proofs/browser.ts +113 -25
  160. package/src/proofs/cache/index.ts +127 -0
  161. package/src/proofs/cache/interface.ts +545 -0
  162. package/src/proofs/cache/key-generator.ts +188 -0
  163. package/src/proofs/cache/lru-cache.ts +481 -0
  164. package/src/proofs/cache/multi-tier-cache.ts +575 -0
  165. package/src/proofs/cache/persistent-cache.ts +788 -0
  166. package/src/proofs/compliance-proof.ts +872 -0
  167. package/src/proofs/composer/base.ts +923 -0
  168. package/src/proofs/composer/index.ts +25 -0
  169. package/src/proofs/composer/interface.ts +518 -0
  170. package/src/proofs/composer/types.ts +383 -0
  171. package/src/proofs/converters/halo2.ts +452 -0
  172. package/src/proofs/converters/index.ts +208 -0
  173. package/src/proofs/converters/interface.ts +363 -0
  174. package/src/proofs/converters/kimchi.ts +462 -0
  175. package/src/proofs/converters/noir.ts +451 -0
  176. package/src/proofs/fallback.ts +888 -0
  177. package/src/proofs/halo2.ts +42 -0
  178. package/src/proofs/index.ts +471 -0
  179. package/src/proofs/interface.ts +13 -0
  180. package/src/proofs/kimchi.ts +42 -0
  181. package/src/proofs/lazy.ts +1004 -0
  182. package/src/proofs/mock.ts +25 -1
  183. package/src/proofs/noir.ts +111 -30
  184. package/src/proofs/orchestrator.ts +960 -0
  185. package/src/proofs/parallel/concurrency.ts +297 -0
  186. package/src/proofs/parallel/dependency-graph.ts +602 -0
  187. package/src/proofs/parallel/executor.ts +420 -0
  188. package/src/proofs/parallel/index.ts +131 -0
  189. package/src/proofs/parallel/interface.ts +685 -0
  190. package/src/proofs/parallel/worker-pool.ts +644 -0
  191. package/src/proofs/providers/halo2.ts +560 -0
  192. package/src/proofs/providers/index.ts +34 -0
  193. package/src/proofs/providers/kimchi.ts +641 -0
  194. package/src/proofs/validator.ts +881 -0
  195. package/src/proofs/verifier.ts +867 -0
  196. package/src/quantum/index.ts +112 -0
  197. package/src/quantum/winternitz-vault.ts +639 -0
  198. package/src/quantum/wots.ts +611 -0
  199. package/src/settlement/backends/direct-chain.ts +1 -0
  200. package/src/settlement/index.ts +9 -0
  201. package/src/settlement/router.ts +732 -46
  202. package/src/solana/index.ts +72 -0
  203. package/src/solana/jito-relayer.ts +687 -0
  204. package/src/solana/noir-verifier-types.ts +430 -0
  205. package/src/solana/noir-verifier.ts +816 -0
  206. package/src/stealth/address-derivation.ts +193 -0
  207. package/src/stealth/ed25519.ts +431 -0
  208. package/src/stealth/index.ts +233 -0
  209. package/src/stealth/meta-address.ts +221 -0
  210. package/src/stealth/secp256k1.ts +368 -0
  211. package/src/stealth/utils.ts +194 -0
  212. package/src/stealth.ts +50 -1504
  213. package/src/surveillance/algorithms/address-reuse.ts +143 -0
  214. package/src/surveillance/algorithms/cluster.ts +247 -0
  215. package/src/surveillance/algorithms/exchange.ts +295 -0
  216. package/src/surveillance/algorithms/temporal.ts +337 -0
  217. package/src/surveillance/analyzer.ts +442 -0
  218. package/src/surveillance/index.ts +64 -0
  219. package/src/surveillance/scoring.ts +372 -0
  220. package/src/surveillance/types.ts +264 -0
  221. package/src/sync/index.ts +106 -0
  222. package/src/sync/manager.ts +504 -0
  223. package/src/sync/mock-provider.ts +318 -0
  224. package/src/sync/oblivious.ts +625 -0
  225. package/src/tokens/index.ts +15 -0
  226. package/src/tokens/registry.ts +301 -0
  227. package/src/utils/deprecation.ts +94 -0
  228. package/src/utils/index.ts +9 -0
  229. package/src/wallet/ethereum/index.ts +68 -0
  230. package/src/wallet/ethereum/metamask-privacy.ts +420 -0
  231. package/src/wallet/ethereum/multi-wallet.ts +646 -0
  232. package/src/wallet/ethereum/privacy-adapter.ts +700 -0
  233. package/src/wallet/ethereum/types.ts +3 -1
  234. package/src/wallet/ethereum/walletconnect-adapter.ts +675 -0
  235. package/src/wallet/hardware/index.ts +10 -0
  236. package/src/wallet/hardware/ledger-privacy.ts +414 -0
  237. package/src/wallet/index.ts +71 -0
  238. package/src/wallet/near/adapter.ts +626 -0
  239. package/src/wallet/near/index.ts +86 -0
  240. package/src/wallet/near/meteor-wallet.ts +1153 -0
  241. package/src/wallet/near/my-near-wallet.ts +790 -0
  242. package/src/wallet/near/wallet-selector.ts +702 -0
  243. package/src/wallet/solana/adapter.ts +6 -4
  244. package/src/wallet/solana/index.ts +13 -0
  245. package/src/wallet/solana/privacy-adapter.ts +567 -0
  246. package/src/wallet/sui/types.ts +6 -4
  247. package/src/zcash/rpc-client.ts +13 -6
  248. package/dist/chunk-3INS3PR5.mjs +0 -884
  249. package/dist/chunk-3OVABDRH.mjs +0 -17096
  250. package/dist/chunk-DLDWZFYC.mjs +0 -1495
  251. package/dist/chunk-E6SZWREQ.mjs +0 -57
  252. package/dist/chunk-G33LB27A.mjs +0 -16166
  253. package/dist/chunk-HGU6HZRC.mjs +0 -231
  254. package/dist/chunk-L2K34JCU.mjs +0 -1496
  255. package/dist/chunk-SN4ZDTVW.mjs +0 -16166
  256. package/dist/constants-VOI7BSLK.mjs +0 -27
  257. package/dist/index-BYZbDjal.d.ts +0 -11390
  258. package/dist/index-CHB3KuOB.d.mts +0 -11859
  259. package/dist/index-CzWPI6Le.d.ts +0 -11859
  260. package/dist/index-xbWjohNq.d.mts +0 -11390
  261. package/dist/solana-5EMCTPTS.mjs +0 -46
  262. 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
+ }