Ethereum
Keystone is now integrated with multiple Ethereum wallets, including MetaMask, Rabby and OKX, etc.
Meanwhile the Keystone team has raised an EIP for the QR Code Signing protocol.
Connect with Keystone
For Ethereum and other EVM Chains, Keystone uses the UR type crypto-hdkey to expose the extended public key. 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 HDKey from the scanned QR code data.
const account = KeystoneSDK.parseHDKey(new UR(Buffer.from(cbor, "hex"), type))
console.log("crypto hdkey: ", account);
}
/**
* 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.CryptoHDKey]} />
}
Here is an example of the resulting data:
{
"chain": "ETH",
"path": "m/44'/60'/0'",
"publicKey": "02...",
"name": "Keystone",
"xfp": "f23f9fd2",
"chainCode": "000...",
"extendedPublicKey": "xpub..."
}
Keystone will provide the master fingerprint and the extended public key, allowing software wallets to select the necessary data to generate the desired addresses.
Genereate the sign request
For Ethereum and other EVM chains, Keystone introdue the new UR type eth-sign-request
to encode the Ethereum transaction data or message. The request can also be splited into these four types:
- Legacy Transaction
- EIP1559 Transaction
- Personal Message
- Typed Data
Here is the sample data structure for eth-sign-request
:
requestId: String // UUID for current request
signData: String // the serialized unsigned transaction data or unsigned message, in hex string
dataType: Enum // supported data type. transaction, typed transaction, personal message and typed data.
chainId: Int // the EVM chain ID
path: String // the HD path to tell which private key should be used to sign the data
xfp: String // master fingerprint provided by Keystone when getting accounts
origin: Optional(String) // source of the request, wallet name etc
address: Optional(String) // the address for request this signing
Here is a sample code snippet demonstrating how to use the SDK to generate the sign request :
- Javascript
- Swift
- Kotlin
import KeystoneSDK, {KeystoneEthereumSDK} from "@keystonehq/keystone-sdk";
import {AnimatedQRCode} from "@keystonehq/animated-qr";
const ethSignRequest = {
requestId: "6c3633c0-02c0-4313-9cd7-e25f4f296729", // uuid.v4()
signData: "48656c6c6f2c204b657973746f6e652e",
dataType: KeystoneEthereumSDK.DataType.personalMessage,
path: "m/44'/60'/0'/0/0",
xfp: "F23F9FD2",
chainId: 1,
origin: "MetaMask"
}
const Ethereum = () => {
const keystoneSDK = new KeystoneSDK();
const ur = keystoneSDK.eth.generateSignRequest(ethSignRequest);
return <AnimatedQRCode type={ur.type} cbor={ur.cbor.toString("hex")}/>
}
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
}}
Here is a javascript sample code snippet demonstrating how to use the Keystone SDK to encode a rlp encoded eth transaction into the UR type eth-sign-request and embed it into QR codes.
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.
import KeystoneSDK
let ethSignRequest = EthSignRequest(
requestId: "6c3633c0-02c0-4313-9cd7-e25f4f296729",
signData: "48656c6c6f2c204b657973746f6e652e",
dataType: .personalMessage,
chainId: 1,
path: "m/44'/60'/0'/0/0",
xfp: "F23F9FD2",
origin: "MetaMask"
)
let keystoneSDK = KeystoneSDK()
let qrCode = try keystoneSDK.eth.generateSignRequest(ethSignRequest: ethSignRequest)
// Check if a single QR code can contain all the transaction information
let isSingleQRCode = qrCode.isSinglePart()
if isSingleQRCode {
// Return the content that should be shown in QR code
qrCode.nextPart()
} else {
while true {
// generate the data for anmiated QR Code
let qr = qrCode.nextPart()
// render the each QR Code
render(qr)
}
}
Here is a Swift sample code snippet demonstrating how to use the Keystone SDK to encode a rlp encoded eth transaction into the UR type eth-sign-request and embed it into QR codes.
The value of KeystoneSDK.maxFragmentLen
can be modified to adjust the capacity of a single QR code. The default length is 400. Please avoid setting this value too high, as larger fragment lengths can make it more difficult for Keystone to scan.
import com.keystone.sdk.KeystoneSDK
val ethSignRequest = EthSignRequest(
requestId = "6c3633c0-02c0-4313-9cd7-e25f4f296729",
signData = "48656c6c6f2c204b657973746f6e652e",
dataType = KeystoneEthereumSDK.DataType.PersonalMessage,
path = "m/44'/60'/0'/0/0",
xfp = "F23F9FD2",
origin = "MetaMask",
chainId = 1,
)
val keystoneSDK = KeystoneSDK()
val qrCode = keystoneSDK.eth.generateSignRequest(ethSignRequest)
// Check if a single QR code can contain all the transaction information
val isSingleQRCode = qrCode.isSinglePart()
if (isSingleQRCode) {
// Return the content that should be shown in QR code
qrCode.nextPart()
} else {
while(true) {
// generate the data for anmiated QR Code
val qr = qrCode.nextPart()
// re-render each data for QR Code
render(qr)
}
}
Here is a Kotlin sample code snippet demonstrating how to use the Keystone SDK to encode a rlp encoded eth transaction into the UR type eth-sign-request and embed it into QR codes.
The value of KeystoneSDK.maxFragmentLen
can be modified to adjust the capacity of a single QR code. The default length is 400. Please avoid setting this value too high, as larger fragment lengths can make it more difficult for Keystone to scan.
Sign request examples
Legacy Transaction
import { bufArrToArr } from '@ethereumjs/util'
import { RLP } from '@ethereumjs/rlp'
import { Transaction } from '@ethereumjs/tx';
import { Hardfork, Chain, Common } from '@ethereumjs/common';
import KeystoneSDK, {KeystoneEthereumSDK} from "@keystonehq/keystone-sdk";
import {AnimatedQRCode} from "@keystonehq/animated-qr";
const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.London });
const txParams = {
to: "0x31bA53Ca350975007B27CF43AcB4D9Bc3db2641c",
gasLimit: 200000,
gasPrice: 120000000000,
data: "0x",
nonce: 1,
value: 1200000000000000000n, // 1.2 ETH
};
const tx = Transaction.fromTxData(txParams, { common: common, freeze: false });
const message = tx.getMessageToSign(false); // generate the unsigned transaction
const serializedMessage = Buffer.from(RLP.encode(bufArrToArr(message))).toString("hex") // use this for the HW wallet input
const ethSignRequest = {
requestId: uuid.v4(),
signData: serializedMessage,
dataType: KeystoneEthereumSDK.DataType.transaction,
path: "m/44'/60'/0'/0/0",
xfp: "F23F9FD2", // master fingerprint
chainId: 1,
origin: "MetaMask"
}
const Ethereum = () => {
const keystoneSDK = new KeystoneSDK();
const ur = keystoneSDK.eth.generateSignRequest(ethSignRequest);
return <AnimatedQRCode type={ur.type} cbor={ur.cbor.toString("hex")} />
}
EIP-1559 Typed Transaction
import { FeeMarketEIP1559Transaction } from '@ethereumjs/tx';
import { Hardfork, Chain, Common } from '@ethereumjs/common';
import KeystoneSDK, {KeystoneEthereumSDK} from "@keystonehq/keystone-sdk";
import {AnimatedQRCode} from "@keystonehq/animated-qr";
const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.London });
const txParams = {
to: "0x31bA53Ca350975007B27CF43AcB4D9Bc3db2641c",
gasLimit: 35552,
maxPriorityFeePerGas: 75853,
maxFeePerGas: 121212,
data: "0x",
nonce: 1,
value: 1200000000000000000n, // 1.2 ETH
};
const eip1559Tx = FeeMarketEIP1559Transaction.fromTxData(txParams, { common });
const unsignedMessage = Buffer.from(eip1559Tx.getMessageToSign(false)).toString("hex");
const ethSignRequest = {
requestId: uuid.v4(),
signData: unsignedMessage,
dataType: KeystoneEthereumSDK.DataType.typedTransaction,
path: "m/44'/60'/0'/0/0",
xfp: "F23F9FD2", // master fingerprint
chainId: 1,
origin: "MetaMask"
}
const Ethereum = () => {
const keystoneSDK = new KeystoneSDK();
const ur = keystoneSDK.eth.generateSignRequest(ethSignRequest);
return <AnimatedQRCode type={ur.type} cbor={ur.cbor.toString("hex")} />
}
Message
import KeystoneSDK, {KeystoneEthereumSDK} from "@keystonehq/keystone-sdk";
import {AnimatedQRCode} from "@keystonehq/animated-qr";
const unsignedMessage = "68656c6c6f" // hex string of the message "hello"
const ethSignRequest = {
requestId: uuid.v4(),
signData: unsignedMessage,
dataType: KeystoneEthereumSDK.DataType.personalMessage,
path: "m/44'/60'/0'/0/0",
xfp: "F23F9FD2", // master fingerprint
chainId: 1,
origin: "MetaMask"
}
const Ethereum = () => {
const keystoneSDK = new KeystoneSDK();
const ur = keystoneSDK.eth.generateSignRequest(ethSignRequest);
return <AnimatedQRCode type={ur.type} cbor={ur.cbor.toString("hex")} />
}
EIP-712 Typed Data
import KeystoneSDK, {KeystoneEthereumSDK} from "@keystonehq/keystone-sdk";
import {AnimatedQRCode} from "@keystonehq/animated-qr";
const data = {
types: {
EIP712Domain: [{name: 'name', type: 'string'}],
Message: [{ name: 'data', type: 'string' }],
},
primaryType: 'Message',
domain: {name: 'Keystone'},
message: {data: 'Hello!'},
}
const unsignedMessage = Buffer.from(JSON.stringify(data), 'utf-8');
const ethSignRequest = {
requestId: uuid.v4(),
signData: unsignedMessage,
dataType: KeystoneEthereumSDK.DataType.typedData,
path: "m/44'/60'/0'/0/0",
xfp: "F23F9FD2", // master fingerprint
chainId: 1,
origin: "MetaMask"
}
const Ethereum = () => {
const keystoneSDK = new KeystoneSDK();
const ur = keystoneSDK.eth.generateSignRequest(ethSignRequest);
return <AnimatedQRCode type={ur.type} cbor={ur.cbor.toString("hex")} />
}
Extract signature
After Keystone scans the QR Codes, it will verify and display the transaction details for user confirmation. Once Keystone signs the data, it generates a signature and encodes it into a QR Code. An new UR type eth-signature
is introduced, After the signing is completed, a software wallet can scan the QR Code to retrieve the signature. The signature is a 65-byte hex string, which is composed of 32 bytes for the R, 32 bytes for the S, and 1 byte for the V value.
Signature (
requestId: String // the requestId from sign request
signature: String // the serialized signature (r,s,v) in hex string
)
Here are some code samples demonstrating how to use the SDK to achieve this.
- Javascript
- Swift
- Kotlin
import KeystoneSDK, {UR, URType} from "@keystonehq/keystone-sdk"
import {AnimatedQRScanner} from "@keystonehq/animated-qr"
const Ethereum = () => {
const keystoneSDK = new KeystoneSDK();
const onSucceed = ({type, cbor}) => {
const signature = keystoneSDK.eth.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.EthSignature]} />
}
AnimatedQRScanner
helps scan the QR code on Keystone hardware wallet and returns signature which can be parsed by KeystoneSDK
.
import KeystoneSDK
let keystoneSDK = KeystoneSDK()
let decodedResult = try keystoneSDK.decodeQR(qrCode: qrCodeString)
if decodedResult.progress == 100 {
let signature = try keystoneSDK.eth.parseSignature(ur: decodedResult.ur!)
}
import com.keystone.sdk.KeystoneSDK
val keystoneSDK = KeystoneSDK()
val decodedResult = keystoneSDK.decodeQR(qrCodeString)
if (decodedResult.progress == 100) {
val signature = keystoneSDK.eth.parseSignature(decodedResult.ur!!)
}
After getting the signature, software wallet can get the it and construct the transaction, then broadcast it.