Skip to content

NEAR Same-Chain Privacy Guide

This guide covers how to integrate NEAR same-chain privacy into your dApp using SIP Protocol. You’ll learn to send private transfers, manage viewing keys for compliance, and scan for incoming payments.

Before starting, ensure you have:

  • Node.js 18+ installed
  • A NEAR account (mainnet or testnet)
  • Basic understanding of NEAR transactions
Terminal window
npm install @sip-protocol/sdk @sip-protocol/types

For React applications:

Terminal window
npm install @sip-protocol/sdk @sip-protocol/react
import { SIP, NEARIntentsAdapter } from '@sip-protocol/sdk'
import { PrivacyLevel } from '@sip-protocol/types'
// Create adapter for NEAR Intents (cross-chain settlement)
const nearAdapter = new NEARIntentsAdapter({
jwtToken: process.env.NEAR_INTENTS_JWT,
})
// Create SIP client
const sip = new SIP({
network: 'mainnet',
})

Users need a stealth meta-address to receive private payments:

import {
generateEd25519StealthMetaAddress,
encodeStealthMetaAddress,
} from '@sip-protocol/sdk'
// Generate meta-address for NEAR (uses ed25519)
// Returns: { metaAddress, spendingPrivateKey, viewingPrivateKey }
const metaAddress = generateEd25519StealthMetaAddress('near')
// Encode the public meta-address for sharing
const encoded = encodeStealthMetaAddress(metaAddress.metaAddress)
console.log('Share this address:', encoded)
// Output: sip:near:0x...spendingKey...:0x...viewingKey...
// Store privately (for deriving stealth keys)
// metaAddress.spendingPrivateKey - NEVER share!
// metaAddress.viewingPrivateKey - Share only with auditors
import { PrivacyLevel } from '@sip-protocol/types'
// Prepare private NEAR transfer
const prepared = await nearAdapter.prepareSwap(
{
requestId: crypto.randomUUID(),
privacyLevel: PrivacyLevel.SHIELDED,
inputAsset: { chain: 'near', symbol: 'NEAR', decimals: 24 },
inputAmount: 1_000_000_000_000_000_000_000_000n, // 1 NEAR
outputAsset: { chain: 'near', symbol: 'NEAR', decimals: 24 },
},
recipientMetaAddress, // Recipient's stealth meta-address
senderWalletAddress, // Your wallet for refunds
)
// Get quote and deposit address
const quote = await nearAdapter.getQuote(prepared)
console.log('Deposit to:', quote.depositAddress)
console.log('Amount:', quote.amountIn)
console.log('Expected output:', quote.amountOut)

SIP Protocol supports three privacy levels:

LevelSenderAmountRecipientCompliance
TRANSPARENTVisibleVisibleVisibleN/A
SHIELDEDHiddenHiddenHiddenNone
COMPLIANTHiddenHiddenHiddenViewing key

Standard NEAR transfer with no privacy:

const prepared = await nearAdapter.prepareSwap(
{
requestId: crypto.randomUUID(),
privacyLevel: PrivacyLevel.TRANSPARENT,
inputAsset: { chain: 'near', symbol: 'NEAR', decimals: 24 },
inputAmount: 1_000_000_000_000_000_000_000_000n,
outputAsset: { chain: 'near', symbol: 'NEAR', decimals: 24 },
},
undefined, // No meta-address needed
senderAddress,
recipientAddress, // Direct recipient
)

Full privacy - amount, sender, and recipient are hidden:

const prepared = await nearAdapter.prepareSwap(
{
requestId: crypto.randomUUID(),
privacyLevel: PrivacyLevel.SHIELDED,
inputAsset: { chain: 'near', symbol: 'NEAR', decimals: 24 },
inputAmount: 1_000_000_000_000_000_000_000_000n,
outputAsset: { chain: 'near', symbol: 'NEAR', decimals: 24 },
},
recipientMetaAddress,
senderAddress,
)
// Stealth address is generated automatically
console.log('Stealth recipient:', prepared.stealthAddress?.address)
console.log('Ephemeral key:', prepared.stealthAddress?.ephemeralPublicKey)

Privacy with viewing key for authorized auditors:

import { generateViewingKey, encryptForViewing } from '@sip-protocol/sdk'
// Create a viewing key (returns { key, path, hash })
const viewingKey = generateViewingKey('m/0/auditor')
const prepared = await nearAdapter.prepareSwap(
{
requestId: crypto.randomUUID(),
privacyLevel: PrivacyLevel.COMPLIANT,
inputAsset: { chain: 'near', symbol: 'NEAR', decimals: 24 },
inputAmount: 1_000_000_000_000_000_000_000_000n,
outputAsset: { chain: 'near', symbol: 'NEAR', decimals: 24 },
},
recipientMetaAddress,
senderAddress,
)
// Encrypt transaction details for auditor
const encryptedDetails = encryptForViewing(
{
sender: senderAddress,
recipient: recipientAddress,
amount: '1000000000000000000000000',
timestamp: Date.now(),
},
viewingKey,
)
// Store encryptedDetails on-chain or in database
// Auditor can decrypt with the same viewingKey (decryptWithViewing)

Recipients scan the chain for incoming stealth payments using getTransactionHistory, which derives the stealth addresses that belong to your viewing key and returns the matching transactions:

