Skip to main content

Control dWallets from Sui

Sui Light Client

The dWallet Network leverages the Sui Light Client to integrate seamlessly with the Sui Network, enabling trustless interactions between dWallets and Sui. This integration allows developers to enforce dWallet logic within Sui modules on the Sui Network, bringing trustless and programmable assets—including Bitcoin, Ethereum, and other Web3 assets—to Sui. Additionally, it supports fast, decentralized custody solutions.

The dWallet module is currently deployed on the Sui Testnet at:

0x96c235dfd098a3e0404cfe5bf9c05bbc268b75649d051d4808019f5eb81d3eec

Developers building on Sui should import this module within their own modules, as end-user applications will not interact with it directly. To import the module, use the following configuration in your Cargo.toml file:

dwallet_network = { git = "https://github.com/dwallet-labs/dwallet-network.git", subdir = "integrations/sui", rev = "main" }

Note: This module is in its early stages, so expect breaking changes and improvements over time.

Setup

A full example can be found here.

Start by setting up the environment and importing the necessary functions:

import {bcs} from '@dwallet-network/dwallet.js/bcs'
import {
DWalletClient,
OwnedObjectRef,
SuiHTTPTransport,
} from '@dwallet-network/dwallet.js/client'
import {requestSuiFromFaucetV0 as requestDwltFromFaucetV0} from '@dwallet-network/dwallet.js/faucet'
import {Ed25519Keypair} from '@dwallet-network/dwallet.js/keypairs/ed25519'
import {
createActiveEncryptionKeysTable,
createDWallet,
createPartialUserSignedMessages,
getOrCreateEncryptionKey,
submitDWalletCreationProof,
submitTxStateProof,
} from '@dwallet-network/dwallet.js/signature-mpc'
import {SuiClient} from '@mysten/sui.js/client'
import {TransactionBlock as TransactionBlockSUI} from '@mysten/sui.js/transactions'

Define the following constants to work with the dWallet Network Testnet & Sui Testnet:

type NetworkConfig = {
// Service to get TX data from SUI, a temporary solution.
lightClientTxDataService: string
// The URL of the dWallet node.
dWalletNodeUrl: string
// The dwallet package ID in SUI network where the dWallet cap is defined.
dWalletCapPackageIDInSUI: string
// The SUI RPC URL (full node).
suiRPCURL: string
// The object ID of the registry in dWallet network.
dWalletRegistryObjectID: string
// The object ID of the config in dWallet network.
dWalletConfigObjectID: string
// The URL of the faucet in dwallet network.
dWalletFaucetURL: string
}

function getTestNetConf(): NetworkConfig {
return {
lightClientTxDataService:
'https://lightclient-rest-server.alpha.testnet.dwallet.cloud/gettxdata',
dWalletNodeUrl: 'https://fullnode.alpha.testnet.dwallet.cloud',
dWalletFaucetURL: 'https://faucet.alpha.testnet.dwallet.cloud/gas',
dWalletCapPackageIDInSUI:
'0x96c235dfd098a3e0404cfe5bf9c05bbc268b75649d051d4808019f5eb81d3eec',
suiRPCURL: 'https://fullnode.testnet.sui.io:443',
dWalletRegistryObjectID:
'0x4de2a30287ed40600b53c40bfb3eeae7ef4ecf9ba9a90df732c363318612f084',
dWalletConfigObjectID:
'0xcc88a86628098c1472959ba6ad5e1c0fc0c1fd632b7ec21d265fb8efd5d55aea',
}
}

const {
dWalletConfigObjectID,
dWalletCapPackageIDInSUI,
dWalletNodeUrl,
lightClientTxDataService,
dWalletRegistryObjectID,
suiRPCURL,
dWalletFaucetURL,
} = getTestNetConf()

After creating a dWallet, you’ll own the DWalletCap object, granting full control over the dWallet. Extract the dWallet Cap ID from the output, as it will be required later:

const createdDWallet = await createDWallet(
keyPair,
dwalletClient,
senderEncryptionKeyObj.encryptionKey,
senderEncryptionKeyObj.objectID,
)
if (createdDWallet == null) {
throw new Error('createDWallet() returned null')
}
const dWalletCapID = createdDWallet.dwalletCapID

To control this dWallet from a Sui module, you’ll use the DWalletCap object defined in the Sui dwallet_cap module. Ownership of this object grants full control over the linked dWallet. Wrapping this object in your own module can limit access, allowing you to control the linked dWallet indirectly.

Here’s the structure of the DWalletCap in Move:

/// Represents the primary dWallet capability.
///
/// This struct wraps an instance of the dWallet capability, identified by a unique
/// ID (`UID`) and a `dwallet_network_cap_id` that links it to the dWallet network.
struct DWalletCap has key, store {
/// Unique identifier for this dWallet capability instance.
id: UID,
/// Identifier linking this capability to the dWallet network.
dwallet_network_cap_id: ID,
}

To create a DWalletCap object, call the dwallet_cap::create_cap() method on Sui:

async function buildCreateDWalletCapTx(
dwalletCapID: string | undefined,
dWalletCapPackageIDInSUI: string,
keyPair: Ed25519Keypair,
) {
let txb = new TransactionBlockSUI()
let dWalletCapArg = txb.pure(dwalletCapID)
let [cap] = txb.moveCall({
target: `${dWalletCapPackageIDInSUI}::dwallet_cap::create_cap`,
arguments: [dWalletCapArg],
})
txb.transferObjects([cap], keyPair.toSuiAddress())
txb.setGasBudget(10000000)
return txb
}

const dwalletCapTxB = await buildCreateDWalletCapTx(
dWalletCapID,
dWalletCapPackageIDInSUI,
keyPair,
)
const suiClient = new SuiClient({url: suiRPCURL})
const createCapInSuiRes = await suiClient.signAndExecuteTransactionBlock({
signer: keyPair,
transactionBlock: dwalletCapTxB,
options: {
showEffects: true,
},
})
const createdCapObjInSui = createCapInSuiRes.effects?.created?.[0]
if (createdCapObjInSui) {
console.log(
`dWallet cap wrapper created in Sui network, ID: ${createdCapObjInSui.reference.objectId}`,
)
} else {
throw new Error('dwallet_cap::create_cap failed: No objects were created')
}
const createCapInInSuiTxID = createCapInSuiRes.digest

Prove dWallet Capability Creation to the dWallet Network

After executing the dwallet_cap::create_cap transaction on Sui with the transaction ID createCapInInSuiTxID, you can prove this action to the dWallet Network.

This transaction emits a DWalletNetworkInitCapRequest event:

// Emit event to notify the initialization of a new dWallet capability.
event::emit(DWalletNetworkInitCapRequest {
// The object ID of the newly created `DWalletCap` object.
cap_id: object::id(&cap),
// The object ID of the dWallet capability on the dWallet Network that you wish to control.
dwallet_network_cap_id,
});

To prove on the dWallet Network that the DWalletNetworkInitCapRequest event was emitted, call the submitDWalletCreationProof() function, submitting a state proof that the Sui Network transaction created a new DWalletCap.

The DKG process output allows us to extract the DWalletCap ID on the dWallet Network, enabling linking to the Sui capability.

const createCapInSuiTxID = createCapInSuiRes.digest
let dwalletCreationProofRes = await submitDWalletCreationProof(
dwalletClient,
suiClient,
dWalletConfigObjectID,
dWalletRegistryObjectID,
dWalletCapID,
createCapInSuiTxID,
lightClientTxDataService,
keyPair,
)
const capWrapperInDwalletRef =
dwalletCreationProofRes.effects?.created?.[0]?.reference
if (!capWrapperInDwalletRef) {
throw new Error(
'submitDWalletCreationProof failed: No objects were created',
)
}
console.log(
'dWallet cap wrapper creation proof created in dWallet Network, Tx ID:',
dwalletCreationProofRes.digest,
)

This action creates a new CapWrapper object in the dWallet Network, wrapping the DWalletCap and registering the corresponding cap_id_sui on Sui, thus linking the two objects:

struct CapWrapper has key, store {
id: UID,
cap_id_sui: ID,
cap: DWalletCap,
}

Approve Message on Sui for Signing in dWallet Network

With the dWallet linked to a DWalletCap on Sui, the owner can approve a message for signing. For example, to sign the message "dWallets are coming... to Sui", call the dwallet_cap::approve_message() method on Sui:

const message: Uint8Array = new TextEncoder().encode(
'dWallets are coming... to Sui',
)

function buildApproveMsgTx(
message: Uint8Array,
dWalletCapPackageIDInSUI: string,
createdCapObjInSui: OwnedObjectRef,
) {
let txb = new TransactionBlockSUI()

let signMsgArg = txb.pure(
bcs.vector(bcs.vector(bcs.u8())).serialize([message]),
)
const createdCapObjInSuiArg = txb.objectRef(createdCapObjInSui.reference)
// Approve the message for the given dWallet cap.
txb.moveCall({
target: `${dWalletCapPackageIDInSUI}::dwallet_cap::approve_message`,
arguments: [createdCapObjInSuiArg, signMsgArg],
})
txb.setGasBudget(10000000)

return txb
}

let approveMsgTxB = buildApproveMsgTx(
message,
dWalletCapPackageIDInSUI,
createdCapObjInSui,
)
let approveMsgRes = await suiClient.signAndExecuteTransactionBlock({
signer: keyPair,
transactionBlock: approveMsgTxB,
options: {
showEffects: true,
},
})
const approveMsgTxID = approveMsgRes.digest

Now that we have executed the approve_message transaction on Sui with the ID approveMsgTxID, we can prove it to the dWallet Network.
This transaction emits a DWalletNetworkApproveRequest event, which specifies the object ID of the DWalletCap object and the approved message bytes:

/// Event emitted when messages are approved for a dWallet capability.
///
/// This struct captures the ID of the `DWalletCap` and the messages
/// associated with the approval request.
struct DWalletNetworkApproveRequest has copy, drop {
/// The unique identifier of the capability for which messages are being approved.
cap_id: ID,
/// A vector of messages to be approved, where each message is a vector of bytes.
messages: vector<vector<u8>>,
}

Submit Proof

Follow the steps to sign with a dWallet. But stop after creating the signMessages object. Next, call the submitTxStateProof() function, which will submit a state proof to the dWallet network that the transaction on Sui network approved this message for signing.

const res = await submitTxStateProof(
dwalletClient,
suiClient,
createdDWallet.dwalletID,
dWalletConfigObjectID,
dWalletRegistryObjectID,
capWrapperInDwalletRef,
signMessagesIDSHA256,
approveMsgTxID,
lightClientTxDataService,
keyPair,
);
console.log('submitTxStateProof result', res);

This generates a signature:

res = {
signOutputId: '0x876fa89ee94ef75116a72dc7b92365f85a83e25be629ac4757e05ad3ac58c78f',
signatures:
[
[
86, 107, 94, 207, 24, 127, 170, 14, 209, 83, 87,
20, 40, 109, 197, 57, 212, 181, 5, 197, 248, 49,
179, 48, 101, 182, 117, 119, 128, 215, 28, 137, 92,
143, 15, 210, 48, 43, 134, 160, 120, 104, 2, 194,
117, 210, 187, 37, 30, 225, 113, 206, 240, 166, 130,
84, 34, 35, 52, 93, 168, 60, 27, 247
]
]
}