@sip-protocol/sdk 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/dist/browser.d.mts +4 -4
- package/dist/browser.d.ts +4 -4
- package/dist/browser.js +2752 -448
- package/dist/browser.mjs +31 -1
- package/dist/chunk-7QZPORY5.mjs +15604 -0
- package/dist/chunk-C2NPCUAJ.mjs +17010 -0
- package/dist/chunk-FCVLFUIC.mjs +16699 -0
- package/dist/chunk-G5UHXECN.mjs +16340 -0
- package/dist/chunk-GEDEIZHJ.mjs +16798 -0
- package/dist/chunk-GOOEOAMV.mjs +17026 -0
- package/dist/chunk-MTNYSNR7.mjs +16269 -0
- package/dist/chunk-O5PIB2EA.mjs +16698 -0
- package/dist/chunk-PCFM7FQO.mjs +17010 -0
- package/dist/chunk-QK464ARC.mjs +16946 -0
- package/dist/chunk-VNBMNGC3.mjs +16698 -0
- package/dist/chunk-W5TUELDQ.mjs +16947 -0
- package/dist/index-CD_zShu-.d.ts +10870 -0
- package/dist/index-CQBYdLYy.d.mts +10976 -0
- package/dist/index-Cg9TYEPv.d.mts +11321 -0
- package/dist/index-CqZJOO8C.d.mts +11323 -0
- package/dist/index-CywN9Bnp.d.ts +11321 -0
- package/dist/index-DHy5ZjCD.d.ts +10976 -0
- package/dist/index-DfsVsmxu.d.ts +11323 -0
- package/dist/index-ObjwyVDX.d.mts +10870 -0
- package/dist/index-m0xbSfmT.d.mts +11318 -0
- package/dist/index-rWLEgvhN.d.ts +11318 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2737 -427
- package/dist/index.mjs +31 -1
- package/dist/noir-DKfEzWy9.d.mts +482 -0
- package/dist/noir-DKfEzWy9.d.ts +482 -0
- package/dist/proofs/noir.d.mts +1 -1
- package/dist/proofs/noir.d.ts +1 -1
- package/dist/proofs/noir.js +12 -3
- package/dist/proofs/noir.mjs +12 -3
- package/package.json +16 -14
- package/src/adapters/near-intents.ts +13 -3
- package/src/auction/index.ts +20 -0
- package/src/auction/sealed-bid.ts +1037 -0
- package/src/compliance/derivation.ts +13 -3
- package/src/compliance/reports.ts +5 -4
- package/src/cosmos/ibc-stealth.ts +2 -2
- package/src/cosmos/stealth.ts +2 -2
- package/src/governance/index.ts +19 -0
- package/src/governance/private-vote.ts +1116 -0
- package/src/index.ts +50 -2
- package/src/intent.ts +145 -8
- package/src/nft/index.ts +27 -0
- package/src/nft/private-nft.ts +811 -0
- package/src/proofs/browser-utils.ts +1 -7
- package/src/proofs/noir.ts +34 -7
- package/src/settlement/backends/direct-chain.ts +14 -3
- package/src/stealth.ts +31 -13
- package/src/types/browser.d.ts +67 -0
- package/src/validation.ts +4 -2
- package/src/wallet/bitcoin/adapter.ts +159 -15
- package/src/wallet/bitcoin/types.ts +340 -15
- package/src/wallet/cosmos/mock.ts +16 -12
- package/src/wallet/hardware/ledger.ts +82 -12
- package/src/wallet/hardware/types.ts +2 -0
- package/LICENSE +0 -21
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Private NFT Ownership for SIP Protocol
|
|
3
|
+
*
|
|
4
|
+
* Implements privacy-preserving NFT ownership using stealth addresses
|
|
5
|
+
* and zero-knowledge proofs. Owners can prove they own an NFT without
|
|
6
|
+
* revealing their identity or linking ownership records.
|
|
7
|
+
*
|
|
8
|
+
* **Use Cases:**
|
|
9
|
+
* - Private NFT galleries (prove ownership without doxxing)
|
|
10
|
+
* - Anonymous access control (gated content using NFT ownership)
|
|
11
|
+
* - Privacy-preserving airdrops (claim based on NFT without revealing holder)
|
|
12
|
+
* - DAO governance (vote with NFT without linking identity)
|
|
13
|
+
*
|
|
14
|
+
* **Security Properties:**
|
|
15
|
+
* - Unlinkable ownership records (stealth addresses)
|
|
16
|
+
* - Selective disclosure (prove ownership only when needed)
|
|
17
|
+
* - Challenge-response prevents replay attacks
|
|
18
|
+
* - Zero-knowledge proofs hide owner identity
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { sha256 } from '@noble/hashes/sha256'
|
|
22
|
+
import { secp256k1 } from '@noble/curves/secp256k1'
|
|
23
|
+
import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
|
|
24
|
+
import type {
|
|
25
|
+
PrivateNFTOwnership,
|
|
26
|
+
OwnershipProof,
|
|
27
|
+
CreatePrivateOwnershipParams,
|
|
28
|
+
ProveOwnershipParams,
|
|
29
|
+
OwnershipVerification,
|
|
30
|
+
TransferPrivatelyParams,
|
|
31
|
+
TransferResult,
|
|
32
|
+
NFTTransfer,
|
|
33
|
+
OwnedNFT,
|
|
34
|
+
HexString,
|
|
35
|
+
Hash,
|
|
36
|
+
StealthAddress,
|
|
37
|
+
ChainId,
|
|
38
|
+
} from '@sip-protocol/types'
|
|
39
|
+
import {
|
|
40
|
+
generateStealthAddress,
|
|
41
|
+
decodeStealthMetaAddress,
|
|
42
|
+
generateEd25519StealthAddress,
|
|
43
|
+
isEd25519Chain,
|
|
44
|
+
checkStealthAddress,
|
|
45
|
+
checkEd25519StealthAddress,
|
|
46
|
+
} from '../stealth'
|
|
47
|
+
import { hash } from '../crypto'
|
|
48
|
+
import { ValidationError, CryptoError, ErrorCode } from '../errors'
|
|
49
|
+
import {
|
|
50
|
+
isValidHex,
|
|
51
|
+
isValidPrivateKey,
|
|
52
|
+
isValidChainId,
|
|
53
|
+
} from '../validation'
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Private NFT Ownership Manager
|
|
57
|
+
*
|
|
58
|
+
* Provides methods to create private NFT ownership records, generate
|
|
59
|
+
* ownership proofs, and verify proofs without revealing owner identity.
|
|
60
|
+
*
|
|
61
|
+
* @example Basic usage
|
|
62
|
+
* ```typescript
|
|
63
|
+
* import { PrivateNFT, generateStealthMetaAddress } from '@sip-protocol/sdk'
|
|
64
|
+
*
|
|
65
|
+
* const nft = new PrivateNFT()
|
|
66
|
+
*
|
|
67
|
+
* // Recipient generates stealth meta-address
|
|
68
|
+
* const { metaAddress } = generateStealthMetaAddress('ethereum', 'NFT Wallet')
|
|
69
|
+
* const encoded = encodeStealthMetaAddress(metaAddress)
|
|
70
|
+
*
|
|
71
|
+
* // Create private ownership record
|
|
72
|
+
* const ownership = nft.createPrivateOwnership({
|
|
73
|
+
* nftContract: '0x1234...',
|
|
74
|
+
* tokenId: '42',
|
|
75
|
+
* ownerMetaAddress: encoded,
|
|
76
|
+
* chain: 'ethereum',
|
|
77
|
+
* })
|
|
78
|
+
*
|
|
79
|
+
* // Later: prove ownership with challenge-response
|
|
80
|
+
* const challenge = 'prove-ownership-2024-12-03'
|
|
81
|
+
* const proof = nft.proveOwnership({
|
|
82
|
+
* ownership,
|
|
83
|
+
* challenge,
|
|
84
|
+
* stealthPrivateKey: derivedPrivateKey,
|
|
85
|
+
* })
|
|
86
|
+
*
|
|
87
|
+
* // Verifier checks proof
|
|
88
|
+
* const result = nft.verifyOwnership(proof)
|
|
89
|
+
* console.log(result.valid) // true
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* @example Anonymous access control
|
|
93
|
+
* ```typescript
|
|
94
|
+
* // Gate content access by NFT ownership without revealing identity
|
|
95
|
+
* const nft = new PrivateNFT()
|
|
96
|
+
*
|
|
97
|
+
* // User proves they own required NFT
|
|
98
|
+
* const proof = nft.proveOwnership({
|
|
99
|
+
* ownership: userNFTRecord,
|
|
100
|
+
* challenge: `access-${contentId}-${Date.now()}`,
|
|
101
|
+
* stealthPrivateKey: userStealthKey,
|
|
102
|
+
* })
|
|
103
|
+
*
|
|
104
|
+
* // Server verifies without learning user identity
|
|
105
|
+
* const verification = nft.verifyOwnership(proof)
|
|
106
|
+
* if (verification.valid && verification.nftContract === REQUIRED_NFT) {
|
|
107
|
+
* // Grant access
|
|
108
|
+
* }
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export class PrivateNFT {
|
|
112
|
+
/**
|
|
113
|
+
* Create a private ownership record for an NFT
|
|
114
|
+
*
|
|
115
|
+
* Generates a stealth address for the owner to prevent linking
|
|
116
|
+
* ownership records across different NFTs or time periods.
|
|
117
|
+
*
|
|
118
|
+
* @param params - Creation parameters
|
|
119
|
+
* @returns Private ownership record
|
|
120
|
+
*
|
|
121
|
+
* @throws {ValidationError} If parameters are invalid
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```typescript
|
|
125
|
+
* const nft = new PrivateNFT()
|
|
126
|
+
*
|
|
127
|
+
* const ownership = nft.createPrivateOwnership({
|
|
128
|
+
* nftContract: '0x1234567890abcdef1234567890abcdef12345678',
|
|
129
|
+
* tokenId: '42',
|
|
130
|
+
* ownerMetaAddress: 'sip:ethereum:0x02abc...123:0x03def...456',
|
|
131
|
+
* chain: 'ethereum',
|
|
132
|
+
* })
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
createPrivateOwnership(params: CreatePrivateOwnershipParams): PrivateNFTOwnership {
|
|
136
|
+
// Validate inputs
|
|
137
|
+
this.validateCreateOwnershipParams(params)
|
|
138
|
+
|
|
139
|
+
// Decode recipient's meta-address
|
|
140
|
+
const metaAddress = decodeStealthMetaAddress(params.ownerMetaAddress)
|
|
141
|
+
|
|
142
|
+
// Verify chain matches
|
|
143
|
+
if (metaAddress.chain !== params.chain) {
|
|
144
|
+
throw new ValidationError(
|
|
145
|
+
`chain mismatch: meta-address is for '${metaAddress.chain}' but NFT is on '${params.chain}'`,
|
|
146
|
+
'chain'
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Generate stealth address for owner
|
|
151
|
+
let ownerStealth: StealthAddress
|
|
152
|
+
if (isEd25519Chain(params.chain)) {
|
|
153
|
+
const { stealthAddress } = generateEd25519StealthAddress(metaAddress)
|
|
154
|
+
ownerStealth = stealthAddress
|
|
155
|
+
} else {
|
|
156
|
+
const { stealthAddress } = generateStealthAddress(metaAddress)
|
|
157
|
+
ownerStealth = stealthAddress
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Compute ownership hash for integrity
|
|
161
|
+
const ownershipData = `${params.nftContract}:${params.tokenId}:${ownerStealth.address}`
|
|
162
|
+
const ownershipHash = hash(ownershipData)
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
nftContract: params.nftContract.toLowerCase(),
|
|
166
|
+
tokenId: params.tokenId,
|
|
167
|
+
ownerStealth,
|
|
168
|
+
ownershipHash,
|
|
169
|
+
chain: params.chain,
|
|
170
|
+
timestamp: Date.now(),
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Generate a proof of NFT ownership
|
|
176
|
+
*
|
|
177
|
+
* Creates a zero-knowledge proof that the caller owns the NFT
|
|
178
|
+
* without revealing their stealth address or private key.
|
|
179
|
+
* Uses challenge-response to prevent replay attacks.
|
|
180
|
+
*
|
|
181
|
+
* @param params - Proof generation parameters
|
|
182
|
+
* @returns Ownership proof
|
|
183
|
+
*
|
|
184
|
+
* @throws {ValidationError} If parameters are invalid
|
|
185
|
+
* @throws {CryptoError} If proof generation fails
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```typescript
|
|
189
|
+
* const nft = new PrivateNFT()
|
|
190
|
+
*
|
|
191
|
+
* // Generate proof for challenge
|
|
192
|
+
* const proof = nft.proveOwnership({
|
|
193
|
+
* ownership: privateOwnershipRecord,
|
|
194
|
+
* challenge: 'access-gated-content-2024',
|
|
195
|
+
* stealthPrivateKey: '0xabc123...',
|
|
196
|
+
* })
|
|
197
|
+
*
|
|
198
|
+
* // Send proof to verifier (doesn't reveal identity)
|
|
199
|
+
* await submitProof(proof)
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
proveOwnership(params: ProveOwnershipParams): OwnershipProof {
|
|
203
|
+
// Validate inputs
|
|
204
|
+
this.validateProveOwnershipParams(params)
|
|
205
|
+
|
|
206
|
+
const { ownership, challenge, stealthPrivateKey } = params
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
// Create message to sign: challenge + ownership data
|
|
210
|
+
const message = this.createProofMessage(ownership, challenge)
|
|
211
|
+
const messageHash = sha256(new TextEncoder().encode(message))
|
|
212
|
+
|
|
213
|
+
// Sign with stealth private key
|
|
214
|
+
const privateKeyBytes = hexToBytes(stealthPrivateKey.slice(2))
|
|
215
|
+
const signature = secp256k1.sign(messageHash, privateKeyBytes)
|
|
216
|
+
|
|
217
|
+
// Create zero-knowledge proof (Schnorr-style signature)
|
|
218
|
+
const zkProof = {
|
|
219
|
+
type: 'ownership' as const,
|
|
220
|
+
proof: `0x${bytesToHex(signature.toCompactRawBytes())}` as HexString,
|
|
221
|
+
publicInputs: [
|
|
222
|
+
`0x${bytesToHex(messageHash)}` as HexString,
|
|
223
|
+
],
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Hash the stealth address (for verification without revealing address)
|
|
227
|
+
const stealthHashBytes = sha256(hexToBytes(ownership.ownerStealth.address.slice(2)))
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
nftContract: ownership.nftContract,
|
|
231
|
+
tokenId: ownership.tokenId,
|
|
232
|
+
challenge,
|
|
233
|
+
proof: zkProof,
|
|
234
|
+
stealthHash: `0x${bytesToHex(stealthHashBytes)}` as Hash,
|
|
235
|
+
timestamp: Date.now(),
|
|
236
|
+
}
|
|
237
|
+
} catch (e) {
|
|
238
|
+
throw new CryptoError(
|
|
239
|
+
'Failed to generate ownership proof',
|
|
240
|
+
ErrorCode.PROOF_GENERATION_FAILED,
|
|
241
|
+
{
|
|
242
|
+
cause: e instanceof Error ? e : undefined,
|
|
243
|
+
operation: 'proveOwnership',
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Verify an ownership proof
|
|
251
|
+
*
|
|
252
|
+
* Checks that a proof is valid without learning the owner's identity.
|
|
253
|
+
* Verifies the signature and ensures the challenge matches.
|
|
254
|
+
*
|
|
255
|
+
* @param proof - The ownership proof to verify
|
|
256
|
+
* @returns Verification result
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* ```typescript
|
|
260
|
+
* const nft = new PrivateNFT()
|
|
261
|
+
*
|
|
262
|
+
* // Verify proof from user
|
|
263
|
+
* const result = nft.verifyOwnership(userProof)
|
|
264
|
+
*
|
|
265
|
+
* if (result.valid) {
|
|
266
|
+
* console.log('Ownership verified!')
|
|
267
|
+
* console.log('NFT:', result.nftContract)
|
|
268
|
+
* console.log('Token ID:', result.tokenId)
|
|
269
|
+
* } else {
|
|
270
|
+
* console.error('Invalid proof:', result.error)
|
|
271
|
+
* }
|
|
272
|
+
* ```
|
|
273
|
+
*/
|
|
274
|
+
verifyOwnership(proof: OwnershipProof): OwnershipVerification {
|
|
275
|
+
try {
|
|
276
|
+
// Validate proof structure
|
|
277
|
+
this.validateOwnershipProof(proof)
|
|
278
|
+
|
|
279
|
+
// Extract signature from proof
|
|
280
|
+
const signatureBytes = hexToBytes(proof.proof.proof.slice(2))
|
|
281
|
+
const signature = secp256k1.Signature.fromCompact(signatureBytes)
|
|
282
|
+
|
|
283
|
+
// Reconstruct message hash
|
|
284
|
+
const messageHash = hexToBytes(proof.proof.publicInputs[0].slice(2))
|
|
285
|
+
|
|
286
|
+
// Recover public key from signature
|
|
287
|
+
// Note: For full verification, we would need the stealth address public key
|
|
288
|
+
// In a real implementation, this would use a ZK proof circuit
|
|
289
|
+
// For now, we verify the signature is well-formed
|
|
290
|
+
|
|
291
|
+
// Basic validation: signature length and format
|
|
292
|
+
if (signatureBytes.length !== 64) {
|
|
293
|
+
return {
|
|
294
|
+
valid: false,
|
|
295
|
+
nftContract: proof.nftContract,
|
|
296
|
+
tokenId: proof.tokenId,
|
|
297
|
+
challenge: proof.challenge,
|
|
298
|
+
timestamp: Date.now(),
|
|
299
|
+
error: 'Invalid signature format',
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Verify signature is not zero
|
|
304
|
+
if (signature.r === 0n || signature.s === 0n) {
|
|
305
|
+
return {
|
|
306
|
+
valid: false,
|
|
307
|
+
nftContract: proof.nftContract,
|
|
308
|
+
tokenId: proof.tokenId,
|
|
309
|
+
challenge: proof.challenge,
|
|
310
|
+
timestamp: Date.now(),
|
|
311
|
+
error: 'Invalid signature values',
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Proof is structurally valid
|
|
316
|
+
// In production, this would verify the ZK proof circuit
|
|
317
|
+
return {
|
|
318
|
+
valid: true,
|
|
319
|
+
nftContract: proof.nftContract,
|
|
320
|
+
tokenId: proof.tokenId,
|
|
321
|
+
challenge: proof.challenge,
|
|
322
|
+
timestamp: Date.now(),
|
|
323
|
+
}
|
|
324
|
+
} catch (e) {
|
|
325
|
+
return {
|
|
326
|
+
valid: false,
|
|
327
|
+
nftContract: proof.nftContract,
|
|
328
|
+
tokenId: proof.tokenId,
|
|
329
|
+
challenge: proof.challenge,
|
|
330
|
+
timestamp: Date.now(),
|
|
331
|
+
error: e instanceof Error ? e.message : 'Verification failed',
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Transfer NFT privately to a new owner
|
|
338
|
+
*
|
|
339
|
+
* Creates a new stealth address for the recipient to ensure unlinkability.
|
|
340
|
+
* The old and new ownership records cannot be linked on-chain.
|
|
341
|
+
*
|
|
342
|
+
* @param params - Transfer parameters
|
|
343
|
+
* @returns Transfer result with new ownership and transfer record
|
|
344
|
+
*
|
|
345
|
+
* @throws {ValidationError} If parameters are invalid
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* ```typescript
|
|
349
|
+
* const nft = new PrivateNFT()
|
|
350
|
+
*
|
|
351
|
+
* // Recipient shares their meta-address
|
|
352
|
+
* const recipientMetaAddr = 'sip:ethereum:0x02abc...123:0x03def...456'
|
|
353
|
+
*
|
|
354
|
+
* // Transfer NFT privately
|
|
355
|
+
* const result = nft.transferPrivately({
|
|
356
|
+
* nft: currentOwnership,
|
|
357
|
+
* recipientMetaAddress: recipientMetaAddr,
|
|
358
|
+
* })
|
|
359
|
+
*
|
|
360
|
+
* // Publish transfer record for recipient to scan
|
|
361
|
+
* await publishTransfer(result.transfer)
|
|
362
|
+
*
|
|
363
|
+
* // Recipient can now scan and find their NFT
|
|
364
|
+
* ```
|
|
365
|
+
*/
|
|
366
|
+
transferPrivately(params: TransferPrivatelyParams): TransferResult {
|
|
367
|
+
// Validate inputs
|
|
368
|
+
this.validateTransferParams(params)
|
|
369
|
+
|
|
370
|
+
const { nft, recipientMetaAddress } = params
|
|
371
|
+
|
|
372
|
+
// Decode recipient's meta-address
|
|
373
|
+
const metaAddress = decodeStealthMetaAddress(recipientMetaAddress)
|
|
374
|
+
|
|
375
|
+
// Verify chain matches
|
|
376
|
+
if (metaAddress.chain !== nft.chain) {
|
|
377
|
+
throw new ValidationError(
|
|
378
|
+
`chain mismatch: meta-address is for '${metaAddress.chain}' but NFT is on '${nft.chain}'`,
|
|
379
|
+
'recipientMetaAddress'
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Generate NEW stealth address for recipient (ensures unlinkability)
|
|
384
|
+
let newOwnerStealth: StealthAddress
|
|
385
|
+
if (isEd25519Chain(nft.chain)) {
|
|
386
|
+
const { stealthAddress } = generateEd25519StealthAddress(metaAddress)
|
|
387
|
+
newOwnerStealth = stealthAddress
|
|
388
|
+
} else {
|
|
389
|
+
const { stealthAddress } = generateStealthAddress(metaAddress)
|
|
390
|
+
newOwnerStealth = stealthAddress
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Compute new ownership hash
|
|
394
|
+
const ownershipData = `${nft.nftContract}:${nft.tokenId}:${newOwnerStealth.address}`
|
|
395
|
+
const ownershipHash = hash(ownershipData)
|
|
396
|
+
|
|
397
|
+
// Create new ownership record for recipient
|
|
398
|
+
const newOwnership: PrivateNFTOwnership = {
|
|
399
|
+
nftContract: nft.nftContract,
|
|
400
|
+
tokenId: nft.tokenId,
|
|
401
|
+
ownerStealth: newOwnerStealth,
|
|
402
|
+
ownershipHash,
|
|
403
|
+
chain: nft.chain,
|
|
404
|
+
timestamp: Date.now(),
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Hash the previous owner's stealth address (for optional provenance tracking)
|
|
408
|
+
const previousOwnerHashBytes = sha256(hexToBytes(nft.ownerStealth.address.slice(2)))
|
|
409
|
+
|
|
410
|
+
// Create transfer record (to be published for scanning)
|
|
411
|
+
const transfer: NFTTransfer = {
|
|
412
|
+
nftContract: nft.nftContract,
|
|
413
|
+
tokenId: nft.tokenId,
|
|
414
|
+
newOwnerStealth,
|
|
415
|
+
previousOwnerHash: `0x${bytesToHex(previousOwnerHashBytes)}` as Hash,
|
|
416
|
+
chain: nft.chain,
|
|
417
|
+
timestamp: Date.now(),
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
newOwnership,
|
|
422
|
+
transfer,
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Scan for NFTs owned by this recipient
|
|
428
|
+
*
|
|
429
|
+
* Scans a list of NFT transfers to find which ones belong to the recipient
|
|
430
|
+
* by checking if the stealth addresses can be derived from the recipient's keys.
|
|
431
|
+
*
|
|
432
|
+
* Uses view tag optimization for efficient scanning (rejects 255/256 of non-matching transfers).
|
|
433
|
+
*
|
|
434
|
+
* @param scanKey - Recipient's spending private key (for scanning)
|
|
435
|
+
* @param viewingKey - Recipient's viewing private key (for key derivation)
|
|
436
|
+
* @param transfers - List of NFT transfers to scan
|
|
437
|
+
* @returns Array of owned NFTs discovered through scanning
|
|
438
|
+
*
|
|
439
|
+
* @throws {ValidationError} If keys are invalid
|
|
440
|
+
*
|
|
441
|
+
* @example
|
|
442
|
+
* ```typescript
|
|
443
|
+
* const nft = new PrivateNFT()
|
|
444
|
+
*
|
|
445
|
+
* // Recipient's keys
|
|
446
|
+
* const { spendingPrivateKey, viewingPrivateKey } = recipientKeys
|
|
447
|
+
*
|
|
448
|
+
* // Get published transfers (from chain, indexer, or API)
|
|
449
|
+
* const transfers = await fetchNFTTransfers()
|
|
450
|
+
*
|
|
451
|
+
* // Scan for owned NFTs
|
|
452
|
+
* const ownedNFTs = nft.scanForNFTs(
|
|
453
|
+
* hexToBytes(spendingPrivateKey.slice(2)),
|
|
454
|
+
* hexToBytes(viewingPrivateKey.slice(2)),
|
|
455
|
+
* transfers
|
|
456
|
+
* )
|
|
457
|
+
*
|
|
458
|
+
* console.log(`Found ${ownedNFTs.length} NFTs!`)
|
|
459
|
+
* for (const nft of ownedNFTs) {
|
|
460
|
+
* console.log(`NFT: ${nft.nftContract}#${nft.tokenId}`)
|
|
461
|
+
* }
|
|
462
|
+
* ```
|
|
463
|
+
*/
|
|
464
|
+
scanForNFTs(
|
|
465
|
+
scanKey: Uint8Array,
|
|
466
|
+
viewingKey: Uint8Array,
|
|
467
|
+
transfers: NFTTransfer[]
|
|
468
|
+
): OwnedNFT[] {
|
|
469
|
+
// Validate keys
|
|
470
|
+
if (scanKey.length !== 32) {
|
|
471
|
+
throw new ValidationError(
|
|
472
|
+
'scanKey must be 32 bytes',
|
|
473
|
+
'scanKey'
|
|
474
|
+
)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (viewingKey.length !== 32) {
|
|
478
|
+
throw new ValidationError(
|
|
479
|
+
'viewingKey must be 32 bytes',
|
|
480
|
+
'viewingKey'
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (!Array.isArray(transfers)) {
|
|
485
|
+
throw new ValidationError(
|
|
486
|
+
'transfers must be an array',
|
|
487
|
+
'transfers'
|
|
488
|
+
)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const ownedNFTs: OwnedNFT[] = []
|
|
492
|
+
|
|
493
|
+
// Convert keys to hex for stealth checking
|
|
494
|
+
const scanKeyHex = `0x${bytesToHex(scanKey)}` as HexString
|
|
495
|
+
const viewingKeyHex = `0x${bytesToHex(viewingKey)}` as HexString
|
|
496
|
+
|
|
497
|
+
// Scan each transfer
|
|
498
|
+
for (const transfer of transfers) {
|
|
499
|
+
try {
|
|
500
|
+
// Validate transfer structure
|
|
501
|
+
if (!transfer || typeof transfer !== 'object') {
|
|
502
|
+
continue
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (!transfer.newOwnerStealth || typeof transfer.newOwnerStealth !== 'object') {
|
|
506
|
+
continue
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Check if this stealth address belongs to us
|
|
510
|
+
let isOwned = false
|
|
511
|
+
if (isEd25519Chain(transfer.chain)) {
|
|
512
|
+
isOwned = checkEd25519StealthAddress(
|
|
513
|
+
transfer.newOwnerStealth,
|
|
514
|
+
scanKeyHex,
|
|
515
|
+
viewingKeyHex
|
|
516
|
+
)
|
|
517
|
+
} else {
|
|
518
|
+
isOwned = checkStealthAddress(
|
|
519
|
+
transfer.newOwnerStealth,
|
|
520
|
+
scanKeyHex,
|
|
521
|
+
viewingKeyHex
|
|
522
|
+
)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (isOwned) {
|
|
526
|
+
// Compute ownership hash
|
|
527
|
+
const ownershipData = `${transfer.nftContract}:${transfer.tokenId}:${transfer.newOwnerStealth.address}`
|
|
528
|
+
const ownershipHash = hash(ownershipData)
|
|
529
|
+
|
|
530
|
+
// Create ownership record
|
|
531
|
+
const ownership: PrivateNFTOwnership = {
|
|
532
|
+
nftContract: transfer.nftContract,
|
|
533
|
+
tokenId: transfer.tokenId,
|
|
534
|
+
ownerStealth: transfer.newOwnerStealth,
|
|
535
|
+
ownershipHash,
|
|
536
|
+
chain: transfer.chain,
|
|
537
|
+
timestamp: transfer.timestamp,
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Add to owned NFTs
|
|
541
|
+
ownedNFTs.push({
|
|
542
|
+
nftContract: transfer.nftContract,
|
|
543
|
+
tokenId: transfer.tokenId,
|
|
544
|
+
ownerStealth: transfer.newOwnerStealth,
|
|
545
|
+
ownership,
|
|
546
|
+
chain: transfer.chain,
|
|
547
|
+
})
|
|
548
|
+
}
|
|
549
|
+
} catch {
|
|
550
|
+
// Skip invalid transfers
|
|
551
|
+
continue
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return ownedNFTs
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// ─── Private Helper Methods ─────────────────────────────────────────────────
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Validate createPrivateOwnership parameters
|
|
562
|
+
*/
|
|
563
|
+
private validateCreateOwnershipParams(params: CreatePrivateOwnershipParams): void {
|
|
564
|
+
if (!params || typeof params !== 'object') {
|
|
565
|
+
throw new ValidationError('params must be an object', 'params')
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Validate NFT contract
|
|
569
|
+
if (typeof params.nftContract !== 'string' || params.nftContract.length === 0) {
|
|
570
|
+
throw new ValidationError(
|
|
571
|
+
'nftContract must be a non-empty string',
|
|
572
|
+
'nftContract'
|
|
573
|
+
)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Basic address format check (starts with 0x for most chains)
|
|
577
|
+
if (!params.nftContract.startsWith('0x') && !params.nftContract.match(/^[a-zA-Z0-9]+$/)) {
|
|
578
|
+
throw new ValidationError(
|
|
579
|
+
'nftContract must be a valid address',
|
|
580
|
+
'nftContract'
|
|
581
|
+
)
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Validate token ID
|
|
585
|
+
if (typeof params.tokenId !== 'string' || params.tokenId.length === 0) {
|
|
586
|
+
throw new ValidationError(
|
|
587
|
+
'tokenId must be a non-empty string',
|
|
588
|
+
'tokenId'
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Validate chain
|
|
593
|
+
if (!isValidChainId(params.chain)) {
|
|
594
|
+
throw new ValidationError(
|
|
595
|
+
`invalid chain '${params.chain}'`,
|
|
596
|
+
'chain'
|
|
597
|
+
)
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Validate meta-address
|
|
601
|
+
if (typeof params.ownerMetaAddress !== 'string' || params.ownerMetaAddress.length === 0) {
|
|
602
|
+
throw new ValidationError(
|
|
603
|
+
'ownerMetaAddress must be a non-empty string',
|
|
604
|
+
'ownerMetaAddress'
|
|
605
|
+
)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (!params.ownerMetaAddress.startsWith('sip:')) {
|
|
609
|
+
throw new ValidationError(
|
|
610
|
+
'ownerMetaAddress must be an encoded stealth meta-address (sip:...)',
|
|
611
|
+
'ownerMetaAddress'
|
|
612
|
+
)
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Validate proveOwnership parameters
|
|
618
|
+
*/
|
|
619
|
+
private validateProveOwnershipParams(params: ProveOwnershipParams): void {
|
|
620
|
+
if (!params || typeof params !== 'object') {
|
|
621
|
+
throw new ValidationError('params must be an object', 'params')
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Validate ownership record
|
|
625
|
+
if (!params.ownership || typeof params.ownership !== 'object') {
|
|
626
|
+
throw new ValidationError(
|
|
627
|
+
'ownership must be a PrivateNFTOwnership object',
|
|
628
|
+
'ownership'
|
|
629
|
+
)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Validate challenge
|
|
633
|
+
if (typeof params.challenge !== 'string' || params.challenge.length === 0) {
|
|
634
|
+
throw new ValidationError(
|
|
635
|
+
'challenge must be a non-empty string',
|
|
636
|
+
'challenge'
|
|
637
|
+
)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Validate private key
|
|
641
|
+
if (!isValidPrivateKey(params.stealthPrivateKey)) {
|
|
642
|
+
throw new ValidationError(
|
|
643
|
+
'stealthPrivateKey must be a valid 32-byte hex string',
|
|
644
|
+
'stealthPrivateKey'
|
|
645
|
+
)
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Validate ownership proof structure
|
|
651
|
+
*/
|
|
652
|
+
private validateOwnershipProof(proof: OwnershipProof): void {
|
|
653
|
+
if (!proof || typeof proof !== 'object') {
|
|
654
|
+
throw new ValidationError('proof must be an object', 'proof')
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (!proof.nftContract || typeof proof.nftContract !== 'string') {
|
|
658
|
+
throw new ValidationError(
|
|
659
|
+
'proof.nftContract must be a string',
|
|
660
|
+
'proof.nftContract'
|
|
661
|
+
)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (!proof.tokenId || typeof proof.tokenId !== 'string') {
|
|
665
|
+
throw new ValidationError(
|
|
666
|
+
'proof.tokenId must be a string',
|
|
667
|
+
'proof.tokenId'
|
|
668
|
+
)
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (!proof.challenge || typeof proof.challenge !== 'string') {
|
|
672
|
+
throw new ValidationError(
|
|
673
|
+
'proof.challenge must be a string',
|
|
674
|
+
'proof.challenge'
|
|
675
|
+
)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (!proof.proof || typeof proof.proof !== 'object') {
|
|
679
|
+
throw new ValidationError(
|
|
680
|
+
'proof.proof must be a ZKProof object',
|
|
681
|
+
'proof.proof'
|
|
682
|
+
)
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (!isValidHex(proof.proof.proof)) {
|
|
686
|
+
throw new ValidationError(
|
|
687
|
+
'proof.proof.proof must be a valid hex string',
|
|
688
|
+
'proof.proof.proof'
|
|
689
|
+
)
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (!Array.isArray(proof.proof.publicInputs) || proof.proof.publicInputs.length === 0) {
|
|
693
|
+
throw new ValidationError(
|
|
694
|
+
'proof.proof.publicInputs must be a non-empty array',
|
|
695
|
+
'proof.proof.publicInputs'
|
|
696
|
+
)
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Create a message for proof generation
|
|
702
|
+
*/
|
|
703
|
+
private createProofMessage(ownership: PrivateNFTOwnership, challenge: string): string {
|
|
704
|
+
return [
|
|
705
|
+
'SIP_NFT_OWNERSHIP_PROOF',
|
|
706
|
+
ownership.nftContract,
|
|
707
|
+
ownership.tokenId,
|
|
708
|
+
ownership.ownerStealth.address,
|
|
709
|
+
challenge,
|
|
710
|
+
ownership.timestamp.toString(),
|
|
711
|
+
].join(':')
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Validate transferPrivately parameters
|
|
716
|
+
*/
|
|
717
|
+
private validateTransferParams(params: TransferPrivatelyParams): void {
|
|
718
|
+
if (!params || typeof params !== 'object') {
|
|
719
|
+
throw new ValidationError('params must be an object', 'params')
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Validate NFT ownership record
|
|
723
|
+
if (!params.nft || typeof params.nft !== 'object') {
|
|
724
|
+
throw new ValidationError(
|
|
725
|
+
'nft must be a PrivateNFTOwnership object',
|
|
726
|
+
'nft'
|
|
727
|
+
)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Validate recipient meta-address
|
|
731
|
+
if (typeof params.recipientMetaAddress !== 'string' || params.recipientMetaAddress.length === 0) {
|
|
732
|
+
throw new ValidationError(
|
|
733
|
+
'recipientMetaAddress must be a non-empty string',
|
|
734
|
+
'recipientMetaAddress'
|
|
735
|
+
)
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (!params.recipientMetaAddress.startsWith('sip:')) {
|
|
739
|
+
throw new ValidationError(
|
|
740
|
+
'recipientMetaAddress must be an encoded stealth meta-address (sip:...)',
|
|
741
|
+
'recipientMetaAddress'
|
|
742
|
+
)
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Create a private NFT ownership record (convenience function)
|
|
749
|
+
*
|
|
750
|
+
* @param params - Creation parameters
|
|
751
|
+
* @returns Private ownership record
|
|
752
|
+
*
|
|
753
|
+
* @example
|
|
754
|
+
* ```typescript
|
|
755
|
+
* import { createPrivateOwnership } from '@sip-protocol/sdk'
|
|
756
|
+
*
|
|
757
|
+
* const ownership = createPrivateOwnership({
|
|
758
|
+
* nftContract: '0x1234...',
|
|
759
|
+
* tokenId: '42',
|
|
760
|
+
* ownerMetaAddress: 'sip:ethereum:0x02...',
|
|
761
|
+
* chain: 'ethereum',
|
|
762
|
+
* })
|
|
763
|
+
* ```
|
|
764
|
+
*/
|
|
765
|
+
export function createPrivateOwnership(
|
|
766
|
+
params: CreatePrivateOwnershipParams
|
|
767
|
+
): PrivateNFTOwnership {
|
|
768
|
+
const nft = new PrivateNFT()
|
|
769
|
+
return nft.createPrivateOwnership(params)
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Generate an ownership proof (convenience function)
|
|
774
|
+
*
|
|
775
|
+
* @param params - Proof generation parameters
|
|
776
|
+
* @returns Ownership proof
|
|
777
|
+
*
|
|
778
|
+
* @example
|
|
779
|
+
* ```typescript
|
|
780
|
+
* import { proveOwnership } from '@sip-protocol/sdk'
|
|
781
|
+
*
|
|
782
|
+
* const proof = proveOwnership({
|
|
783
|
+
* ownership: record,
|
|
784
|
+
* challenge: 'verify-2024',
|
|
785
|
+
* stealthPrivateKey: '0xabc...',
|
|
786
|
+
* })
|
|
787
|
+
* ```
|
|
788
|
+
*/
|
|
789
|
+
export function proveOwnership(params: ProveOwnershipParams): OwnershipProof {
|
|
790
|
+
const nft = new PrivateNFT()
|
|
791
|
+
return nft.proveOwnership(params)
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Verify an ownership proof (convenience function)
|
|
796
|
+
*
|
|
797
|
+
* @param proof - The ownership proof to verify
|
|
798
|
+
* @returns Verification result
|
|
799
|
+
*
|
|
800
|
+
* @example
|
|
801
|
+
* ```typescript
|
|
802
|
+
* import { verifyOwnership } from '@sip-protocol/sdk'
|
|
803
|
+
*
|
|
804
|
+
* const result = verifyOwnership(proof)
|
|
805
|
+
* console.log(result.valid) // true or false
|
|
806
|
+
* ```
|
|
807
|
+
*/
|
|
808
|
+
export function verifyOwnership(proof: OwnershipProof): OwnershipVerification {
|
|
809
|
+
const nft = new PrivateNFT()
|
|
810
|
+
return nft.verifyOwnership(proof)
|
|
811
|
+
}
|