Skip to content

Commit

Permalink
Improve memory handling
Browse files Browse the repository at this point in the history
  • Loading branch information
michaeljscript committed May 21, 2024
1 parent 63d904e commit 5e84dd0
Show file tree
Hide file tree
Showing 2 changed files with 191 additions and 73 deletions.
205 changes: 132 additions & 73 deletions apps/wallet-mobile/src/yoroi-wallets/cardano/cip30.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,70 +16,112 @@ import {getTransactionSigners} from './common/signatureUtils'
import {Pagination, YoroiWallet} from './types'
import {createRawTxSigningKey, identifierToCardanoAsset} from './utils'
import {collateralConfig, findCollateralCandidates, utxosMaker} from './utxoManager/utxos'
import {wrappedCsl} from './wrappedCsl'
import {WasmModuleProxy} from '@emurgo/cross-csl-core'

export const cip30ExtensionMaker = (wallet: YoroiWallet) => {
return new CIP30Extension(wallet)
}

const getCSL = () => wrappedCsl()

const recreateValue = async (value: CSL.Value) => {
return CardanoMobile.Value.fromHex(await value.toHex())
}

const recreateAddress = async (address: CSL.Address) => {
return CardanoMobile.Address.fromBech32(await address.toBech32(undefined))
}

const recreateMultiple = async <T>(items: T[], recreate: (item: T) => Promise<T>) => {
return Promise.all(items.map(recreate))
}

const recreateTransactionUnspentOutput = async (utxo: CSL.TransactionUnspentOutput) => {
return CardanoMobile.TransactionUnspentOutput.fromHex(await utxo.toHex())
}

const recreateWitnessSet = async (witnessSet: CSL.TransactionWitnessSet) => {
return CardanoMobile.TransactionWitnessSet.fromHex(await witnessSet.toHex())
}

