@koralabs/cardano-wallets
Version:
Library for connecting cardano wallets in the browser using CIP-30
324 lines (323 loc) • 11 kB
JavaScript
import { WalletKey } from '../enums/WalletName';
import { Buffer } from 'buffer';
import { Blockfrost } from '../lib/blockfrost';
import { WalletError } from '../enums/WalletError';
import { loadCardanoWasm } from '../lib/serialize';
export class CardanoWallets {
static _enabledWallet;
static wallet;
static localStorageKey = 'cardanoWallet';
static supportedWalletNames = [
WalletKey.Nami,
WalletKey.Eternl,
WalletKey.GeroWallet,
WalletKey.Flint
];
// Private methods
/**
*
* Used to set the wallet in local storage
*
* @param walletKey string
*/
static async _setWallet(walletKey) {
window.localStorage.setItem(this.localStorageKey, JSON.stringify({
name: this.wallet.name,
icon: this.wallet.icon,
apiVersion: this.wallet.apiVersion,
key: walletKey
}));
}
/**
*
* Uses CIP-30 'enable' function to enable the wallet
*
* @param wallet Wallet to enable
* @returns
*/
static _enableWallet = async (wallet) => {
this._enabledWallet = await wallet.enable();
};
// Public methods
/**
*
* Validation method used to check if the wallet is supported
*
* @param walletKey string
*/
static validateSupportedWallet(walletKey) {
if (!this.supportedWalletNames.includes(walletKey)) {
throw new Error(`${walletKey} is not supported. Only ${this.supportedWalletNames.join(', ')} are supported.`);
}
}
/**
*
* Validation method used to check the window.cardano object
*
* @param walletKey string
*/
static validateWallet(walletKey) {
if (!window) {
throw new Error(WalletError.WindowNotDefined);
}
if (!window.cardano) {
throw new Error(WalletError.NoWalletsFound);
}
if (!window.cardano[walletKey]) {
throw new Error(WalletError.SpecificWalletNotFound);
}
}
/**
*
* Used to enable the wallet and set the wallet in local storage
*
* @param walletKey string e.g. 'nami', 'eternal', etc
* @returns an enabled wallet
*/
static async connect(walletKey) {
this.validateSupportedWallet(walletKey);
this.validateWallet(walletKey);
const wallet = window.cardano[walletKey];
await this._enableWallet(wallet);
this.wallet = wallet;
this._setWallet(walletKey);
return this.wallet;
}
/**
*
* Gets the wallet from local storage if available. If not, method will return null
*
* @returns an enabled wallet or null
*/
static async getWallet() {
const wallet = window.localStorage.getItem(this.localStorageKey);
if (!wallet) {
return null;
}
const walletDetails = JSON.parse(wallet);
if (!walletDetails) {
return null;
}
return await this.connect(walletDetails.key);
}
static disableWallet = async () => {
localStorage.removeItem(this.localStorageKey);
};
// CIP-30 methods
/**
*
* CIP-30 method to check if wallet is enabled
* https://cips.cardano.org/cips/cip30/#cardanowalletnameisenabledpromisebool
*
* @returns CIP-30 Wallet
*/
static isWalletEnabled = async () => {
return this.wallet.isEnabled();
};
/**
*
* CIP-30 method to get the balance of the wallet
* https://cips.cardano.org/cips/cip30/#apigetbalancepromisecborvalue
*
* @returns balanceHex
*/
static getBalance = async () => {
const balanceHex = await this._enabledWallet.getBalance();
return balanceHex;
};
/**
*
* CIP-30 method to get the network id of the wallet
* https://cips.cardano.org/cips/cip30/#apigetnetworkidpromisenumber
*
* @returns 0 or 1 (0 = testnet, 1 = mainnet)
*/
static getNetworkId = async () => {
const networkId = await this._enabledWallet.getNetworkId();
return networkId;
};
/**
*
* CIP-30 method to get the UTXOs of the wallet
* https://cips.cardano.org/cips/cip30/#apigetutxosamountcborvalueundefinedpaginatepaginateundefinedpromisetransactionunspentoutputnull
*
* @returns an array of hex encoded utxos
*/
static getUtxos = async (amount, paginate) => {
const rawUtxos = await this._enabledWallet.getUtxos(amount, paginate);
return rawUtxos;
};
/**
*
* CIP-30 method to get wallet collateral
* https://cips.cardano.org/cips/cip30/#apigetcollateralparamsamountcborcoinpromisetransactionunspentoutputnull
*
* @returns list of Utxos
*/
static getCollateral = async () => {
const collateral = await this._enabledWallet.getCollateral();
return collateral;
};
/**
*
* CIP-30 method to get unused addresses
* https://cips.cardano.org/cips/cip30/#apigetunusedaddressespromiseaddress
*
* @returns list of unused addresses
*/
static getUnusedAddresses = async () => {
const unusedAddresses = await this._enabledWallet.getUnusedAddresses();
return unusedAddresses;
};
/**
*
* CIP-30 method to get a change address
* https://cips.cardano.org/cips/cip30/#apigetchangeaddresspromiseaddress
*
* @returns change address
*/
static getChangeAddress = async () => {
const changeAddress = await this._enabledWallet.getChangeAddress();
return changeAddress;
};
/**
*
* CIP-30 method to get a reward address
* https://cips.cardano.org/cips/cip30/#apigetrewardaddressespromiseaddress
*
* @returns a reward address
*/
static getRewardAddresses = async () => {
const rewardAddresses = await this._enabledWallet.getRewardAddresses();
const serializationLib = await loadCardanoWasm();
const bech32RewardAddress = rewardAddresses.map((addr) => serializationLib.Address.from_bytes(Buffer.from(addr, 'hex')).to_bech32());
return bech32RewardAddress;
};
/**
*
* CIP-30 method to get a sign a transaction
* https://cips.cardano.org/cips/cip30/#apisigntxtxcbortransactionpartialsignboolfalsepromisecbortransaction_witness_set
*
* @param tx hex encoded transaction
* @param partialSign boolean
* @returns
*/
static signTx = async (tx, partialSign = false) => {
const result = await this._enabledWallet.signTx(tx, partialSign);
return result;
};
// public static signData = async (): Promise<string[]> => {
// const rawUtxos = await this._enabledWallet.signData();
// return rawUtxos;
// };
/**
*
* CIP-30 method to submit a transaction
* https://cips.cardano.org/cips/cip30/#apisubmittxtxcbortransactionpromisehash32
*
* @param tx hex encoded transaction
* @returns transaction id
*/
static submitTx = async (tx) => {
const transactionId = await this._enabledWallet.submitTx(tx);
return transactionId;
};
// Custom Methods
/**
*
* Uses the CIP-30 getNetworkId to check if wallet is in mainnet or testnet
*
* @returns boolean
*/
static isMainnet = async () => {
const networkId = await this._enabledWallet.getNetworkId();
return networkId === '1';
};
/**
*
* Used to verify that a wallet has the minimum necessary funds
*
* @param minimumBalance number
*/
static verifyBalance = async (minimumBalance) => {
if (minimumBalance <= 0) {
throw new Error(WalletError.MinimumBalanceIsZero);
}
const balanceHex = (await this.getBalance());
const serializationLib = await loadCardanoWasm();
const balance = serializationLib.Value.from_bytes(Buffer.from(balanceHex, 'hex')).coin().to_str();
if (parseInt(balance) / 1000000 <= minimumBalance) {
throw new Error(WalletError.InsufficientBalance);
}
};
/**
*
* Used to verify a wallet is actively staked
*
* @param { rewardAddress: string }
* @returns void if staked, throws NotDelegated error if not
*
*/
static async verifyStaking({ rewardAddress }) {
const result = await Blockfrost.getAccountsRegistrations(rewardAddress);
if (result.error)
throw new Error(WalletError.NotDelegated);
if (!result.data)
throw new Error(WalletError.NotDelegated);
const { data: [currentRegistration] } = result;
if (!currentRegistration) {
throw new Error(WalletError.NotDelegated);
}
}
/**
*
* Uses the serialization library to convert hex encoded utxos to human readable values
*
* @param rawUtxos Raw utxos from getUtxos()
* @returns Utxo[]
*/
static async buildUtxos(rawUtxos) {
const serializationLib = await loadCardanoWasm();
const utxoDetails = [];
for (const rawUtxo of rawUtxos) {
const utxo = serializationLib.TransactionUnspentOutput.from_bytes(Buffer.from(rawUtxo, 'hex'));
const input = utxo.input();
const txIdBytes = input.transaction_id().to_bytes();
const txId = Buffer.from(txIdBytes, 'utf8').toString('hex');
const txIndx = input.index();
const output = utxo.output();
const lovelaceAmount = output.amount().coin().to_str();
const multiasset = output.amount().multiasset();
let assetNameString;
let assetNameHex;
let policyHex;
if (multiasset) {
const keys = multiasset.keys();
const keysLength = keys.len();
for (let i = 0; i < keysLength; i++) {
const policy = keys.get(i);
const policyBytes = policy.to_bytes();
policyHex = Buffer.from(policyBytes, 'utf8').toString('hex');
const assets = multiasset.get(policy);
const assetNames = assets.keys();
const assetLenth = assetNames.len();
for (let j = 0; j < assetLenth; j++) {
const assetName = assetNames.get(j);
const assetNameBytes = assetName.name();
assetNameString = Buffer.from(assetNameBytes, 'utf8').toString();
assetNameHex = Buffer.from(assetNameBytes, 'utf8').toString('hex');
}
}
}
utxoDetails.push({
txId,
txIndx,
lovelaceAmount,
policyHex,
assetNameHex,
assetName: assetNameString
});
}
return utxoDetails;
}
}