SolanaSolana support coming soon. dWallets are expanding to Solana for native cross-chain signing.
Ika LogoIka Docs
Examples

Bitcoin Multisig Example

This example demonstrates a production-ready multi-signature wallet for Bitcoin transactions, showcasing all major Ika integration patterns.

Overview

The Bitcoin Multisig contract enables:

  • Multiple members to collectively approve Bitcoin transactions
  • Configurable approval and rejection thresholds
  • Time-based request expiration
  • Irrevocable voting to prevent manipulation
  • Automatic presign pool management

Source Code: examples/multisig-bitcoin/contract/

Architecture

Multisig Transaction Flow

1. Create Request
Member proposes transaction
request_future_sign()
Store PartialSigCap
with request
2. Voting
Members approve or reject
Approve
Reject
3. Execute
When threshold reached
Complete Signature
Broadcast to Bitcoin

Key Data Structures

Multisig Contract

public struct Multisig has key, store {
    id: UID,
    /// Bitcoin dWallet capability
    dwallet_cap: DWalletCap,
    /// Member addresses who can vote
    members: vector<address>,
    /// Votes needed to approve
    approval_threshold: u64,
    /// Votes needed to reject
    rejection_threshold: u64,
    /// Pending requests
    requests: Table<u64, Request>,
    /// Request expiration time (ms)
    expiration_duration: u64,
    /// Request ID counter
    request_id_counter: u64,
    /// Presign pool for signing
    presigns: vector<UnverifiedPresignCap>,
    /// Protocol fee balances
    ika_balance: Balance<IKA>,
    sui_balance: Balance<SUI>,
    /// Network encryption key
    dwallet_network_encryption_key_id: ID,
}

Request Types

public enum RequestType has copy, drop, store {
    Transaction(vector<u8>, vector<u8>, vector<u8>),  // preimage, sig, psbt
    AddMember(address),
    RemoveMember(address),
    ChangeApprovalThreshold(u64),
    ChangeRejectionThreshold(u64),
    ChangeExpirationDuration(u64),
}

Workflow

1. Create Multisig

public fun new_multisig(
    coordinator: &mut DWalletCoordinator,
    mut initial_ika: Coin<IKA>,
    mut initial_sui: Coin<SUI>,
    dwallet_network_encryption_key_id: ID,
    // DKG parameters
    centralized_public_key_share_and_proof: vector<u8>,
    user_public_output: vector<u8>,
    public_user_secret_key_share: vector<u8>,
    session_identifier: vector<u8>,
    // Governance config
    members: vector<address>,
    approval_threshold: u64,
    rejection_threshold: u64,
    expiration_duration: u64,
    ctx: &mut TxContext,
) {
    // Validate thresholds
    assert!(approval_threshold > 0, EInvalidThreshold);
    assert!(rejection_threshold > 0, EInvalidThreshold);
    assert!(approval_threshold <= members.length(), EThresholdTooHigh);
    assert!(rejection_threshold <= members.length(), EThresholdTooHigh);
 
    // Deduplicate members
    let members = vec_set::from_keys(members).into_keys();
 
    // Register session and perform DKG
    let session = coordinator.register_session_identifier(session_identifier, ctx);
 
    let (dwallet_cap, _) = coordinator.request_dwallet_dkg_with_public_user_secret_key_share(
        dwallet_network_encryption_key_id,
        constants::curve!(),              // secp256k1
        centralized_public_key_share_and_proof,
        user_public_output,
        public_user_secret_key_share,
        option::none(),
        session,
        &mut initial_ika,
        &mut initial_sui,
        ctx,
    );
 
    // Create multisig with initial presign
    let mut multisig = Multisig { /* ... */ };
 
    // Request initial presign
    let presign_session = random_session(coordinator, ctx);
    multisig.presigns.push_back(coordinator.request_global_presign(
        dwallet_network_encryption_key_id,
        constants::curve!(),
        constants::signature_algorithm!(),  // Taproot
        presign_session,
        &mut initial_ika,
        &mut initial_sui,
        ctx,
    ));
 
    // Share the multisig
    transfer::public_share_object(multisig);
}

2. Create Transaction Request

Uses future signing to create a partial signature that's stored with the request:

public fun transaction_request(
    self: &mut Multisig,
    coordinator: &mut DWalletCoordinator,
    preimage: vector<u8>,              // Bitcoin transaction hash (BIP 341)
    message_centralized_signature: vector<u8>,
    psbt: vector<u8>,                  // Full transaction data
    clock: &Clock,
    ctx: &mut TxContext,
): u64 {
    assert!(self.members.contains(&ctx.sender()), ENotMember);
 
    let (mut ika, mut sui) = self.withdraw_payment_coins(ctx);
 
    // Pop and verify presign
    let unverified = self.presigns.swap_remove(0);
    let verified = coordinator.verify_presign_cap(unverified, ctx);
 
    let session = random_session(coordinator, ctx);
 
    // Create partial signature (future sign - Phase 1)
    let partial_cap = coordinator.request_future_sign(
        self.dwallet_cap.dwallet_id(),
        verified,
        preimage,
        constants::hash_scheme!(),  // SHA256
        message_centralized_signature,
        session,
        &mut ika,
        &mut sui,
        ctx,
    );
 
    // Replenish presign pool
    if (self.presigns.length() == 0) {
        let replenish_session = random_session(coordinator, ctx);
        self.presigns.push_back(coordinator.request_global_presign(
            self.dwallet_network_encryption_key_id,
            constants::curve!(),
            constants::signature_algorithm!(),
            replenish_session,
            &mut ika,
            &mut sui,
            ctx,
        ));
    };
 
    self.return_payment_coins(ika, sui);
 
    // Create request with partial signature
    self.new_request(
        multisig_request::request_transaction(preimage, message_centralized_signature, psbt),
        option::some(partial_cap),
        clock,
        ctx,
    )
}

3. Vote on Request

Voting is irrevocable - once cast, votes cannot be changed:

public fun vote_request(
    self: &mut Multisig,
    request_id: u64,
    vote: bool,
    clock: &Clock,
    ctx: &mut TxContext,
) {
    assert!(self.requests.contains(request_id), ERequestNotFound);
 
    let request = self.requests.borrow_mut(request_id);
 
    // Validate
    assert!(request.status() == pending(), ERequestNotPending);
    assert!(self.members.contains(&ctx.sender()), ENotMember);
    assert!(!request.votes().contains(ctx.sender()), EAlreadyVoted);
 
    // Check expiration
    if (clock.timestamp_ms() > *request.created_at() + self.expiration_duration) {
        self.reject_request(request_id);
        return
    };
 
    // Record vote (irrevocable)
    request.votes().add(ctx.sender(), vote);
 
    if (vote) {
        *request.approvers_count() = *request.approvers_count() + 1;
    } else {
        *request.rejecters_count() = *request.rejecters_count() + 1;
    };
 
    // Auto-reject if rejection threshold met
    if (*request.rejecters_count() >= self.rejection_threshold) {
        self.reject_request(request_id);
    };
}

4. Execute Approved Request

Complete the signature using the stored partial signature:

public fun execute_request(
    self: &mut Multisig,
    coordinator: &mut DWalletCoordinator,
    request_id: u64,
    clock: &Clock,
    ctx: &mut TxContext,
) {
    let request = self.requests.borrow_mut(request_id);
 
    // Validate
    assert!(request.status() == pending(), ERequestNotPending);
    assert!(*request.approvers_count() >= self.approval_threshold, EInsufficientApprovals);
 
    // Check expiration
    if (clock.timestamp_ms() > *request.created_at() + self.expiration_duration) {
        self.reject_request(request_id);
        return
    };
 
    let (mut ika, mut sui) = self.withdraw_payment_coins(ctx);
 
    // Handle transaction request (future sign - Phase 2)
    if (request.request_type().is_transaction()) {
        let (preimage, _, _) = request.request_type().get_transaction_data();
 
        // Extract and verify partial signature
        let partial_cap = request.partial_sig_cap().extract();
        let verified = coordinator.verify_partial_user_signature_cap(partial_cap, ctx);
 
        // Create message approval
        let approval = coordinator.approve_message(
            &self.dwallet_cap,
            constants::signature_algorithm!(),
            constants::hash_scheme!(),
            preimage,
        );
 
        let session = random_session(coordinator, ctx);
 
        // Complete signature
        let sign_id = coordinator.request_sign_with_partial_user_signature_and_return_id(
            verified,
            approval,
            session,
            &mut ika,
            &mut sui,
            ctx,
        );
 
        // Store result
        request.resolve(transaction_result(sign_id));
    };
 
    // Handle governance requests...
 
    self.return_payment_coins(ika, sui);
}