class CIP30Extension {
constructor(private wallet: YoroiWallet) {}

getBalance(tokenId = '*'): Promise<CSL.Value> {
return _getBalance(tokenId, this.wallet.utxos, this.wallet.primaryTokenInfo.id)
async getBalance(tokenId = '*'): Promise<CSL.Value> {
const {csl, release} = getCSL()
try {
const value = await _getBalance(csl, tokenId, this.wallet.utxos, this.wallet.primaryTokenInfo.id)
return recreateValue(value)
} finally {
release()
}
}

async getUnusedAddresses(): Promise<CSL.Address[]> {
const bech32Addresses = this.wallet.receiveAddresses.filter((address) => !this.wallet.isUsedAddressIndex[address])
return Promise.all(bech32Addresses.map((addr) => Cardano.Wasm.Address.fromBech32(addr)))
return Promise.all(bech32Addresses.map((addr) => CardanoMobile.Address.fromBech32(addr)))
}

getUsedAddresses(pagination?: Pagination): Promise<CSL.Address[]> {
const allAddresses = this.wallet.externalAddresses
const selectedAddresses = paginate(allAddresses, pagination)
return Promise.all(selectedAddresses.map((addr) => Cardano.Wasm.Address.fromBech32(addr)))
return Promise.all(selectedAddresses.map((addr) => CardanoMobile.Address.fromBech32(addr)))
}

getChangeAddress(): Promise<CSL.Address> {
const changeAddr = this.wallet.getChangeAddress()
return Cardano.Wasm.Address.fromBech32(changeAddr)
return CardanoMobile.Address.fromBech32(changeAddr)
}

async getRewardAddresses(): Promise<CSL.Address[]> {
const address = await CardanoMobile.Address.fromHex(this.wallet.rewardAddressHex)
return [address]
}

getUtxos(value?: string, pagination?: Pagination): Promise<CSL.TransactionUnspentOutput[] | null> {
return _getUtxos(this.wallet, value, pagination)
async getUtxos(value?: string, pagination?: Pagination): Promise<CSL.TransactionUnspentOutput[] | null> {
const {csl, release} = getCSL()
try {
const utxos = await _getUtxos(csl, this.wallet, value, pagination)
if (utxos === null) return null
return Promise.all(utxos.map(async (u) => CardanoMobile.TransactionUnspentOutput.fromHex(await u.toHex())))
} finally {
release()
}
}

async getCollateral(value?: string): Promise<CSL.TransactionUnspentOutput[] | null> {
const valueStr = value?.trim() ?? collateralConfig.minLovelace.toString()
const valueNum = new BigNumber(valueStr)

if (valueNum.gte(collateralConfig.maxLovelace)) {
throw new Error('Collateral value is too high')
}

const currentCollateral = this.wallet.getCollateralInfo()
const canUseCurrentCollateral = currentCollateral.utxo && valueNum.lte(currentCollateral.utxo.amount)

if (canUseCurrentCollateral && currentCollateral.utxo) {
const utxo = await cardanoUtxoFromRemoteFormat(rawUtxoToRemoteUnspentOutput(currentCollateral.utxo))
return [utxo]
}

const oneUtxoCollateral = await _drawCollateralInOneUtxo(this.wallet, asQuantity(valueNum))
if (oneUtxoCollateral) {
return [oneUtxoCollateral]
}

const multipleUtxosCollateral = await _drawCollateralInMultipleUtxos(this.wallet, asQuantity(valueNum))
if (multipleUtxosCollateral && multipleUtxosCollateral.length > 0) {
return multipleUtxosCollateral
const {csl, release} = getCSL()
try {
const valueStr = value?.trim() ?? collateralConfig.minLovelace.toString()
const valueNum = new BigNumber(valueStr)

if (valueNum.gte(collateralConfig.maxLovelace)) {
throw new Error('Collateral value is too high')
}

const currentCollateral = this.wallet.getCollateralInfo()
const canUseCurrentCollateral = currentCollateral.utxo && valueNum.lte(currentCollateral.utxo.amount)

if (canUseCurrentCollateral && currentCollateral.utxo) {
const utxo = await cardanoUtxoFromRemoteFormat(rawUtxoToRemoteUnspentOutput(currentCollateral.utxo))
return [await recreateTransactionUnspentOutput(utxo)]
}

const oneUtxoCollateral = await _drawCollateralInOneUtxo(this.wallet, asQuantity(valueNum))
if (oneUtxoCollateral) {
return [await recreateTransactionUnspentOutput(oneUtxoCollateral)]
}

const multipleUtxosCollateral = await _drawCollateralInMultipleUtxos(csl, this.wallet, asQuantity(valueNum))
if (multipleUtxosCollateral && multipleUtxosCollateral.length > 0) {
return recreateMultiple(multipleUtxosCollateral, recreateTransactionUnspentOutput)
}

return null
} finally {
release()
}

return null
}

async submitTx(cbor: string): Promise<string> {
Expand All @@ -90,60 +132,72 @@ class CIP30Extension {
}

async signData(_rootKey: string, address: string, _payload: string): Promise<{signature: string; key: string}> {
const normalisedAddress = await normalizeToAddress(CardanoMobile, address)
const bech32Address = await normalisedAddress?.toBech32(undefined)
if (!bech32Address) throw new Error('Invalid wallet state')
throw new Error('Not implemented')
const {csl, release} = getCSL()
try {
const normalisedAddress = await normalizeToAddress(csl, address)
const bech32Address = await normalisedAddress?.toBech32(undefined)
if (!bech32Address) throw new Error('Invalid wallet state')
throw new Error('Not implemented')
} finally {
release()
}
}

async signTx(rootKey: string, cbor: string, partial = false): Promise<CSL.TransactionWitnessSet> {
const signers = await getTransactionSigners(cbor, this.wallet, partial)
const keys = await Promise.all(signers.map(async (signer) => createRawTxSigningKey(rootKey, signer)))
const signedTxBytes = await signRawTransaction(CardanoMobile, cbor, keys)
const signedTx = await CardanoMobile.Transaction.fromBytes(signedTxBytes)
return signedTx.witnessSet()
const {csl, release} = getCSL()
try {
const signers = await getTransactionSigners(cbor, this.wallet, partial)
const keys = await Promise.all(signers.map(async (signer) => createRawTxSigningKey(rootKey, signer)))
const signedTxBytes = await signRawTransaction(csl, cbor, keys)
const signedTx = await csl.Transaction.fromBytes(signedTxBytes)
return recreateWitnessSet(await signedTx.witnessSet())
} finally {
release()
}
}
}

const remoteAssetToMultiasset = async (remoteAssets: UtxoAsset[]): Promise<CSL.MultiAsset> => {
const remoteAssetToMultiasset = async (csl: WasmModuleProxy, remoteAssets: UtxoAsset[]): Promise<CSL.MultiAsset> => {
const groupedAssets = remoteAssets.reduce((res, a) => {
;(res[toPolicyId(a.assetId)] = res[toPolicyId(a.assetId)] || []).push(a)
return res
}, {} as Record<string, UtxoAsset[]>)
const multiasset = await CardanoMobile.MultiAsset.new()
const multiasset = await csl.MultiAsset.new()
for (const policyHex of Object.keys(groupedAssets)) {
const assetGroup = groupedAssets[policyHex]
const policyId = await CardanoMobile.ScriptHash.fromBytes(Buffer.from(policyHex, 'hex'))
const assets = await CardanoMobile.Assets.new()
const policyId = await csl.ScriptHash.fromBytes(Buffer.from(policyHex, 'hex'))
const assets = await csl.Assets.new()
for (const asset of assetGroup) {
await assets.insert(
await CardanoMobile.AssetName.new(Buffer.from(toAssetNameHex(asset.assetId), 'hex')),
await CardanoMobile.BigNum.fromStr(asset.amount),
await csl.AssetName.new(Buffer.from(toAssetNameHex(asset.assetId), 'hex')),
await csl.BigNum.fromStr(asset.amount),
)
}
await multiasset.insert(policyId, assets)
}
return multiasset
}
const cardanoUtxoFromRemoteFormat = async (u: RemoteUnspentOutput): Promise<CSL.TransactionUnspentOutput> => {
const input = await CardanoMobile.TransactionInput.new(
await CardanoMobile.TransactionHash.fromHex(u.txHash),
u.txIndex,
)
const value = await CardanoMobile.Value.new(await CardanoMobile.BigNum.fromStr(u.amount))
if ((u.assets || []).length > 0) {
await value.setMultiasset(await remoteAssetToMultiasset([...u.assets]))
const {csl, release} = getCSL()
try {
const input = await csl.TransactionInput.new(await csl.TransactionHash.fromHex(u.txHash), u.txIndex)
const value = await csl.Value.new(await csl.BigNum.fromStr(u.amount))
if ((u.assets || []).length > 0) {
await value.setMultiasset(await remoteAssetToMultiasset(csl, [...u.assets]))
}
const receiver = await csl.Address.fromBech32(u.receiver)
if (!receiver) throw new Error('Invalid receiver')
const output = await csl.TransactionOutput.new(receiver, value)
return csl.TransactionUnspentOutput.new(input, output)
} finally {
release()
}
const receiver = await CardanoMobile.Address.fromBech32(u.receiver)
if (!receiver) throw new Error('Invalid receiver')
const output = await CardanoMobile.TransactionOutput.new(receiver, value)
return CardanoMobile.TransactionUnspentOutput.new(input, output)
}

const _getBalance = async (tokenId = '*', utxos: RawUtxo[], primaryTokenId: string) => {
const _getBalance = async (csl: WasmModuleProxy, tokenId = '*', utxos: RawUtxo[], primaryTokenId: string) => {
if (tokenId === 'TADA' || tokenId === 'ADA') tokenId = '.'
const amounts = Utxos.toAmounts(utxos, primaryTokenId)
const value = await CardanoMobile.Value.new(await CardanoMobile.BigNum.fromStr(amounts[primaryTokenId]))
const value = await csl.Value.new(await csl.BigNum.fromStr(amounts[primaryTokenId]))
const normalizedInHex = await Promise.all(
Object.keys(amounts)
.filter((t) => {
Expand All @@ -160,16 +214,16 @@ const _getBalance = async (tokenId = '*', utxos: RawUtxo[], primaryTokenId: stri

const groupedByPolicyId = _.groupBy(normalizedInHex.filter(Boolean), 'policyIdHex')

const multiAsset = await CardanoMobile.MultiAsset.new()
const multiAsset = await csl.MultiAsset.new()
for (const policyIdHex of Object.keys(groupedByPolicyId)) {
const assetValue = groupedByPolicyId[policyIdHex]
if (!assetValue) continue
const policyId = await CardanoMobile.ScriptHash.fromHex(policyIdHex)
const assets = await CardanoMobile.Assets.new()
const policyId = await csl.ScriptHash.fromHex(policyIdHex)
const assets = await csl.Assets.new()
for (const asset of assetValue) {
if (!asset) continue
const assetName = await CardanoMobile.AssetName.fromHex(asset.nameHex)
const assetValue = await CardanoMobile.BigNum.fromStr(asset.amount)
const assetName = await csl.AssetName.fromHex(asset.nameHex)
const assetValue = await csl.BigNum.fromStr(asset.amount)
await assets.insert(assetName, assetValue)
}
await multiAsset.insert(policyId, assets)
Expand All @@ -178,7 +232,7 @@ const _getBalance = async (tokenId = '*', utxos: RawUtxo[], primaryTokenId: stri
return value
}

export const _getUtxos = async (wallet: YoroiWallet, value?: string, pagination?: Pagination) => {
export const _getUtxos = async (csl: WasmModuleProxy, wallet: YoroiWallet, value?: string, pagination?: Pagination) => {
const valueStr = value?.trim() ?? ''

if (valueStr.length === 0) {
Expand All @@ -196,24 +250,25 @@ export const _getUtxos = async (wallet: YoroiWallet, value?: string, pagination?
amounts[wallet.primaryTokenInfo.id] = asQuantity(valueStr)
} else {
try {
Object.assign(amounts, getAmountsFromValue(valueStr, wallet.primaryTokenInfo.id))
Object.assign(amounts, getAmountsFromValue(csl, valueStr, wallet.primaryTokenInfo.id))
} catch (e) {
//
}
}

const validUtxos = await _getRequiredUtxos(wallet, amounts, wallet.utxos)
const validUtxos = await _getRequiredUtxos(csl, wallet, amounts, wallet.utxos)
if (validUtxos === null) return null
return paginate(validUtxos, pagination)
}

const _getRequiredUtxos = async (
csl: WasmModuleProxy,
wallet: YoroiWallet,
amounts: Balance.Amounts,
allUtxos: RawUtxo[],
): Promise<CSL.TransactionUnspentOutput[] | null> => {
const remoteUnspentOutputs: RemoteUnspentOutput[] = allUtxos.map((utxo) => rawUtxoToRemoteUnspentOutput(utxo))
const rewardAddress = await (await normalizeToAddress(CardanoMobile, wallet.rewardAddressHex))?.toBech32(undefined)
const rewardAddress = await (await normalizeToAddress(csl, wallet.rewardAddressHex))?.toBech32(undefined)
if (!rewardAddress) throw new Error('Invalid wallet state')

try {
Expand Down Expand Up @@ -266,22 +321,26 @@ const _drawCollateralInOneUtxo = async (wallet: YoroiWallet, quantity: Balance.Q
return cardanoUtxoFromRemoteFormat(rawUtxoToRemoteUnspentOutput(collateralUtxo))
}

const _drawCollateralInMultipleUtxos = async (wallet: YoroiWallet, quantity: Balance.Quantity) => {
const _drawCollateralInMultipleUtxos = async (
csl: WasmModuleProxy,
wallet: YoroiWallet,
quantity: Balance.Quantity,
) => {
const possibleUtxos = findCollateralCandidates(wallet.utxos, {
maxLovelace: collateralConfig.maxLovelace,
minLovelace: asQuantity('0'),
})

const utxos = await _getRequiredUtxos(wallet, {[wallet.primaryTokenInfo.id]: quantity}, possibleUtxos)
const utxos = await _getRequiredUtxos(csl, wallet, {[wallet.primaryTokenInfo.id]: quantity}, possibleUtxos)

if (utxos !== null && utxos.length > 0) {
return utxos
}
return null
}

const getAmountsFromValue = async (value: string, primaryTokenId: string) => {
const valueFromHex = await CardanoMobile.Value.fromHex(value)
const getAmountsFromValue = async (csl: WasmModuleProxy, value: string, primaryTokenId: string) => {
const valueFromHex = await csl.Value.fromHex(value)
const amounts: BalanceAmounts = {}

if (valueFromHex.hasValue()) {
Expand Down
59 changes: 59 additions & 0 deletions apps/wallet-mobile/src/yoroi-wallets/cardano/wrappedCsl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {init} from '@emurgo/cross-csl-mobile'
import {WasmModuleProxy} from '@emurgo/cross-csl-core'

const cardano = init('wrappedCSL')

export type CslPointer = {ptr: number; free: () => void}

export const wrappedCsl = (): {csl: WasmModuleProxy; release: VoidFunction} => {
let pointers: CslPointer[] = []
const track = (p: CslPointer) => pointers.push(p)
const release = () => {
pointers.forEach((p) => {
if (p?.ptr !== 0) {
try {
p.free()
} catch (e) {}
}
})
pointers = []
}

const trackIfNeeded = <T>(obj: T): T => {
if (typeof (obj as any)?.free === 'function') track(obj as any)
if (obj instanceof Promise) {
return obj.then((result) => trackIfNeeded(result)) as any
}
if (Array.isArray(obj)) return obj.map((o) => trackIfNeeded(o) as any) as any
return obj
}

const proxy = new Proxy(cardano, {
get(target: WasmModuleProxy, p: string | symbol, receiver: any): any {
const prop = Reflect.get(target, p, receiver)
if (!isClass(prop)) {
return prop
}

return new Proxy(prop, {
get: (target: any, name: string) => {
if (name === 'prototype') return target[name]
const isFunc = typeof target[name] === 'function'
if (!isFunc) return target[name]
return (...args: any[]) => {
const result = target[name](...args)
return trackIfNeeded(result)
}
},
})
},
})

return {csl: proxy, release}
}

const isClass = (func: any) => {
return (
typeof func === 'function' && (/^\s*class\s+/.test(func.toString()) || func.name[0] === func.name[0].toUpperCase())
)
}

0 comments on commit 5e84dd0

Please sign in to comment.