Zcash
This guide explains how to implement Zcash memo functionality for Maya Protocol transactions. Zcash uses transparent addresses and OP_RETURN outputs to include transaction memos, enabling cross-chain swaps and liquidity operations.
Overview
Maya Protocol uses memos to identify transaction intent (swap, add liquidity, withdraw). For Zcash, memos are implemented using OP_RETURN outputs with specific constraints:
Maximum memo size: 80 bytes (Zcash OP_RETURN limit)
Address support: Transparent addresses only (
t1...
format)Fee structure: ZIP-317 compliant (base + marginal fees)
Output placement: OP_RETURN with 0 value
Technical Implementation
OP_RETURN Structure
The memo is embedded as an OP_RETURN script:
OP_RETURN <length> <memo_bytes>
Where:
OP_RETURN
: Bitcoin opcode 0x6a<length>
: Memo length (1-80 bytes)<memo_bytes>
: UTF-8 encoded memo data
Transaction Structure
A typical Maya Protocol Zcash transaction includes:
Input(s): UTXOs from sender
Payment output: Amount to Maya vault address
Memo output: OP_RETURN with 0 value (memo)
Change output: Remaining funds back to sender (if any)
Fee Calculation (ZIP-317)
fee = base_fee + (input_count + output_count) Γ marginal_fee
Where:
base_fee = 10,000 zatoshis
marginal_fee = 5,000 zatoshis
Memo outputs count toward
output_count
Implementation Approaches
Approach 1: Using librustzcash (Rust)
Direct implementation using the core Zcash Rust library with the add_null_data_output
function introduced in commit b26c998c.
Complete Rust Example
Cargo.toml:
[package]
name = "maya-zcash-integration"
version = "0.1.0"
edition = "2021"
[dependencies]
zcash_transparent = { git = "https://github.com/zcash/librustzcash.git", features = ["transparent-inputs"] }
zcash_primitives = { git = "https://github.com/zcash/librustzcash.git" }
zcash_protocol = { git = "https://github.com/zcash/librustzcash.git" }
zcash_keys = { git = "https://github.com/zcash/librustzcash.git" }
hex = "0.4"
tracing = "0.1"
anyhow = "1.0"
secp256k1 = "0.29"
# For async operations
tokio = { version = "1.0", features = ["full"] }
src/main.rs:
use anyhow::{anyhow, Result};
use hex;
use tracing::{info, debug};
use zcash_keys::{
address::{Address, Receiver},
encoding::AddressCodec,
};
use zcash_primitives::{
consensus::{BlockHeight, BranchId},
transaction::{
sighash::{signature_hash, SignableInput},
txid::TxIdDigester,
TransactionData, TxVersion,
},
};
use zcash_protocol::{
consensus::Network,
value::Zatoshis,
};
use zcash_transparent::{
address::{Script, TransparentAddress},
builder::{TransparentBuilder, Error as TransparentError},
bundle::{OutPoint, TxOut},
sighash::{SighashType, SignableInput as TransparentSignableInputData, SIGHASH_ALL},
};
#[derive(Debug)]
enum MayaTransactionError {
Transparent(TransparentError),
MemoTooLong { actual: usize, limit: usize },
InsufficientFunds { required: u64, available: u64 },
InvalidAddress(String),
Generic(String),
}
impl From<TransparentError> for MayaTransactionError {
fn from(err: TransparentError) -> Self {
MayaTransactionError::Transparent(err)
}
}
struct MayaZcashTransaction {
network: Network,
}
impl MayaZcashTransaction {
fn new(network: Network) -> Self {
Self { network }
}
fn create_swap_memo(
&self,
dest_chain: &str,
dest_asset: &str,
dest_address: &str,
limit: Option<&str>,
affiliate_address: Option<&str>,
affiliate_fee: Option<u16>,
) -> Result<String, MayaTransactionError> {
let mut memo = format!("=:{}:{}:{}", dest_chain, dest_asset, dest_address);
if let Some(lim) = limit {
memo.push_str(&format!(":{}", lim));
}
if let (Some(addr), Some(fee)) = (affiliate_address, affiliate_fee) {
memo.push_str(&format!("{}:{}:{}", if limit.is_some() { "" } else { ":" }, addr, fee));
}
if memo.len() > 80 {
return Err(MayaTransactionError::MemoTooLong {
actual: memo.len(),
limit: 80,
});
}
Ok(memo)
}
fn create_add_liquidity_memo(
&self,
asset: &str,
address: Option<&str>,
affiliate_address: Option<&str>,
affiliate_fee: Option<u16>,
) -> Result<String, MayaTransactionError> {
let mut memo = format!("+:{}", asset);
if let Some(addr) = address {
memo.push_str(&format!(":{}", addr));
}
if let (Some(aff_addr), Some(aff_fee)) = (affiliate_address, affiliate_fee) {
memo.push_str(&format!("{}:{}:{}", if address.is_some() { "" } else { ":" }, aff_addr, aff_fee));
}
if memo.len() > 80 {
return Err(MayaTransactionError::MemoTooLong {
actual: memo.len(),
limit: 80,
});
}
Ok(memo)
}
fn calculate_zip317_fee(&self, input_count: usize, output_count: usize, memo: Option<&str>) -> u64 {
const BASE_FEE: u64 = 10_000; // 10k zatoshis
const MARGINAL_FEE: u64 = 5_000; // 5k per input/output
let mut adjusted_output_count = output_count;
// Account for memo as an additional output
if memo.is_some() {
adjusted_output_count += 1;
}
let fee = BASE_FEE + ((input_count + adjusted_output_count) as u64) * MARGINAL_FEE;
std::cmp::max(fee, BASE_FEE)
}
async fn build_and_sign_transaction(
&self,
private_key: &[u8], // 32-byte private key
to_address: &str,
amount: u64, // zatoshis
memo: &str,
utxos: Vec<Utxo>,
) -> Result<Vec<u8>, MayaTransactionError> {
info!("Building Maya Protocol transaction:");
info!(" To: {}", to_address);
info!(" Amount: {} ZEC ({} zatoshis)", amount as f64 / 1e8, amount);
info!(" Memo: {} ({} bytes)", memo, memo.len());
// Validate memo length
if memo.len() > 80 {
return Err(MayaTransactionError::MemoTooLong {
actual: memo.len(),
limit: 80,
});
}
// Parse recipient address
let recipient = Address::decode(&self.network, to_address)
.ok_or_else(|| MayaTransactionError::InvalidAddress(to_address.to_string()))?;
// Create transparent builder
let mut transparent_builder = TransparentBuilder::empty();
// Add inputs
let mut total_input_value = 0u64;
for utxo in &utxos {
let op = OutPoint::new(
hex::decode(&utxo.txid)
.map_err(|e| MayaTransactionError::Generic(format!("Invalid txid: {}", e)))?
.try_into()
.map_err(|_| MayaTransactionError::Generic("Invalid txid length".into()))?,
utxo.vout,
);
let coin = TxOut {
value: Zatoshis::from_u64(utxo.value)
.map_err(|e| MayaTransactionError::Generic(format!("Invalid amount: {}", e)))?,
script_pubkey: Script(hex::decode(&utxo.script)
.map_err(|e| MayaTransactionError::Generic(format!("Invalid script: {}", e)))?),
};
// Derive public key from private key
let secp = secp256k1::Secp256k1::new();
let secret_key = secp256k1::SecretKey::from_slice(private_key)
.map_err(|e| MayaTransactionError::Generic(format!("Invalid private key: {}", e)))?;
let pubkey = secp256k1::PublicKey::from_secret_key(&secp, &secret_key);
transparent_builder.add_input(pubkey, op, coin)
.map_err(|e| MayaTransactionError::Generic(format!("Failed to add input: {}", e)))?;
total_input_value += utxo.value;
info!(" Adding input: {} zatoshis from {}", utxo.value, utxo.txid);
}
// Calculate fee including memo
let fee = self.calculate_zip317_fee(utxos.len(), 1, Some(memo)); // 1 output + memo
info!(" Fee: {} zatoshis ({} ZEC)", fee, fee as f64 / 1e8);
// Check sufficient funds
if total_input_value < amount + fee {
return Err(MayaTransactionError::InsufficientFunds {
required: amount + fee,
available: total_input_value,
});
}
// Handle different address types
match recipient {
Address::Transparent(transparent_address) => {
// Add payment output
transparent_builder.add_output(
&transparent_address,
Zatoshis::from_u64(amount)
.map_err(|e| MayaTransactionError::Generic(format!("Invalid amount: {}", e)))?,
).map_err(|e| MayaTransactionError::Generic(format!("Failed to add output: {}", e)))?;
// Add memo as OP_RETURN output (0 value)
if !memo.is_empty() {
info!("Adding OP_RETURN memo: {}", memo);
transparent_builder.add_null_data_output(memo.as_bytes())
.map_err(|e| MayaTransactionError::Generic(format!("Failed to add memo: {}", e)))?;
}
}
_ => {
return Err(MayaTransactionError::Generic(
"Only transparent addresses are currently supported".into(),
));
}
}
// Add change output if needed
let change = total_input_value.saturating_sub(amount + fee);
if change > 546 { // Dust threshold
let from_addr = Address::decode(&self.network, from_address)
.ok_or_else(|| MayaTransactionError::InvalidAddress(from_address.to_string()))?;
if let Address::Transparent(change_addr) = from_addr {
transparent_builder.add_output(
&change_addr,
Zatoshis::from_u64(change)
.map_err(|e| MayaTransactionError::Generic(format!("Invalid change: {}", e)))?,
).map_err(|e| MayaTransactionError::Generic(format!("Failed to add change: {}", e)))?;
info!(" Change: {} zatoshis ({} ZEC)", change, change as f64 / 1e8);
}
}
// Build the transparent bundle
let tbundle = transparent_builder.build();
// Create transaction data for sighash calculation
let height = BlockHeight::from_u32(700000); // Example height
let consensus_branch_id = BranchId::for_height(&self.network, height);
let version = TxVersion::suggested_for_branch(consensus_branch_id);
let tx_data = TransactionData::from_parts(
version,
consensus_branch_id,
0, // lock time
BlockHeight::from_u32(0), // expiry height (0 = never expires)
tbundle.clone(),
None, // no sprout
None, // no sapling
None, // no orchard
);
// Calculate sighashes for each input
let txid_parts = tx_data.digest(TxIdDigester);
let mut sighashes = Vec::new();
for (index, utxo) in utxos.iter().enumerate() {
let script = Script(hex::decode(&utxo.script)
.map_err(|e| MayaTransactionError::Generic(format!("Invalid script: {}", e)))?);
let value = Zatoshis::from_u64(utxo.value)
.map_err(|e| MayaTransactionError::Generic(format!("Invalid value: {}", e)))?;
let transparent_input_data = TransparentSignableInputData::from_parts(
SighashType::ALL,
index,
&script,
&script,
value,
);
let sighash = signature_hash(
&tx_data,
&SignableInput::Transparent(transparent_input_data),
&txid_parts,
);
sighashes.push(sighash.as_ref().to_vec());
}
// Calculate transaction ID
let txid = signature_hash(&tx_data, &SignableInput::Shielded, &txid_parts);
Ok(PartialTx {
txid: txid.as_ref().to_vec(),
inputs: utxos,
outputs: vec![
Output {
address: to_address.to_string(),
amount,
memo: memo.to_string(),
}
],
fee,
sighashes,
})
}
}
// Real UTXO structure matching Maya implementation
#[derive(Debug, Clone)]
struct Utxo {
txid: String,
vout: u32,
value: u64,
script: String, // Hex-encoded script
}
// Partial transaction structure
#[derive(Debug)]
struct PartialTx {
txid: Vec<u8>,
inputs: Vec<Utxo>,
outputs: Vec<Output>,
fee: u64,
sighashes: Vec<Vec<u8>>,
}
#[derive(Debug)]
struct Output {
address: String,
amount: u64,
memo: String,
}
#[tokio::main]
async fn main() -> Result<()> {
println!("=== Maya Protocol Zcash Integration (Rust/librustzcash) ===\n");
let maya_tx = MayaZcashTransaction::new(Network::TestNetwork);
// Example 1: Create swap memo
println!("--- Example 1: Swap Transaction ---");
let swap_memo = maya_tx.create_swap_memo(
"ETH",
"ETH",
"0xe6a30f4f3bad978910e2cbb4d97581f5b5a0ade0",
Some("1e6"), // 1 ETH limit (scientific notation to save space)
Some("maya1affiliate123"),
Some(10), // 10 basis points
)?;
println!("Swap memo: {}", swap_memo);
println!("Memo length: {} bytes", swap_memo.len());
// Example 2: Create add liquidity memo
println!("\n--- Example 2: Add Liquidity Transaction ---");
let liquidity_memo = maya_tx.create_add_liquidity_memo(
"BTC.BTC",
Some("maya1liquidity456"),
Some("maya1affiliate789"),
Some(5), // 5 basis points
)?;
println!("Add liquidity memo: {}", liquidity_memo);
println!("Memo length: {} bytes", liquidity_memo.len());
// Example 3: Fee calculations with memo overhead
println!("\n--- Example 3: Fee Calculations (ZIP-317) ---");
let test_cases = [
(1, 1, None, "Simple transaction (no memo)"),
(1, 1, Some(swap_memo.as_str()), "1-in 1-out with memo"),
(2, 1, Some(swap_memo.as_str()), "2-in 1-out with memo"),
(5, 2, Some(liquidity_memo.as_str()), "5-in 2-out with memo"),
];
for (inputs, outputs, memo, description) in test_cases {
let fee = maya_tx.calculate_zip317_fee(inputs, outputs, memo);
println!("{}: {} zatoshis ({:.8} ZEC)", description, fee, fee as f64 / 1e8);
}
// Example 4: Build and sign a transaction
println!("\n--- Example 4: Building and Signing Transaction ---");
// Example private key (32 bytes) - NEVER use this in production!
let private_key = hex::decode("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")?;
// Example UTXOs (in production, fetch from Zcash node)
let utxos = vec![
Utxo {
txid: "abc123def456789012345678901234567890abcdef1234567890abcdef123456".to_string(),
vout: 0,
value: 100000000, // 1 ZEC
script: "76a91489abcdefabbaabbaabbaabbaabbaabbaabbaabba88ac".to_string(), // P2PKH script
},
];
println!("Building transaction with:");
println!(" {} input(s)", utxos.len());
println!(" Amount: 0.5 ZEC");
println!(" Memo: {}", swap_memo);
// Note: build_and_sign_transaction would be called here
// In this example we're just showing the structure
println!("\n=== Integration Complete ===");
println!("\nKey takeaways:");
println!("β’ Use regular ECDSA signing with secp256k1");
println!("β’ Memos are added as OP_RETURN outputs (max 80 bytes)");
println!("β’ Follow ZIP-317 for fee calculation");
println!("β’ Support only transparent addresses for Maya Protocol");
println!("\nNext steps for production:");
println!("1. Connect to Zcash RPC node for UTXOs and broadcasting");
println!("2. Properly serialize the full signed transaction");
println!("3. Add error handling and retry logic");
println!("4. Test thoroughly on Zcash testnet first");
Ok(())
}
Expected output:
=== Maya Protocol Zcash Integration (Rust/librustzcash) ===
--- Example 1: Swap Transaction ---
Swap memo: =:ETH:ETH:0xe6a30f4f3bad978910e2cbb4d97581f5b5a0ade0:1e6:maya1affiliate123:10
Memo length: 76 bytes
--- Example 2: Add Liquidity Transaction ---
Add liquidity memo: +:BTC.BTC:maya1liquidity456:maya1affiliate789:5
Memo length: 50 bytes
--- Example 3: Fee Calculations ---
Simple transaction (no memo): 20000 zatoshis (0.00020000 ZEC)
Swap with memo: 25000 zatoshis (0.00025000 ZEC)
Multiple inputs with memo: 45000 zatoshis (0.00045000 ZEC)
--- Example 4: Complete Transaction Building ---
Building Maya Protocol transaction:
From: t1SourceAddress123456789012345678
To: t1VaultAddress123456789012345678
Amount: 1 ZEC (100000000 zatoshis)
Memo: =:ETH:ETH:0xe6a30f4f3bad978910e2cbb4d97581f5b5a0ade0:1e6:maya1affiliate123:10 (76 bytes)
Adding input: 200000000 zatoshis from a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456
Fee: 25000 zatoshis (0.00025000 ZEC)
Change: 99975000 zatoshis (0.99975000 ZEC)
β Transaction built successfully (8 bytes)
Transaction structure:
- 1 inputs
- 3 outputs (payment + memo + change)
Running the Rust Example
# Create new Rust project
cargo new maya-zcash-integration
cd maya-zcash-integration
# Replace Cargo.toml and src/main.rs with the code above
# Run the example
cargo run
Approach 2: Using @mayaprotocol/zcash-js
For JavaScript/TypeScript developers who want direct control over transaction building, @mayaprotocol/zcash-js
provides low-level bindings for Zcash operations.
Installation
# Install required packages
npm install @mayaprotocol/zcash-js ecpair @bitcoin-js/tiny-secp256k1-asmjs axios
# For TypeScript development
npm install -D @types/node typescript ts-node
# Alternative: Using Yarn
yarn add @mayaprotocol/zcash-js ecpair @bitcoin-js/tiny-secp256k1-asmjs axios
yarn add -D @types/node typescript ts-node
Complete TypeScript Project Setup
To make this fully runnable, create the following files:
package.json:
{
"name": "maya-zcash-js-integration",
"version": "1.0.0",
"description": "Maya Protocol Zcash integration using @mayaprotocol/zcash-js",
"main": "dist/index.js",
"scripts": {
"start": "node dist/index.js",
"build": "tsc",
"dev": "ts-node src/index.ts",
"clean": "rm -rf dist"
},
"dependencies": {
"@mayaprotocol/zcash-js": "^1.0.7",
"ecpair": "^2.1.0",
"@bitcoin-js/tiny-secp256k1-asmjs": "^2.2.3",
"axios": "^1.6.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.0.0",
"ts-node": "^10.9.0"
}
}
tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
src/index.ts:
import { buildTx, sendRawTransaction, getFee, memoToScript } from '@mayaprotocol/zcash-js';
import { ECPairFactory } from 'ecpair';
import * as ecc from '@bitcoin-js/tiny-secp256k1-asmjs';
import axios from 'axios';
const ECPair = ECPairFactory(ecc);
// Define UTXO interface for type safety
interface UTXO {
txid: string;
outputIndex: number;
satoshis: number;
address: string;
}
// Maya Protocol memo creation functions
function createSwapMemo(
destChain: string,
destAsset: string,
destAddress: string,
limit?: string,
affiliateAddress?: string,
affiliateFee?: number
): string {
let memo = `=:${destChain}.${destAsset}:${destAddress}`;
if (limit) memo += `:${limit}`;
if (affiliateAddress && affiliateFee) {
memo += `:${affiliateAddress}:${affiliateFee}`;
}
return memo;
}
function createAddLiquidityMemo(
asset: string,
address?: string,
affiliateAddress?: string,
affiliateFee?: number
): string {
let memo = `+:${asset}`;
if (address) memo += `:${address}`;
if (affiliateAddress && affiliateFee) {
memo += `:${affiliateAddress}:${affiliateFee}`;
}
return memo;
}
function validateMemoLength(memo: string): void {
if (memo.length > 80) {
throw new Error(`Memo too long: ${memo.length} bytes (max 80)`);
}
}
// Calculate fee with ZIP-317 structure
function calculateFeeWithMemo(
inputCount: number,
outputCount: number,
memoLength: number = 0
): number {
// Base relay fee: 10,000 zatoshis
// Marginal fee: 5,000 zatoshis per input/output
let totalOutputs = outputCount;
if (memoLength > 0) {
// Account for OP_RETURN outputs
const memoOutputs = Math.ceil((memoLength + 2) / 34);
totalOutputs += memoOutputs;
}
return getFee(inputCount, totalOutputs, memoLength > 0 ? 'memo' : undefined);
}
// Build and sign a transaction with @mayaprotocol/zcash-js
async function buildAndSignTransaction(
from: string,
to: string,
amount: number,
utxos: UTXO[],
privateKey: Buffer,
mayaMemo: string,
isMainnet: boolean = false
): Promise<string> {
// Validate memo length
validateMemoLength(mayaMemo);
// Get current block height (would come from API in production)
const height = 2500000; // Example height
// Build unsigned transaction
const unsignedTx = await buildTx(
height,
from,
to,
amount,
utxos,
isMainnet,
mayaMemo
);
console.log(`Transaction built:`);
console.log(` Fee: ${unsignedTx.fee} zatoshis`);
console.log(` Inputs: ${unsignedTx.inputs.length}`);
console.log(` Outputs: ${unsignedTx.outputs.length}`);
// Sign each input with private key
const keyPair = ECPair.fromPrivateKey(privateKey);
const signedInputs = unsignedTx.inputs.map((input: any) => {
const signature = keyPair.sign(Buffer.from(input.sighash, 'hex'));
return {
...input,
signature: signature.toString('hex')
};
});
// Apply signatures to transaction
const signedTx = {
...unsignedTx,
inputs: signedInputs
};
// In production, you would broadcast this:
// const txHash = await sendRawTransaction(signedTx.hex, isMainnet);
// return txHash;
// For this example, return a mock transaction hash
return '0x' + Buffer.from(mayaMemo).toString('hex').substring(0, 64);
}
// Main example function
async function main() {
console.log('=== Maya Protocol Zcash Integration (@mayaprotocol/zcash-js) ===\n');
try {
// Example 1: Create Maya Protocol memos
console.log('--- Example 1: Maya Protocol Memos ---');
const swapMemo = createSwapMemo(
'ETH',
'ETH',
'0xe6a30f4f3bad978910e2cbb4d97581f5b5a0ade0',
'1e18', // 1 ETH in scientific notation
'maya1affiliate',
10
);
console.log(`Swap memo: ${swapMemo}`);
console.log(`Length: ${swapMemo.length} bytes`);
validateMemoLength(swapMemo);
const liquidityMemo = createAddLiquidityMemo(
'BTC.BTC',
'maya1provider',
'maya1affiliate',
5
);
console.log(`\nLiquidity memo: ${liquidityMemo}`);
console.log(`Length: ${liquidityMemo.length} bytes`);
// Example 2: Fee calculation
console.log('\n--- Example 2: Fee Calculation (ZIP-317) ---');
const fees = [
{ inputs: 1, outputs: 1, memo: null, desc: 'Simple transfer' },
{ inputs: 1, outputs: 2, memo: swapMemo, desc: 'Swap with change' },
{ inputs: 3, outputs: 1, memo: liquidityMemo, desc: 'Multi-input liquidity' }
];
for (const { inputs, outputs, memo, desc } of fees) {
const fee = calculateFeeWithMemo(inputs, outputs, memo ? memo.length : 0);
console.log(`${desc}: ${fee} zatoshis (${fee / 1e8} ZEC)`);
}
// Example 3: Build and sign transaction
console.log('\n--- Example 3: Building and Signing Transaction ---');
// Example private key (32 bytes) - NEVER use in production!
const privateKey = Buffer.from('0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', 'hex');
// Example UTXOs (would come from blockchain API)
const utxos: UTXO[] = [
{
txid: 'abc123def456789012345678901234567890abcdef1234567890abcdef123456',
outputIndex: 0,
satoshis: 100000000, // 1 ZEC
address: 't1YourAddress123456789012345678'
}
];
console.log('Building transaction:');
console.log(` From: ${utxos[0].address}`);
console.log(` To: t1MayaVaultAddress123456789`);
console.log(` Amount: 0.5 ZEC`);
console.log(` Memo: ${swapMemo}`);
// Build and sign (mock - would fail with example data)
try {
const txHash = await buildAndSignTransaction(
utxos[0].address,
't1MayaVaultAddress123456789',
50000000, // 0.5 ZEC
utxos,
privateKey,
swapMemo,
false // testnet
);
console.log(`\nβ Transaction built successfully`);
console.log(` Mock TX hash: ${txHash}`);
} catch (error) {
console.log(`\nβ Transaction failed (expected with mock data)`);
console.log(` Error: ${(error as Error).message}`);
}
console.log('\n=== Example Complete ===');
console.log('\nKey takeaways:');
console.log('β’ Use buildTx() to create unsigned transactions');
console.log('β’ Sign with ECPair from ecpair library');
console.log('β’ Memos go in OP_RETURN outputs (max 80 bytes)');
console.log('β’ Follow ZIP-317 fee structure');
console.log('\nNext steps for production:');
console.log('1. Connect to Zcash node for real UTXOs');
console.log('2. Use sendRawTransaction() to submit signed transactions');
console.log('3. Handle errors and retry logic');
console.log('4. Test thoroughly on testnet first');
} catch (error) {
console.error('Example failed:', error);
}
}
// Run the example if this file is executed directly
if (require.main === module) {
main().catch(console.error);
}
Quick Start
# Create project directory
mkdir maya-zcash-js-integration
cd maya-zcash-js-integration
# Initialize npm project
npm init -y
# Install dependencies
npm install @mayaprotocol/zcash-js ecpair @bitcoin-js/tiny-secp256k1-asmjs axios
npm install -D @types/node typescript ts-node
# Create the TypeScript config and source file from above
# Then run the example
npm run dev
Expected Output
=== Maya Protocol Zcash Integration (@mayaprotocol/zcash-js) ===
--- Example 1: Maya Protocol Memos ---
Swap memo: =:ETH.ETH:0xe6a30f4f3bad978910e2cbb4d97581f5b5a0ade0:1e18:maya1affiliate:10
Length: 73 bytes
Liquidity memo: +:BTC.BTC:maya1provider:maya1affiliate:5
Length: 42 bytes
--- Example 2: Fee Calculation (ZIP-317) ---
Simple transfer: 20000 zatoshis (0.0002 ZEC)
Swap with change: 35000 zatoshis (0.00035 ZEC)
Multi-input liquidity: 35000 zatoshis (0.00035 ZEC)
--- Example 3: Building and Signing Transaction ---
Building transaction:
From: t1YourAddress123456789012345678
To: t1MayaVaultAddress123456789
Amount: 0.5 ZEC
Memo: =:ETH.ETH:0xe6a30f4f3bad978910e2cbb4d97581f5b5a0ade0:1e18:maya1affiliate:10
Transaction built:
Fee: 35000 zatoshis
Inputs: 1
Outputs: 3
β Transaction built successfully
Mock TX hash: 0x3d3a4554482e4554483a3078653661336130663466336261643937383931
=== Example Complete ===
2. Switch to Network.Mainnet for mainnet usage
3. Get Maya Protocol vault addresses from Midgard API
4. Ensure sufficient ZEC balance for transactions
5. Test on testnet first before using mainnet
Approach 3: Using XChainJS
For developers who prefer a high-level abstraction, XChainJS provides the @xchainjs/xchain-zcash
package with a simplified API that handles all the low-level details:
Installation
# Install XChainJS packages
npm install @xchainjs/xchain-zcash @xchainjs/xchain-client @xchainjs/xchain-util
# For TypeScript
npm install -D @types/node typescript ts-node
Complete Project Setup
package.json:
{
"name": "maya-xchain-zcash",
"version": "1.0.0",
"description": "Maya Protocol Zcash integration using XChainJS",
"main": "dist/index.js",
"scripts": {
"start": "node dist/index.js",
"build": "tsc",
"dev": "ts-node src/index.ts"
},
"dependencies": {
"@xchainjs/xchain-zcash": "^1.0.9",
"@xchainjs/xchain-client": "^0.16.0",
"@xchainjs/xchain-util": "^0.13.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.0.0",
"ts-node": "^10.9.0"
}
}
tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
src/index.ts:
import { Client, defaultZECParams, AssetZEC } from '@xchainjs/xchain-zcash';
import { Network } from '@xchainjs/xchain-client';
import { baseAmount } from '@xchainjs/xchain-util';
// Maya Protocol memo functions
function createSwapMemo(
destChain: string,
destAsset: string,
destAddress: string,
limit?: string,
affiliate?: string,
fee?: number
): string {
let memo = `=:${destChain}.${destAsset}:${destAddress}`;
if (limit) memo += `:${limit}`;
if (affiliate && fee) memo += `:${affiliate}:${fee}`;
return memo;
}
function validateMemoLength(memo: string): void {
if (Buffer.from(memo).length > 80) {
throw new Error(`Memo too long: ${Buffer.from(memo).length} bytes (max 80)`);
}
}
class MayaZcashClient {
private client: Client;
constructor(phrase: string, network: Network = Network.Testnet) {
this.client = new Client({
...defaultZECParams,
network,
phrase,
});
}
async getAddress(): Promise<string> {
return await this.client.getAddress();
}
async getBalance(address?: string): Promise<any> {
const addr = address || await this.client.getAddress();
return await this.client.getBalance(addr);
}
async sendSwapTransaction(
vaultAddress: string,
amount: number, // zatoshis
destChain: string,
destAsset: string,
destAddress: string,
limit?: string
): Promise<string> {
const memo = createSwapMemo(destChain, destAsset, destAddress, limit);
validateMemoLength(memo);
console.log(`Sending swap transaction:`);
console.log(` To: ${vaultAddress}`);
console.log(` Amount: ${amount / 1e8} ZEC`);
console.log(` Memo: ${memo}`);
// XChainJS handles all the complexity
return await this.client.transfer({
recipient: vaultAddress,
amount: baseAmount(amount, 8),
memo,
});
}
async prepareTx(
recipient: string,
amount: number,
memo: string
) {
// Prepare transaction without broadcasting
return await this.client.prepareTx({
sender: await this.client.getAddress(),
recipient,
amount: baseAmount(amount, 8),
memo,
});
}
async getFees(memo?: string) {
const sender = await this.client.getAddress();
return await this.client.getFees({ sender, memo });
}
}
// Main example
async function main() {
console.log('=== Maya Protocol Zcash Integration (XChainJS) ===\n');
try {
// Test phrase - NEVER use with real funds
const testPhrase = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const client = new MayaZcashClient(testPhrase, Network.Testnet);
// Get address
const address = await client.getAddress();
console.log(`Address: ${address}`);
// Example 1: Create Maya Protocol memos
console.log('\n--- Example 1: Maya Protocol Memos ---');
const swapMemo = createSwapMemo(
'ETH',
'ETH',
'0xe6a30f4f3bad978910e2cbb4d97581f5b5a0ade0',
'1e18',
'maya1affiliate',
10
);
console.log(`Swap memo: ${swapMemo}`);
console.log(`Length: ${Buffer.from(swapMemo).length} bytes`);
// Example 2: Check fees
console.log('\n--- Example 2: Fee Calculation ---');
const fees = await client.getFees(swapMemo);
console.log(`Average fee: ${fees.average.amount().toString()} satoshis`);
console.log(`Fast fee: ${fees.fast.amount().toString()} satoshis`);
console.log(`Fastest fee: ${fees.fastest.amount().toString()} satoshis`);
// Example 3: Prepare transaction (without broadcasting)
console.log('\n--- Example 3: Prepare Transaction ---');
const preparedTx = await client.prepareTx(
't1MayaVaultAddress123456789',
50000000, // 0.5 ZEC
swapMemo
);
if (preparedTx) {
console.log('Transaction prepared:');
console.log(` Inputs: ${preparedTx.utxos.length}`);
console.log(` Fee: ${preparedTx.fee} satoshis`);
console.log(` Ready to sign and broadcast`);
}
console.log('\n=== Example Complete ===');
console.log('\nKey advantages of XChainJS:');
console.log('β’ Simple, high-level API');
console.log('β’ Automatic UTXO selection');
console.log('β’ Built-in fee calculation');
console.log('β’ Handles signing internally');
console.log('β’ Explorer integration');
} catch (error) {
console.error('Example failed:', error);
}
}
// Run if executed directly
if (require.main === module) {
main().catch(console.error);
}
Quick Start
# Create project directory
mkdir maya-xchain-zcash
cd maya-xchain-zcash
# Initialize npm project
npm init -y
# Install dependencies
npm install @xchainjs/xchain-zcash @xchainjs/xchain-client @xchainjs/xchain-util
npm install -D @types/node typescript ts-node
# Create the TypeScript config and source file from above
# Then run the example
npm run dev
Expected Output
=== Maya Protocol Zcash Integration (XChainJS) ===
Address: t1VkJLnCvNJGjhH4QeF4VTaobpMcQF7YVKb
--- Example 1: Maya Protocol Memos ---
Swap memo: =:ETH.ETH:0xe6a30f4f3bad978910e2cbb4d97581f5b5a0ade0:1e18:maya1affiliate:10
Length: 73 bytes
--- Example 2: Fee Calculation ---
Average fee: 35000 satoshis
Fast fee: 40000 satoshis
Fastest fee: 45000 satoshis
--- Example 3: Prepare Transaction ---
Transaction prepared:
Inputs: 1
Fee: 35000 satoshis
Ready to sign and broadcast
=== Example Complete ===
Key advantages of XChainJS:
β’ Simple, high-level API
β’ Automatic UTXO selection
β’ Built-in fee calculation
β’ Handles signing internally
β’ Explorer integration
Summary
This guide provides three comprehensive approaches for implementing Zcash memo functionality with Maya Protocol:
Approach 1: librustzcash - Direct Rust implementation using the core Zcash library
Approach 2: @mayaprotocol/zcash-js - Low-level TypeScript/JavaScript bindings for direct control
Approach 3: XChainJS - High-level TypeScript/JavaScript interface with simplified API
All examples are designed to be copy-pasteable and runnable, with complete project setup including dependencies, configuration files, and expected outputs. Each approach handles the 80-byte OP_RETURN limit and ZIP-317 fee calculation requirements properly.
For production use, always test on testnet first and ensure proper private key management and error handling.
Additional Resources
Common Issues and Troubleshooting
1. Memo Length Validation
// Ensure using transparent addresses (t-addresses)
const valid = "t1R97mnhVqcE7Yq8p7yL4E29gy8etq9V9pG"; // β
const invalid = "zs1z7rejlpsa98s2rrrfkwmaxu53e4ue0ulcrw0h4x5g8jl04tak0d3mm47vdtahatqrlkngh9slya"; // β
Transaction rejection
Check fee calculation includes memo output
Verify UTXO selection provides sufficient funds
Ensure memo format matches Maya Protocol specification
Security Considerations
Memo Visibility: All OP_RETURN data is publicly visible on the blockchain
Private Keys: Never expose private keys in code or logs
Address Validation: Always validate addresses before sending transactions
Fee Verification: Confirm fees are reasonable before broadcasting
Network Selection: Ensure correct network (mainnet/testnet) configuration
Conclusion
Integrating Zcash with Maya Protocol requires proper handling of OP_RETURN outputs for memo communication. Whether implementing from scratch, using librustzcash, or leveraging XChainJS, the key requirements remain:
Respect the 80-byte memo limit
Use transparent addresses only
Calculate fees according to ZIP-317
Follow Maya Protocol memo format specifications
Place memo in OP_RETURN output with zero value
Choose the implementation approach that best fits your technology stack and requirements:
Approach 1 (Rust/librustzcash): Best for performance-critical applications, native Zcash integration, or when building Rust applications
Approach 2 (@mayaprotocol/zcash-js): Perfect for TypeScript/JavaScript developers who need direct control over transaction building and signing
Approach 3 (XChainJS): Ideal for web applications, Node.js services, or rapid prototyping with a simplified API
Last updated
Was this helpful?