Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Guide for integrating Ika dWallet 2PC-MPC protocol into Sui Move contracts. Use when building Move contracts that need cross-chain signing, dWallet creation, presigning, signing, future signing, key importing, or any Ika on-chain integration. Triggers on Move/Sui contract tasks involving dWallets, cross-chain signing, or Ika protocol operations.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 95% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 186% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 256% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 104% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 188% | 0% |
Build Sui Move contracts integrating Ika dWallet 2PC-MPC for programmable cross-chain signing.
references/protocols-detailed.md - All coordinator function signatures with full parameter listsreferences/patterns.md - Complete integration patterns: treasury, DAO governance, imported key wallet, presign pool, events, enumsreferences/typescript-integration.md - Full TypeScript SDK flows: DKG prep, signing, key import, polling, IkaTransaction APItoml[package] name = "my_project" edition = "2024.beta" [dependencies] Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" } ika_dwallet_2pc_mpc = { git = "https://github.com/dwallet-labs/ika.git", subdir = "deployed_contracts/testnet/ika_dwallet_2pc_mpc", rev = "main" } ika = { git = "https://github.com/dwallet-labs/ika.git", subdir = "deployed_contracts/testnet/ika", rev = "main" } [addresses] my_project = "0x0"
For mainnet: change testnet to mainnet in paths.
bashpnpm add @ika.xyz/sdk
typescriptimport { getNetworkConfig, IkaClient } from '@ika.xyz/sdk'; import { getJsonRpcFullnodeUrl, SuiJsonRpcClient } from '@mysten/sui/jsonRpc'; const suiClient = new SuiJsonRpcClient({ url: getJsonRpcFullnodeUrl('testnet'), network: 'testnet' }); const ikaClient = new IkaClient({ suiClient, config: getNetworkConfig('testnet'), cache: true }); await ikaClient.initialize();
0 = SECP256K1 (Bitcoin, Ethereum)1 = SECP256R1 (WebAuthn)2 = ED25519 (Solana, Substrate)3 = RISTRETTO (Privacy)0=ECDSA, 1=Taproot0=ECDSA0=EdDSA0=Schnorrkel0=KECCAK256(Ethereum), 1=SHA256, 2=DoubleSHA256(Bitcoin)0=SHA2560=SHA2560=SHA5120=Merlinrustuse ika::ika::IKA; use ika_dwallet_2pc_mpc::{ coordinator::DWalletCoordinator, coordinator_inner::{ DWalletCap, ImportedKeyDWalletCap, UnverifiedPresignCap, VerifiedPresignCap, UnverifiedPartialUserSignatureCap, VerifiedPartialUserSignatureCap, MessageApproval, ImportedKeyMessageApproval }, sessions_manager::SessionIdentifier }; use sui::{balance::Balance, coin::Coin, sui::SUI};
rustpublic struct MyContract has key, store { id: UID, dwallet_cap: DWalletCap, presigns: vector<UnverifiedPresignCap>, ika_balance: Balance<IKA>, sui_balance: Balance<SUI>, dwallet_network_encryption_key_id: ID, }
rustfun random_session(coordinator: &mut DWalletCoordinator, ctx: &mut TxContext): SessionIdentifier { coordinator.register_session_identifier(ctx.fresh_object_address().to_bytes(), ctx) } fun withdraw_payment_coins(self: &mut MyContract, 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 MyContract, ika: Coin<IKA>, sui: Coin<SUI>) { self.ika_balance.join(ika.into_balance()); self.sui_balance.join(sui.into_balance()); } public fun add_ika_balance(self: &mut MyContract, coin: Coin<IKA>) { self.ika_balance.join(coin.into_balance()); } public fun add_sui_balance(self: &mut MyContract, coin: Coin<SUI>) { self.sui_balance.join(coin.into_balance()); }
Central shared object for all operations. Pass as &mut DWalletCoordinator for mutations, &DWalletCoordinator for reads.
Get ID in TypeScript: ikaClient.ikaConfig.objects.ikaDWalletCoordinator.objectID
| Capability | Purpose | Created By | |---|---|---| | DWalletCap | Authorize signing | DKG | | ImportedKeyDWalletCap | Authorize imported key signing | Import verification | | UnverifiedPresignCap | Presign reference (needs verify) | Presign request | | VerifiedPresignCap | Ready for signing | verify_presign_cap() | | UnverifiedPartialUserSignatureCap | Partial sig (needs verify) | Future sign | | VerifiedPartialUserSignatureCap | Ready for completion | verify_partial_user_signature_cap() | | MessageApproval | Auth to sign specific message | approve_message() | | ImportedKeyMessageApproval | Auth for imported keys | approve_imported_key_message() |
Every protocol op needs a unique SessionIdentifier. Create via:
rustlet session = coordinator.register_session_identifier(ctx.fresh_object_address().to_bytes(), ctx);
Rules: create just before use, never reuse, one per operation.
All ops require IKA+SUI fees. Pattern: withdraw all -> perform ops (fees auto-deducted from &mut Coin) -> return remainder.
Public user share, network signs without user interaction.
rustlet (dwallet_cap, _) = coordinator.request_dwallet_dkg_with_public_user_secret_key_share( dwallet_network_encryption_key_id, curve, centralized_public_key_share_and_proof, user_public_output, public_user_secret_key_share, option::none(), // sign_during_dkg_request session, &mut ika, &mut sui, ctx, );
Encrypted user share, user must participate in every signature.
rustlet (dwallet_cap, _) = coordinator.request_dwallet_dkg( dwallet_network_encryption_key_id, curve, centralized_public_key_share_and_proof, encrypted_centralized_secret_share_and_proof, encryption_key_address, user_public_output, signer_public_key, option::none(), session, &mut ika, &mut sui, ctx, );
Important: After zero-trust DKG (or key import), the dWallet is in AwaitingKeyHolderSignature state. The user must call acceptEncryptedUserShare to transition the dWallet to Active state before it can be used for signing:
typescript// Wait for DKG to complete and dWallet to reach AwaitingKeyHolderSignature state const awaitingDWallet = await ikaClient.getDWalletInParticularState(dwalletId, 'AwaitingKeyHolderSignature'); // The encrypted user secret key share ID comes from the DKG transaction event, // NOT from the dWallet ID. const encryptedShare = await ikaClient.getEncryptedUserSecretKeyShare(encryptedUserSecretKeyShareId); const tx = new Transaction(); const ikaTx = new IkaTransaction({ ikaClient, transaction: tx, userShareEncryptionKeys: keys }); await ikaTx.acceptEncryptedUserShare({ dWallet: awaitingDWallet, encryptedUserSecretKeyShareId: encryptedShare.id, userPublicOutput: new Uint8Array(dkgData.userPublicOutput), }); await suiClient.core.signAndExecuteTransaction({ transaction: tx, signer: keypair }); // dWallet is now Active and ready for signing const activeDWallet = await ikaClient.getDWalletInParticularState(dwalletId, 'Active');
This step is NOT needed for shared dWallets (created with request_dwallet_dkg_with_public_user_secret_key_share).
typescriptimport { prepareDKGAsync, UserShareEncryptionKeys, Curve, createRandomSessionIdentifier } from '@ika.xyz/sdk'; const keys = await UserShareEncryptionKeys.fromRootSeedKey(seed, Curve.SECP256K1); const bytesToHash = createRandomSessionIdentifier(); // bytes hashed to derive the session identifier const dkgData = await prepareDKGAsync(ikaClient, Curve.SECP256K1, keys, bytesToHash, signerAddress); const networkKey = await ikaClient.getLatestNetworkEncryptionKey(); // dkgData has: userDKGMessage, userPublicOutput, encryptedUserShareAndProof, userSecretKeyShare
Pass existing presign to get signature during DKG:
rustlet sign_req = coordinator.sign_during_dkg_request(verified_presign, hash_scheme, message, msg_sig); // Pass option::some(sign_req) instead of option::none() in DKG call
Each signature consumes one presign. Manage a pool.
rustlet cap = coordinator.request_global_presign( dwallet_network_encryption_key_id, curve, signature_algorithm, session, &mut ika, &mut sui, ctx, ); self.presigns.push_back(cap);
rustlet cap = coordinator.request_presign( dwallet_id, signature_algorithm, session, &mut ika, &mut sui, ctx, );
rustlet is_ready = coordinator.is_presign_valid(&unverified_cap); let verified = coordinator.verify_presign_cap(unverified_cap, ctx); // fails if not ready
rust// Pop from pool let unverified = self.presigns.swap_remove(0); // Auto-replenish after signing if (self.presigns.length() < MIN_POOL) { let s = random_session(coordinator, ctx); self.presigns.push_back(coordinator.request_global_presign( self.dwallet_network_encryption_key_id, curve, sig_algo, s, &mut ika, &mut sui, ctx, )); }; // Batch add let mut i = 0; while (i < count) { let s = random_session(coordinator, ctx); self.presigns.push_back(coordinator.request_global_presign(..., s, &mut ika, &mut sui, ctx)); i = i + 1; };
Single-phase, immediate signature. Use when no governance needed.
rust// 1. Verify presign let verified = coordinator.verify_presign_cap(self.presigns.swap_remove(0), ctx); // 2. Approve message let approval = coordinator.approve_message(&self.dwallet_cap, sig_algo, hash_scheme, message); // 3. Sign let sign_id = coordinator.request_sign_and_return_id( verified, approval, message_centralized_signature, session, &mut ika, &mut sui, ctx, ); // Also: coordinator.request_sign(...) without return ID
typescriptimport { createUserSignMessageWithPublicOutput, Curve, SignatureAlgorithm, Hash } from '@ika.xyz/sdk'; const completedPresign = await ikaClient.getPresignInParticularState(presignId, 'Completed'); const protocolPublicParameters = await ikaClient.getProtocolPublicParameters(undefined, Curve.SECP256K1); const msgSig = await createUserSignMessageWithPublicOutput( protocolPublicParameters, dWallet.state.Active!.public_output, dWallet.public_user_secret_key_share, completedPresign.presign, message, Hash.SHA256, SignatureAlgorithm.Taproot, Curve.SECP256K1, ); // Pass msgSig as message_centralized_signature to Move
typescriptconst signSession = await ikaClient.getSignInParticularState( signId, Curve.SECP256K1, SignatureAlgorithm.Taproot, 'Completed', ); const sig = await parseSignatureFromSignOutput(Curve.SECP256K1, SignatureAlgorithm.Taproot, signSession.signature);
Separates commitment from execution. For governance/multisig/delayed signing.
rustlet partial_cap = coordinator.request_future_sign( self.dwallet_cap.dwallet_id(), verified_presign, message, hash_scheme, message_centralized_signature, session, &mut ika, &mut sui, ctx, ); // Store partial_cap with request for later
rust// Verify partial sig let verified_partial = coordinator.verify_partial_user_signature_cap(partial_cap, ctx); // Create approval let approval = coordinator.approve_message(&self.dwallet_cap, sig_algo, hash_scheme, message); // Complete signature let sign_id = coordinator.request_sign_with_partial_user_signature_and_return_id( verified_partial, approval, session, &mut ika, &mut sui, ctx, );
rustlet ready = coordinator.is_partial_user_signature_valid(&unverified_cap); let matches = coordinator.match_partial_user_signature_with_message_approval(&verified, &approval);
Import existing private key into dWallet system. Returns ImportedKeyDWalletCap.
rustlet imported_cap = coordinator.request_imported_key_dwallet_verification( dwallet_network_encryption_key_id, curve, centralized_party_message, encrypted_centralized_secret_share_and_proof, encryption_key_address, user_public_output, signer_public_key, session, &mut ika, &mut sui, ctx, );
ImportedKeyDWalletCap instead of DWalletCapcoordinator.approve_imported_key_message(...) instead of approve_messagecoordinator.request_imported_key_sign_and_return_id(...) for signingcoordinator.request_presign(dwallet_id, ...) for ECDSA presigns (dWallet-specific)request_imported_key_sign_with_partial_user_signature_and_return_idIrreversible. Makes user secret key share public, enabling contract-owned signing.
rustcoordinator.request_make_dwallet_user_secret_key_shares_public( dwallet_id, public_user_secret_key_shares, session, &mut ika, &mut sui, ctx, );
TypeScript: save dkgData.userSecretKeyShare during DKG for potential later conversion.
rustmodule my_protocol::treasury; use ika::ika::IKA; use ika_dwallet_2pc_mpc::{ coordinator::DWalletCoordinator, coordinator_inner::{DWalletCap, UnverifiedPresignCap, UnverifiedPartialUserSignatureCap} }; use sui::{balance::Balance, coin::Coin, sui::SUI, table::{Self, Table}}; const SECP256K1: u32 = 0; const TAPROOT: u32 = 1; const SHA256: u32 = 0; const MIN_PRESIGNS: u64 = 3; public struct Treasury has key, store { id: UID, dwallet_cap: DWalletCap, presigns: vector<UnverifiedPresignCap>, members: vector<address>, approval_threshold: u64, proposals: Table<u64, Proposal>, next_id: u64, ika_balance: Balance<IKA>, sui_balance: Balance<SUI>, dwallet_network_encryption_key_id: ID, } public struct Proposal has store { message: vector<u8>, partial_cap: Option<UnverifiedPartialUserSignatureCap>, approvals: u64, voters: Table<address, bool>, executed: bool, } // Create treasury with shared dWallet public fun create( coordinator: &mut DWalletCoordinator, mut ika: Coin<IKA>, mut sui: Coin<SUI>, encryption_key_id: ID, dkg_msg: vector<u8>, user_output: vector<u8>, user_share: vector<u8>, session_bytes: vector<u8>, members: vector<address>, threshold: u64, ctx: &mut TxContext, ) { let session = coordinator.register_session_identifier(session_bytes, ctx); let (dwallet_cap, _) = coordinator.request_dwallet_dkg_with_public_user_secret_key_share( encryption_key_id, SECP256K1, dkg_msg, user_output, user_share, option::none(), session, &mut ika, &mut sui, ctx, ); let treasury = Treasury { id: object::new(ctx), dwallet_cap, presigns: vector::empty(), members, approval_threshold: threshold, proposals: table::new(ctx), next_id: 0, ika_balance: ika.into_balance(), sui_balance: sui.into_balance(), dwallet_network_encryption_key_id: encryption_key_id, }; transfer::public_share_object(treasury); } // Create proposal with future sign (Phase 1) public fun propose( self: &mut Treasury, coordinator: &mut DWalletCoordinator, message: vector<u8>, msg_sig: vector<u8>, ctx: &mut TxContext, ): u64 { assert!(self.members.contains(&ctx.sender()), 0); let (mut ika, mut sui) = self.withdraw_payment_coins(ctx); let verified = coordinator.verify_presign_cap(self.presigns.swap_remove(0), ctx); let session = random_session(coordinator, ctx); let partial = coordinator.request_future_sign( self.dwallet_cap.dwallet_id(), verified, message, SHA256, msg_sig, session, &mut ika, &mut sui, ctx, ); let id = self.next_id; self.next_id = id + 1; self.proposals.add(id, Proposal { message, partial_cap: option::some(partial), approvals: 0, voters: table::new(ctx), executed: false, }); // Auto-replenish if (self.presigns.length() < MIN_PRESIGNS) { let s = random_session(coordinator, ctx); self.presigns.push_back(coordinator.request_global_presign( self.dwallet_network_encryption_key_id, SECP256K1, TAPROOT, s, &mut ika, &mut sui, ctx, )); }; self.return_payment_coins(ika, sui); id } // Vote public fun vote(self: &mut Treasury, id: u64, approve: bool, ctx: &TxContext) { assert!(self.members.contains(&ctx.sender()), 0); let p = self.proposals.borrow_mut(id); assert!(!p.voters.contains(ctx.sender()) && !p.executed, 1); p.voters.add(ctx.sender(), approve); if (approve) { p.approvals = p.approvals + 1; }; } // Execute after approval (Phase 2) public fun execute( self: &mut Treasury, coordinator: &mut DWalletCoordinator, id: u64, ctx: &mut TxContext, ): ID { let p = self.proposals.borrow_mut(id); assert!(p.approvals >= self.approval_threshold && !p.executed, 2); let (mut ika, mut sui) = self.withdraw_payment_coins(ctx); let verified = coordinator.verify_partial_user_signature_cap(p.partial_cap.extract(), ctx); let approval = coordinator.approve_message(&self.dwallet_cap, TAPROOT, SHA256, p.message); let session = random_session(coordinator, ctx); let sign_id = coordinator.request_sign_with_partial_user_signature_and_return_id( verified, approval, session, &mut ika, &mut sui, ctx, ); p.executed = true; self.return_payment_coins(ika, sui); sign_id } fun random_session(c: &mut DWalletCoordinator, ctx: &mut TxContext): SessionIdentifier { c.register_session_identifier(ctx.fresh_object_address().to_bytes(), ctx) } fun withdraw_payment_coins(self: &mut Treasury, ctx: &mut TxContext): (Coin<IKA>, Coin<SUI>) { (self.ika_balance.withdraw_all().into_coin(ctx), self.sui_balance.withdraw_all().into_coin(ctx)) } fun return_payment_coins(self: &mut Treasury, ika: Coin<IKA>, sui: Coin<SUI>) { self.ika_balance.join(ika.into_balance()); self.sui_balance.join(sui.into_balance()); }
typescriptconst tx = new Transaction(); tx.moveCall({ target: `${PACKAGE_ID}::treasury::create`, 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(threshold), ], }); await suiClient.core.signAndExecuteTransaction({ transaction: tx, signer: keypair });
rustmodule my_protocol::constants; public macro fun curve(): u32 { 0 } public macro fun signature_algorithm(): u32 { 1 } public macro fun hash_scheme(): u32 { 0 }
| Chain | Curve | Sig Algo | Hash | |---|---|---|---| | Bitcoin (Taproot) | 0 | 1 | 0 (SHA256) | | Bitcoin (Legacy) | 0 | 0 | 2 (DoubleSHA256) | | Ethereum | 0 | 0 | 0 (KECCAK256) | | Solana | 2 | 0 | 0 (SHA512) | | WebAuthn | 1 | 0 | 0 (SHA256) | | Substrate | 3 | 0 | 0 (Merlin) |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 25,571 | 22,907 | -10% | 1 | 1 | 0% | 6,210 | 12,090 | +95% | 0 | 0 | — |
case-02 | fail→fail | 20,511 | 18,038 | -12% | 1 | 1 | 0% | 3,976 | 10,561 | +166% | 0 | 0 | — |
case-03 | fail→pass | 20,522 | 21,425 | +4% | 1 | 1 | 0% | 3,988 | 11,411 | +186% | 0 | 0 | — |
case-04 | pass→pass | 16,720 | 6,871 | -59% | 1 | 1 | 0% | 2,667 | 7,573 | +184% | 0 | 0 | — |
case-05 | fail→fail | 10,763 | 8,623 | -20% | 1 | 1 | 0% | 2,353 | 8,143 | +246% | 0 | 0 | — |
case-06 | fail→fail | 14,721 | 15,025 | +2% | 1 | 1 | 0% | 3,136 | 9,462 | +202% | 0 | 0 | — |
case-07 | fail→pass | 11,544 | 6,195 | -46% | 1 | 1 | 0% | 2,109 | 7,508 | +256% | 0 | 0 | — |
case-08 | fail→pass | 16,910 | 4,767 | -72% | 1 | 1 | 0% | 3,520 | 7,166 | +104% | 0 | 0 | — |
case-09 | fail→pass | 13,168 | 5,028 | -62% | 1 | 1 | 0% | 2,484 | 7,153 | +188% | 0 | 0 | — |
case-10 | fail→pass | 10,660 | 3,445 | -68% | 1 | 1 | 0% | 2,073 | 6,889 | +232% | 0 | 0 | — |
case-11 | fail→pass | 8,646 | 5,456 | -37% | 1 | 1 | 0% | 1,633 | 7,367 | +351% | 0 | 0 | — |
case-12 | fail→pass | 20,558 | 4,332 | -79% | 1 | 1 | 0% | 1,179 | 7,097 | +502% | 0 | 0 | — |
case-13 | fail→pass | 24,063 | 8,693 | -64% | 1 | 1 | 0% | 2,981 | 7,845 | +163% | 0 | 0 | — |
case-14 | fail→pass | 14,015 | 4,852 | -65% | 1 | 1 | 0% | 2,611 | 7,142 | +174% | 0 | 0 | — |
case-15 | fail→pass | 15,372 | 8,602 | -44% | 1 | 1 | 0% | 2,660 | 7,775 | +192% | 0 | 0 | — |
case-16 | fail→pass | 12,168 | 3,648 | -70% | 1 | 1 | 0% | 2,202 | 6,958 | +216% | 0 | 0 | — |
case-17 | fail→pass | 17,441 | 4,919 | -72% | 1 | 1 | 0% | 3,149 | 7,058 | +124% | 0 | 0 | — |
case-18 | fail→pass | 13,789 | 4,949 | -64% | 1 | 1 | 0% | 2,700 | 7,208 | +167% | 0 | 0 | — |
case-19 | fail→pass | 12,800 | 5,122 | -60% | 1 | 1 | 0% | 2,182 | 7,197 | +230% | 0 | 0 | — |
case-20 | fail→pass | 17,903 | 9,434 | -47% | 1 | 1 | 0% | 3,301 | 8,122 | +146% | 0 | 0 | — |
case-21 | fail→pass | 13,704 | 7,037 | -49% | 1 | 1 | 0% | 2,588 | 7,616 | +194% | 0 | 0 | — |
case-22 | fail→pass | 16,444 | 5,931 | -64% | 1 | 1 | 0% | 2,839 | 7,323 | +158% | 0 | 0 | — |
case-23 | fail→pass | 15,415 | 10,714 | -30% | 1 | 1 | 0% | 2,943 | 8,406 | +186% | 0 | 0 | — |
case-24 | fail→pass | 15,973 | 9,421 | -41% | 1 | 1 | 0% | 2,912 | 8,098 | +178% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 24 cases were attempted, and 23 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +83 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.