@sip-protocol/sdk 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { HexString, StealthAddress } from '@sip-protocol/types'
11
- import { SIP_MEMO_PREFIX, VIEW_TAG_MIN, VIEW_TAG_MAX } from './constants'
11
+ import { SIP_MEMO_PREFIX_V2, VIEW_TAG_MIN, VIEW_TAG_MAX } from './constants'
12
12
 
13
13
  // ─── Announcement Types ──────────────────────────────────────────────────────
14
14
 
@@ -18,6 +18,8 @@ import { SIP_MEMO_PREFIX, VIEW_TAG_MIN, VIEW_TAG_MAX } from './constants'
18
18
  * Contains the information needed for recipients to scan for payments.
19
19
  */
20
20
  export interface NEARAnnouncement {
21
+ /** Announcement scheme version: '1' = legacy swapped, '2' = canonical EIP-5564 */
22
+ version?: string
21
23
  /** Ephemeral public key (ed25519, 0x-prefixed hex) */
22
24
  ephemeralPublicKey: HexString
23
25
  /** View tag for efficient filtering (0-255) */
@@ -35,16 +37,21 @@ export interface NEARAnnouncement {
35
37
  /**
36
38
  * Parse an announcement from a NEAR memo string
37
39
  *
38
- * Format: SIP:1:<ephemeral_pubkey_hex>:<view_tag_hex>
40
+ * Accepts SIP:1 (legacy swapped scheme) and SIP:2 (canonical EIP-5564); the detected
41
+ * version is returned. NEAR derives canonically regardless, so the version is recorded
42
+ * for consistency rather than to route the claim.
43
+ *
44
+ * Format: SIP:<version>:<ephemeral_pubkey_hex>:<view_tag_hex>
39
45
  *
40
46
  * @param memo - The memo string to parse
41
47
  * @returns Parsed announcement or null if invalid
42
48
  *
43
49
  * @example
44
50
  * ```typescript
45
- * const memo = 'SIP:1:1234...abcd:0f'
51
+ * const memo = 'SIP:2:1234...abcd:0f'
46
52
  * const announcement = parseAnnouncement(memo)
47
53
  * if (announcement) {
54
+ * console.log(announcement.version) // '2'
48
55
  * console.log(announcement.ephemeralPublicKey)
49
56
  * console.log(announcement.viewTag) // 15
50
57
  * }
@@ -55,13 +62,15 @@ export function parseAnnouncement(memo: string): Partial<NEARAnnouncement> | nul
55
62
  return null
56
63
  }
57
64
 
58
- // Check prefix
59
- if (!memo.startsWith(SIP_MEMO_PREFIX)) {
65
+ // Accept SIP:1 (legacy) and SIP:2 (canonical); capture the version.
66
+ const versionMatch = /^SIP:([12]):/.exec(memo)
67
+ if (!versionMatch) {
60
68
  return null
61
69
  }
70
+ const version = versionMatch[1]
62
71
 
63
- // Parse parts: SIP:1:<ephemeral_pubkey>:<view_tag>
64
- const content = memo.slice(SIP_MEMO_PREFIX.length)
72
+ // Parse parts: <ephemeral_pubkey_hex>:<view_tag_hex>
73
+ const content = memo.slice(versionMatch[0].length)
65
74
  const parts = content.split(':')
66
75
 
67
76
  if (parts.length < 2) {
@@ -86,6 +95,7 @@ export function parseAnnouncement(memo: string): Partial<NEARAnnouncement> | nul
86
95
  }
87
96
 
88
97
  return {
98
+ version,
89
99
  ephemeralPublicKey: `0x${ephemeralKeyHex.toLowerCase()}` as HexString,
90
100
  viewTag,
91
101
  }
@@ -104,7 +114,7 @@ export function parseAnnouncement(memo: string): Partial<NEARAnnouncement> | nul
104
114
  * stealthAddress.ephemeralPublicKey,
105
115
  * stealthAddress.viewTag
106
116
  * )
107
- * // => 'SIP:1:1234...abcd:0f'
117
+ * // => 'SIP:2:1234...abcd:0f'
108
118
  * ```
109
119
  */
110
120
  export function createAnnouncementMemo(
@@ -117,7 +127,8 @@ export function createAnnouncementMemo(
117
127
  // Convert view tag to 2-char hex
118
128
  const viewTagHex = viewTag.toString(16).padStart(2, '0')
119
129
 
120
- return `${SIP_MEMO_PREFIX}${ephemeralKeyHex}:${viewTagHex}`
130
+ // Emit the canonical SIP:2 announcement (EIP-5564). Legacy SIP:1 remains parseable.
131
+ return `${SIP_MEMO_PREFIX_V2}${ephemeralKeyHex}:${viewTagHex}`
121
132
  }
122
133
 
123
134
  // ─── Transfer Types ──────────────────────────────────────────────────────────
@@ -218,7 +218,7 @@ export async function sendPrivateSPLTransfer(
218
218
  )
219
219
 
220
220
  // 4. Add memo with announcement for recipient scanning
221
- // Format: SIP:1:<ephemeral_pubkey_base58>:<view_tag_hex>
221
+ // Format: SIP:2:<ephemeral_pubkey_base58>:<view_tag_hex>:<stealth_address_base58>
222
222
  // viewTag is a number (0-255), convert to 2-char hex
223
223
  const viewTagHex = stealthAddress.viewTag.toString(16).padStart(2, '0')
224
224
  const memoContent = createAnnouncementMemo(
@@ -173,11 +173,17 @@ export function generateSecp256k1StealthAddress(
173
173
  // Hash the shared secret for use as a scalar
174
174
  const sharedSecretHash = sha256(sharedSecretPoint)
175
175
 
176
- // Compute stealth address: A = K_spend + hash(S)*G
177
- const hashTimesG = secp256k1.getPublicKey(sharedSecretHash, true)
176
+ // Compute stealth address: A = K_spend + hash(S)*G.
177
+ // Reduce hash(S) to a scalar in [1, n-1] before deriving the point: secp256k1.getPublicKey
178
+ // throws for a scalar >= n or == 0 (~3.7e-39 for a random SHA-256 digest). Reducing mod n
179
+ // mirrors the ed25519 path and keeps generate symmetric with derive (k_spend + hash(S) mod n).
180
+ const hashScalar = bytesToBigInt(sharedSecretHash) % secp256k1.CURVE.n
181
+ if (hashScalar === 0n) {
182
+ throw new Error('CRITICAL: zero hash scalar after reduction - investigate hash computation')
183
+ }
178
184
 
179
185
  const spendingKeyPoint = secp256k1.ProjectivePoint.fromHex(spendingKeyBytes)
180
- const hashTimesGPoint = secp256k1.ProjectivePoint.fromHex(hashTimesG)
186
+ const hashTimesGPoint = secp256k1.ProjectivePoint.BASE.multiply(hashScalar)
181
187
  const stealthPoint = spendingKeyPoint.add(hashTimesGPoint)
182
188
  const stealthAddressBytes = stealthPoint.toRawBytes(true)
183
189
 
@@ -378,10 +384,15 @@ export function checkSecp256k1StealthAddress(
378
384
  return false
379
385
  }
380
386
 
381
- // Expected address: A = K_spend + hash(S)*G (no spending private key needed)
382
- const hashTimesG = secp256k1.getPublicKey(sharedSecretHash, true)
387
+ // Expected address: A = K_spend + hash(S)*G (no spending private key needed).
388
+ // Reduce hash(S) mod n (symmetric with generate); a zero scalar can't correspond to a
389
+ // real stealth payment, so treat it as a non-match.
390
+ const hashScalar = bytesToBigInt(sharedSecretHash) % secp256k1.CURVE.n
391
+ if (hashScalar === 0n) {
392
+ return false
393
+ }
383
394
  const expectedPoint = secp256k1.ProjectivePoint.fromHex(spendingPubBytes).add(
384
- secp256k1.ProjectivePoint.fromHex(hashTimesG),
395
+ secp256k1.ProjectivePoint.BASE.multiply(hashScalar),
385
396
  )
386
397
 
387
398
  // Compare with provided stealth address