@sip-protocol/sdk 0.1.9 → 0.2.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.
- package/LICENSE +21 -0
- package/dist/browser.d.mts +2 -0
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +12925 -0
- package/dist/browser.mjs +432 -0
- package/dist/chunk-O4Y2ZUDL.mjs +12721 -0
- package/dist/index.d.mts +800 -91
- package/dist/index.d.ts +800 -91
- package/dist/index.js +1854 -378
- package/dist/index.mjs +262 -11114
- package/package.json +23 -14
- package/src/adapters/near-intents.ts +138 -30
- package/src/browser.ts +33 -0
- package/src/commitment.ts +4 -4
- package/src/index.ts +71 -0
- package/src/oracle/index.ts +12 -0
- package/src/oracle/serialization.ts +237 -0
- package/src/oracle/types.ts +257 -0
- package/src/oracle/verification.ts +257 -0
- package/src/proofs/browser-utils.ts +141 -0
- package/src/proofs/browser.ts +884 -0
- package/src/proofs/index.ts +14 -0
- package/src/stealth.ts +868 -12
- package/src/validation.ts +7 -0
package/src/stealth.ts
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { secp256k1 } from '@noble/curves/secp256k1'
|
|
16
|
+
import { ed25519 } from '@noble/curves/ed25519'
|
|
16
17
|
import { sha256 } from '@noble/hashes/sha256'
|
|
18
|
+
import { sha512 } from '@noble/hashes/sha512'
|
|
17
19
|
import { keccak_256 } from '@noble/hashes/sha3'
|
|
18
20
|
import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils'
|
|
19
21
|
import type {
|
|
@@ -28,6 +30,7 @@ import {
|
|
|
28
30
|
isValidChainId,
|
|
29
31
|
isValidHex,
|
|
30
32
|
isValidCompressedPublicKey,
|
|
33
|
+
isValidEd25519PublicKey,
|
|
31
34
|
isValidPrivateKey,
|
|
32
35
|
} from './validation'
|
|
33
36
|
import { secureWipe, secureWipeAll } from './secure-memory'
|
|
@@ -405,19 +408,38 @@ export function decodeStealthMetaAddress(encoded: string): StealthMetaAddress {
|
|
|
405
408
|
)
|
|
406
409
|
}
|
|
407
410
|
|
|
408
|
-
// Validate keys
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
411
|
+
// Validate keys based on chain's curve type
|
|
412
|
+
const chainId = chain as ChainId
|
|
413
|
+
if (isEd25519Chain(chainId)) {
|
|
414
|
+
// Ed25519 chains (Solana, NEAR) use 32-byte public keys
|
|
415
|
+
if (!isValidEd25519PublicKey(spendingKey)) {
|
|
416
|
+
throw new ValidationError(
|
|
417
|
+
'spendingKey must be a valid 32-byte ed25519 public key',
|
|
418
|
+
'encoded.spendingKey'
|
|
419
|
+
)
|
|
420
|
+
}
|
|
415
421
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
422
|
+
if (!isValidEd25519PublicKey(viewingKey)) {
|
|
423
|
+
throw new ValidationError(
|
|
424
|
+
'viewingKey must be a valid 32-byte ed25519 public key',
|
|
425
|
+
'encoded.viewingKey'
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
} else {
|
|
429
|
+
// secp256k1 chains (Ethereum, etc.) use 33-byte compressed public keys
|
|
430
|
+
if (!isValidCompressedPublicKey(spendingKey)) {
|
|
431
|
+
throw new ValidationError(
|
|
432
|
+
'spendingKey must be a valid compressed secp256k1 public key',
|
|
433
|
+
'encoded.spendingKey'
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (!isValidCompressedPublicKey(viewingKey)) {
|
|
438
|
+
throw new ValidationError(
|
|
439
|
+
'viewingKey must be a valid compressed secp256k1 public key',
|
|
440
|
+
'encoded.viewingKey'
|
|
441
|
+
)
|
|
442
|
+
}
|
|
421
443
|
}
|
|
422
444
|
|
|
423
445
|
return {
|
|
@@ -510,3 +532,837 @@ function toChecksumAddress(address: string): HexString {
|
|
|
510
532
|
|
|
511
533
|
return checksummed as HexString
|
|
512
534
|
}
|
|
535
|
+
|
|
536
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
537
|
+
// ED25519 STEALTH ADDRESSES
|
|
538
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
539
|
+
//
|
|
540
|
+
// ed25519 stealth address implementation for Solana and NEAR chains.
|
|
541
|
+
// Uses DKSAP (Dual-Key Stealth Address Protocol) pattern adapted for ed25519.
|
|
542
|
+
//
|
|
543
|
+
// Key differences from secp256k1:
|
|
544
|
+
// - Public keys are 32 bytes (not 33 compressed)
|
|
545
|
+
// - Uses SHA-512 for key derivation (matches ed25519 spec)
|
|
546
|
+
// - Scalar arithmetic modulo ed25519 curve order (L)
|
|
547
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* ed25519 curve order (L) - the order of the base point
|
|
551
|
+
*/
|
|
552
|
+
const ED25519_ORDER = 2n ** 252n + 27742317777372353535851937790883648493n
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Chains that use ed25519 for stealth addresses
|
|
556
|
+
*/
|
|
557
|
+
const ED25519_CHAINS: ChainId[] = ['solana', 'near']
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Check if a chain uses ed25519 for stealth addresses
|
|
561
|
+
*/
|
|
562
|
+
export function isEd25519Chain(chain: ChainId): boolean {
|
|
563
|
+
return ED25519_CHAINS.includes(chain)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Curve type used for stealth addresses
|
|
568
|
+
*/
|
|
569
|
+
export type StealthCurve = 'secp256k1' | 'ed25519'
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Get the curve type used by a chain for stealth addresses
|
|
573
|
+
*
|
|
574
|
+
* @param chain - Chain identifier
|
|
575
|
+
* @returns 'ed25519' for Solana/NEAR, 'secp256k1' for EVM chains
|
|
576
|
+
*/
|
|
577
|
+
export function getCurveForChain(chain: ChainId): StealthCurve {
|
|
578
|
+
return isEd25519Chain(chain) ? 'ed25519' : 'secp256k1'
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Validate an ed25519 StealthMetaAddress object
|
|
583
|
+
*/
|
|
584
|
+
function validateEd25519StealthMetaAddress(
|
|
585
|
+
metaAddress: StealthMetaAddress,
|
|
586
|
+
field: string = 'recipientMetaAddress'
|
|
587
|
+
): void {
|
|
588
|
+
if (!metaAddress || typeof metaAddress !== 'object') {
|
|
589
|
+
throw new ValidationError('must be an object', field)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Validate chain is ed25519-compatible
|
|
593
|
+
if (!isValidChainId(metaAddress.chain)) {
|
|
594
|
+
throw new ValidationError(
|
|
595
|
+
`invalid chain '${metaAddress.chain}'`,
|
|
596
|
+
`${field}.chain`
|
|
597
|
+
)
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (!isEd25519Chain(metaAddress.chain)) {
|
|
601
|
+
throw new ValidationError(
|
|
602
|
+
`chain '${metaAddress.chain}' does not use ed25519, use secp256k1 functions instead`,
|
|
603
|
+
`${field}.chain`
|
|
604
|
+
)
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Validate spending key (32 bytes for ed25519)
|
|
608
|
+
if (!isValidEd25519PublicKey(metaAddress.spendingKey)) {
|
|
609
|
+
throw new ValidationError(
|
|
610
|
+
'spendingKey must be a valid ed25519 public key (32 bytes)',
|
|
611
|
+
`${field}.spendingKey`
|
|
612
|
+
)
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Validate viewing key (32 bytes for ed25519)
|
|
616
|
+
if (!isValidEd25519PublicKey(metaAddress.viewingKey)) {
|
|
617
|
+
throw new ValidationError(
|
|
618
|
+
'viewingKey must be a valid ed25519 public key (32 bytes)',
|
|
619
|
+
`${field}.viewingKey`
|
|
620
|
+
)
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Validate an ed25519 StealthAddress object
|
|
626
|
+
*/
|
|
627
|
+
function validateEd25519StealthAddress(
|
|
628
|
+
stealthAddress: StealthAddress,
|
|
629
|
+
field: string = 'stealthAddress'
|
|
630
|
+
): void {
|
|
631
|
+
if (!stealthAddress || typeof stealthAddress !== 'object') {
|
|
632
|
+
throw new ValidationError('must be an object', field)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Validate address (32-byte ed25519 public key)
|
|
636
|
+
if (!isValidEd25519PublicKey(stealthAddress.address)) {
|
|
637
|
+
throw new ValidationError(
|
|
638
|
+
'address must be a valid ed25519 public key (32 bytes)',
|
|
639
|
+
`${field}.address`
|
|
640
|
+
)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Validate ephemeral public key (32 bytes for ed25519)
|
|
644
|
+
if (!isValidEd25519PublicKey(stealthAddress.ephemeralPublicKey)) {
|
|
645
|
+
throw new ValidationError(
|
|
646
|
+
'ephemeralPublicKey must be a valid ed25519 public key (32 bytes)',
|
|
647
|
+
`${field}.ephemeralPublicKey`
|
|
648
|
+
)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Validate view tag (0-255)
|
|
652
|
+
if (typeof stealthAddress.viewTag !== 'number' ||
|
|
653
|
+
!Number.isInteger(stealthAddress.viewTag) ||
|
|
654
|
+
stealthAddress.viewTag < 0 ||
|
|
655
|
+
stealthAddress.viewTag > 255) {
|
|
656
|
+
throw new ValidationError(
|
|
657
|
+
'viewTag must be an integer between 0 and 255',
|
|
658
|
+
`${field}.viewTag`
|
|
659
|
+
)
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Get the scalar from an ed25519 private key
|
|
665
|
+
*
|
|
666
|
+
* ed25519 key derivation:
|
|
667
|
+
* 1. Hash the 32-byte seed with SHA-512 to get 64 bytes
|
|
668
|
+
* 2. First 32 bytes are the scalar (after clamping)
|
|
669
|
+
* 3. Last 32 bytes are used for nonce generation (not needed here)
|
|
670
|
+
*/
|
|
671
|
+
function getEd25519Scalar(privateKey: Uint8Array): bigint {
|
|
672
|
+
// Hash the private key seed with SHA-512
|
|
673
|
+
const hash = sha512(privateKey)
|
|
674
|
+
|
|
675
|
+
// Take first 32 bytes and clamp as per ed25519 spec
|
|
676
|
+
const scalar = hash.slice(0, 32)
|
|
677
|
+
|
|
678
|
+
// Clamp: clear lowest 3 bits, clear highest bit, set second highest bit
|
|
679
|
+
scalar[0] &= 248
|
|
680
|
+
scalar[31] &= 127
|
|
681
|
+
scalar[31] |= 64
|
|
682
|
+
|
|
683
|
+
// Convert to bigint (little-endian for ed25519)
|
|
684
|
+
return bytesToBigIntLE(scalar)
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Convert bytes to bigint (little-endian, used by ed25519)
|
|
689
|
+
*/
|
|
690
|
+
function bytesToBigIntLE(bytes: Uint8Array): bigint {
|
|
691
|
+
let result = 0n
|
|
692
|
+
for (let i = bytes.length - 1; i >= 0; i--) {
|
|
693
|
+
result = (result << 8n) + BigInt(bytes[i])
|
|
694
|
+
}
|
|
695
|
+
return result
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Convert bigint to bytes (little-endian, used by ed25519)
|
|
700
|
+
*/
|
|
701
|
+
function bigIntToBytesLE(value: bigint, length: number): Uint8Array {
|
|
702
|
+
const bytes = new Uint8Array(length)
|
|
703
|
+
for (let i = 0; i < length; i++) {
|
|
704
|
+
bytes[i] = Number(value & 0xffn)
|
|
705
|
+
value >>= 8n
|
|
706
|
+
}
|
|
707
|
+
return bytes
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Generate a new ed25519 stealth meta-address keypair
|
|
712
|
+
*
|
|
713
|
+
* @param chain - Target chain (must be ed25519-compatible: solana, near)
|
|
714
|
+
* @param label - Optional human-readable label
|
|
715
|
+
* @returns Stealth meta-address and private keys
|
|
716
|
+
* @throws {ValidationError} If chain is invalid or not ed25519-compatible
|
|
717
|
+
*/
|
|
718
|
+
export function generateEd25519StealthMetaAddress(
|
|
719
|
+
chain: ChainId,
|
|
720
|
+
label?: string,
|
|
721
|
+
): {
|
|
722
|
+
metaAddress: StealthMetaAddress
|
|
723
|
+
spendingPrivateKey: HexString
|
|
724
|
+
viewingPrivateKey: HexString
|
|
725
|
+
} {
|
|
726
|
+
// Validate chain
|
|
727
|
+
if (!isValidChainId(chain)) {
|
|
728
|
+
throw new ValidationError(
|
|
729
|
+
`invalid chain '${chain}', must be one of: solana, ethereum, near, zcash, polygon, arbitrum, optimism, base`,
|
|
730
|
+
'chain'
|
|
731
|
+
)
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (!isEd25519Chain(chain)) {
|
|
735
|
+
throw new ValidationError(
|
|
736
|
+
`chain '${chain}' does not use ed25519, use generateStealthMetaAddress() for secp256k1 chains`,
|
|
737
|
+
'chain'
|
|
738
|
+
)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Generate random private keys (32-byte seeds)
|
|
742
|
+
const spendingPrivateKey = randomBytes(32)
|
|
743
|
+
const viewingPrivateKey = randomBytes(32)
|
|
744
|
+
|
|
745
|
+
try {
|
|
746
|
+
// Derive public keys using ed25519
|
|
747
|
+
const spendingKey = ed25519.getPublicKey(spendingPrivateKey)
|
|
748
|
+
const viewingKey = ed25519.getPublicKey(viewingPrivateKey)
|
|
749
|
+
|
|
750
|
+
// Convert to hex strings before wiping buffers
|
|
751
|
+
const result = {
|
|
752
|
+
metaAddress: {
|
|
753
|
+
spendingKey: `0x${bytesToHex(spendingKey)}` as HexString,
|
|
754
|
+
viewingKey: `0x${bytesToHex(viewingKey)}` as HexString,
|
|
755
|
+
chain,
|
|
756
|
+
label,
|
|
757
|
+
},
|
|
758
|
+
spendingPrivateKey: `0x${bytesToHex(spendingPrivateKey)}` as HexString,
|
|
759
|
+
viewingPrivateKey: `0x${bytesToHex(viewingPrivateKey)}` as HexString,
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return result
|
|
763
|
+
} finally {
|
|
764
|
+
// Securely wipe private key buffers
|
|
765
|
+
secureWipeAll(spendingPrivateKey, viewingPrivateKey)
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Generate a one-time ed25519 stealth address for a recipient
|
|
771
|
+
*
|
|
772
|
+
* Algorithm (DKSAP for ed25519):
|
|
773
|
+
* 1. Generate ephemeral keypair (r, R = r*G)
|
|
774
|
+
* 2. Compute shared secret: S = r * P_spend (ephemeral scalar * spending public)
|
|
775
|
+
* 3. Hash shared secret: h = SHA256(S)
|
|
776
|
+
* 4. Derive stealth public key: P_stealth = P_view + h*G
|
|
777
|
+
*
|
|
778
|
+
* @param recipientMetaAddress - Recipient's published stealth meta-address
|
|
779
|
+
* @returns Stealth address data (address + ephemeral key for publication)
|
|
780
|
+
* @throws {ValidationError} If recipientMetaAddress is invalid
|
|
781
|
+
*/
|
|
782
|
+
export function generateEd25519StealthAddress(
|
|
783
|
+
recipientMetaAddress: StealthMetaAddress,
|
|
784
|
+
): {
|
|
785
|
+
stealthAddress: StealthAddress
|
|
786
|
+
sharedSecret: HexString
|
|
787
|
+
} {
|
|
788
|
+
// Validate input
|
|
789
|
+
validateEd25519StealthMetaAddress(recipientMetaAddress)
|
|
790
|
+
|
|
791
|
+
// Generate ephemeral keypair
|
|
792
|
+
const ephemeralPrivateKey = randomBytes(32)
|
|
793
|
+
|
|
794
|
+
try {
|
|
795
|
+
const ephemeralPublicKey = ed25519.getPublicKey(ephemeralPrivateKey)
|
|
796
|
+
|
|
797
|
+
// Parse recipient's keys (remove 0x prefix)
|
|
798
|
+
const spendingKeyBytes = hexToBytes(recipientMetaAddress.spendingKey.slice(2))
|
|
799
|
+
const viewingKeyBytes = hexToBytes(recipientMetaAddress.viewingKey.slice(2))
|
|
800
|
+
|
|
801
|
+
// Get ephemeral scalar from private key and reduce mod L
|
|
802
|
+
// ed25519 clamping produces values that may exceed L, so we reduce
|
|
803
|
+
const rawEphemeralScalar = getEd25519Scalar(ephemeralPrivateKey)
|
|
804
|
+
const ephemeralScalar = rawEphemeralScalar % ED25519_ORDER
|
|
805
|
+
if (ephemeralScalar === 0n) {
|
|
806
|
+
throw new Error('CRITICAL: Zero ephemeral scalar after reduction - investigate RNG')
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Convert spending public key to extended point and multiply by ephemeral scalar
|
|
810
|
+
// S = ephemeral_scalar * P_spend
|
|
811
|
+
const spendingPoint = ed25519.ExtendedPoint.fromHex(spendingKeyBytes)
|
|
812
|
+
const sharedSecretPoint = spendingPoint.multiply(ephemeralScalar)
|
|
813
|
+
|
|
814
|
+
// Hash the shared secret point (compress to bytes first)
|
|
815
|
+
const sharedSecretHash = sha256(sharedSecretPoint.toRawBytes())
|
|
816
|
+
|
|
817
|
+
// Derive stealth public key: P_stealth = P_view + hash(S)*G
|
|
818
|
+
// Convert hash to scalar (mod L to ensure it's valid and non-zero)
|
|
819
|
+
const hashScalar = bytesToBigInt(sharedSecretHash) % ED25519_ORDER
|
|
820
|
+
if (hashScalar === 0n) {
|
|
821
|
+
throw new Error('CRITICAL: Zero hash scalar after reduction - investigate hash computation')
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Compute hash(S) * G
|
|
825
|
+
const hashTimesG = ed25519.ExtendedPoint.BASE.multiply(hashScalar)
|
|
826
|
+
|
|
827
|
+
// Add to viewing key: P_stealth = P_view + hash(S)*G
|
|
828
|
+
const viewingPoint = ed25519.ExtendedPoint.fromHex(viewingKeyBytes)
|
|
829
|
+
const stealthPoint = viewingPoint.add(hashTimesG)
|
|
830
|
+
const stealthAddressBytes = stealthPoint.toRawBytes()
|
|
831
|
+
|
|
832
|
+
// Compute view tag (first byte of hash for efficient scanning)
|
|
833
|
+
const viewTag = sharedSecretHash[0]
|
|
834
|
+
|
|
835
|
+
return {
|
|
836
|
+
stealthAddress: {
|
|
837
|
+
address: `0x${bytesToHex(stealthAddressBytes)}` as HexString,
|
|
838
|
+
ephemeralPublicKey: `0x${bytesToHex(ephemeralPublicKey)}` as HexString,
|
|
839
|
+
viewTag,
|
|
840
|
+
},
|
|
841
|
+
sharedSecret: `0x${bytesToHex(sharedSecretHash)}` as HexString,
|
|
842
|
+
}
|
|
843
|
+
} finally {
|
|
844
|
+
// Securely wipe ephemeral private key
|
|
845
|
+
secureWipe(ephemeralPrivateKey)
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Derive the private key for an ed25519 stealth address (for recipient to claim funds)
|
|
851
|
+
*
|
|
852
|
+
* Algorithm:
|
|
853
|
+
* 1. Compute shared secret: S = spend_scalar * R (spending scalar * ephemeral public)
|
|
854
|
+
* 2. Hash shared secret: h = SHA256(S)
|
|
855
|
+
* 3. Derive stealth private key: s_stealth = s_view + h (mod L)
|
|
856
|
+
*
|
|
857
|
+
* **IMPORTANT: Derived Key Format**
|
|
858
|
+
*
|
|
859
|
+
* The returned `privateKey` is a **raw scalar** in little-endian format, NOT a standard
|
|
860
|
+
* ed25519 seed. This is because the stealth private key is derived mathematically
|
|
861
|
+
* (s_view + hash), not generated from a seed.
|
|
862
|
+
*
|
|
863
|
+
* To compute the public key from the derived private key:
|
|
864
|
+
* ```typescript
|
|
865
|
+
* // CORRECT: Direct scalar multiplication
|
|
866
|
+
* const scalar = bytesToBigIntLE(hexToBytes(privateKey.slice(2)))
|
|
867
|
+
* const publicKey = ed25519.ExtendedPoint.BASE.multiply(scalar)
|
|
868
|
+
*
|
|
869
|
+
* // WRONG: Do NOT use ed25519.getPublicKey() - it will hash and clamp the input,
|
|
870
|
+
* // producing a different (incorrect) public key
|
|
871
|
+
* ```
|
|
872
|
+
*
|
|
873
|
+
* @param stealthAddress - The stealth address to recover
|
|
874
|
+
* @param spendingPrivateKey - Recipient's spending private key
|
|
875
|
+
* @param viewingPrivateKey - Recipient's viewing private key
|
|
876
|
+
* @returns Recovery data including derived private key (raw scalar, little-endian)
|
|
877
|
+
* @throws {ValidationError} If any input is invalid
|
|
878
|
+
*/
|
|
879
|
+
export function deriveEd25519StealthPrivateKey(
|
|
880
|
+
stealthAddress: StealthAddress,
|
|
881
|
+
spendingPrivateKey: HexString,
|
|
882
|
+
viewingPrivateKey: HexString,
|
|
883
|
+
): StealthAddressRecovery {
|
|
884
|
+
// Validate stealth address
|
|
885
|
+
validateEd25519StealthAddress(stealthAddress)
|
|
886
|
+
|
|
887
|
+
// Validate private keys (32 bytes)
|
|
888
|
+
if (!isValidPrivateKey(spendingPrivateKey)) {
|
|
889
|
+
throw new ValidationError(
|
|
890
|
+
'must be a valid 32-byte hex string',
|
|
891
|
+
'spendingPrivateKey'
|
|
892
|
+
)
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (!isValidPrivateKey(viewingPrivateKey)) {
|
|
896
|
+
throw new ValidationError(
|
|
897
|
+
'must be a valid 32-byte hex string',
|
|
898
|
+
'viewingPrivateKey'
|
|
899
|
+
)
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Parse keys
|
|
903
|
+
const spendingPrivBytes = hexToBytes(spendingPrivateKey.slice(2))
|
|
904
|
+
const viewingPrivBytes = hexToBytes(viewingPrivateKey.slice(2))
|
|
905
|
+
const ephemeralPubBytes = hexToBytes(stealthAddress.ephemeralPublicKey.slice(2))
|
|
906
|
+
|
|
907
|
+
try {
|
|
908
|
+
// Get spending scalar from private key and reduce mod L
|
|
909
|
+
const rawSpendingScalar = getEd25519Scalar(spendingPrivBytes)
|
|
910
|
+
const spendingScalar = rawSpendingScalar % ED25519_ORDER
|
|
911
|
+
if (spendingScalar === 0n) {
|
|
912
|
+
throw new Error('CRITICAL: Zero spending scalar after reduction - investigate key derivation')
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// Compute shared secret: S = spending_scalar * R
|
|
916
|
+
const ephemeralPoint = ed25519.ExtendedPoint.fromHex(ephemeralPubBytes)
|
|
917
|
+
const sharedSecretPoint = ephemeralPoint.multiply(spendingScalar)
|
|
918
|
+
|
|
919
|
+
// Hash the shared secret
|
|
920
|
+
const sharedSecretHash = sha256(sharedSecretPoint.toRawBytes())
|
|
921
|
+
|
|
922
|
+
// Get viewing scalar from private key and reduce mod L
|
|
923
|
+
const rawViewingScalar = getEd25519Scalar(viewingPrivBytes)
|
|
924
|
+
const viewingScalar = rawViewingScalar % ED25519_ORDER
|
|
925
|
+
if (viewingScalar === 0n) {
|
|
926
|
+
throw new Error('CRITICAL: Zero viewing scalar after reduction - investigate key derivation')
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// Derive stealth private key: s_stealth = s_view + hash(S) mod L
|
|
930
|
+
const hashScalar = bytesToBigInt(sharedSecretHash) % ED25519_ORDER
|
|
931
|
+
if (hashScalar === 0n) {
|
|
932
|
+
throw new Error('CRITICAL: Zero hash scalar after reduction - investigate hash computation')
|
|
933
|
+
}
|
|
934
|
+
const stealthPrivateScalar = (viewingScalar + hashScalar) % ED25519_ORDER
|
|
935
|
+
if (stealthPrivateScalar === 0n) {
|
|
936
|
+
throw new Error('CRITICAL: Zero stealth scalar after reduction - investigate key derivation')
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Convert back to bytes (little-endian for ed25519)
|
|
940
|
+
// Note: We need to store this as a seed that will produce this scalar
|
|
941
|
+
// For simplicity, we store the scalar directly (32 bytes, little-endian)
|
|
942
|
+
const stealthPrivateKey = bigIntToBytesLE(stealthPrivateScalar, 32)
|
|
943
|
+
|
|
944
|
+
const result = {
|
|
945
|
+
stealthAddress: stealthAddress.address,
|
|
946
|
+
ephemeralPublicKey: stealthAddress.ephemeralPublicKey,
|
|
947
|
+
privateKey: `0x${bytesToHex(stealthPrivateKey)}` as HexString,
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// Wipe derived key buffer after converting to hex
|
|
951
|
+
secureWipe(stealthPrivateKey)
|
|
952
|
+
|
|
953
|
+
return result
|
|
954
|
+
} finally {
|
|
955
|
+
// Securely wipe input private key buffers
|
|
956
|
+
secureWipeAll(spendingPrivBytes, viewingPrivBytes)
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Check if an ed25519 stealth address was intended for this recipient
|
|
962
|
+
* Uses view tag for efficient filtering before full computation
|
|
963
|
+
*
|
|
964
|
+
* @param stealthAddress - Stealth address to check
|
|
965
|
+
* @param spendingPrivateKey - Recipient's spending private key
|
|
966
|
+
* @param viewingPrivateKey - Recipient's viewing private key
|
|
967
|
+
* @returns true if this address belongs to the recipient
|
|
968
|
+
* @throws {ValidationError} If any input is invalid
|
|
969
|
+
*/
|
|
970
|
+
export function checkEd25519StealthAddress(
|
|
971
|
+
stealthAddress: StealthAddress,
|
|
972
|
+
spendingPrivateKey: HexString,
|
|
973
|
+
viewingPrivateKey: HexString,
|
|
974
|
+
): boolean {
|
|
975
|
+
// Validate stealth address
|
|
976
|
+
validateEd25519StealthAddress(stealthAddress)
|
|
977
|
+
|
|
978
|
+
// Validate private keys
|
|
979
|
+
if (!isValidPrivateKey(spendingPrivateKey)) {
|
|
980
|
+
throw new ValidationError(
|
|
981
|
+
'must be a valid 32-byte hex string',
|
|
982
|
+
'spendingPrivateKey'
|
|
983
|
+
)
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (!isValidPrivateKey(viewingPrivateKey)) {
|
|
987
|
+
throw new ValidationError(
|
|
988
|
+
'must be a valid 32-byte hex string',
|
|
989
|
+
'viewingPrivateKey'
|
|
990
|
+
)
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Parse keys
|
|
994
|
+
const spendingPrivBytes = hexToBytes(spendingPrivateKey.slice(2))
|
|
995
|
+
const viewingPrivBytes = hexToBytes(viewingPrivateKey.slice(2))
|
|
996
|
+
const ephemeralPubBytes = hexToBytes(stealthAddress.ephemeralPublicKey.slice(2))
|
|
997
|
+
|
|
998
|
+
try {
|
|
999
|
+
// Get spending scalar from private key and reduce mod L
|
|
1000
|
+
const rawSpendingScalar = getEd25519Scalar(spendingPrivBytes)
|
|
1001
|
+
const spendingScalar = rawSpendingScalar % ED25519_ORDER
|
|
1002
|
+
if (spendingScalar === 0n) {
|
|
1003
|
+
throw new Error('CRITICAL: Zero spending scalar after reduction - investigate key derivation')
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Compute shared secret: S = spending_scalar * R
|
|
1007
|
+
const ephemeralPoint = ed25519.ExtendedPoint.fromHex(ephemeralPubBytes)
|
|
1008
|
+
const sharedSecretPoint = ephemeralPoint.multiply(spendingScalar)
|
|
1009
|
+
|
|
1010
|
+
// Hash the shared secret
|
|
1011
|
+
const sharedSecretHash = sha256(sharedSecretPoint.toRawBytes())
|
|
1012
|
+
|
|
1013
|
+
// View tag check (optimization - reject quickly if doesn't match)
|
|
1014
|
+
if (sharedSecretHash[0] !== stealthAddress.viewTag) {
|
|
1015
|
+
return false
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Full check: derive the expected stealth address
|
|
1019
|
+
const rawViewingScalar = getEd25519Scalar(viewingPrivBytes)
|
|
1020
|
+
const viewingScalar = rawViewingScalar % ED25519_ORDER
|
|
1021
|
+
if (viewingScalar === 0n) {
|
|
1022
|
+
throw new Error('CRITICAL: Zero viewing scalar after reduction - investigate key derivation')
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const hashScalar = bytesToBigInt(sharedSecretHash) % ED25519_ORDER
|
|
1026
|
+
if (hashScalar === 0n) {
|
|
1027
|
+
throw new Error('CRITICAL: Zero hash scalar after reduction - investigate hash computation')
|
|
1028
|
+
}
|
|
1029
|
+
const stealthPrivateScalar = (viewingScalar + hashScalar) % ED25519_ORDER
|
|
1030
|
+
if (stealthPrivateScalar === 0n) {
|
|
1031
|
+
throw new Error('CRITICAL: Zero stealth scalar after reduction - investigate key derivation')
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Compute expected public key from derived scalar
|
|
1035
|
+
const expectedPubKey = ed25519.ExtendedPoint.BASE.multiply(stealthPrivateScalar)
|
|
1036
|
+
const expectedPubKeyBytes = expectedPubKey.toRawBytes()
|
|
1037
|
+
|
|
1038
|
+
// Compare with provided stealth address
|
|
1039
|
+
const providedAddress = hexToBytes(stealthAddress.address.slice(2))
|
|
1040
|
+
|
|
1041
|
+
return bytesToHex(expectedPubKeyBytes) === bytesToHex(providedAddress)
|
|
1042
|
+
} finally {
|
|
1043
|
+
// Securely wipe input private key buffers
|
|
1044
|
+
secureWipeAll(spendingPrivBytes, viewingPrivBytes)
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// ─── Base58 Encoding for Solana ────────────────────────────────────────────────
|
|
1049
|
+
|
|
1050
|
+
/** Base58 alphabet (Bitcoin/Solana standard) */
|
|
1051
|
+
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Encode bytes to base58 string
|
|
1055
|
+
* Used for Solana address encoding
|
|
1056
|
+
*/
|
|
1057
|
+
function bytesToBase58(bytes: Uint8Array): string {
|
|
1058
|
+
// Count leading zeros
|
|
1059
|
+
let leadingZeros = 0
|
|
1060
|
+
for (let i = 0; i < bytes.length && bytes[i] === 0; i++) {
|
|
1061
|
+
leadingZeros++
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Convert bytes to bigint
|
|
1065
|
+
let value = 0n
|
|
1066
|
+
for (const byte of bytes) {
|
|
1067
|
+
value = value * 256n + BigInt(byte)
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Convert to base58
|
|
1071
|
+
let result = ''
|
|
1072
|
+
while (value > 0n) {
|
|
1073
|
+
const remainder = value % 58n
|
|
1074
|
+
value = value / 58n
|
|
1075
|
+
result = BASE58_ALPHABET[Number(remainder)] + result
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// Add leading '1's for each leading zero byte
|
|
1079
|
+
return '1'.repeat(leadingZeros) + result
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* Decode base58 string to bytes
|
|
1084
|
+
* Used for Solana address validation
|
|
1085
|
+
*/
|
|
1086
|
+
function base58ToBytes(str: string): Uint8Array {
|
|
1087
|
+
// Count leading '1's (they represent leading zero bytes)
|
|
1088
|
+
let leadingOnes = 0
|
|
1089
|
+
for (let i = 0; i < str.length && str[i] === '1'; i++) {
|
|
1090
|
+
leadingOnes++
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// Convert from base58 to bigint
|
|
1094
|
+
let value = 0n
|
|
1095
|
+
for (const char of str) {
|
|
1096
|
+
const index = BASE58_ALPHABET.indexOf(char)
|
|
1097
|
+
if (index === -1) {
|
|
1098
|
+
throw new ValidationError(`Invalid base58 character: ${char}`, 'address')
|
|
1099
|
+
}
|
|
1100
|
+
value = value * 58n + BigInt(index)
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// Convert bigint to bytes
|
|
1104
|
+
const bytes: number[] = []
|
|
1105
|
+
while (value > 0n) {
|
|
1106
|
+
bytes.unshift(Number(value % 256n))
|
|
1107
|
+
value = value / 256n
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Add leading zeros
|
|
1111
|
+
const result = new Uint8Array(leadingOnes + bytes.length)
|
|
1112
|
+
for (let i = 0; i < leadingOnes; i++) {
|
|
1113
|
+
result[i] = 0
|
|
1114
|
+
}
|
|
1115
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1116
|
+
result[leadingOnes + i] = bytes[i]
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
return result
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// ─── Solana Address Derivation ─────────────────────────────────────────────────
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Convert an ed25519 public key (hex) to a Solana address (base58)
|
|
1126
|
+
*
|
|
1127
|
+
* Solana addresses are base58-encoded 32-byte ed25519 public keys.
|
|
1128
|
+
*
|
|
1129
|
+
* @param publicKey - 32-byte ed25519 public key as hex string (with 0x prefix)
|
|
1130
|
+
* @returns Base58-encoded Solana address
|
|
1131
|
+
* @throws {ValidationError} If public key is invalid
|
|
1132
|
+
*
|
|
1133
|
+
* @example
|
|
1134
|
+
* ```typescript
|
|
1135
|
+
* const { stealthAddress } = generateEd25519StealthAddress(metaAddress)
|
|
1136
|
+
* const solanaAddress = ed25519PublicKeyToSolanaAddress(stealthAddress.address)
|
|
1137
|
+
* // Returns: "7Vbmv1jt4vyuqBZcpYPpnVhrqVe5e6ZPBJCyqLqzQPvN" (example)
|
|
1138
|
+
* ```
|
|
1139
|
+
*/
|
|
1140
|
+
export function ed25519PublicKeyToSolanaAddress(publicKey: HexString): string {
|
|
1141
|
+
// Validate input
|
|
1142
|
+
if (!isValidHex(publicKey)) {
|
|
1143
|
+
throw new ValidationError(
|
|
1144
|
+
'publicKey must be a valid hex string with 0x prefix',
|
|
1145
|
+
'publicKey'
|
|
1146
|
+
)
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
if (!isValidEd25519PublicKey(publicKey)) {
|
|
1150
|
+
throw new ValidationError(
|
|
1151
|
+
'publicKey must be 32 bytes (64 hex characters)',
|
|
1152
|
+
'publicKey'
|
|
1153
|
+
)
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Convert hex to bytes (remove 0x prefix)
|
|
1157
|
+
const publicKeyBytes = hexToBytes(publicKey.slice(2))
|
|
1158
|
+
|
|
1159
|
+
// Encode as base58
|
|
1160
|
+
return bytesToBase58(publicKeyBytes)
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Validate a Solana address format
|
|
1165
|
+
*
|
|
1166
|
+
* Checks that the address:
|
|
1167
|
+
* - Is a valid base58 string
|
|
1168
|
+
* - Decodes to exactly 32 bytes (ed25519 public key size)
|
|
1169
|
+
*
|
|
1170
|
+
* @param address - Base58-encoded Solana address
|
|
1171
|
+
* @returns true if valid, false otherwise
|
|
1172
|
+
*
|
|
1173
|
+
* @example
|
|
1174
|
+
* ```typescript
|
|
1175
|
+
* isValidSolanaAddress('7Vbmv1jt4vyuqBZcpYPpnVhrqVe5e6ZPBJCyqLqzQPvN') // true
|
|
1176
|
+
* isValidSolanaAddress('invalid') // false
|
|
1177
|
+
* ```
|
|
1178
|
+
*/
|
|
1179
|
+
export function isValidSolanaAddress(address: string): boolean {
|
|
1180
|
+
if (typeof address !== 'string' || address.length === 0) {
|
|
1181
|
+
return false
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// Solana addresses are typically 32-44 characters
|
|
1185
|
+
if (address.length < 32 || address.length > 44) {
|
|
1186
|
+
return false
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
try {
|
|
1190
|
+
const decoded = base58ToBytes(address)
|
|
1191
|
+
// Valid Solana address is exactly 32 bytes
|
|
1192
|
+
return decoded.length === 32
|
|
1193
|
+
} catch {
|
|
1194
|
+
return false
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* Convert a Solana address (base58) back to ed25519 public key (hex)
|
|
1200
|
+
*
|
|
1201
|
+
* @param address - Base58-encoded Solana address
|
|
1202
|
+
* @returns 32-byte ed25519 public key as hex string (with 0x prefix)
|
|
1203
|
+
* @throws {ValidationError} If address is invalid
|
|
1204
|
+
*
|
|
1205
|
+
* @example
|
|
1206
|
+
* ```typescript
|
|
1207
|
+
* const publicKey = solanaAddressToEd25519PublicKey('7Vbmv1jt4vyuqBZcpYPpnVhrqVe5e6ZPBJCyqLqzQPvN')
|
|
1208
|
+
* // Returns: "0x..." (64 hex characters)
|
|
1209
|
+
* ```
|
|
1210
|
+
*/
|
|
1211
|
+
export function solanaAddressToEd25519PublicKey(address: string): HexString {
|
|
1212
|
+
if (!isValidSolanaAddress(address)) {
|
|
1213
|
+
throw new ValidationError(
|
|
1214
|
+
'Invalid Solana address format',
|
|
1215
|
+
'address'
|
|
1216
|
+
)
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const decoded = base58ToBytes(address)
|
|
1220
|
+
return `0x${bytesToHex(decoded)}` as HexString
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// ─── NEAR Address Derivation ────────────────────────────────────────────────────
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* Convert ed25519 public key to NEAR implicit account address
|
|
1227
|
+
*
|
|
1228
|
+
* NEAR implicit accounts are lowercase hex-encoded ed25519 public keys (64 characters).
|
|
1229
|
+
* No prefix, just raw 32 bytes as lowercase hex.
|
|
1230
|
+
*
|
|
1231
|
+
* @param publicKey - 32-byte ed25519 public key as hex string (with 0x prefix)
|
|
1232
|
+
* @returns NEAR implicit account address (64 lowercase hex characters, no prefix)
|
|
1233
|
+
* @throws {ValidationError} If public key is invalid
|
|
1234
|
+
*
|
|
1235
|
+
* @example
|
|
1236
|
+
* ```typescript
|
|
1237
|
+
* const { stealthAddress } = generateEd25519StealthAddress(metaAddress)
|
|
1238
|
+
* const nearAddress = ed25519PublicKeyToNearAddress(stealthAddress.address)
|
|
1239
|
+
* // Returns: "ab12cd34..." (64 hex chars)
|
|
1240
|
+
* ```
|
|
1241
|
+
*/
|
|
1242
|
+
export function ed25519PublicKeyToNearAddress(publicKey: HexString): string {
|
|
1243
|
+
// Validate input
|
|
1244
|
+
if (!isValidHex(publicKey)) {
|
|
1245
|
+
throw new ValidationError(
|
|
1246
|
+
'publicKey must be a valid hex string with 0x prefix',
|
|
1247
|
+
'publicKey'
|
|
1248
|
+
)
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
if (!isValidEd25519PublicKey(publicKey)) {
|
|
1252
|
+
throw new ValidationError(
|
|
1253
|
+
'publicKey must be 32 bytes (64 hex characters)',
|
|
1254
|
+
'publicKey'
|
|
1255
|
+
)
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// NEAR implicit accounts are lowercase hex without 0x prefix
|
|
1259
|
+
return publicKey.slice(2).toLowerCase()
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/**
|
|
1263
|
+
* Convert NEAR implicit account address back to ed25519 public key
|
|
1264
|
+
*
|
|
1265
|
+
* @param address - NEAR implicit account address (64 hex characters)
|
|
1266
|
+
* @returns ed25519 public key as HexString (with 0x prefix)
|
|
1267
|
+
* @throws {ValidationError} If address is invalid
|
|
1268
|
+
*
|
|
1269
|
+
* @example
|
|
1270
|
+
* ```typescript
|
|
1271
|
+
* const publicKey = nearAddressToEd25519PublicKey("ab12cd34...")
|
|
1272
|
+
* // Returns: "0xab12cd34..."
|
|
1273
|
+
* ```
|
|
1274
|
+
*/
|
|
1275
|
+
export function nearAddressToEd25519PublicKey(address: string): HexString {
|
|
1276
|
+
if (!isValidNearImplicitAddress(address)) {
|
|
1277
|
+
throw new ValidationError(
|
|
1278
|
+
'Invalid NEAR implicit address format',
|
|
1279
|
+
'address'
|
|
1280
|
+
)
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
return `0x${address.toLowerCase()}` as HexString
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/**
|
|
1287
|
+
* Validate a NEAR implicit account address
|
|
1288
|
+
*
|
|
1289
|
+
* NEAR implicit accounts are:
|
|
1290
|
+
* - Exactly 64 lowercase hex characters
|
|
1291
|
+
* - No prefix (no "0x")
|
|
1292
|
+
* - Represent a 32-byte ed25519 public key
|
|
1293
|
+
*
|
|
1294
|
+
* @param address - Address to validate
|
|
1295
|
+
* @returns true if valid NEAR implicit account address
|
|
1296
|
+
*
|
|
1297
|
+
* @example
|
|
1298
|
+
* ```typescript
|
|
1299
|
+
* isValidNearImplicitAddress("ab12cd34ef...") // true (64 hex chars)
|
|
1300
|
+
* isValidNearImplicitAddress("0xab12...") // false (has prefix)
|
|
1301
|
+
* isValidNearImplicitAddress("alice.near") // false (named account)
|
|
1302
|
+
* isValidNearImplicitAddress("AB12CD...") // false (uppercase)
|
|
1303
|
+
* ```
|
|
1304
|
+
*/
|
|
1305
|
+
export function isValidNearImplicitAddress(address: string): boolean {
|
|
1306
|
+
// Must be a string
|
|
1307
|
+
if (typeof address !== 'string' || address.length === 0) {
|
|
1308
|
+
return false
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// Must be exactly 64 characters (32 bytes as hex)
|
|
1312
|
+
if (address.length !== 64) {
|
|
1313
|
+
return false
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// Must be lowercase hex only (no 0x prefix)
|
|
1317
|
+
return /^[0-9a-f]{64}$/.test(address)
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Check if a string is a valid NEAR account ID (named or implicit)
|
|
1322
|
+
*
|
|
1323
|
+
* Supports both:
|
|
1324
|
+
* - Named accounts: alice.near, bob.testnet
|
|
1325
|
+
* - Implicit accounts: 64 hex characters
|
|
1326
|
+
*
|
|
1327
|
+
* @param accountId - Account ID to validate
|
|
1328
|
+
* @returns true if valid NEAR account ID
|
|
1329
|
+
*
|
|
1330
|
+
* @example
|
|
1331
|
+
* ```typescript
|
|
1332
|
+
* isValidNearAccountId("alice.near") // true
|
|
1333
|
+
* isValidNearAccountId("bob.testnet") // true
|
|
1334
|
+
* isValidNearAccountId("ab12cd34...") // true (64 hex chars)
|
|
1335
|
+
* isValidNearAccountId("ALICE.near") // false (uppercase)
|
|
1336
|
+
* isValidNearAccountId("a") // false (too short)
|
|
1337
|
+
* ```
|
|
1338
|
+
*/
|
|
1339
|
+
export function isValidNearAccountId(accountId: string): boolean {
|
|
1340
|
+
// Must be a string
|
|
1341
|
+
if (typeof accountId !== 'string' || accountId.length === 0) {
|
|
1342
|
+
return false
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// Check if it's a valid implicit account (64 hex chars)
|
|
1346
|
+
if (isValidNearImplicitAddress(accountId)) {
|
|
1347
|
+
return true
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// Named accounts: 2-64 characters, lowercase alphanumeric with . _ -
|
|
1351
|
+
// Must start and end with alphanumeric
|
|
1352
|
+
if (accountId.length < 2 || accountId.length > 64) {
|
|
1353
|
+
return false
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// NEAR account ID pattern
|
|
1357
|
+
const nearAccountPattern = /^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/
|
|
1358
|
+
if (!nearAccountPattern.test(accountId)) {
|
|
1359
|
+
return false
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// Cannot have consecutive dots
|
|
1363
|
+
if (accountId.includes('..')) {
|
|
1364
|
+
return false
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
return true
|
|
1368
|
+
}
|