Last active
May 22, 2023 11:31
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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