Transaction signing is fundamental to Stellar operations. PayOnProof uses Stellar SDK’s signing capabilities for SEP-10 authentication and other cryptographic operations.
Overview
Stellar transactions must be signed with private keys to prove authorization. PayOnProof implements secure transaction signing for:
SEP-10 authentication : Challenge transaction signing
Payment transactions : Future transfer operations
Trustline operations : Asset trustline establishment
Keypair management
PayOnProof uses Stellar SDK’s Keypair class for key operations:
services/api/lib/stellar/sep10.ts
import { Keypair , TransactionBuilder , WebAuth } from "@stellar/stellar-sdk" ;
const keypair = Keypair . fromSecret ( input . secretKey . trim ());
const account = input . accountPublicKey ?. trim () || keypair . publicKey ();
Key derivation
From secret : Keypair.fromSecret(secretKey) creates a keypair from a secret key
Public key : keypair.publicKey() extracts the public key
Secret key : Private keys are never logged or exposed
Secret keys should always be stored securely and never committed to version control or exposed in logs.
SEP-10 challenge signing
The primary signing use case is SEP-10 authentication:
services/api/lib/stellar/sep10.ts
export async function requestSep10Token (
input : Sep10TokenInput
) : Promise < Sep10TokenResult > {
// ... fetch challenge transaction
const challengeJson = await challengeRes . json () as {
transaction ?: string ;
network_passphrase ?: string ;
};
const networkPassphrase =
challengeJson . network_passphrase || getStellarConfig (). networkPassphrase ;
// Verify challenge before signing
const { clientAccountID } = WebAuth . readChallengeTx (
challengeJson . transaction ,
serverSigningKey ,
networkPassphrase ,
expectedHomeDomain ,
expectedWebAuthDomain
);
if ( clientAccountID !== account ) {
throw new Error ( "SEP-10 challenge account mismatch" );
}
// Sign the transaction
const tx = TransactionBuilder . fromXDR (
challengeJson . transaction ,
networkPassphrase
);
tx . sign ( keypair );
const signedTx = tx . toEnvelope (). toXDR ( "base64" );
// Submit signed transaction
// ...
}
Full implementation at services/api/lib/stellar/sep10.ts:53
Transaction construction
Stellar SDK’s TransactionBuilder is used to construct and sign transactions:
From XDR
Reconstructing a transaction from XDR (External Data Representation):
const tx = TransactionBuilder . fromXDR (
challengeJson . transaction ,
networkPassphrase
);
XDR : Base64-encoded transaction envelope
Network passphrase : Identifies the Stellar network (mainnet/testnet)
Signing
Adding a signature to a transaction:
This:
Creates an Ed25519 signature of the transaction hash
Adds the signature to the transaction envelope
Associates the signature with the keypair’s public key
Serialization
Converting the signed transaction back to XDR:
const signedTx = tx . toEnvelope (). toXDR ( "base64" );
toEnvelope() : Wraps transaction with signatures
toXDR(“base64”) : Serializes to base64-encoded string
Network passphrases
Transactions are network-specific and must use the correct passphrase:
services/api/lib/stellar.ts
export function getStellarConfig () {
const popEnv = getPopEnv ();
const defaultPassphrase =
popEnv === "production"
? "Public Global Stellar Network ; September 2015"
: "Test SDF Network ; September 2015" ;
return {
popEnv ,
horizonUrl: process . env . STELLAR_HORIZON_URL ?? defaultHorizonUrl ,
networkPassphrase: process . env . STELLAR_NETWORK_PASSPHRASE ?? defaultPassphrase ,
};
}
Always verify you’re using the correct network passphrase. Transactions signed for testnet cannot be submitted to mainnet and vice versa.
Network identification
Mainnet : Public Global Stellar Network ; September 2015
Testnet : Test SDF Network ; September 2015
Custom : Can be configured via environment variable
Challenge verification
Before signing, PayOnProof verifies the challenge transaction:
const { clientAccountID } = WebAuth . readChallengeTx (
challengeJson . transaction ,
serverSigningKey ,
networkPassphrase ,
expectedHomeDomain ,
expectedWebAuthDomain
);
Verification checks
WebAuth.readChallengeTx performs:
Signature validation : Verifies anchor’s signature
Time bounds : Ensures transaction hasn’t expired
Sequence number : Validates sequence is zero (challenge pattern)
Operations : Verifies manage data operation format
Domain matching : Confirms home domain and web auth domain
Network : Ensures correct network passphrase
Never sign a challenge transaction without verification. The Stellar SDK’s WebAuth module provides secure validation.
Security considerations
Private key handling
const keypair = Keypair . fromSecret ( input . secretKey . trim ());
Secret keys are trimmed but never logged
Keys are used only for signing operations
Keys are not stored persistently in the implementation
Account validation
if ( clientAccountID !== account ) {
throw new Error ( "SEP-10 challenge account mismatch" );
}
Ensures the challenge is for the expected account before signing.
Timeout protection
const DEFAULT_TIMEOUT_MS = 8000 ;
async function fetchWithTimeout (
url : string ,
init : RequestInit ,
timeoutMs : number
) : Promise < Response > {
const controller = new AbortController ();
const timer = setTimeout (() => controller . abort (), timeoutMs );
try {
return await fetch ( url , { ... init , signal: controller . signal });
} finally {
clearTimeout ( timer );
}
}
Prevents hanging during authentication flows.
Multi-signature support
While not explicitly shown in the current implementation, Stellar supports multiple signatures:
// Example: Multiple signers (not in current codebase)
tx . sign ( keypair1 );
tx . sign ( keypair2 );
const signedTx = tx . toEnvelope (). toXDR ( "base64" );
This enables:
Multi-signature accounts
Co-signed transactions
Delegated signing patterns
The signed transaction envelope includes:
interface TransactionEnvelope {
tx : Transaction ; // The transaction
signatures : Signature []; // Array of signatures
}
interface Signature {
hint : Buffer ; // Last 4 bytes of public key
signature : Buffer ; // Ed25519 signature (64 bytes)
}
Error handling
Comprehensive error handling for signing operations:
if ( ! challengeJson . transaction ) {
throw new Error ( "SEP-10 challenge response missing transaction" );
}
try {
const { clientAccountID } = WebAuth . readChallengeTx ( ... );
if ( clientAccountID !== account ) {
throw new Error ( "SEP-10 challenge account mismatch" );
}
} catch ( error ) {
throw new Error ( `Challenge verification failed: ${ error . message } ` );
}
Environment configuration
Signing operations depend on proper environment setup:
export type PopEnv = "production" | "staging" ;
export function getPopEnv () : PopEnv {
const explicit = ( process . env . POP_ENV ?? "" ). trim (). toLowerCase ();
if ( explicit === "staging" ) return "staging" ;
if ( explicit === "production" ) return "production" ;
const passphrase = ( process . env . STELLAR_NETWORK_PASSPHRASE ?? "" ). trim ();
if ( passphrase === "Test SDF Network ; September 2015" ) {
return "staging" ;
}
const horizon = ( process . env . STELLAR_HORIZON_URL ?? "" ). trim (). toLowerCase ();
if ( horizon . includes ( "horizon-testnet.stellar.org" )) {
return "staging" ;
}
return "production" ;
}
The environment is auto-detected from network passphrase or Horizon URL if POP_ENV is not explicitly set.
Best practices
Always verify before signing : Use WebAuth.readChallengeTx or equivalent validation
Use correct network : Ensure network passphrase matches your target network
Secure key storage : Never log or expose secret keys
Timeout protection : Set reasonable timeouts for network operations
Error handling : Catch and handle signing errors appropriately
Account matching : Verify the challenge is for the intended account
Next steps
SEP-10 authentication Complete authentication flow
Network configuration Configure Horizon and networks
Keypair generation Development environment setup
Stellar SDK docs Official Stellar SDK documentation