Quick Start
Build your first Hoosat canister in minutes. This guide walks you through creating a simple wallet canister that can generate addresses, check balances, and send transactions.
Prerequisites
Before you begin, make sure you have:
- Completed the Installation guide
- DFX and Mops installed
- Basic understanding of Motoko
Project Setup
1. Create a New Project
# Create project directory
mkdir my-hoosat-wallet
cd my-hoosat-wallet
# Initialize DFX project
dfx new hoosat_wallet --type motoko
cd hoosat_wallet
# Initialize Mops
mops init
2. Install Hoosat-mo
mops add hoosat-mo
3. Configure dfx.json
Update your dfx.json:
{
"canisters": {
"hoosat_wallet": {
"main": "src/hoosat_wallet/main.mo",
"type": "motoko"
}
},
"defaults": {
"build": {
"packtool": "mops sources"
}
},
"version": 1
}
Your First Hoosat Canister
Create the Wallet Canister
Replace the contents of src/hoosat_wallet/main.mo:
import Wallet "mo:hoosat-mo/wallet";
import Result "mo:base/Result";
import Errors "mo:hoosat-mo/errors";
import Debug "mo:base/Debug";
actor HoosatWallet {
// Initialize mainnet wallet with dfx_test_key
let wallet = Wallet.createMainnetWallet("dfx_test_key", ?"hoosat");
// Store our address
private var myAddress : Text = "";
// Generate and store Hoosat address
public func initWallet() : async Text {
let result = await wallet.generateAddress(null, null);
switch (result) {
case (#ok(addr)) {
myAddress := addr.address;
Debug.print("Address generated: " # addr.address);
return addr.address;
};
case (#err(e)) {
let errorMsg = Errors.errorToText(e);
Debug.print("Error: " # errorMsg);
return "Error: " # errorMsg;
};
};
};
// Get current wallet address
public query func getAddress() : async Text {
return myAddress;
};
// Check balance for our address
public func getBalance() : async Nat64 {
if (myAddress == "") {
return 0;
};
let result = await wallet.getBalance(myAddress);
switch (result) {
case (#ok(balance)) {
Debug.print("Balance: " # debug_show(balance) # " sompi");
return balance;
};
case (#err(e)) {
Debug.print("Error getting balance: " # Errors.errorToText(e));
return 0;
};
};
};
// Send transaction
public func sendHoosat(to: Text, amount: Nat64) : async ?Text {
if (myAddress == "") {
Debug.print("Error: Wallet not initialized");
return null;
};
let result = await wallet.sendTransaction(
myAddress,
to,
amount,
null, // Use default fee
null // Use default derivation path
);
switch (result) {
case (#ok(tx)) {
Debug.print("Transaction sent: " # tx.transactionId);
return ?tx.transactionId;
};
case (#err(e)) {
Debug.print("Error: " # Errors.errorToText(e));
return null;
};
};
};
};
Deploy and Test
1. Start Local Replica
dfx start --background
2. Deploy the Canister
dfx deploy
You should see output like:
Deploying: hoosat_wallet
...
Deployed canisters.
URLs:
hoosat_wallet: http://127.0.0.1:4943/?canisterId=xxxxx-xxxxx-xxxxx-xxxxx-xxx
3. Test the Canister
Generate an Address
dfx canister call hoosat_wallet initWallet
Expected output:
("hoosat:qpxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
Get the Address
dfx canister call hoosat_wallet getAddress
Expected output:
("hoosat:qpxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
Check Balance
dfx canister call hoosat_wallet getBalance
Expected output (for new address):
(0 : nat64)
Send a Transaction (requires funded address)
dfx canister call hoosat_wallet sendHoosat '("hoosat:qprecipient_address_here", 100000000)'
Understanding the Code
Wallet Initialization
let wallet = Wallet.createMainnetWallet("dfx_test_key", ?"hoosat");
- Creates a wallet instance for Hoosat mainnet
- Uses
dfx_test_keyfor ECDSA operations (local development) - Optional prefix
"hoosat"for address generation
Address Generation
let result = await wallet.generateAddress(null, null);
- Generates ECDSA address using IC's threshold ECDSA
- First
null: use default address type (ECDSA) - Second
null: use default derivation path
Balance Checking
let result = await wallet.getBalance(myAddress);
- Fetches balance via HTTP outcall to Hoosat API
- Returns balance in sompi (smallest unit)
- 1 HTN = 100,000,000 sompi
Sending Transactions
let result = await wallet.sendTransaction(
myAddress, // From address
to, // Recipient address
amount, // Amount in sompi
null, // Optional fee (auto-calculated if null)
null // Optional derivation path
);
- Builds, signs, and broadcasts transaction
- Returns transaction ID on success
- Handles errors gracefully
Next: Advanced Features
Add Balance Formatting
import Nat64 "mo:base/Nat64";
import Float "mo:base/Float";
public query func getBalanceFormatted() : async Text {
if (myAddress == "") {
return "0 HTN";
};
let result = await wallet.getBalance(myAddress);
switch (result) {
case (#ok(balance)) {
// Convert sompi to HTN (1 HTN = 100,000,000 sompi)
let htn = Float.fromInt64(Int64.fromNat64(balance)) / 100000000.0;
return Float.toText(htn) # " HTN";
};
case (#err(_)) {
return "Error fetching balance";
};
};
};
Add Transaction History Storage
import Array "mo:base/Array";
private stable var transactions : [(Text, Nat64, Text)] = [];
public func sendHoosatWithHistory(to: Text, amount: Nat64) : async ?Text {
let result = await sendHoosat(to, amount);
switch (result) {
case (?txId) {
// Store transaction history
transactions := Array.append(
transactions,
[(to, amount, txId)]
);
return ?txId;
};
case (null) {
return null;
};
};
};
public query func getTransactionHistory() : async [(Text, Nat64, Text)] {
return transactions;
};
Add Multiple Address Support
import HashMap "mo:base/HashMap";
import Principal "mo:base/Principal";
import Hash "mo:base/Hash";
private var addresses = HashMap.HashMap<Principal, Text>(
10,
Principal.equal,
Principal.hash
);
public shared(msg) func getMyAddress() : async Text {
let caller = msg.caller;
switch (addresses.get(caller)) {
case (?addr) {
return addr;
};
case (null) {
// Generate new address for this caller
let result = await wallet.generateAddress(null, null);
switch (result) {
case (#ok(addr)) {
addresses.put(caller, addr.address);
return addr.address;
};
case (#err(_)) {
return "";
};
};
};
};
};
Testing with Candid UI
After deployment, you can test via the Candid web interface:
-
Open Candid UI:
# Get the canister URL
dfx canister call hoosat_wallet --query http_requestOr visit:
http://127.0.0.1:4943/?canisterId=YOUR_CANISTER_ID -
Test Functions:
- Click
initWalletto generate address - Click
getAddressto view address - Click
getBalanceto check balance - Use
sendHoosatwith parameters to send transactions
- Click
Common Patterns
Error Handling
public func safeOperation() : async Result.Result<Text, Errors.HoosatError> {
let result = await wallet.generateAddress(null, null);
switch (result) {
case (#ok(addr)) {
return #ok(addr.address);
};
case (#err(e)) {
// Log error and return
Debug.print("Error: " # Errors.errorToText(e));
return #err(e);
};
};
};
Validation Before Sending
import Validation "mo:hoosat-mo/validation";
public func safeSend(to: Text, amount: Nat64) : async ?Text {
// Validate recipient address
let addrValidation = Validation.validateAddress(to);
switch (addrValidation) {
case (#err(e)) {
Debug.print("Invalid address: " # Errors.errorToText(e));
return null;
};
case (#ok(_)) {
// Validate amount
if (amount < 1000) {
Debug.print("Amount too small (dust threshold)");
return null;
};
// Send transaction
return await sendHoosat(to, amount);
};
};
};
Next Steps
Now that you have a basic wallet, explore:
- Address Module - Learn about different address types
- Wallet Module - Explore all wallet features
- Production Guide - Deploy to IC mainnet
- Examples - See more complex implementations
Troubleshooting
Address Generation Fails
Issue: ecdsa_public_key failed
Solution:
- Ensure you're using
dfx_test_keyfor local development - For IC mainnet, you need production key access
- Check that your canister has sufficient cycles
Balance Always Returns 0
Issue: Balance is 0 for new addresses
Solution:
- New addresses have no balance until funded
- Send HTN to the address using a wallet or faucet
- Verify address format is correct
Transaction Fails
Issue: sendHoosat returns null
Solution:
- Ensure address has sufficient balance
- Check recipient address format
- Verify amount is above dust threshold (1000 sompi)
- Ensure wallet is initialized