Skip to content

Instantly share code, notes, and snippets.

@will-break-it
Last active May 22, 2023 11:31
Show Gist options
  • Save will-break-it/5b4e6c9ffe26a2858e718e43abece3bd to your computer and use it in GitHub Desktop.
Save will-break-it/5b4e6c9ffe26a2858e718e43abece3bd to your computer and use it in GitHub Desktop.
CIP30 Cardano Wallet Service for deserializing wallet cbor to standard typescript types
export interface Freeable {
free: () => void;
}
/**
* A scope to ease the management of objects that require manual resource management.
*
*/
export class ManagedFreeableScope {
private scopeStack: Freeable[] = [];
private disposed = false;
/**
* Objects passed to this method will then be managed by the instance.
*
* @param freeable An object with a free function, or undefined. This makes it suitable for wrapping functions that
* may or may not return a value, to minimise the implementation logic.
* @returns The freeable object passed in, which can be undefined.
*/
public manage<T extends Freeable | undefined>(freeable: T): T {
if (freeable === undefined) return freeable;
if (this.disposed) throw new Error('This scope is already disposed.');
this.scopeStack.push(freeable);
return freeable;
}
/**
* Once the freeable objects being managed are no longer being accessed, call this method.
*/
public dispose(): void {
if (this.disposed) return;
for (const resource of this.scopeStack) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((resource as any)?.ptr === 0 || !resource?.free) {
continue;
}
resource?.free();
}
this.disposed = true;
}
}
import { SerializableAssets } from '@/store/slice/walletSlice';
import {
Assets,
C,
Network,
UTxO,
WalletApi,
fromHex,
toHex,
} from 'lucid-cardano';
import { ManagedFreeableScope } from '../utils/freeable';
export type SerializableAssets = Record<string, string>;
export interface IWalletService {
getBech32Address(): Promise<string>;
getBech32RewardAddress(): Promise<string>;
getCollateralUtxos(): Promise<UTxO[]>;
getLovelaceBalance(): Promise<bigint>;
getNativeAssets(): Promise<SerializableAssets>;
getNetwork(): Promise<Network>;
getStakingKey(): Promise<string>;
getWalletUtxos(): Promise<UTxO[]>;
getWallet(): WalletApi;
}
class WalletService implements IWalletService {
readonly #wallet: WalletApi;
constructor(wallet: WalletApi) {
this.#wallet = wallet;
}
getWallet(): WalletApi {
return this.#wallet;
}
async getNetwork(): Promise<Network> {
return (await this.#wallet.getNetworkId()) === 1 ? 'Mainnet' : 'Preprod';
}
/**
* @returns bech32 encoded address.
*/
async getBech32Address(): Promise<string> {
const mfs = new ManagedFreeableScope();
const network = await this.getNetwork();
const addrBytes = await this.#wallet.getChangeAddress().then(fromHex);
const bech32Addr = mfs
.manage(C.Address.from_bytes(addrBytes))
.to_bech32(network === 'Mainnet' ? undefined : 'addr_test');
mfs.dispose();
return bech32Addr;
}
async getStakingKey(): Promise<string> {
const stake_key_hex = (await this.#wallet.getRewardAddresses()).at(0);
if (!stake_key_hex) return Promise.reject('No stake/ reward address found');
return Promise.resolve(stake_key_hex);
}
/**
* @returns bech32 encoded reward address.
*/
async getBech32RewardAddress(): Promise<string> {
const mfs = new ManagedFreeableScope();
const rawRewardAddress = (await this.#wallet.getRewardAddresses()).at(0);
if (!rawRewardAddress)
return Promise.reject('No stake/ reward address found');
const rewardAddress = mfs.manage(
C.RewardAddress.from_address(
mfs.manage(C.Address.from_bytes(fromHex(rawRewardAddress)))
)
);
if (!rewardAddress) {
mfs.dispose();
return Promise.reject('No reward address found');
}
const network =
(await this.getNetwork()) === 'Mainnet' ? undefined : 'stake_test';
const result = mfs.manage(rewardAddress.to_address()).to_bech32(network);
mfs.dispose();
return result;
}
/**
* @returns list of wallet's collateral outputs. Might be undefined or empty.
*/
async getCollateralUtxos(): Promise<UTxO[]> {
const getCollateral =
typeof this.#wallet.getCollateral !== 'undefined'
? this.#wallet.getCollateral
: this.#wallet.experimental.getCollateral;
if (typeof getCollateral === 'undefined')
return Promise.reject(new Error('No collateral function found'));
return getCollateral().then(
mapCborUtxoArrayToUTxOTypeBuilder(await this.getNetwork())
);
}
/**
* @returns wallet balance in lovelace.
*/
async getLovelaceBalance(): Promise<bigint> {
const mfs = new ManagedFreeableScope();
const balance = mfs
.manage(
mfs
.manage(C.Value.from_bytes(fromHex(await this.#wallet.getBalance())))
.coin()
)
.to_str();
mfs.dispose();
return BigInt(balance);
}
async getNativeAssets(): Promise<SerializableAssets> {
const result: SerializableAssets = {};
for (const utxo of await this.getWalletUtxos()) {
for (const assetId of Object.keys(utxo.assets)) {
if (result[assetId] === undefined) {
result[assetId] = utxo.assets[assetId].toString();
} else {
result[assetId] += utxo.assets[assetId].toString();
}
}
}
return result;
}
/**
* @returns decoded unspent transaction outputs (utxos) from wallet.
*/
async getWalletUtxos(): Promise<UTxO[]> {
return this.#wallet
.getUtxos()
.then(mapCborUtxoArrayToUTxOTypeBuilder(await this.getNetwork()));
}
}
/* Helper Mapper */
const mapCborUtxoArrayToUTxOTypeBuilder =
(network: Network) => (cbor: string[] | undefined) =>
cbor?.map((u) => mapUnspentTransactionOutputToUTxO(u, network)) ?? [];
const mapUnspentTransactionOutputToUTxO: (
_utxoCborHex: string,
_network: Network
) => UTxO = (utxoCborHex, network) => {
const mfs = new ManagedFreeableScope();
const utxo = mfs.manage(
C.TransactionUnspentOutput.from_bytes(fromHex(utxoCborHex))
);
const input = mfs.manage(utxo.input());
const output = mfs.manage(utxo.output());
const data = mfs.manage(output.datum());
const datum = mfs.manage(data?.as_data())?.to_bytes();
const datumHash = mfs.manage(data?.as_data_hash())?.to_hex();
const scriptRef = mfs.manage(output.script_ref())?.to_bytes();
const assets: Assets = {};
const value = mfs.manage(output.amount());
assets['lovelace'] = BigInt(mfs.manage(value.coin()).to_str());
const multiAssets = mfs.manage(value.multiasset());
for (let i = 0; i < (multiAssets?.len() ?? 0); i++) {
const scriptHash = mfs.manage(multiAssets?.keys())?.get(i);
if (!scriptHash) continue;
const currency_symbol = scriptHash.to_hex();
const sameCurrencySymbolAssets = multiAssets?.get(scriptHash);
for (let j = 0; j < (sameCurrencySymbolAssets?.len() ?? 0); j++) {
const asset_name = mfs.manage(sameCurrencySymbolAssets?.keys())?.get(j);
if (!asset_name) continue;
const amount = mfs
.manage(sameCurrencySymbolAssets?.get(asset_name))
?.to_str();
if (!amount) continue;
assets[`${currency_symbol}${toHex(asset_name.name())}`] = BigInt(amount);
}
}
const result: UTxO = {
txHash: mfs.manage(input.transaction_id()).to_hex(),
outputIndex: parseInt(mfs.manage(input.index()).to_str()),
address: mfs
.manage(output.address())
.to_bech32(network === 'Mainnet' ? undefined : 'addr_test'),
assets: assets,
scriptRef: scriptRef
? { type: 'PlutusV2', script: toHex(scriptRef) }
: null,
datum: datum ? toHex(datum) : undefined,
datumHash: datumHash,
};
mfs.dispose();
return result;
};
export default WalletService;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment