Skip to main content

Cardano

Keystone is now integrated with the Eternl Wallet.

Connect with Keystone

For Cardano, Keystone defines the new UR type crypto-multi-accounts to expose the extend public keys. Software can utilize these data to generate the desired addresses. Developers can use the SDK to retrieve and parse this data from the QR Code displayed on the Keystone device.

Here is a sample code snippet to scan the animated QR code and parse the data:

import KeystoneSDK, {UR, URType} from "@keystonehq/keystone-sdk"
import {AnimatedQRScanner} from "@keystonehq/animated-qr"

/**
* Represents a component that handles the scanning of an animated QR code to retrieve
* the crypto hdkey information from a Keystone hardware wallet.
*
* The component uses the `AnimatedQRScanner` from `@keystonehq/animated-qr` to scan the QR code,
* and the `KeystoneSDK` to parse the scanned data into a human-readable account information format.
*/

const Account = () => {

/**
* Callback function to handle successful QR code scans.
*
* @param {Object} data - The data object containing the type and cbor encoded string.
* @param {string} data.type - The type of the scanned data.
* @param {string} data.cbor - The cbor encoded string representing the account information.
*/
const onSucceed = ({type, cbor}) => {
// Parses the crypto multi accounts from the scanned QR code data.
const account = KeystoneSDK.parseMultiAccounts(new UR(Buffer.from(cbor, "hex"), type))
console.log("multiAccounts: ", multiAccounts);
}

/**
* Callback function to handle errors during QR code scanning.
*
* @param {string} errorMessage - The error message describing what went wrong during scanning.
*/
const onError = (errorMessage) => {
console.log("error: ", errorMessage);
}

// Renders the AnimatedQRScanner component with the specified handlers for success and error events.
return <AnimatedQRScanner handleScan={onSucceed} handleError={onError} urTypes={[URType.CryptoMultiAccounts]} />
}

Here is an example of the resulting data:

{
"masterFingerprint": "f23f9fd2",
"keys": [
{
"chain": "ADA",
"path": "m/1852'/1815'/0'",
"publicKey": "b6...",
"name": "",
"chainCode": "9e...",
"extendedPublicKey": ""
}
],
"device": "Keystone"
}

Here is the type defination of the CryptoMutliAccounts:

interface MultiAccounts {
masterFingerprint: string // A 4 bytes hex string indicates the current mnemonic, e.g. 'f23f9fd2'
keys: Account[] // An array of public keys
device?: string // The device name, e.g. 'Keystone'
deviceId?: string // The device id, e.g. '28475c8d80f6c06bafbe46a7d1750f3fcf2565f7'
deviceVersion?: String // The device firmware version, e.g. '1.0.2'
}

interface Account {
chain: string // The symbol of the coin this key belongs to, e.g. 'ADA'
path: string // The full derivation path of current key
publicKey: string // Public key in hex string
name?: string // The address name in hardware wallet
chainCode: string // The chain code if exist
extendedPublicKey?: string // The bip32 extended public key, e.g. xpub...
note?: string // The note for current account
}

Keystone will provide the master fingerprint and the public keys, allowing software wallets to select the necessary data to generate the desired addresses. If you have tried Keystone with the Eternl wallet, you may find that the connecting process is not quite similar to others. This is because the Eternl wallet requires the user to select the account they would like to connect. To achieve this, Keystone introduces the Hardware Call. By using this feature, the software wallet can let the user decide which account they would like to connect.

For more info, please refer to Hardware Call

Genereate the sign request

For Cardano, Keystone introduced the new UR type cardano-sign-request to encode the transaction data for Cardano. It supports cardano-signature for standard transactions (already supporting the Conway era), cardano-sign-data-request, and cardano-catalyst-voting-registration.

Sign request examples

Here is the sample data structure for cardona-sign-request:

requestId: String // UUID for current request
signData: String // the serialized unsigned transaction data, in hex string
utxos: Array ( // the transactions inputs
transactionHash: String
index: Number
amount: String
xfp: String // master fingerprint provided by Keystone when getting accounts
hdPath: String // the HD path to tell which private key should be used to sign the data
address: String
)
extraSigners: Array( // the stake keys need to use in this transaction
keyHash: String // the stake key hash
xfp: String // master fingerprint
keyPath: String // the related private key path of this input
)
origin: Optional(String) // source of the request, wallet name etc

Here is a sample code snippet demonstrating how to use the SDK to generate the sign request :

    import KeystoneSDK from "@keystonehq/keystone-sdk"
import {AnimatedQRCode} from "@keystonehq/animated-qr"

