Skip to main content

Private Transfers

This guide covers sending tokens privately between privacy addresses.

Basic Transfer

const result = await sdk.vault.send({
  to: '0x04...recipient-privacy-address',
  tokenAddress: '0x...token-address',
  amount: 1000000000000000000n, // 1 token
});

console.log('Transaction hash:', result.txHash);

Transfer Parameters

interface SendParams {
  to: PrivacyAddress;       // Recipient's privacy address
  tokenAddress: Hex;        // Token to send
  amount: bigint;           // Amount in wei
}
ParameterTypeRequiredDescription
toPrivacyAddressYesRecipient’s privacy address
tokenAddressHexYesToken contract address
amountbigintYesAmount in smallest unit

Transfer Result

interface TransactionResult {
  txHash: Hex;  // Transaction hash
}

Privacy Address Validation

Always validate privacy addresses before sending:
import { isValidPrivacyAddress, validatePrivacyAddress } from '@testinprod-io/privacy-boost';

// Check if valid
if (!isValidPrivacyAddress(recipientAddress)) {
  throw new Error('Invalid privacy address');
}

// Or throw on invalid
validatePrivacyAddress(recipientAddress); // Throws if invalid

// Then send
await sdk.vault.send({
  to: recipientAddress,
  tokenAddress: '0x...',
  amount: 1000000000000000000n,
});

Using Contacts

Send to a saved contact:
// Find contact by name
const contact = sdk.contacts.findByAddress(privacyAddress);
// or
const contacts = sdk.contacts.search('alice');

if (contact) {
  await sdk.vault.send({
    to: contact.privacyAddress,
    tokenAddress: '0x...',
    amount: 1000000000000000000n,
  });
}

Transfer to MPK

If you have the recipient’s MPK instead of privacy address:
import { encodePrivacyAddress } from '@testinprod-io/privacy-boost';

// Convert MPK to privacy address
const privacyAddress = encodePrivacyAddress(mpk, viewingKey);

await sdk.vault.send({
  to: privacyAddress,
  tokenAddress: '0x...',
  amount: 1000000000000000000n,
});

Checking Balance Before Transfer

async function safeTransfer(params: SendParams) {
  const balance = await sdk.vault.getBalance(params.tokenAddress);

  if (params.amount > balance) {
    throw new Error(
      `Insufficient balance. Have: ${balance}, Need: ${params.amount}`
    );
  }

  return sdk.vault.send(params);
}

Transfer Privacy

Private transfers provide:
  • Sender Privacy: Your wallet address is not linked to the transaction
  • Recipient Privacy: The recipient’s wallet address is not revealed
  • Amount Privacy: The transfer amount is encrypted
  • Relationship Privacy: No public link between sender and recipient
Only the sender and recipient can see the transfer details.

Error Handling

try {
  await sdk.vault.send(params);
} catch (error) {
  switch (error.code) {
    case 'INSUFFICIENT_BALANCE':
      console.log('Not enough shielded balance');
      break;
    case 'INVALID_RECIPIENT':
      console.log('Invalid privacy address');
      break;
    case 'INVALID_AMOUNT':
      console.log('Invalid amount');
      break;
    case 'TRANSFER_FAILED':
      console.log('Transfer transaction failed');
      break;
    default:
      console.log('Transfer error:', error.message);
  }
}

UI Example

import { useState } from 'react';
import { isValidPrivacyAddress } from '@testinprod-io/privacy-boost';

function TransferForm() {
  const [recipient, setRecipient] = useState('');
  const [amount, setAmount] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const handleTransfer = async () => {
    setError('');

    // Validate recipient
    if (!isValidPrivacyAddress(recipient)) {
      setError('Invalid privacy address');
      return;
    }

    setLoading(true);
    try {
      const parsedAmount = await sdk.vault.parseAmount(tokenAddress, amount);

      await sdk.vault.send({
        to: recipient,
        tokenAddress,
        amount: parsedAmount,
      });

      alert('Transfer successful!');
      setRecipient('');
      setAmount('');
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div>
      <input
        type="text"
        value={recipient}
        onChange={(e) => setRecipient(e.target.value)}
        placeholder="Recipient privacy address"
        disabled={loading}
      />
      <input
        type="text"
        value={amount}
        onChange={(e) => setAmount(e.target.value)}
        placeholder="Amount"
        disabled={loading}
      />
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <button onClick={handleTransfer} disabled={loading}>
        {loading ? 'Sending...' : 'Send'}
      </button>
    </div>
  );
}

Transfer with Contact Selection

function TransferWithContacts() {
  const [selectedContact, setSelectedContact] = useState<Contact | null>(null);
  const contacts = sdk.contacts.getContacts();

  return (
    <div>
      <select
        value={selectedContact?.privacyAddress || ''}
        onChange={(e) => {
          const contact = contacts.find((c) => c.privacyAddress === e.target.value);
          setSelectedContact(contact || null);
        }}
      >
        <option value="">Select contact...</option>
        {contacts.map((contact) => (
          <option key={contact.id} value={contact.privacyAddress}>
            {contact.name}
          </option>
        ))}
      </select>

      {selectedContact && (
        <TransferForm recipient={selectedContact.privacyAddress} />
      )}
    </div>
  );
}

Best Practices

1. Validate All Inputs

function validateTransferParams(params: SendParams) {
  if (!isValidPrivacyAddress(params.to)) {
    throw new Error('Invalid recipient privacy address');
  }
  if (params.amount <= 0n) {
    throw new Error('Amount must be positive');
  }
  if (!isEvmAddress(params.tokenAddress)) {
    throw new Error('Invalid token address');
  }
}

2. Confirm Large Transfers

async function transferWithConfirmation(params: SendParams) {
  const tokenMetadata = await sdk.vault.getToken(params.tokenAddress);
  const formattedAmount = sdk.vault.formatAmount(
    params.amount,
    tokenMetadata.decimals
  );

  const confirmed = confirm(
    `Send ${formattedAmount} ${tokenMetadata.symbol} to ${params.to.slice(0, 10)}...?`
  );

  if (!confirmed) {
    throw new Error('Transfer cancelled by user');
  }

  return sdk.vault.send(params);
}

3. Handle Network Delays

async function transferWithRetry(params: SendParams, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await sdk.vault.send(params);
    } catch (error) {
      if (attempt === maxRetries) throw error;
      if (error.code === 'NETWORK_ERROR') {
        await new Promise((r) => setTimeout(r, 2000 * attempt));
        continue;
      }
      throw error;
    }
  }
}

Next Steps