Constants Configuration

module ika_btc_multisig::constants;
 
/// Bitcoin uses secp256k1 curve
public macro fun curve(): u32 { 0 }
 
/// Taproot signature algorithm
public macro fun signature_algorithm(): u32 { 1 }
 
/// SHA256 hash for Taproot
public macro fun hash_scheme(): u32 { 0 }

Key Patterns Demonstrated

1. Shared dWallet Ownership

The contract owns the DWalletCap, enabling automated signing:

public struct Multisig has key, store {
    dwallet_cap: DWalletCap,  // Contract owns this
}

2. Presign Pool with Auto-Replenishment

// After using a presign, replenish if pool is empty
if (self.presigns.length() == 0) {
    self.presigns.push_back(coordinator.request_global_presign(...));
};

3. Future Signing for Governance

Transaction requests use future signing to separate commitment from execution:

// Phase 1: Create partial signature when request is made
let partial_cap = coordinator.request_future_sign(...);
 
// Phase 2: Complete signature after approval
let verified = coordinator.verify_partial_user_signature_cap(partial_cap, ctx);
coordinator.request_sign_with_partial_user_signature(...);

4. Payment Management

fun withdraw_payment_coins(self: &mut Multisig, ctx: &mut TxContext): (Coin<IKA>, Coin<SUI>) {
    let ika = self.ika_balance.withdraw_all().into_coin(ctx);
    let sui = self.sui_balance.withdraw_all().into_coin(ctx);
    (ika, sui)
}
 
fun return_payment_coins(self: &mut Multisig, ika: Coin<IKA>, sui: Coin<SUI>) {
    self.ika_balance.join(ika.into_balance());
    self.sui_balance.join(sui.into_balance());
}

5. Irrevocable Voting

Using Table ensures votes cannot be modified:

// Once added, the entry cannot be changed
request.votes().add(ctx.sender(), vote);

File Structure

examples/multisig-bitcoin/contract/
├── Move.toml
└── sources/
    ├── multisig.move        # Main contract logic
    ├── request.move         # Request types and lifecycle
    ├── events.move          # Event definitions
    ├── constants.move       # Bitcoin protocol constants
    ├── error.move           # Error codes
    └── lib/
        └── event_wrapper.move  # Event emission utility

Testing

Build

cd examples/multisig-bitcoin/contract
sui move build

Test

sui move test

Deploy to Testnet

sui client publish --gas-budget 100000000

TypeScript Integration

To interact with the deployed contract:

import { IkaClient, IkaTransaction, prepareDKGAsync } from '@ika.xyz/sdk';
import { Transaction } from '@mysten/sui/transactions';
 
// 1. Prepare DKG data
const dkgData = await prepareDKGAsync(ikaClient, Curve.SECP256K1, ...);
 
// 2. Create multisig
const tx = new Transaction();
tx.moveCall({
    target: `${PACKAGE_ID}::multisig::new_multisig`,
    arguments: [
        tx.object(coordinatorId),
        tx.object(ikaCoinId),
        tx.splitCoins(tx.gas, [1000000]),
        tx.pure.id(networkKeyId),
        tx.pure.vector('u8', Array.from(dkgData.userDKGMessage)),
        tx.pure.vector('u8', Array.from(dkgData.userPublicOutput)),
        tx.pure.vector('u8', Array.from(dkgData.userSecretKeyShare)),
        tx.pure.vector('u8', Array.from(sessionIdentifier)),
        tx.pure.vector('address', members),
        tx.pure.u64(approvalThreshold),
        tx.pure.u64(rejectionThreshold),
        tx.pure.u64(expirationDuration),
    ],
});
 
// 3. Execute
await suiClient.core.signAndExecuteTransaction({ transaction: tx, signer: keypair });

Security Considerations

  1. Irrevocable Votes: Once cast, votes cannot be changed
  2. Expiration: Requests automatically expire to prevent stale states
  3. Threshold Validation: Thresholds must be valid (greater than 0, at most member count)
  4. Member Deduplication: Duplicate members are automatically removed

Next Steps