let cardanoSignRequest = {
requestId: "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
signData: Buffer.from("84a400828258204e3a6e7fdcb0d0efa17bf79c13aed2b4cb9baf37fb1aa2e39553d5bd720c5c99038258204e3a6e7fdcb0d0efa17bf79c13aed2b4cb9baf37fb1aa2e39553d5bd720c5c99040182a200581d6179df4c75f7616d7d1fd39cbc1a6ea6b40a0d7b89fea62fc0909b6c370119c350a200581d61c9b0c9761fd1dc0404abd55efc895026628b5035ac623c614fbad0310119c35002198ecb0300a0f5f6", "hex"),
utxos: [
{
transactionHash: "4e3a6e7fdcb0d0efa17bf79c13aed2b4cb9baf37fb1aa2e39553d5bd720c5c99",
index: 3,
amount: "10000000",
xfp: "73c5da0a",
hdPath: "m/1852'/1815'/0'/0/0",
address: "addr1qy8ac7qqy0vtulyl7wntmsxc6wex80gvcyjy33qffrhm7sh927ysx5sftuw0dlft05dz3c7revpf7jx0xnlcjz3g69mq4afdhv",
},
{
transactionHash: "4e3a6e7fdcb0d0efa17bf79c13aed2b4cb9baf37fb1aa2e39553d5bd720c5c99",
index: 4,
amount: "18020000",
xfp: "73c5da0a",
hdPath: "m/1852'/1815'/0'/0/1",
address: "addr1qyz85693g4fr8c55mfyxhae8j2u04pydxrgqr73vmwpx3azv4dgkyrgylj5yl2m0jlpdpeswyyzjs0vhwvnl6xg9f7ssrxkz90",
},
],
extraSigners: [
{
keyHash: "e557890352095f1cf6fd2b7d1a28e3c3cb029f48cf34ff890a28d176",
xfp: "73c5da0a",
keyPath: "m/1852'/1815'/0'/2/0",
},
],
origin: "cardano-wallet"
}

const Cardano = () => {
const keystoneSDK = new KeystoneSDK();
const ur = keystoneSDK.cardano.generateSignRequest(cardanoSignRequest);

return <AnimatedQRCode type={ur.type} cbor={ur.cbor.toString("hex")}/>
}

After Keystone scans the QR Codes, it will verify and display the transaction details for user confirmation. Once Keystone signs the data, it generates the signature witness set and encodes them into the QR Codes. An new UR type cardano-signature is introduced, After the signing is completed, a software wallet can scan the QR Code to retrieve the signature.

Signature (
requestId: String // the requestId from sign request
witnessSet: String // the transaction witness set
)

Here are some code samples demonstrating how to use the SDK to achieve this.

import KeystoneSDK, {UR, URType} from "@keystonehq/keystone-sdk"
import {AnimatedQRScanner} from "@keystonehq/animated-qr"

const Cardano = () => {
const keystoneSDK = new KeystoneSDK();

const onSucceed = ({type, cbor}) => {
const signature = keystoneSDK.cardano.parseSignature(new UR(Buffer.from(cbor, "hex"), type))
console.log("signature: ", signature);
}
const onError = (errorMessage) => {
console.log("error: ", errorMessage);
}

return <AnimatedQRScanner handleScan={onSucceed} handleError={onError} urTypes={[URType.CardanoSignature]} />
}

After getting the signature, software wallet can get the it and construct the transaction, then broadcast it.

Sign Data examples

Here is the sample data structure for cardano-sign-data-request:

requestId: String // UUID for current request
sigStructure: String // the pending Sig_structure's hex, https://cips.cardano.org/cip/CIP-8#:~:text=from%20the%20structure-,Sig_structure,-%3D%20%5B
path: String // the HD path to tell which private key should be used to sign the data
pubKey: String // the public key derived from the HD path
xfp: String // master fingerprint provided by Keystone when getting accounts
origin: Optional(String) // source of the request, wallet name etc

Here is a sample code snippet demonstrating how to use the SDK to generate the sign data request :

import KeystoneSDK from "@keystonehq/keystone-sdk"
import {AnimatedQRCode} from "@keystonehq/animated-qr"

let cardanoSignDataRequest = {
requestId: "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
path: "m/1852'/1815'/0'/0/0",
xfp: "52744703",
pubKey: "ca0e65d9bb8d0dca5e88adc5e1c644cc7d62e5a139350330281ed7e3a6938d2c",
sigStructure: "846a5369676e6174757265315882a301270458390069fa1bd9338574702283d8fb71f8cce1831c3ea4854563f5e4043aea33a4f1f468454744b2ff3644b2ab79d48e76a3187f902fe8a1bcfaad676164647265737358390069fa1bd9338574702283d8fb71f8cce1831c3ea4854563f5e4043aea33a4f1f468454744b2ff3644b2ab79d48e76a3187f902fe8a1bcfaad4043abc123",
origin: "cardano-wallet"
}

