Advanced SDK Usage
This guide covers advanced SDK features and patterns for building sophisticated blockchain applications.
Overview
Every extrinsic method in the SDK provides multiple ways to interact with the blockchain:
- Direct execution: Simple one-call transaction submission
- Manual workflow: Step-by-step control over build → sign → submit → status
- Fee estimation: Calculate transaction costs before execution
- Batch operations: Execute multiple transactions atomically
- Custom options: Fine-tune transaction parameters and confirmation behavior
Extrinsic methods
All extrinsic methods (like balance.transfer
, collection.create
, token.mint
, etc.) share the same underlying API structure. This section uses balance.transfer
as an example, but the patterns apply to all extrinsics.
Direct execution (default)
The simplest way to execute a transaction:
const { result, extrinsicOutput } = await unique.balance.transfer({
amount: '100',
to: "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
});
console.log('Transfer result:', result);
console.log('Transaction hash:', extrinsicOutput.hash);
console.log('Block hash:', extrinsicOutput.blockHash);
This method:
- Builds the transaction
- Signs it with your account
- Submits to the network
- Waits for confirmation
- Returns the result and transaction details
Manual workflow
For advanced control, you can handle each step separately:
1. Build
Build an unsigned transaction payload:
const unsignedTx = await unique.balance.transfer.build({
amount: '100',
to: "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
});
2. Sign
Sign the transaction with your account:
const signature = await unique.balance.transfer.sign(unsignedTx);
// Returns: "0x..." (hex signature)
You can also use a different account:
const signature = await unique.balance.transfer.sign(unsignedTx, customAccount);
3. Submit
Submit the signed transaction to the network:
const txHash = await unique.balance.transfer.submit(unsignedTx, signature);
// Returns: "0x..." (transaction hash)
4. Get status
Wait for and retrieve the transaction result:
const { result, extrinsicOutput } = await unique.balance.transfer.getStatus(
'/balances/transfer', // route - each method has its own route
txHash
);
Complete example
// Step-by-step transaction workflow
const unsignedTx = await unique.balance.transfer.build({
amount: '100',
to: "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
});
const signature = await unique.balance.transfer.sign(unsignedTx);
const txHash = await unique.balance.transfer.submit(unsignedTx, signature);
const { result, extrinsicOutput } = await unique.balance.transfer.getStatus(
'/balances/transfer',
txHash
);
console.log('Transfer completed:', result);
console.log('Block number:', extrinsicOutput.blockNumber);
Encode
Encode transaction parameters into a compact extrinsic format (used for batch operations):
const encoded = await unique.balance.transfer.encode({
amount: '100',
to: "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
});
console.log(encoded.compactExtrinsic); // "0x..."
This method is primarily used for creating batches (see Batch operations below).
Fee estimation
Calculate transaction fees before execution:
const fees = await unique.balance.transfer.fee({
amount: '100',
to: "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
});
console.log(fees);
Returns:
{
baseFee: '124000000', // Base transaction fee
lenFee: '0', // Length-based fee component
adjustedWeightFee: '124000000', // Weight-based fee component
totalFee: '124000000', // Total fee in wei
xcmFee: '0' // Cross-chain messaging fee (if applicable)
}
Build options
Customize how transactions are built:
await unique.balance.transfer(
{
amount: '100',
to: "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
},
{
signerAddress: "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
nonce: 5,
mortalLength: 64
}
);
signerAddress
Override the default account used for signing:
// Use a different account than the one configured in SDK
const result = await unique.balance.transfer(
{ amount: '100', to: recipientAddress },
{ signerAddress: customAccount.address }
);
nonce
Manually specify the transaction nonce. Useful for:
- Parallel transaction submission
- Replacing pending transactions
- Advanced queue management
const currentNonce = await unique.account.nextIndex({
address: account.address
});
// Submit multiple transactions with sequential nonces
await Promise.all([
unique.balance.transfer(
{ amount: '100', to: address1 },
{ nonce: currentNonce }
),
unique.balance.transfer(
{ amount: '200', to: address2 },
{ nonce: currentNonce + 1 }
),
unique.balance.transfer(
{ amount: '300', to: address3 },
{ nonce: currentNonce + 2 }
)
]);
mortalLength
Control transaction validity period:
// Transaction valid for 64 blocks
await unique.balance.transfer(
{ amount: '100', to: recipientAddress },
{ mortalLength: 64 }
);
// Immortal transaction (valid indefinitely)
await unique.balance.transfer(
{ amount: '100', to: recipientAddress },
{ mortalLength: -1 }
);
// Default mortal period
await unique.balance.transfer(
{ amount: '100', to: recipientAddress },
{ mortalLength: 0 }
);
Values:
- Positive number: Transaction valid for N blocks from current block
- 0: Use chain default mortal period
- -1: Immortal transaction (never expires)
WARNING
Immortal transactions can be replayed indefinitely if not included in a block. Use with caution.
Status options
Control how the SDK waits for transaction confirmation:
await unique.balance.transfer(
{ amount: '100', to: recipientAddress },
undefined, // buildOptions
undefined, // account override
{
retries: 20,
timeout: 5000,
isFinalized: true
}
);
retries
Maximum number of attempts to fetch transaction status:
// Check status up to 30 times
await unique.balance.transfer(
{ amount: '100', to: recipientAddress },
undefined,
undefined,
{ retries: 30 }
);
Default: 15 retries
timeout
Delay in milliseconds between status check attempts:
// Wait 10 seconds between each check
await unique.balance.transfer(
{ amount: '100', to: recipientAddress },
undefined,
undefined,
{ timeout: 10000 }
);
Default: 3000ms (3 seconds)
isFinalized
Wait for finalized block confirmation instead of just block inclusion:
// Wait for finalized confirmation (more secure)
await unique.balance.transfer(
{ amount: '100', to: recipientAddress },
undefined,
undefined,
{ isFinalized: true }
);
Default: false (wait for inclusion in a block)
TIP
For high-value transactions or when finality is critical, use isFinalized: true
.
Global status options
You can set default status options when initializing the SDK:
import { UniqueChain } from '@unique-nft/sdk/full';
const unique = UniqueChain({
baseUrl: 'https://rest.unique.network/opal/v1',
statusOptions: {
retries: 30,
timeout: 5000,
isFinalized: true
}
});
// All transactions will use these defaults
await unique.balance.transfer({ amount: '100', to: recipientAddress });
// You can still override per-transaction
await unique.balance.transfer(
{ amount: '100', to: recipientAddress },
undefined,
undefined,
{ isFinalized: false } // Override global setting
);
Batch operations
Execute multiple transactions atomically using utility.batchAll()
. If any transaction fails, the entire batch is reverted.
Basic batch
// Encode multiple operations
const operations = [
unique.balance.transfer.encode({
amount: '100',
to: "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
}),
unique.balance.transfer.encode({
amount: '200',
to: "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"
}),
unique.balance.transfer.encode({
amount: '300',
to: "5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy"
})
];
// Execute all operations in a single transaction
const { result, extrinsicOutput } = await unique.utility.batchAll(operations);
console.log('Batch successful:', result.isSuccess);
console.log('Number of operations:', result.calls.length);
console.log('Transaction hash:', extrinsicOutput.hash);
Account override
Override the signing account for a specific transaction:
import { KeyringProvider } from '@unique-nft/accounts/keyring';
// Initialize SDK with default account
const defaultAccount = await KeyringProvider.fromMnemonic(defaultMnemonic);
const unique = UniqueChain({
baseUrl: 'https://rest.unique.network/opal/v1',
account: defaultAccount
});
// Use a different account for a specific transaction
const customAccount = await KeyringProvider.fromMnemonic(customMnemonic);
await unique.balance.transfer(
{ amount: '100', to: recipientAddress },
{ signerAddress: customAccount.address }, // Specify address in build options
customAccount // Provide account for signing
);
Practical examples
Parallel transaction submission
async function sendToMultipleRecipientsParallel() {
// Get current nonce
const currentNonce = await unique.account.nextIndex({
address: account.address
});
const recipients = [
'5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
'5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty',
'5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy'
];
// Submit all transactions in parallel with sequential nonces
const results = await Promise.all(
recipients.map((address, index) =>
unique.balance.transfer(
{ amount: '100', to: address },
{ nonce: currentNonce + index }
)
)
);
console.log(`Sent ${results.length} transfers`);
results.forEach((result, index) => {
console.log(`Transfer ${index + 1}:`, result.extrinsicOutput.hash);
});
}
Custom signing workflow (external wallet)
async function signWithExternalWallet() {
// 1. Build transaction
const unsignedTx = await unique.balance.transfer.build(
{ amount: '100', to: recipientAddress },
{ signerAddress: externalWalletAddress }
);
// 2. Get user to sign with their external wallet
// (e.g., browser extension, hardware wallet)
const signature = await externalWallet.signPayload(
unsignedTx.signerPayloadJSON
);
// 3. Submit the signed transaction
const txHash = await unique.balance.transfer.submit(
unsignedTx,
signature
);
// 4. Wait for confirmation
const { result, extrinsicOutput } = await unique.balance.transfer.getStatus(
'/balances/transfer',
txHash,
{ isFinalized: true }
);
return result;
}
Best practices
Batch when possible
Instead of:
// Inefficient - multiple transactions
for (const recipient of recipients) {
await unique.balance.transfer({ amount: '100', to: recipient });
}
Use:
// Efficient - single atomic transaction
const batch = recipients.map(recipient =>
unique.balance.transfer.encode({ amount: '100', to: recipient })
);
await unique.utility.batchAll(batch);
Estimate fees for user confirmation
// Show user estimated fees before execution
const fees = await unique.collection.create.fee({
name: 'My Collection',
description: 'Description',
symbol: 'COL',
mode: 'Nft'
});
const { decimals } = await unique.balance.get({ address: account.address });
const feeInCoins = Number(fees.totalFee) / Math.pow(10, decimals);
// Show confirmation dialog
const confirmed = await showConfirmation(
`This operation will cost approximately ${feeInCoins} OPL. Continue?`
);
if (confirmed) {
await unique.collection.create({ ... });
}
Handle nonce management
// Good - let SDK manage nonce
await unique.balance.transfer({ amount: '100', to: address });
// Advanced - manual nonce for parallel submissions
const nonce = await unique.account.nextIndex({ address: account.address });
await Promise.all([
unique.balance.transfer({ amount: '100', to: address1 }, { nonce }),
unique.balance.transfer({ amount: '200', to: address2 }, { nonce: nonce + 1 })
]);
Error handling
try {
const result = await unique.balance.transfer(
{ amount: '100', to: recipientAddress },
undefined,
undefined,
{
retries: 30,
timeout: 5000,
isFinalized: true
}
);
console.log('Success:', result);
} catch (error) {
if (error.message.includes('Extrinsic not found')) {
console.error('Transaction timeout - network might be congested');
} else if (error.message.includes('balance too low')) {
console.error('Insufficient balance');
} else {
console.error('Transaction failed:', error);
}
}