Transactions
Learn how to create, sign, and submit transactions to GLIN Network.
Overview
Transactions are state-changing operations that:
- 💸 Transfer tokens between accounts
- 📝 Execute contract methods
- 🗳️ Participate in governance
- ⚙️ Change system state
All transactions require gas fees paid in GLIN tokens.
Transaction Lifecycle
1. Create → 2. Sign → 3. Submit → 4. In Block → 5. Finalized
1. Create Transaction
- TypeScript
- Rust
import { GlinClient } from '@glin-ai/sdk';
const client = await GlinClient.connect('wss://testnet.glin.ai');
// Create a transfer transaction
const tx = client.api.tx.balances.transfer(
recipientAddress,
1000n * 10n ** 18n // 1000 GLIN
);
use glin_client::create_client;
let client = create_client("wss://testnet.glin.ai").await?;
// Create a transfer transaction
let tx = subxt::dynamic::tx(
"Balances",
"transfer",
vec![
subxt::dynamic::Value::from_bytes(recipient_address),
subxt::dynamic::Value::u128(1000u128 * 10u128.pow(18)),
],
);
2. Sign Transaction
- TypeScript
- Rust
import { Keyring } from '@glin-ai/sdk';
const keyring = new Keyring({ type: 'sr25519' });
const sender = keyring.addFromUri('//Alice');
// Sign the transaction
const signedTx = tx.sign(sender);
use subxt::tx::PairSigner;
let sender = glin_client::get_dev_account("alice")?;
let signer = PairSigner::new(sender);
// Signing happens during submission
3. Submit Transaction
- TypeScript
- Rust
// Sign and submit in one step
const hash = await tx.signAndSend(sender);
console.log('Transaction hash:', hash.toHex());
// Sign and submit
let hash = client
.tx()
.sign_and_submit_default(&tx, &signer)
.await?;
println!("Transaction hash: {:?}", hash);
4. Wait for Finalization
- TypeScript
- Rust
await new Promise((resolve, reject) => {
tx.signAndSend(sender, ({ status, events, dispatchError }) => {
if (status.isInBlock) {
console.log(`In block: ${status.asInBlock.toHex()}`);
}
if (status.isFinalized) {
console.log(`Finalized: ${status.asFinalized.toHex()}`);
if (dispatchError) {
reject(new Error('Transaction failed'));
} else {
resolve(status.asFinalized);
}
}
});
});
// Wait for finalization
let events = client.wait_for_finalization(hash).await?;
println!("Transaction finalized!");
println!("Events: {:?}", events);
Common Transactions
Transfer GLIN
- TypeScript
- Rust
const transfer = client.api.tx.balances.transfer(
recipientAddress,
100n * 10n ** 18n // 100 GLIN
);
await transfer.signAndSend(sender);
let transfer_tx = subxt::dynamic::tx(
"Balances",
"transfer",
vec![
recipient_address.into(),
(100u128 * 10u128.pow(18)).into(),
],
);
client.tx()
.sign_and_submit_default(&transfer_tx, &signer)
.await?;
Batch Transactions
Execute multiple transactions atomically:
- TypeScript
- Rust
const tx1 = client.api.tx.balances.transfer(alice.address, 100n);
const tx2 = client.api.tx.balances.transfer(bob.address, 200n);
const tx3 = client.api.tx.balances.transfer(charlie.address, 300n);
// Batch all transactions
const batch = client.api.tx.utility.batch([tx1, tx2, tx3]);
await batch.signAndSend(sender);
let batch_tx = subxt::dynamic::tx(
"Utility",
"batch",
vec![
tx1.into(),
tx2.into(),
tx3.into(),
],
);
client.tx()
.sign_and_submit_default(&batch_tx, &signer)
.await?;
Force Batch (Continue on Error)
// batchAll: fails if any transaction fails
const batchAll = client.api.tx.utility.batchAll([tx1, tx2, tx3]);
// forceBatch: continues even if some transactions fail
const forceBatch = client.api.tx.utility.forceBatch([tx1, tx2, tx3]);
Transaction Fees
Estimate Fees
- TypeScript
- Rust
const info = await tx.paymentInfo(sender);
console.log('Estimated fee:', info.partialFee.toString());
// Fee estimation in Rust
let payment_info = client
.tx()
.payment_details(&tx, &signer)
.await?;
println!("Estimated fee: {:?}", payment_info);
Add Tip
Prioritize your transaction by adding a tip:
- TypeScript
- Rust
const tip = 10n ** 16n; // 0.01 GLIN
await tx.signAndSend(sender, { tip });
// Tips in Rust (implementation depends on SDK version)
Transaction Status
Track transaction status:
- TypeScript
- Rust
tx.signAndSend(sender, ({ status, events }) => {
if (status.isReady) {
console.log('🔄 Ready to be included');
}
if (status.isBroadcast) {
console.log('📡 Broadcast to network');
}
if (status.isInBlock) {
console.log('📦 Included in block');
}
if (status.isFinalized) {
console.log('✅ Finalized');
// Process events
events.forEach(({ event }) => {
console.log('Event:', event.toHuman());
});
}
if (status.isInvalid) {
console.log('❌ Invalid transaction');
}
if (status.isDropped) {
console.log('⚠️ Dropped from pool');
}
});
// Transaction progress tracking
let mut progress = client
.tx()
.sign_and_submit_then_watch_default(&tx, &signer)
.await?;
while let Some(status) = progress.next().await {
match status? {
TxStatus::Future => println!("🔄 Future"),
TxStatus::Ready => println!("📡 Ready"),
TxStatus::Broadcast(_) => println!("📡 Broadcast"),
TxStatus::InBlock(block) => println!("📦 In block: {:?}", block),
TxStatus::Finalized(block) => {
println!("✅ Finalized: {:?}", block);
break;
}
TxStatus::Invalid => println!("❌ Invalid"),
TxStatus::Dropped => println!("⚠️ Dropped"),
_ => {}
}
}
Error Handling
- TypeScript
- Rust
try {
await new Promise((resolve, reject) => {
tx.signAndSend(sender, ({ status, dispatchError }) => {
if (status.isFinalized) {
if (dispatchError) {
if (dispatchError.isModule) {
const decoded = client.api.registry.findMetaError(
dispatchError.asModule
);
reject(new Error(`${decoded.section}.${decoded.name}: ${decoded.docs}`));
} else {
reject(new Error(dispatchError.toString()));
}
} else {
resolve(status.asFinalized);
}
}
});
});
} catch (error) {
console.error('Transaction failed:', error.message);
}
match client.tx().sign_and_submit_default(&tx, &signer).await {
Ok(hash) => {
println!("Transaction submitted: {:?}", hash);
match client.wait_for_finalization(hash).await {
Ok(events) => println!("Success! Events: {:?}", events),
Err(e) => eprintln!("Transaction failed: {}", e),
}
}
Err(e) => {
eprintln!("Failed to submit: {}", e);
}
}
Best Practices
1. Always Wait for Finalization
// ❌ Bad - don't assume success
const hash = await tx.signAndSend(sender);
// Transaction might still fail!
// ✅ Good - wait for finalization
await new Promise((resolve) => {
tx.signAndSend(sender, ({ status }) => {
if (status.isFinalized) resolve();
});
});
2. Check Balance Before Transfer
const balance = await client.getBalance(sender.address);
const amount = 1000n * 10n ** 18n;
if (balance.free < amount) {
throw new Error('Insufficient balance');
}
3. Handle Nonce Manually (Advanced)
// Get next nonce
const nonce = await client.api.rpc.system.accountNextIndex(sender.address);
// Use specific nonce
await tx.signAndSend(sender, { nonce });
4. Set Mortality Period
// Transaction expires after 64 blocks
await tx.signAndSend(sender, { era: 64 });
Transaction Events
Process events emitted by transactions:
- TypeScript
- Rust
tx.signAndSend(sender, ({ status, events }) => {
if (status.isFinalized) {
events.forEach(({ event }) => {
if (client.api.events.system.ExtrinsicSuccess.is(event)) {
console.log('✅ Transaction succeeded');
}
if (client.api.events.system.ExtrinsicFailed.is(event)) {
console.log('❌ Transaction failed');
}
if (client.api.events.balances.Transfer.is(event)) {
const [from, to, amount] = event.data;
console.log(`💸 Transfer: ${from} → ${to}: ${amount}`);
}
});
}
});
let events = client.wait_for_finalization(hash).await?;
for event in events.iter() {
let event = event?;
if let Some(transfer) = event.as_event::<BalancesTransfer>()? {
println!("💸 Transfer: {:?} → {:?}: {}",
transfer.from,
transfer.to,
transfer.amount
);
}
}
#[derive(Debug, subxt::Event)]
#[event(module = "Balances")]
struct BalancesTransfer {
from: subxt::utils::AccountId32,
to: subxt::utils::AccountId32,
amount: u128,
}
Next Steps
- 💰 Accounts - Manage accounts
- 📝 Deploy Contracts - Deploy smart contracts
- 💡 Examples - Practical examples
Need help? Join our Discord