@sip-protocol/sdk 0.2.6 → 0.2.8
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/dist/browser.js +140 -15
- package/dist/browser.mjs +1 -1
- package/dist/chunk-5BAS4D44.mjs +10283 -0
- package/dist/chunk-UPTISVCY.mjs +10304 -0
- package/dist/index.js +140 -15
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/src/adapters/near-intents.ts +56 -4
- package/src/validation.ts +126 -0
package/dist/index.js
CHANGED
|
@@ -644,6 +644,91 @@ function validateScalar(value, field) {
|
|
|
644
644
|
);
|
|
645
645
|
}
|
|
646
646
|
}
|
|
647
|
+
function isValidEvmAddress(address) {
|
|
648
|
+
if (typeof address !== "string") return false;
|
|
649
|
+
return /^0x[0-9a-fA-F]{40}$/.test(address);
|
|
650
|
+
}
|
|
651
|
+
function isValidSolanaAddressFormat(address) {
|
|
652
|
+
if (typeof address !== "string") return false;
|
|
653
|
+
return /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address);
|
|
654
|
+
}
|
|
655
|
+
function isValidNearAddressFormat(address) {
|
|
656
|
+
if (typeof address !== "string") return false;
|
|
657
|
+
if (/^[0-9a-f]{64}$/.test(address)) return true;
|
|
658
|
+
if (address.length < 2 || address.length > 64) return false;
|
|
659
|
+
if (!/^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/.test(address)) return false;
|
|
660
|
+
if (address.includes("..")) return false;
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
function getChainAddressType(chain) {
|
|
664
|
+
switch (chain) {
|
|
665
|
+
case "ethereum":
|
|
666
|
+
case "polygon":
|
|
667
|
+
case "arbitrum":
|
|
668
|
+
case "optimism":
|
|
669
|
+
case "base":
|
|
670
|
+
return "evm";
|
|
671
|
+
case "solana":
|
|
672
|
+
return "solana";
|
|
673
|
+
case "near":
|
|
674
|
+
return "near";
|
|
675
|
+
case "zcash":
|
|
676
|
+
return "zcash";
|
|
677
|
+
default:
|
|
678
|
+
return "unknown";
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
function validateAddressForChain(address, chain, field = "address") {
|
|
682
|
+
const addressType = getChainAddressType(chain);
|
|
683
|
+
switch (addressType) {
|
|
684
|
+
case "evm":
|
|
685
|
+
if (!isValidEvmAddress(address)) {
|
|
686
|
+
throw new ValidationError(
|
|
687
|
+
`Invalid address format for ${chain}. Expected EVM address (0x + 40 hex chars), got: ${address.slice(0, 20)}...`,
|
|
688
|
+
field,
|
|
689
|
+
{ chain, expectedFormat: "0x...", receivedFormat: address.startsWith("0x") ? "hex but wrong length" : "not hex" }
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
break;
|
|
693
|
+
case "solana":
|
|
694
|
+
if (!isValidSolanaAddressFormat(address)) {
|
|
695
|
+
throw new ValidationError(
|
|
696
|
+
`Invalid address format for ${chain}. Expected Solana address (base58, 32-44 chars), got: ${address.slice(0, 20)}...`,
|
|
697
|
+
field,
|
|
698
|
+
{ chain, expectedFormat: "base58", receivedFormat: address.startsWith("0x") ? "looks like EVM" : "unknown" }
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
break;
|
|
702
|
+
case "near":
|
|
703
|
+
if (!isValidNearAddressFormat(address)) {
|
|
704
|
+
throw new ValidationError(
|
|
705
|
+
`Invalid address format for ${chain}. Expected NEAR account ID (named or implicit), got: ${address.slice(0, 20)}...`,
|
|
706
|
+
field,
|
|
707
|
+
{ chain, expectedFormat: "alice.near or 64 hex chars" }
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
break;
|
|
711
|
+
case "zcash":
|
|
712
|
+
if (!address || address.length === 0) {
|
|
713
|
+
throw new ValidationError(
|
|
714
|
+
`Invalid address format for ${chain}. Expected Zcash address.`,
|
|
715
|
+
field,
|
|
716
|
+
{ chain }
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
break;
|
|
720
|
+
default:
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
function isAddressValidForChain(address, chain) {
|
|
725
|
+
try {
|
|
726
|
+
validateAddressForChain(address, chain);
|
|
727
|
+
return true;
|
|
728
|
+
} catch {
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
647
732
|
|
|
648
733
|
// src/secure-memory.ts
|
|
649
734
|
var import_utils = require("@noble/hashes/utils");
|
|
@@ -2371,20 +2456,43 @@ var DEFAULT_ASSET_MAPPINGS = {
|
|
|
2371
2456
|
// NEAR assets
|
|
2372
2457
|
"near:NEAR": "nep141:wrap.near",
|
|
2373
2458
|
"near:wNEAR": "nep141:wrap.near",
|
|
2459
|
+
"near:USDC": "nep141:17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1",
|
|
2374
2460
|
// Ethereum assets (via OMFT bridge)
|
|
2375
2461
|
"ethereum:ETH": "nep141:eth.omft.near",
|
|
2376
|
-
"ethereum:USDC": "nep141:
|
|
2377
|
-
"ethereum:USDT": "nep141:
|
|
2462
|
+
"ethereum:USDC": "nep141:eth-0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.omft.near",
|
|
2463
|
+
"ethereum:USDT": "nep141:eth-0xdac17f958d2ee523a2206206994597c13d831ec7.omft.near",
|
|
2378
2464
|
// Solana assets (via OMFT bridge)
|
|
2379
2465
|
"solana:SOL": "nep141:sol.omft.near",
|
|
2466
|
+
"solana:USDC": "nep141:sol-5ce3bf3a31af18be40ba30f721101b4341690186.omft.near",
|
|
2467
|
+
"solana:USDT": "nep141:sol-c800a4bd850783ccb82c2b2c7e84175443606352.omft.near",
|
|
2380
2468
|
// Zcash assets
|
|
2381
2469
|
"zcash:ZEC": "nep141:zec.omft.near",
|
|
2382
2470
|
// Arbitrum assets
|
|
2383
2471
|
"arbitrum:ETH": "nep141:arb.omft.near",
|
|
2472
|
+
"arbitrum:ARB": "nep141:arb-0x912ce59144191c1204e64559fe8253a0e49e6548.omft.near",
|
|
2473
|
+
"arbitrum:USDC": "nep141:arb-0xaf88d065e77c8cc2239327c5edb3a432268e5831.omft.near",
|
|
2384
2474
|
// Base assets
|
|
2385
2475
|
"base:ETH": "nep141:base.omft.near",
|
|
2386
|
-
|
|
2387
|
-
|
|
2476
|
+
"base:USDC": "nep141:base-0x833589fcd6edb6e08f4c7c32d4f71b54bda02913.omft.near",
|
|
2477
|
+
// Optimism assets (via HOT bridge - uses nep245)
|
|
2478
|
+
"optimism:ETH": "nep245:v2_1.omni.hot.tg:10_11111111111111111111",
|
|
2479
|
+
"optimism:OP": "nep245:v2_1.omni.hot.tg:10_vLAiSt9KfUGKpw5cD3vsSyNYBo7",
|
|
2480
|
+
"optimism:USDC": "nep245:v2_1.omni.hot.tg:10_A2ewyUyDp6qsue1jqZsGypkCxRJ",
|
|
2481
|
+
// Polygon assets (via HOT bridge - uses nep245)
|
|
2482
|
+
"polygon:POL": "nep245:v2_1.omni.hot.tg:137_11111111111111111111",
|
|
2483
|
+
"polygon:MATIC": "nep245:v2_1.omni.hot.tg:137_11111111111111111111",
|
|
2484
|
+
// POL is the rebranded MATIC
|
|
2485
|
+
"polygon:USDC": "nep245:v2_1.omni.hot.tg:137_qiStmoQJDQPTebaPjgx5VBxZv6L",
|
|
2486
|
+
// BNB Chain assets (via HOT bridge - uses nep245)
|
|
2487
|
+
"bsc:BNB": "nep245:v2_1.omni.hot.tg:56_11111111111111111111",
|
|
2488
|
+
"bsc:USDC": "nep245:v2_1.omni.hot.tg:56_2w93GqMcEmQFDru84j3HZZWt557r",
|
|
2489
|
+
// Avalanche assets (via HOT bridge - uses nep245)
|
|
2490
|
+
"avalanche:AVAX": "nep245:v2_1.omni.hot.tg:43114_11111111111111111111",
|
|
2491
|
+
"avalanche:USDC": "nep245:v2_1.omni.hot.tg:43114_3atVJH3r5c4GqiSYmg9fECvjc47o",
|
|
2492
|
+
// Bitcoin
|
|
2493
|
+
"bitcoin:BTC": "nep141:btc.omft.near",
|
|
2494
|
+
// Aptos
|
|
2495
|
+
"aptos:APT": "nep141:aptos.omft.near"
|
|
2388
2496
|
};
|
|
2389
2497
|
var CHAIN_BLOCKCHAIN_MAP = {
|
|
2390
2498
|
near: "near",
|
|
@@ -2432,6 +2540,23 @@ var NEARIntentsAdapter = class {
|
|
|
2432
2540
|
*/
|
|
2433
2541
|
async prepareSwap(request, recipientMetaAddress, senderAddress) {
|
|
2434
2542
|
this.validateRequest(request);
|
|
2543
|
+
const inputChain = request.inputAsset.chain;
|
|
2544
|
+
if (senderAddress) {
|
|
2545
|
+
if (!isAddressValidForChain(senderAddress, inputChain)) {
|
|
2546
|
+
const inputChainType = getChainAddressType(inputChain);
|
|
2547
|
+
const senderFormat = senderAddress.startsWith("0x") ? "EVM" : /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(senderAddress) ? "Solana" : /^[0-9a-f]{64}$/.test(senderAddress) || /^[a-z0-9._-]+$/.test(senderAddress) ? "NEAR" : "unknown";
|
|
2548
|
+
throw new ValidationError(
|
|
2549
|
+
`Wallet address format doesn't match input chain. You're swapping FROM ${inputChain} (${inputChainType} format) but your connected wallet uses ${senderFormat} format. Please connect a wallet that matches the source chain (${inputChain}).`,
|
|
2550
|
+
"senderAddress",
|
|
2551
|
+
{
|
|
2552
|
+
inputChain,
|
|
2553
|
+
expectedFormat: inputChainType,
|
|
2554
|
+
receivedFormat: senderFormat,
|
|
2555
|
+
hint: `For ${inputChain} swaps, connect a ${inputChainType === "evm" ? "MetaMask or EVM" : inputChainType} wallet.`
|
|
2556
|
+
}
|
|
2557
|
+
);
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2435
2560
|
let recipientAddress;
|
|
2436
2561
|
let refundAddress = senderAddress;
|
|
2437
2562
|
let stealthData;
|
|
@@ -2496,22 +2621,22 @@ var NEARIntentsAdapter = class {
|
|
|
2496
2621
|
);
|
|
2497
2622
|
}
|
|
2498
2623
|
if (!senderAddress) {
|
|
2499
|
-
const
|
|
2500
|
-
const inputChainType = CHAIN_BLOCKCHAIN_MAP[
|
|
2501
|
-
if (isEd25519Chain(
|
|
2624
|
+
const inputChain2 = request.inputAsset.chain;
|
|
2625
|
+
const inputChainType = CHAIN_BLOCKCHAIN_MAP[inputChain2];
|
|
2626
|
+
if (isEd25519Chain(inputChain2)) {
|
|
2502
2627
|
const inputKeyBytes = (metaAddr.spendingKey.length - 2) / 2;
|
|
2503
2628
|
if (inputKeyBytes === 32) {
|
|
2504
2629
|
const refundStealth = generateEd25519StealthAddress(metaAddr);
|
|
2505
|
-
if (
|
|
2630
|
+
if (inputChain2 === "solana") {
|
|
2506
2631
|
refundAddress = ed25519PublicKeyToSolanaAddress(refundStealth.stealthAddress.address);
|
|
2507
|
-
} else if (
|
|
2632
|
+
} else if (inputChain2 === "near") {
|
|
2508
2633
|
refundAddress = ed25519PublicKeyToNearAddress(refundStealth.stealthAddress.address);
|
|
2509
2634
|
}
|
|
2510
2635
|
} else {
|
|
2511
2636
|
throw new ValidationError(
|
|
2512
|
-
`Cross-curve refunds not supported: input chain ${
|
|
2637
|
+
`Cross-curve refunds not supported: input chain ${inputChain2} requires ed25519 but meta-address uses secp256k1. Please provide a senderAddress for refunds, or use matching curves for input/output chains.`,
|
|
2513
2638
|
"senderAddress",
|
|
2514
|
-
{ inputChain, inputChainType, metaAddressCurve: "secp256k1" }
|
|
2639
|
+
{ inputChain: inputChain2, inputChainType, metaAddressCurve: "secp256k1" }
|
|
2515
2640
|
);
|
|
2516
2641
|
}
|
|
2517
2642
|
} else if (inputChainType === "evm") {
|
|
@@ -2521,16 +2646,16 @@ var NEARIntentsAdapter = class {
|
|
|
2521
2646
|
refundAddress = publicKeyToEthAddress(refundStealth.stealthAddress.address);
|
|
2522
2647
|
} else {
|
|
2523
2648
|
throw new ValidationError(
|
|
2524
|
-
`Cross-curve refunds not supported: input chain ${
|
|
2649
|
+
`Cross-curve refunds not supported: input chain ${inputChain2} requires secp256k1 but meta-address uses ed25519. Please provide a senderAddress for refunds, or use matching curves for input/output chains.`,
|
|
2525
2650
|
"senderAddress",
|
|
2526
|
-
{ inputChain, inputChainType, metaAddressCurve: "ed25519" }
|
|
2651
|
+
{ inputChain: inputChain2, inputChainType, metaAddressCurve: "ed25519" }
|
|
2527
2652
|
);
|
|
2528
2653
|
}
|
|
2529
2654
|
} else {
|
|
2530
2655
|
throw new ValidationError(
|
|
2531
|
-
`senderAddress is required for refunds on ${
|
|
2656
|
+
`senderAddress is required for refunds on ${inputChain2}. Automatic refund address generation is only supported for EVM, Solana, and NEAR chains.`,
|
|
2532
2657
|
"senderAddress",
|
|
2533
|
-
{ inputChain, inputChainType }
|
|
2658
|
+
{ inputChain: inputChain2, inputChainType }
|
|
2534
2659
|
);
|
|
2535
2660
|
}
|
|
2536
2661
|
}
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sip-protocol/sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "Core SDK for Shielded Intents Protocol - Privacy layer for cross-chain transactions",
|
|
5
5
|
"author": "SIP Protocol <hello@sip-protocol.org>",
|
|
6
6
|
"homepage": "https://sip-protocol.org",
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
type StealthCurve,
|
|
41
41
|
} from '../stealth'
|
|
42
42
|
import { ValidationError } from '../errors'
|
|
43
|
+
import { isAddressValidForChain, getChainAddressType } from '../validation'
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* Swap request parameters (simplified interface for adapter)
|
|
@@ -137,26 +138,53 @@ const DEFAULT_ASSET_MAPPINGS: Record<string, DefuseAssetId> = {
|
|
|
137
138
|
// NEAR assets
|
|
138
139
|
'near:NEAR': 'nep141:wrap.near',
|
|
139
140
|
'near:wNEAR': 'nep141:wrap.near',
|
|
141
|
+
'near:USDC': 'nep141:17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1',
|
|
140
142
|
|
|
141
143
|
// Ethereum assets (via OMFT bridge)
|
|
142
144
|
'ethereum:ETH': 'nep141:eth.omft.near',
|
|
143
|
-
'ethereum:USDC': 'nep141:
|
|
144
|
-
'ethereum:USDT': 'nep141:
|
|
145
|
+
'ethereum:USDC': 'nep141:eth-0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.omft.near',
|
|
146
|
+
'ethereum:USDT': 'nep141:eth-0xdac17f958d2ee523a2206206994597c13d831ec7.omft.near',
|
|
145
147
|
|
|
146
148
|
// Solana assets (via OMFT bridge)
|
|
147
149
|
'solana:SOL': 'nep141:sol.omft.near',
|
|
150
|
+
'solana:USDC': 'nep141:sol-5ce3bf3a31af18be40ba30f721101b4341690186.omft.near',
|
|
151
|
+
'solana:USDT': 'nep141:sol-c800a4bd850783ccb82c2b2c7e84175443606352.omft.near',
|
|
148
152
|
|
|
149
153
|
// Zcash assets
|
|
150
154
|
'zcash:ZEC': 'nep141:zec.omft.near',
|
|
151
155
|
|
|
152
156
|
// Arbitrum assets
|
|
153
157
|
'arbitrum:ETH': 'nep141:arb.omft.near',
|
|
158
|
+
'arbitrum:ARB': 'nep141:arb-0x912ce59144191c1204e64559fe8253a0e49e6548.omft.near',
|
|
159
|
+
'arbitrum:USDC': 'nep141:arb-0xaf88d065e77c8cc2239327c5edb3a432268e5831.omft.near',
|
|
154
160
|
|
|
155
161
|
// Base assets
|
|
156
162
|
'base:ETH': 'nep141:base.omft.near',
|
|
163
|
+
'base:USDC': 'nep141:base-0x833589fcd6edb6e08f4c7c32d4f71b54bda02913.omft.near',
|
|
157
164
|
|
|
158
|
-
//
|
|
159
|
-
'
|
|
165
|
+
// Optimism assets (via HOT bridge - uses nep245)
|
|
166
|
+
'optimism:ETH': 'nep245:v2_1.omni.hot.tg:10_11111111111111111111',
|
|
167
|
+
'optimism:OP': 'nep245:v2_1.omni.hot.tg:10_vLAiSt9KfUGKpw5cD3vsSyNYBo7',
|
|
168
|
+
'optimism:USDC': 'nep245:v2_1.omni.hot.tg:10_A2ewyUyDp6qsue1jqZsGypkCxRJ',
|
|
169
|
+
|
|
170
|
+
// Polygon assets (via HOT bridge - uses nep245)
|
|
171
|
+
'polygon:POL': 'nep245:v2_1.omni.hot.tg:137_11111111111111111111',
|
|
172
|
+
'polygon:MATIC': 'nep245:v2_1.omni.hot.tg:137_11111111111111111111', // POL is the rebranded MATIC
|
|
173
|
+
'polygon:USDC': 'nep245:v2_1.omni.hot.tg:137_qiStmoQJDQPTebaPjgx5VBxZv6L',
|
|
174
|
+
|
|
175
|
+
// BNB Chain assets (via HOT bridge - uses nep245)
|
|
176
|
+
'bsc:BNB': 'nep245:v2_1.omni.hot.tg:56_11111111111111111111',
|
|
177
|
+
'bsc:USDC': 'nep245:v2_1.omni.hot.tg:56_2w93GqMcEmQFDru84j3HZZWt557r',
|
|
178
|
+
|
|
179
|
+
// Avalanche assets (via HOT bridge - uses nep245)
|
|
180
|
+
'avalanche:AVAX': 'nep245:v2_1.omni.hot.tg:43114_11111111111111111111',
|
|
181
|
+
'avalanche:USDC': 'nep245:v2_1.omni.hot.tg:43114_3atVJH3r5c4GqiSYmg9fECvjc47o',
|
|
182
|
+
|
|
183
|
+
// Bitcoin
|
|
184
|
+
'bitcoin:BTC': 'nep141:btc.omft.near',
|
|
185
|
+
|
|
186
|
+
// Aptos
|
|
187
|
+
'aptos:APT': 'nep141:aptos.omft.near',
|
|
160
188
|
}
|
|
161
189
|
|
|
162
190
|
/**
|
|
@@ -247,6 +275,30 @@ export class NEARIntentsAdapter {
|
|
|
247
275
|
// Validate request
|
|
248
276
|
this.validateRequest(request)
|
|
249
277
|
|
|
278
|
+
// Validate senderAddress matches input chain if provided
|
|
279
|
+
const inputChain = request.inputAsset.chain
|
|
280
|
+
if (senderAddress) {
|
|
281
|
+
if (!isAddressValidForChain(senderAddress, inputChain)) {
|
|
282
|
+
const inputChainType = getChainAddressType(inputChain)
|
|
283
|
+
const senderFormat = senderAddress.startsWith('0x') ? 'EVM' :
|
|
284
|
+
/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(senderAddress) ? 'Solana' :
|
|
285
|
+
/^[0-9a-f]{64}$/.test(senderAddress) || /^[a-z0-9._-]+$/.test(senderAddress) ? 'NEAR' : 'unknown'
|
|
286
|
+
|
|
287
|
+
throw new ValidationError(
|
|
288
|
+
`Wallet address format doesn't match input chain. ` +
|
|
289
|
+
`You're swapping FROM ${inputChain} (${inputChainType} format) but your connected wallet uses ${senderFormat} format. ` +
|
|
290
|
+
`Please connect a wallet that matches the source chain (${inputChain}).`,
|
|
291
|
+
'senderAddress',
|
|
292
|
+
{
|
|
293
|
+
inputChain,
|
|
294
|
+
expectedFormat: inputChainType,
|
|
295
|
+
receivedFormat: senderFormat,
|
|
296
|
+
hint: `For ${inputChain} swaps, connect a ${inputChainType === 'evm' ? 'MetaMask or EVM' : inputChainType} wallet.`
|
|
297
|
+
}
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
250
302
|
// Determine recipient address
|
|
251
303
|
let recipientAddress: string
|
|
252
304
|
let refundAddress: string | undefined = senderAddress
|
package/src/validation.ts
CHANGED
|
@@ -377,6 +377,132 @@ export function validateTimestamp(
|
|
|
377
377
|
}
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
+
// ─── Address Format Validation ──────────────────────────────────────────────────
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Check if an address is a valid EVM address (0x + 40 hex chars)
|
|
384
|
+
*/
|
|
385
|
+
export function isValidEvmAddress(address: string): boolean {
|
|
386
|
+
if (typeof address !== 'string') return false
|
|
387
|
+
return /^0x[0-9a-fA-F]{40}$/.test(address)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Check if an address is a valid Solana address (base58, 32-44 chars)
|
|
392
|
+
*/
|
|
393
|
+
export function isValidSolanaAddressFormat(address: string): boolean {
|
|
394
|
+
if (typeof address !== 'string') return false
|
|
395
|
+
// Solana addresses are base58-encoded 32-byte public keys
|
|
396
|
+
// Typically 32-44 characters, using base58 alphabet (no 0, O, I, l)
|
|
397
|
+
return /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Check if an address is a valid NEAR account ID (named or implicit)
|
|
402
|
+
*/
|
|
403
|
+
export function isValidNearAddressFormat(address: string): boolean {
|
|
404
|
+
if (typeof address !== 'string') return false
|
|
405
|
+
|
|
406
|
+
// Implicit account: 64 hex characters
|
|
407
|
+
if (/^[0-9a-f]{64}$/.test(address)) return true
|
|
408
|
+
|
|
409
|
+
// Named account: 2-64 chars, lowercase alphanumeric with . _ -
|
|
410
|
+
if (address.length < 2 || address.length > 64) return false
|
|
411
|
+
if (!/^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/.test(address)) return false
|
|
412
|
+
if (address.includes('..')) return false
|
|
413
|
+
|
|
414
|
+
return true
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Get the expected address format for a chain
|
|
419
|
+
*/
|
|
420
|
+
export function getChainAddressType(chain: ChainId): 'evm' | 'solana' | 'near' | 'zcash' | 'unknown' {
|
|
421
|
+
switch (chain) {
|
|
422
|
+
case 'ethereum':
|
|
423
|
+
case 'polygon':
|
|
424
|
+
case 'arbitrum':
|
|
425
|
+
case 'optimism':
|
|
426
|
+
case 'base':
|
|
427
|
+
return 'evm'
|
|
428
|
+
case 'solana':
|
|
429
|
+
return 'solana'
|
|
430
|
+
case 'near':
|
|
431
|
+
return 'near'
|
|
432
|
+
case 'zcash':
|
|
433
|
+
return 'zcash'
|
|
434
|
+
default:
|
|
435
|
+
return 'unknown'
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Validate that an address matches the expected chain format
|
|
441
|
+
*
|
|
442
|
+
* @param address - The address to validate
|
|
443
|
+
* @param chain - The chain the address should be valid for
|
|
444
|
+
* @param field - Field name for error messages
|
|
445
|
+
* @throws {ValidationError} If address format doesn't match chain
|
|
446
|
+
*/
|
|
447
|
+
export function validateAddressForChain(address: string, chain: ChainId, field: string = 'address'): void {
|
|
448
|
+
const addressType = getChainAddressType(chain)
|
|
449
|
+
|
|
450
|
+
switch (addressType) {
|
|
451
|
+
case 'evm':
|
|
452
|
+
if (!isValidEvmAddress(address)) {
|
|
453
|
+
throw new ValidationError(
|
|
454
|
+
`Invalid address format for ${chain}. Expected EVM address (0x + 40 hex chars), got: ${address.slice(0, 20)}...`,
|
|
455
|
+
field,
|
|
456
|
+
{ chain, expectedFormat: '0x...', receivedFormat: address.startsWith('0x') ? 'hex but wrong length' : 'not hex' }
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
break
|
|
460
|
+
case 'solana':
|
|
461
|
+
if (!isValidSolanaAddressFormat(address)) {
|
|
462
|
+
throw new ValidationError(
|
|
463
|
+
`Invalid address format for ${chain}. Expected Solana address (base58, 32-44 chars), got: ${address.slice(0, 20)}...`,
|
|
464
|
+
field,
|
|
465
|
+
{ chain, expectedFormat: 'base58', receivedFormat: address.startsWith('0x') ? 'looks like EVM' : 'unknown' }
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
break
|
|
469
|
+
case 'near':
|
|
470
|
+
if (!isValidNearAddressFormat(address)) {
|
|
471
|
+
throw new ValidationError(
|
|
472
|
+
`Invalid address format for ${chain}. Expected NEAR account ID (named or implicit), got: ${address.slice(0, 20)}...`,
|
|
473
|
+
field,
|
|
474
|
+
{ chain, expectedFormat: 'alice.near or 64 hex chars' }
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
break
|
|
478
|
+
case 'zcash':
|
|
479
|
+
// Zcash has multiple formats (t-addr, z-addr, u-addr) - accept any non-empty string for now
|
|
480
|
+
if (!address || address.length === 0) {
|
|
481
|
+
throw new ValidationError(
|
|
482
|
+
`Invalid address format for ${chain}. Expected Zcash address.`,
|
|
483
|
+
field,
|
|
484
|
+
{ chain }
|
|
485
|
+
)
|
|
486
|
+
}
|
|
487
|
+
break
|
|
488
|
+
default:
|
|
489
|
+
// Unknown chain - skip validation
|
|
490
|
+
break
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Check if an address format matches a chain (non-throwing version)
|
|
496
|
+
*/
|
|
497
|
+
export function isAddressValidForChain(address: string, chain: ChainId): boolean {
|
|
498
|
+
try {
|
|
499
|
+
validateAddressForChain(address, chain)
|
|
500
|
+
return true
|
|
501
|
+
} catch {
|
|
502
|
+
return false
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
380
506
|
// ─── Composite Validators ──────────────────────────────────────────────────────
|
|
381
507
|
|
|
382
508
|
/**
|