Submitting Transactions
Learn how to submit transactions to modify state on the Convex network.
Overview
Transactions are state-modifying operations that require:
- An account address (e.g.,
#1678) - A cryptographic signer (key pair or hardware wallet)
- Sufficient balance to pay for execution
Unlike queries, transactions are recorded on the network and modify the global state.
Prerequisites
Before submitting transactions, you need:
- Account Address - Your Convex account number
- Ed25519 Seed - Your 32-byte private seed
- Balance - Sufficient Convex Coins for transaction fees
Set up your account:
import { Convex, KeyPair } from '@convex-world/convex-ts';
const convex = new Convex('https://peer.convex.live');
const keyPair = KeyPair.fromSeed(process.env.CONVEX_SEED!);
convex.setAccount('#1678', keyPair);
Basic Transfer
The simplest transaction transfers Convex Coins:
// Transfer 1 Convex Coin (1,000,000,000 copper)
const result = await convex.transfer('#456', 1_000_000_000);
if (result.status === 'success') {
console.log('✅ Transfer successful!');
console.log(' Transaction hash:', result.hash);
} else {
console.error('❌ Transfer failed:', result.error);
}
Copper vs Coins
Amounts are in copper coins where:
- 1 Convex Coin = 1,000,000,000 copper
- Minimum amount = 1 copper
Executing Convex Lisp
Execute arbitrary Convex Lisp code as a transaction:
// Deploy a function
const result = await convex.transact(`
(def greet
(fn [name]
(str "Hello, " name "!")))
`);
// Call the function
const greeting = await convex.transact('(greet "Alice")');
console.log(greeting.result); // "Hello, Alice!"
Transaction Object
Use the object form for more control:
const result = await convex.transact({
to: '#456', // Destination (optional)
amount: 1_000_000_000, // Amount in copper (optional)
data: { // Additional data (optional)
memo: 'Payment for services',
invoice: 'INV-2024-001'
}
});
Transaction Results
Success
interface TransactionResult {
status: 'success';
hash: string; // Transaction hash
result: any; // Return value of executed code
sequence: number; // Transaction sequence number
}
Example:
const result = await convex.transact('(+ 1 2 3)');
if (result.status === 'success') {
console.log('Hash:', result.hash);
console.log('Result:', result.result); // 6
console.log('Sequence:', result.sequence);
}
Failure
interface TransactionResult {
status: 'error';
error: string; // Error message
errorCode?: string; // Error code
}
Example:
try {
const result = await convex.transact('(/ 1 0)'); // Division by zero
if (result.status === 'error') {
console.error('Transaction failed:', result.error);
console.error('Error code:', result.errorCode);
}
} catch (error) {
console.error('Network or signing error:', error);
}
Common Transaction Patterns
Deploy Smart Contract
const contract = await convex.transact(`
(do
(def token-balance
(let [balances {}]
{:balances balances}))
(defn transfer [to amount]
(let [from *caller*
from-bal (get-in token-balance [:balances from] 0)
to-bal (get-in token-balance [:balances to] 0)]
(cond
(< from-bal amount)
(fail "Insufficient balance")
:else
(do
(assoc-in! token-balance [:balances from] (- from-bal amount))
(assoc-in! token-balance [:balances to] (+ to-bal amount))
{:success true})))))
`);
console.log('Contract deployed:', contract.hash);
Call Smart Contract
// Call a deployed actor
const result = await convex.transact('(call #789 (transfer #456 1000))');
if (result.status === 'success') {
console.log('Contract call successful:', result.result);
}
Batch Operations
// Multiple operations in one transaction
const result = await convex.transact(`
(do
(def user-data {:name "Alice" :level 5})
(def user-inventory [:sword :shield :potion])
(transfer #789 100000)
{:user user-data
:inventory user-inventory})
`);
Error Handling
Network Errors
try {
const result = await convex.transact('(+ 1 2)');
} catch (error) {
if (error instanceof Error) {
if (error.message.includes('timeout')) {
console.error('Network timeout - retry?');
} else if (error.message.includes('connect')) {
console.error('Cannot connect to peer');
} else {
console.error('Unexpected error:', error.message);
}
}
}
Transaction Failures
const result = await convex.transact('(transfer #456 999999999999)');
if (result.status === 'error') {
if (result.errorCode === 'FUNDS') {
console.error('Insufficient balance');
} else if (result.errorCode === 'STATE') {
console.error('Invalid state transition');
} else {
console.error('Transaction failed:', result.error);
}
}
Retry Logic
async function transactWithRetry(
convex: Convex,
code: string,
maxRetries = 3
): Promise<TransactionResult> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = await convex.transact(code);
return result;
} catch (error) {
if (attempt === maxRetries) throw error;
console.log(`Attempt ${attempt} failed, retrying...`);
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
throw new Error('Unreachable');
}
Transaction Lifecycle
- Prepare - Client creates transaction with sequence number
- Sign - Transaction is signed with your private key
- Submit - Signed transaction sent to peer
- Validate - Peer validates signature and sequence
- Execute - CVM executes the transaction code
- Consensus - Transaction included in consensus
- Finalise - Transaction permanently recorded
Sequence Numbers
Each account has a sequence number that increments with each transaction:
// Get current sequence
const sequence = await convex.getSequence();
console.log('Next transaction sequence:', sequence);
// Explicitly set sequence (rarely needed)
const result = await convex.transact({
to: '#456',
amount: 1_000_000_000,
sequence: sequence
});
Automatic Sequence Management
The SDK automatically manages sequence numbers. You rarely need to set them manually.
Juice and Fees
Transactions consume juice which is paid in Convex Coins:
// Check juice price
const priceInfo = await convex.query('*juice-price*');
console.log('Juice price:', priceInfo.value);
// Estimate transaction cost
const estimate = await convex.query(`
(juice-cost '(transfer #456 1000000))
`);
console.log('Estimated juice:', estimate.value);
Advanced Patterns
Conditional Transfer
const result = await convex.transact(`
(let [balance (balance *address*)
threshold 10000000]
(if (> balance threshold)
(do
(transfer #456 (- balance threshold))
{:transferred (- balance threshold)})
{:transferred 0}))
`);
Time-locked Transaction
const result = await convex.transact(`
(let [unlock-time 1735689600000] ; Unix timestamp
(if (> (timestamp) unlock-time)
(transfer #456 1000000)
(fail "Funds locked until unlock time")))
`);
Multi-step Transaction
const result = await convex.transact(`
(do
; Step 1: Validate
(assert (> (balance *address*) 1000000) "Insufficient balance")
; Step 2: Transfer
(transfer #456 500000)
; Step 3: Update state
(def last-transfer (timestamp))
; Step 4: Return receipt
{:success true
:amount 500000
:timestamp last-transfer})
`);
Best Practices
✅ Do
- Check balance before large transfers
- Handle both success and error cases
- Use meaningful error messages
- Log transaction hashes for audit trails
- Test on testnet first
❌ Don't
- Hardcode private seeds in code
- Ignore transaction errors
- Submit transactions in tight loops
- Forget to check result status
- Skip validation of user input
Next Steps
- Accounts - Key pair and account management
- Signers - Hardware wallet integration
- Actors - Build smart contracts
- Convex Lisp - Learn the language