import {
getTransactionHistory,
deriveStealthPrivateKey,
} from '@sip-protocol/sdk'
// Scan NEAR for transactions belonging to your keys
const history = await getTransactionHistory({
rpcUrl: 'https://rpc.mainnet.near.org',
viewingPrivateKey: metaAddress.viewingPrivateKey,
spendingPrivateKey: metaAddress.spendingPrivateKey,
network: 'mainnet',
limit: 50,
})
for (const tx of history.transactions) {
console.log('Found payment:', {
amount: tx.amountFormatted,
token: tx.token,
txHash: tx.hash,
ephemeralKey: tx.ephemeralPublicKey,
})
// Reconstruct the stealth address from the transaction
const stealthAddress = {
address: tx.stealthPublicKey,
ephemeralPublicKey: tx.ephemeralPublicKey,
viewTag: tx.viewTag,
}
// Derive the private key to claim this payment
const recovery = deriveStealthPrivateKey(
stealthAddress,
metaAddress.spendingPrivateKey,
metaAddress.viewingPrivateKey,
)
// Use recovery.privateKey to sign transactions from this address
}
import { generateViewingKey } from '@sip-protocol/sdk'
const viewingKey = generateViewingKey('m/0/auditor')
// The viewing key is symmetric — there is no public/private split.
// Share the whole key with authorized auditors so they can decrypt.
console.log('Auditor key:', viewingKey.key)
// Hash uniquely identifies which key can decrypt a given payload
console.log('Key hash:', viewingKey.hash)

Share viewing keys for specific transactions:

import { encryptForViewing, decryptWithViewing } from '@sip-protocol/sdk'
// Sender encrypts transaction details with the auditor's viewing key
const encrypted = encryptForViewing(
{
sender: 'alice.near',
recipient: 'bob.near',
amount: '1000000000000000000000000',
timestamp: Date.now(),
},
viewingKey,
)
// Auditor decrypts with the same viewing key
const details = decryptWithViewing(encrypted, viewingKey)
console.log('Transaction details:', details)

Derive a viewing key that is only valid for a specific time window using createTimeWindowedKey. The auditor can decrypt payments within the window, but the key carries no authority outside [windowStart, windowEnd]:

import { createTimeWindowedKey } from '@sip-protocol/sdk'
const now = Math.floor(Date.now() / 1000)
const windowedKey = createTimeWindowedKey(
masterViewingKey.key, // master viewing key (hex)
now, // window start (Unix seconds)
now + 24 * 60 * 60, // window end — 24 hours later
)
console.log('Valid from:', windowedKey.windowStart)
console.log('Valid until:', windowedKey.windowEnd)
console.log('Epochs covered:', windowedKey.epochs)

Privacy works with any NEP-141 token on NEAR:

// Private USDC transfer
const prepared = await nearAdapter.prepareSwap(
{
requestId: crypto.randomUUID(),
privacyLevel: PrivacyLevel.SHIELDED,
inputAsset: {
chain: 'near',
symbol: 'USDC',
decimals: 6,
address: '17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1',
},
inputAmount: 100_000_000n, // 100 USDC
outputAsset: {
chain: 'near',
symbol: 'USDC',
decimals: 6,
address: '17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1',
},
},
recipientMetaAddress,
senderAddress,
)

Use the React hooks for seamless integration:

import {
useSIP,
useStealthAddress,
useViewingKey,
} from '@sip-protocol/react'
function PrivateTransfer() {
const { sendPrivate, isLoading, error } = useSIP()
const { generate, metaAddress } = useStealthAddress()
const { createKey, viewingKey } = useViewingKey()
const handleSend = async () => {
const result = await sendPrivate({
chain: 'near',
recipient: recipientMetaAddress,
amount: '1000000000000000000000000',
token: 'NEAR',
privacyLevel: 'shielded',
})
console.log('Transaction:', result.txHash)
}
return (
<div>
{!metaAddress && (
<button onClick={generate}>Generate Address</button>
)}
<button onClick={handleSend} disabled={isLoading}>
Send Private NEAR
</button>
{error && <p>Error: {error.message}</p>}
</div>
)
}
import { ValidationError, NetworkError, ProofError } from '@sip-protocol/sdk'
try {
const result = await nearAdapter.prepareSwap(params, metaAddress, sender)
} catch (error) {
if (error instanceof ValidationError) {
console.error('Invalid input:', error.field, error.message)
} else if (error instanceof NetworkError) {
console.error('Network issue:', error.message)
// Retry logic
} else if (error instanceof ProofError) {
console.error('Proof generation failed:', error.code)
}
}

“Meta-address has wrong key size”

NEAR uses ed25519 keys (32 bytes). Ensure you generate the correct type:

// Correct for NEAR
const metaAddress = generateEd25519StealthMetaAddress('near')
// Wrong - secp256k1 is for EVM chains
// const metaAddress = generateStealthMetaAddress('near') // Don't use for NEAR!

“Wallet address format doesn’t match input chain”

Your wallet must match the source chain:

// For NEAR swaps, use a NEAR address
const senderAddress = 'yourname.near' // or implicit: 0x...64-hex-chars
// For EVM swaps, use an EVM address
const senderAddress = '0x...' // 40 hex chars

“Cross-curve refunds not supported”

When swapping between chains with different curves:

// NEAR (ed25519) -> Ethereum (secp256k1)
// Must provide explicit senderAddress for refunds
const prepared = await nearAdapter.prepareSwap(
params,
evmMetaAddress,
'yourname.near', // Required! Cannot auto-generate cross-curve refund
)
  1. Store keys securely - Never expose spending private keys. Use secure storage (Keychain, encrypted storage).

  2. Test on mainnet with small amounts - NEAR Intents only works on mainnet. Start with $5-10 for testing.

  3. Implement proper scanning - Scan regularly for incoming payments. Consider background jobs.

  4. Handle viewing keys carefully - Only share with authorized parties. Use time-limited keys when possible.

  5. Validate addresses - Always validate chain/address format matches before sending.