export const Cardano = () => {
const keystoneSDK = new KeystoneSDK();
const ur = keystoneSDK.cardano.generateSignDataRequest(cardanoSignDataRequest);
return <AnimatedQRCode type={ur.type} cbor={ur.cbor.toString("hex")} />
}

Here are some code snippets to show how to extract the information after the signature is completed.

export const CardanoScanner = () => {
const keystoneSDK = new KeystoneSDK();

const onSucceed = ({type, cbor}) => {
const signature = keystoneSDK.cardano.parseSignDataSignature(new UR(Buffer.from(cbor, "hex"), type))
}
const onError = (errorMessage) => {
console.log("error: ",errorMessage);
}

return <AnimatedQRScanner
handleScan={onSucceed}
handleError={onError}
urTypes={[URType.CardanoSignDataSignature]}
options={{
// ...
}}
/>
}

Catalyst Registration examples

Here is the sample data structure for cardano-catalyst-voting-registration:

requestId: String // UUID for current request
path: String // the HD path to tell which private key should be used to sign the data
delegations: {
pubKey: String
weight: Number
}[] // delegations is an array that follows CIP36
stakePub: String // the public key derived from the HD path
paymentAddress: String // receive voting rewards
nonce: Number // a nonce that identifies that most recent delegation
voting_purpose: Number // the voting_purpose is an unsigned integer (of CBOR major type 0)
xfp: String // master fingerprint provided by Keystone when getting accounts
origin: Optional(String) // source of the request, wallet name etc
tip

In the Catalyst registration requests supported by Keystone, there's partial support for the CIP36 standard. The delegations field uses a combination with CIP15. This means the voting key is still randomly generated on the Software wallet and registered in Keystone, instead of being derived from a specific path as per the CIP36 standard.

Here is a sample code snippet demonstrating how to use the SDK to generate the catalyst registration request :

import KeystoneSDK, { UR, URType } from "@keystonehq/keystone-sdk"
import {AnimatedQRCode, AnimatedQRScanner} from "@keystonehq/animated-qr"

let cardanoCatalystVotingRequest = {
requestId: "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
path: "m/1852'/1815'/0'/2/0",
delegations: [{
pubKey: "a6a3c0447aeb9cc54cf6422ba32b294e5e1c3ef6d782f2acff4a70694c4d1663",
weight: 1
}],
stakePub: "ca0e65d9bb8d0dca5e88adc5e1c644cc7d62e5a139350330281ed7e3a6938d2c",
paymentAddress: "0069fa1bd9338574702283d8fb71f8cce1831c3ea4854563f5e4043aea33a4f1f468454744b2ff3644b2ab79d48e76a3187f902fe8a1bcfaad",
nonce: 100,
voting_purpose: 0,
xfp: "52744703",
origin: "cardano-wallet"
}

export const Cardano = () => {
const keystoneSDK = new KeystoneSDK();
const ur = keystoneSDK.cardano.generateCatalystRequest(cardanoCatalystVotingRequest);
return <AnimatedQRCode type={ur.type} cbor={ur.cbor.toString("hex")} />
}

Here are some code snippets to show how to extract the information after the signature is completed.

export const CardanoScanner = () => {
const keystoneSDK = new KeystoneSDK();

const onSucceed = ({type, cbor}) => {
const signature = keystoneSDK.cardano.parseCatalystSignature(new UR(Buffer.from(cbor, "hex"), type))
}
const onError = (errorMessage) => {
console.log("error: ",errorMessage);
}

return <AnimatedQRScanner
handleScan={onSucceed}
handleError={onError}
urTypes={[URType.CardanoCatalystSignature]}
options={{
// ...
}}
/>
}

AnimatedQRCode Options

AnimatedQRCode will decide whether the animated QR codes are needed, the option props of AnimatedQRCode component can be used to control the size, capacity and the update interval of QR code. Please avoid setting the capacity too high, as larger value can make it more difficult for Keystone to scan.

options={{
size: number, // optional, QR code width and length in UI, default 180px
capacity: number, // optional, the capacity of a single QR code, default 400 bytes per image
interval: number // optional, the QR code change time interval in mill seconds for animated QR code, default 100ms
}}