Skip to content

Commit

Permalink
Add getCollateral
Browse files Browse the repository at this point in the history
  • Loading branch information
michaeljscript committed May 3, 2024
1 parent d01845c commit 1d42e1f
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 28 deletions.
9 changes: 8 additions & 1 deletion apps/wallet-mobile/src/features/Discover/common/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,16 @@ export const createDappConnector = (appStorage: App.Storage, wallet: YoroiWallet
getBalance: (tokenId) => wallet.getBalance(tokenId),
getChangeAddress: () => wallet.CIP30getChangeAddress(),
getRewardAddresses: () => wallet.CIP30getRewardAddresses(),
getCollateral: async (value) => {
// TODO: Move serialisation out of here
const result = await wallet.CIP30getCollateral(value)
if (!result) return null
if (result.length === 0) return []
return Promise.all(result.map((v) => v.toHex()))
},
getUtxos: async (value, pagination) => {
const result = await wallet.CIP30getUtxos(value, pagination)
if (!result) return [] // TODO: return null
if (!result) return [] // TODO: return null if value was given
return Promise.all(result.map((v) => v.toHex()))
},
confirmConnection: async (origin: string) => {
Expand Down
109 changes: 89 additions & 20 deletions apps/wallet-mobile/src/yoroi-wallets/cardano/shelley/ShelleyWallet.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {PrivateKey} from '@emurgo/cross-csl-core'
import {PrivateKey, TransactionUnspentOutput} from '@emurgo/cross-csl-core'
import {createSignedLedgerTxFromCbor, RemoteUnspentOutput, signRawTransaction} from '@emurgo/yoroi-lib'
import {Datum} from '@emurgo/yoroi-lib/dist/internals/models'
import {parseTokenList} from '@emurgo/yoroi-lib/dist/internals/utils/assets'
Expand Down Expand Up @@ -72,7 +72,7 @@ import {
import {yoroiUnsignedTx} from '../unsignedTx'
import {deriveRewardAddressHex, normalizeToAddress, toRecipients} from '../utils'
import {makeUtxoManager, UtxoManager} from '../utxoManager'
import {utxosMaker} from '../utxoManager/utxos'
import {collateralConfig, findCollateralCandidates, utxosMaker} from '../utxoManager/utxos'
import {makeKeys} from './makeKeys'

type WalletState = {
Expand Down Expand Up @@ -943,23 +943,23 @@ export const makeShelleyWallet = (constants: typeof MAINNET | typeof TESTNET | t
return [hex]
}

async CIP30getUtxos(value?: string, pagination?: {page: number; limit: number}) {
private async getUtxos(amounts: Balance.Amounts, allUtxos = this.utxos): Promise<TransactionUnspentOutput[]> {
// TODO: 1) Add try-catch. 2) If amount can't be reached, return null
const allUtxos = this.utxos
const remoteUnspentOutputs: RemoteUnspentOutput[] = allUtxos.map((utxo) => ({
txHash: utxo.tx_hash,
txIndex: utxo.tx_index,
receiver: utxo.receiver,
amount: utxo.amount,
assets: utxo.assets,
utxoId: utxo.utxo_id,
}))
const remoteUnspentOutputs: RemoteUnspentOutput[] = allUtxos.map((utxo) => rawUtxoToRemoteUnspentOutput(utxo))
const rewardAddress = await (await normalizeToAddress(this.rewardAddressHex))?.toBech32(undefined)
if (!rewardAddress) throw new Error('Invalid wallet state')

const unsignedTx = await this.createUnsignedTx([{address: rewardAddress, amounts}])
const requiredUtxos = await findUtxosInUnsignedTx(unsignedTx, remoteUnspentOutputs)
return Promise.all(requiredUtxos.map((o) => cardanoUtxoFromRemoteFormat(o)))
}

async CIP30getUtxos(value?: string, pagination?: {page: number; limit: number}) {
const valueStr = value?.trim() ?? ''

if (valueStr.length === 0) {
const selectedUtxos = paginate(remoteUnspentOutputs, pagination)
return Promise.all(selectedUtxos.map((o) => cardanoUtxoFromRemoteFormat(o)))
const validUtxos = await this.getUtxos({[this.primaryTokenInfo.id]: asQuantity(valueStr)})
return paginate(validUtxos, pagination)
}

const amounts: BalanceAmounts = {}
Expand All @@ -976,12 +976,70 @@ export const makeShelleyWallet = (constants: typeof MAINNET | typeof TESTNET | t
}
}

const unsignedTx = await this.createUnsignedTx([
{address: await (await normalizeToAddress(this.rewardAddressHex))?.toBech32(undefined)!, amounts},
])
const requiredUtxos = await findUtxosInUnsignedTx(unsignedTx, remoteUnspentOutputs)
const selectedUtxos = paginate(requiredUtxos, pagination)
return Promise.all(selectedUtxos.map((o) => cardanoUtxoFromRemoteFormat(o)))
const validUtxos = await this.getUtxos(amounts)
return paginate(validUtxos, pagination)
}

private async _drawCollateralInOneUtxo(quantity: Balance.Quantity) {
const utxos = utxosMaker(this.utxos, {
maxLovelace: collateralConfig.maxLovelace,
minLovelace: quantity,
})

const possibleCollateralId = utxos.drawnCollateral()
if (!possibleCollateralId) return null
const collateralUtxo = utxos.findById(possibleCollateralId)
if (!collateralUtxo) return null
return cardanoUtxoFromRemoteFormat(rawUtxoToRemoteUnspentOutput(collateralUtxo))
}

private async _drawCollateralInMultipleUtxos(quantity: Balance.Quantity) {
const possibleUtxos = findCollateralCandidates(this.utxos, {
maxLovelace: collateralConfig.maxLovelace,
minLovelace: asQuantity('0'),
})
const utxos = await this.getUtxos({[this.primaryTokenInfo.id]: quantity}, possibleUtxos)
if (utxos.length > 0) {
console.log('CIP30getCollateral: draw collateral in multiple utxos')
return utxos
}
return null
}

async CIP30getCollateral(value?: string) {
console.log('CIP30getCollateral', value)
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.getCollateralInfo()

// if has collateral and requested collateral is lower or equal to current collateral
// return current collateral
if (currentCollateral.utxo && valueNum.lte(currentCollateral.utxo.amount)) {
console.log('CIP30getCollateral: return current collateral')
const utxo = await cardanoUtxoFromRemoteFormat(rawUtxoToRemoteUnspentOutput(currentCollateral.utxo))
return [utxo]
}

// if can draw collateral in one utxo, use the utxo as a collateral
const oneUtxoCollateral = await this._drawCollateralInOneUtxo(asQuantity(valueNum))
if (oneUtxoCollateral) {
console.log('CIP30getCollateral: draw collateral in one utxo')
return [oneUtxoCollateral]
}

// if can draw collateral in multiple utxos, use all required utxos
const multipleUtxosCollateral = await this._drawCollateralInMultipleUtxos(asQuantity(valueNum))
if (multipleUtxosCollateral && multipleUtxosCollateral.length > 0) {
console.log('CIP30getCollateral: draw collateral in multiple utxos')
return multipleUtxosCollateral
}

return null
}

async signSwapCancellationWithLedger(cbor: string, useUSB: boolean): Promise<void> {
Expand Down Expand Up @@ -1427,3 +1485,14 @@ const getAmountsFromValue = async (value: string, primaryTokenId: string) => {
}
return amounts
}

const rawUtxoToRemoteUnspentOutput = (utxo: RawUtxo): RemoteUnspentOutput => {
return {
txHash: utxo.tx_hash,
txIndex: utxo.tx_index,
receiver: utxo.receiver,
amount: utxo.amount,
assets: utxo.assets,
utxoId: utxo.utxo_id,
}
}
1 change: 1 addition & 0 deletions apps/wallet-mobile/src/yoroi-wallets/cardano/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ export type YoroiWallet = {
CIP30getChangeAddress(): Promise<string>
CIP30getRewardAddresses(): Promise<string[]>
CIP30getUtxos(value?: string, paginate?: {page: number; limit: number}): Promise<TransactionUnspentOutput[] | null>
CIP30getCollateral(value?: string): Promise<TransactionUnspentOutput[] | null>
}

export const isYoroiWallet = (wallet: unknown): wallet is YoroiWallet => {
Expand Down
24 changes: 17 additions & 7 deletions apps/wallet-mobile/src/yoroi-wallets/cardano/utxoManager/utxos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,29 @@ export const collateralConfig: CollateralConfig = {
export function isPureUtxo(utxo: RawUtxo) {
return utxo.assets.length === 0
}

export const hasValue = (utxo: RawUtxo) => {
return new BigNumber(asQuantity(utxo.amount)).gte(0)
}

export function isAmountInCollateralRange(amount: RawUtxo['amount'], {maxLovelace, minLovelace}: CollateralConfig) {
const value = new BigNumber(asQuantity(amount))
const min = new BigNumber(minLovelace)
const max = new BigNumber(maxLovelace)
return value.gte(min) && value.lte(max)
}

export const findCollateralCandidates = (
utxos: ReadonlyArray<RawUtxo>,
{maxLovelace, minLovelace}: CollateralConfig,
) => {
return utxos
.filter(isPureUtxo)
.filter(hasValue)
.filter((utxo) => isAmountInCollateralRange(utxo.amount, {maxLovelace, minLovelace}))
.sort((a, b) => new BigNumber(asQuantity(a.amount)).comparedTo(asQuantity(b.amount)))
}

export function utxosMaker(
utxos: ReadonlyArray<RawUtxo>,
{maxLovelace, minLovelace}: CollateralConfig = collateralConfig,
Expand All @@ -31,14 +47,8 @@ export function utxosMaker(
return findById(id) !== undefined
}

const findCollateralCandidates = () => {
return utxos
.filter(isPureUtxo)
.filter((utxo) => isAmountInCollateralRange(utxo.amount, {maxLovelace, minLovelace}))
.sort((a, b) => new BigNumber(asQuantity(a.amount)).comparedTo(asQuantity(b.amount)))
}
const drawnCollateral = () => {
const candidates = findCollateralCandidates()
const candidates = findCollateralCandidates(utxos, {maxLovelace, minLovelace})
const collateral = candidates.find(first)
return collateral?.utxo_id
}
Expand Down
1 change: 1 addition & 0 deletions packages/dapp-connector/src/dapp-connector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,5 +288,6 @@ const mockWallet: ResolverWallet = {
),
getRewardAddresses: () => Promise.resolve(['e184d958399bcce03402fd853d43a4e7366f2018932e5aff4eea904693']),
getUtxos: () => Promise.resolve([]),
getCollateral: () => Promise.resolve([]),
}
const trustedUrl = 'https://yoroi-wallet.com/'
21 changes: 21 additions & 0 deletions packages/dapp-connector/src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Resolver = {
getExtensions: ResolvableMethod<Array<{cip: number}>>
getUnusedAddresses: ResolvableMethod<string[]>
getUtxos: ResolvableMethod<string[]>
getCollateral: ResolvableMethod<string[] | null>
}
}

Expand All @@ -48,6 +49,25 @@ export const resolver: Resolver = {
return hasWalletAcceptedConnection(context)
},
api: {
getCollateral: async (params: unknown, context: Context) => {
// offer to reorganise transactions if possible
// check if collateral is less than or equal to 5 ADA
assertOriginsMatch(context)
await assertWalletAcceptedConnection(context)
const value =
isRecord(params) && Array.isArray(params.args) && typeof params.args[0] === 'string'
? params.args[0]
: undefined
const result = await context.wallet.getCollateral(value)

if (value !== undefined && (result === null || result.length === 0)) {
// TODO: Offer to reorganise transactions if possible
return null
}

return result
},

getUnusedAddresses: async (_params: unknown, context: Context) => {
assertOriginsMatch(context)
await assertWalletAcceptedConnection(context)
Expand Down Expand Up @@ -196,6 +216,7 @@ export type ResolverWallet = {
getChangeAddress: () => Promise<string>
getRewardAddresses: () => Promise<string[]>
getUtxos: (value?: string, pagination?: Pagination) => Promise<string[]>
getCollateral: (value?: string) => Promise<string[] | null>
}

type Pagination = {
Expand Down

0 comments on commit 1d42e1f

Please sign in to comment.