HIP-338: Signature and Wallet Providers
Author | Daniel Akhterov |
---|---|
Discussions-To | https://github.com/hashgraph/hedera-improvement-proposal/discussions/355 |
Status | Accepted |
Needs Council Approval | No |
Review period ends | Tue, 22 Feb 2022 07:00:00 +0000 |
Type | Standards Track |
Category | Application |
Created | 2022-02-08 |
Table of Contents
Abstract
Ethereum like signature and wallet providers with the Hedera SDKs
Motivation
Cryptocurrency users are more used to Ethereum’s signature and wallet providers instead of direclty hardcoding private keys into their projects. Supporting third party signature and wallet provider would be better for security and user experience.
Specification
The main 3 classes are Signer
, Provider
, and Wallet
. The Wallet
extends the Signer
.
The Signer
is responsible for Signing
requests while the Provider
is responsible for
communication between an application and a Hedera network, but is not required to communicate
direclty with a Hedera network. Note this means the Provider
can for instance communicate
with some third party service which finally communicates with a Hedera network.
Not really sure how to write the distinction between Wallet
and Signer
though.
abstract class Signer
Methods
async sign
( messages
: List
< bytes
> ): List
< List
< SignerSignature
> >
Sign a list of messages
NOTE: Each element in the outer list of the result is all the signatures for the message at the same index.
async signTransaction
( transaction
: Transaction
): Transaction
Signs the transaction
NOTE: Use Transaction.getSignatures()
to see the actual signatures. message at the same index.
async sendRequest
( request
: Executable
): Response
Sign and send a request using the wallet
async checkTransaction
( transaction
: Transaction
): Transaction
Determines if all the properties required are set and sets the transaction ID. If the transaction ID was already set it checks if the account ID of it is the same as the users.
async populateTransaction
( transaction
: Transaction
): Transaction
Sets the transaction ID of the transaction to the current account ID of the signer.
getLedgerId
(): LedgerId
Return the ledger ID
getAccountId
(): AccountId
Return the account ID associated with this signer
async getAccountBalance
(): AccountBalance
Fetch the account’s balance
async getAccountInfo
(): AccountInfo
Fetch the account’s info
async getTransactionRecords
(): List
< TransactionRecord
>
Fetch the last transaction records for this account using TransactionRecordQuery
async getTransactionRecords
(): List
< TransactionRecord
>
Fetch the last transaction records for this account using TransactionRecordQuery
abstract class Provider
Methods
getLedgerId
(): LedgerId
Return the ID of the current network
getNetwork
(): Map
< string
, AccountId
>
Return the entire network map for the current network
getMirrorNetwork
(): List
< string
>
Return the mirror network
async getAccountBalance
( accountId
: AccountId
): AccountBalance
Get the balance for an account
async getAccountInfo
( accountId
: AccountId
): AccountInfo
Get the info for an account
async getTransactionReceipt
( transactionId
: TransactionId
): TransacitonReceipt
Get a receipt for a transaction ID
async sendRequest
( request
: Executable
): Response
Sign and send a request using the wallet
async waitForReceipt
( response
: TransactionResponse
): TransactionReceipt
Wait for the receipt for a transaction response
NOTE: This is different from getTransactionReceipt()
this method requires a nodeId
which is set
inside TransactionResponse
and as a result should not be able to fail with RECEIPT_NOT_FOUND
abstract class Wallet extends Signer
Static Methods
withPrivateKey
( privateKey
: PrivateKey
): Wallet
Create a wallet using a private key
Methods
getProvider
(): Provider
Return the provider
getAccountKey
(): Key
Return the public key associated with this wallet.
async createRandomED25519
(): Wallet
Creates a wallet with a new ED25519 key
NOTE: This would create an alias key account on Hedera
async createRandomECDSA
(): Wallet
Creates a wallet with a new ECDSA key
NOTE: This would create an alias key account on Hedera
class SignerSignature
Fields
publicKey
: PublicKey
The public key that signed this request
signature
: bytes
The signature for the message
accountId
: AccountId
The account ID associated with the public key which signed the transaction
NOTE: This account ID may be repeated multiple times if an account uses a KeyList
or ThresholdKey
class LocalWallet implements Wallet
Static Methods
Constructor
constructor
()
Creates an LocalWallet
from the environment variables OPERATOR_KEY
, OPERATOR_ID
, and HEDERA_NEWTORK
Methods
async sign
( messages
: List
< bytes
> ): List
< List
< SignatureProviderSignature
> >
Signs all the messages with all the private keys.
class VoidSigner extends Signer
Static Methods
withAccountId
( accountId
: AccountId
): VoidSigner
Create a wallet using a private key
Methods
getProvider
(): Provider
Return the provider
getAccountKey
(): Key
Will return null
async signTransaction
( transaction
: Transaction
): Transaction
Will do nothing
async sendRequest
( request
: Executable
): Response
Will throw an error as sending a transaction using a VoidSigner
is not supported.
All SDK request types should add these methods
async freezeWithSigner
( signer
: Signer
): Transaction
Freezes this transaction using the provided signer; calls Signer.populateTransaction
async signWithSigner
( signer
: Signer
): Transaction
Signs this transaction using the provided signer; calls Signer.signTransaction
async executeWithSigner
( signer
: Signer
): TransactionResponse
Executes this transaction using the provided signer; calls Signer.sendRequest
The difference between Signer
and Provider
is the Signer
signs requests while Provider
provides the network to
which this request should be submitted to.
MyHbarWallet Extension Example
/**
* The following two classes are examples of what `MyHbarWallet` can create to allow users
* to easily use them as Hedera wallets, signers, and providers.
*/
/**
* MyHbarWallet can inject this class into the DOM at global scope
*/
class MyHbarWalletWallet extends Wallet {
private accountId: AccountId;
private provider: MyHbarWalletProvider;
private constructor(privateKey: PrivateKey, accountId: AccountId | null, ledgerId: LedgerId) {
super();
/**
* Use secure storage for the public key and transaction signer
* No access to the private key
*/
window.sessionStorage.setItem("publicKey", privateKey.publicKey);
window.sessionStorage.setItem("transactionSigner", privateKey.sign);
this.accountId = accountId != null
? accountId
: privateKey.publicKey.toAccountId(0, 0);
this.provider = MyHbarWalletProvider.forLedgerId(ledgerId);
}
/**
* Perhaps MyHbarWallet could add a method on this globally accessible class
* to easily get the currently logged in user's wallet or initiate the logging
* in process.
*/
static getCurrent(): MyHbarWalletWallet {
// ...
}
getProvider(): Provider {
return this.provider;
}
getAccountKey(): Key {
return window.sessionStorage.getItem("publicKey");
}
static async createRandomED25519(): Promise<MyHbarWalletWallet> {
return MyHbarWalletWallet(await PrivateKey.generateED25519Async());
}
static async createRandomECDSA(): Promise<MyHbarWalletWallet> {
return MyHbarWalletWallet(await PrivateKey.generateECDSA());
}
async sign(messages: Uint8Array[]): Promise<SignerSignature[][]> {
return Promise.resolve(messages.map((message) => {
return [window.sessionStorage.getItem("transactionSigner")(message)];
}));
}
async signTransaction(transaction: Transaction): Promise<Transaction> {
const publicKey = window.sessionStorage.getItem("publicKey");
const transactionSigner = window.sessionStorage.getItem("transactionSigner");
await transaction.signWith(publicKey, transactionSigner);
return transaction;
}
sendRequest<RequestT, ResponseT, OutputT>(request: Executable<RequestT, ResponseT, OutputT>): Promise<OutputT> {
return this.provider.sendRequest(request);
}
async checkTransaction(transaction: Transaction): Promise<Transaction> {
return Promise.resovle(() => {
const transactionId = transaction.transactionId;
if (transactionId != null && transactionId.accountId.toString() != this.accountId.toString()) {
throw new Error("TransactionID already set to a different account");
}
const nodeAccountIds = transaction.nodeAccountIds
.map((nodeAccountId) => nodeAccountId.toString());
const network = Object.values(this.provider.client.network)
.map((nodeAccountId) => nodeAccountId.toString());
if (!nodeAccountIds.reduce((previous, current) => previous && network.includes(current), true)) {
throw new Error("Transaction already set node account IDs to values not within the current network");
}
return transaction;
});
}
async populateTransaction(transaction: Transaction): Promise<Transaction> {
await this.checkTransaction(transaction);
transaction.setTransactionId(Transaction.generate(this.accountId));
transaction.setNodeAccountIds(Object.values(this.provider.client.network));
return transaction;
}
getLedgerId(): LedgerId {
return this.provider.getLedgerId();
}
getAccountId(): AccountId {
return this.accountId;
}
getAccountBalance(): Promise<AccountBalance> {
return this.provider.getAccountBalance(this.accountId);
}
getAccountInfo(): Promise<AccountInfo> {
return this.provider.getAccountInfo(this.accountId);
}
getAccountRecords(): Promise<TransactionRecord[]> {
return this.provider.getAccountRecords(this.accountId);
}
}
/**
* MyHbarWallet can inject this class into the DOM at global scope
*/
class MyHbarWalletProvider extends Provider {
private client: Client;
private constructor(ledgerId: LedgerId) {
super();
this.client = Client.forName(ledgerId.toString());
}
static forLedgerId(ledgerId: LedgerId): MyHbarWalletProvider {
return new MyHbarWalletProvider(ledgerId);
}
getLedgerId(): LedgerId {
return client.getLedgerId();
}
getNetwork(): Promise<{ [key: string]: AccountId }> {
return Promise.resolve(client.network);
}
getMirrorNetwork(): Promise<string[]> {
return Promise.resolve(client.mirrorNetwork);
}
getAccountBalance(accountId: AccountId): Promise<AccountBalance> {
return new AccountBalanceQuery()
.setAccountId(accountId)
.execute(this.client);
}
getAccountInfo(accountId: AccountId): Promise<AccountInfo> {
return new AccountInfoQuery()
.setAccountId(accountId)
.execute(this.client);
}
getAccountRecords(accountId: AccountId): Promise<TransactionRecord[]> {
return new AccountRecordsQuery()
.setAccountId(accountId)
.execute(this.client);
}
sendRequest<RequestT, ResponseT, OutputT>(request: Executable<RequestT, ResponseT, OutputT>): Promise<OutputT> {
return transaction.execute(this.client);
}
waitForReceipt(response: TransactionResponse): Promise<TransactionReceipt> {
return response.getReceipt(this.client);
}
}
/**
* The following is an example of how someone can use the `MyHbarWalletWallet` from within
* the browser class while on https://myhbarwallet.com
*/
const wallet = MyHbarWalletWallet.getCurrent();
async function main() {
const balance = await wallet.getAccountBalance();
console.log(`Current Balance for account: ${wallet.accountId.toString()} is ${balance.toString()}`);
}
MyHbarWallet Application Example
/**
* The following two classes are examples of what `MyHbarWallet` can create to allow users
* to easily use them as Hedera wallets, signers, and providers.
* This could be put into a npm package.
*/
/**
* This signer does not have access to the private key(s) for the account, instead
* it uses the provider to communicate with MyHbarWallet extension which does the actual
* signing and potentially sending of a transaction.
*/
class MyHbarWalletSigner extends Signer {
private accountKey: Key;
private accountId: AccountId;
private provider: MyHbarWalletProvider;
constructor(accountKey: Key, accountId: AccountId, ledgerId: LedgerId) {
super();
this.accountKey = accountKey;
this.accountId = accountId;
this.provider = new MyHbarWalletProvider(ledgerId);
}
static async login(accountId?: AccountId): Promise<MyHbarWalletSigner> {
const response = await MyHbarWalletProvider.login(accountId);
return new MyHbarWalletSigner(response.accountKey, response.accountId, response.ledgerId);
}
getProvider(): Provider {
return this.provider;
}
getAccountKey(): Key {
return this.accountKey;
}
static async createRandomED25519(): Promise<MyHbarWalletWallet> {
const response = await this.provider.createRandomED25519();
return new MyHbarWalletSigner(response.accountKey, response.accountId);
}
static async createRandomECDSA(): Promise<MyHbarWalletWallet> {
const response = await this.provider.createRandomECDSA();
return new MyHbarWalletSigner(response.accountKey, response.accountId);
}
sign(messages: Uint8Array[]): Promise<SignerSignature[][]> {
return this.provider.sign(messages);
}
async signTransaction(transaction: Transaction): Promise<Transaction> {
return this.provider.signTransaction(transaction);
}
h
sendRequest<RequestT, ResponseT, OutputT>(request: Executable<RequestT, ResponseT, OutputT>): Promise<OutputT> {
return this.provider.sendRequest(request);
}
checkTransaction(transaction: Transaction): Promise<void> {
return this.provider.checkTransaction(transaction);
}
populateTransaction(transaction: Transaction): Promise<void> {
return this.provider.propulateTransaction(transaction);
}
getLedgerId(): LedgerId {
return this.provider.getLedgerId();
}
getAccountId(): AccountId {
return this.accountId;
}
getAccountBalance(): Promise<AccountBalance> {
return this.provider.getAccountBalance(this.accountId);
}
getAccountInfo(): Promise<AccountInfo> {
return this.provider.getAccountInfo(this.accountId);
}
getAccountRecords(): Promise<TransactionRecord[]> {
return this.provider.getAccountRecords(this.accountId);
}
}
/**
* This provider is not a communciation with a Hedera network directly, but instead
* communicates with MyHbarWallet which finally communicates with an actual Hedera
* network.
*
* Note: This provider does not use JSON RPC it just uses a mock REST API. However,
* MyHbarWallet is free to use whatever protocol they'd like since the SDK does
* not put a requirement on the communication protocol.
*/
class MyHbarWalletProvider extends Provider {
/**
* Just an example endpoint
*/
private static API_ENDPOINT: string = `https://myhbarwallet.com/api/v1/`;
private ledgerId: LedgerId;
constructor(ledgerId: LedgerId) {
super();
this.ledgerId = ledgerId;
}
static async login(accountId?: AccountId): Promise<{ accountId: AccountId, accountKey: Key, ledgerId: LedgerId }> {
/**
* Attempt to log into MyHbarWallet or let MyHbarWallet initiate the login process.
* This could for instance communicate with MyHbarWallet extension and let the user
* confirm which account they'd like to login as before proceeding.
*/
const body = JSON.stringify({accountId: accountId?.toString() });
const response = await fetch(`${MyHbarWalletProvider.API_ENDPOINT}/login`, { method: "POST", body });
return response.json();
}
static async createRandomED25519(): Promise<{ accountKey: Key, accountId: AccountId, ledgerId: LedgerId }> {
const response = await fetch(`${MyHbarWalletProvider.API_ENDPOINT}/create_random_ed25519`);
return response.json();
}
static async createRandomECDSA(): Promise<{ accountKey: Key, accountId: AccountId, ledgerId: LedgerId }> {
const response = await fetch(`${MyHbarWalletProvider.API_ENDPOINT}/create_random_ecdsa`);
return response.json();
}
getLedgerId(): LedgerId {
return this.ledgerId;
}
async getNetwork(): Proimse<{ [key: string]: AccountId }> {
const response = await fetch(`${MyHbarWalletProvider.API_ENDPOINT}/get_network`);
return response.json()
}
async getMirrorNetwork(): Proimse<string[]> {
const response = await fetch(`${MyHbarWalletProvider.API_ENDPOINT}/get_mirror_network`);
return response.json().mirrorNetwork;
}
async sign(messages: UintArray[]): Promise<SignerSignature[][]> {
const body = JSON.stringify({messages: messages.map((message) => hex.encode(transaction.toBytes()))});
const response = await fetch(`${MyHbarWalletProvider.API_ENDPOINT}/sign_message`, {method: "POST", body});
return response.json().signerSignature.map((signatureSignure) => SignerSignature.fromJson(signatureSignature));
}
async signTransaction(transaction: Transaction): Promise<Transaction> {
const body = JSON.stringify({transaction: hex.encode(transaction.toBytes())});
const response = await fetch(`${MyHbarWalletProvider.API_ENDPOINT}/sign_transaction`, {method: "POST", body});
return Transaction.fromBytes(hex.decode(response.json().transaction));
}
async sendRequest<RequestT, ResponseT, OutputT>(request: Executable<RequestT, ResponseT, OutputT>): Promise<OutputT> {
const body = JSON.stringify({request: hex.encode(request.toBytes())});
const response = await fetch(`${MyHbarWalletProvider.API_ENDPOINT}/send_request`, {method: "POST", body});
// Decode Response into correct type
return decodedResponse;
}
async checkTransaction(transaction: Transaction): Promise<Transaction> {
const body = JSON.stringify({transaction: hex.encode(transaction.toBytes())});
const response = await fetch(`${MyHbarWalletProvider.API_ENDPOINT}/check_transaction`, {method: "POST", body});
return Transaction.fromBytes(hex.decode(response.json().transaction));
}
async populateTransaction(transaction: Transaction): Promise<Transaction> {
const body = JSON.stringify({transaction: hex.encode(transaction.toBytes())});
const response = await fetch(`${MyHbarWalletProvider.API_ENDPOINT}/populate_transaction`, {
method: "POST",
body
});
return Transaction.fromBytes(hex.decode(response.json().transaction));
}
async getAccountBalance(accountId: AccountId): Promise<AccountBalance> {
const body = JSON.stringify({accountId});
const response = await fetch(`${MyHbarWalletProvider.API_ENDPOINT}/get_account_balance`, {
method: "POST",
body
});
return AccountBalance.fromJson(response.json());
}
async getAccountInfo(accountId: AccountId): Promise<AccountInfo> {
const body = JSON.stringify({accountId});
const response = await fetch(`${MyHbarWalletProvider.API_ENDPOINT}/get_account_info`, {method: "POST", body});
return AccountInfo.fromJson(response.json());
}
async getAccountRecords(accountId: AccountId): Promise<TransactionRecord[]> {
const body = JSON.stringify({accountId});
const response = await fetch(`${MyHbarWalletProvider.API_ENDPOINT}/get_account_records`, {
method: "POST",
body
});
return response.json().transactionRecords.map((record) => TransactionRecord.fromJson(record));
}
async waitForReceipt(transactionResponse: TransactionResponse): Promise<TransactionReceipt> {
const body = JSON.stringify({transactionResponse});
const response = await fetch(`${MyHbarWalletProvider.API_ENDPOINT}/wait_for_receipt`, {method: "POST", body});
return TransactionReceipt.fromJson(response.json());
}
}
/**
* The following is an example of how a user can use MyHbarWallet's package to log into
* their account and query they're balance
*/
async function main() {
const signer = await MyHbarWalletSigner.login();
const balance = await signer.getAccountBalance();
console.log(`Current Balance for account: ${signer.getAccountId().toString()} is ${balance.toString()}`);
// Query the account info using the signer directly
let info = await signer.sendQuery(new AccountInfoQuery().setAccountId(signer.getAccountId()));
console.log(`The account info for the current account is: ${info.toString()}`);
// Query the account info using the SDK's `executeWithSigner()` method
info = await new AccountInfoQuery()
.setAccountId(signer.getAccountId())
.executeWithSigner(signer);
console.log(`The account info for the current account is: ${info.toString()}`);
// You're also able to freeze and sign transactions using `*WithSigner()` metohds
const frozenTransferTransaction = await new TransferTransaction()
.addHbarTransfer("0.0.3", 1)
.addHbarTransfer(signer.getAccountId(), -1)
.freezeWithSigner(signer);
const signedTransferTransaction = await frozenTransferTransaction.signWithSigner(signer);
}
Backwards Compatibility
This is 100% backwards compatible
Security Implications
WIP: hethers.js
How to Teach This
N/A
Reference Implementation
N/A
Rejected Ideas
N/A
Open Issues
N/A
References
Copyright/license
This document is licensed under the Apache License, Version 2.0 – see LICENSE or (https://www.apache.org/licenses/LICENSE-2.0)
Citation
Please cite this document as: