Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add makeMemoryWallet method #599

Merged
merged 3 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- added: Add `makeMemoryWallet` method to ephemeral wallet objects that can query balances and spend funds
- fixed: Correctly handle `null` fetch bodies on Android.

## 2.8.1 (2024-07-11)
Expand Down
14 changes: 14 additions & 0 deletions src/core/account/account-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
EdgeGetActivationAssetsOptions,
EdgeGetActivationAssetsResults,
EdgeLobby,
EdgeMemoryWallet,
EdgePendingVoucher,
EdgePluginMap,
EdgeResult,
Expand Down Expand Up @@ -59,6 +60,7 @@ import { changeWalletStates } from './account-files'
import { AccountState } from './account-reducer'
import { makeDataStoreApi } from './data-store-api'
import { makeLobbyApi } from './lobby-api'
import { makeMemoryWalletInner } from './memory-wallet'
import { CurrencyConfig, SwapConfig } from './plugin-api'

/**
Expand Down Expand Up @@ -498,6 +500,18 @@ export function makeAccountApi(ai: ApiInput, accountId: string): EdgeAccount {
return await finishWalletCreation(ai, accountId, walletInfo.id, opts)
},

async makeMemoryWallet(
swansontec marked this conversation as resolved.
Show resolved Hide resolved
walletType: string,
opts: EdgeCreateCurrencyWalletOptions = {}
): Promise<EdgeMemoryWallet> {
const config = Object.values(currencyConfigs).find(
plugin => plugin.currencyInfo.walletType === walletType
)
if (config == null) throw new Error('Invalid walletType')

return await makeMemoryWalletInner(ai, config, walletType, opts)
},

async createCurrencyWallets(
createWallets: EdgeCreateCurrencyWallet[]
): Promise<Array<EdgeResult<EdgeCurrencyWallet>>> {
Expand Down
198 changes: 198 additions & 0 deletions src/core/account/memory-wallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { makeMemoryDisklet } from 'disklet'
import { bridgifyObject, close, update, watchMethod } from 'yaob'

import {
EdgeBalanceMap,
EdgeCreateCurrencyWalletOptions,
EdgeCurrencyConfig,
EdgeMemoryWallet,
EdgeSpendInfo,
EdgeTokenId,
EdgeTransaction,
EdgeWalletInfo
} from '../../browser'
import { makePeriodicTask, PeriodicTask } from '../../util/periodic-task'
import { snooze } from '../../util/snooze'
import { getMaxSpendableInner } from '../currency/wallet/max-spend'
import { makeLog } from '../log/log'
import { getCurrencyTools } from '../plugins/plugins-selectors'
import { ApiInput } from '../root-pixie'

let memoryWalletCount = 0

export const makeMemoryWalletInner = async (
ai: ApiInput,
config: EdgeCurrencyConfig,
walletType: string,
opts: EdgeCreateCurrencyWalletOptions = {}
): Promise<EdgeMemoryWallet> => {
const { keys } = opts
if (keys == null) throw new Error('No keys provided')

const walletId = `memorywallet-${memoryWalletCount++}`
const walletInfo: EdgeWalletInfo = {
id: walletId,
type: walletType,
keys
}

const tools = await getCurrencyTools(ai, config.currencyInfo.pluginId)
const publicKeys = await tools.derivePublicKey(walletInfo)
walletInfo.keys = { ...publicKeys, ...walletInfo.keys }

const log = makeLog(ai.props.logBackend, `${walletId}-${walletType}`)
let balanceMap: EdgeBalanceMap = new Map()
let detectedTokenIds: string[] = []
let syncRatio: number = 0

let needsUpdate = false
const updateWallet = (): void => {
if (needsUpdate) {
update(out)
needsUpdate = false
}
}
const updater = makePeriodicTask(async () => {
await snooze(1000) // one second
updateWallet()
}, 0)

const plugin = ai.props.state.plugins.currency[config.currencyInfo.pluginId]
const engine = await plugin.makeCurrencyEngine(walletInfo, {
callbacks: {
onAddressChanged: () => {},
onAddressesChecked: (progressRatio: number) => {
if (out.syncRatio === 1) return

if (progressRatio === 1) {
syncRatio = progressRatio
needsUpdate = true
}
},
onNewTokens: (tokenIds: string[]) => {
const sortedTokenIds = tokenIds.sort((a, b) => a.localeCompare(b))

if (detectedTokenIds.length !== sortedTokenIds.length) {
detectedTokenIds = sortedTokenIds
needsUpdate = true
return
}
for (let i = 0; i < sortedTokenIds.length; i++) {
if (detectedTokenIds[i] !== sortedTokenIds[i]) {
detectedTokenIds = sortedTokenIds
needsUpdate = true
return
}
}
},
onStakingStatusChanged: () => {},
onTokenBalanceChanged: (tokenId: EdgeTokenId, balance: string) => {
if (balanceMap.get(tokenId) === balance) return

balanceMap = new Map(balanceMap)
balanceMap.set(tokenId, balance)
needsUpdate = true
},
onTransactionsChanged: () => {},
onTxidsChanged: () => {},
onUnactivatedTokenIdsChanged: () => {},
onWcNewContractCall: () => {},
onBlockHeightChanged: () => {},
onBalanceChanged: () => {}
},
customTokens: { ...config.customTokens },
enabledTokenIds: [...Object.keys(config.allTokens)],
lightMode: true,
log,
userSettings: { ...(config.userSettings ?? {}) },
walletLocalDisklet: makeMemoryDisklet(),
walletLocalEncryptedDisklet: makeMemoryDisklet()
})

const {
unsafeBroadcastTx = false,
unsafeMakeSpend = false,
unsafeSyncNetwork = false
} = plugin.currencyInfo

const privateKeys = { ...keys }

let syncNetworkTask: PeriodicTask
// Setup syncNetwork routine if defined by the currency engine:
if (engine.syncNetwork != null) {
// Get the private keys if required by the engine:
const doNetworkSync = async (): Promise<void> => {
if (engine.syncNetwork != null) {
const delay = await engine.syncNetwork({
privateKeys: unsafeSyncNetwork ? { privateKeys: keys } : undefined
})
syncNetworkTask.setDelay(delay)
} else {
syncNetworkTask.stop()
}
}
syncNetworkTask = makePeriodicTask(doNetworkSync, 10000, {
onError: error => {
ai.props.log.error(error)
}
})
syncNetworkTask.start({ wait: false })
}

const out = bridgifyObject<EdgeMemoryWallet>({
watch: watchMethod,
get balanceMap() {
return balanceMap
},
get detectedTokenIds() {
return detectedTokenIds
},
get syncRatio() {
return syncRatio
},
async changeEnabledTokenIds(tokenIds: string[]) {
if (engine.changeEnabledTokenIds != null) {
await engine.changeEnabledTokenIds(tokenIds)
}
},
async startEngine() {
await engine.startEngine()
syncNetworkTask?.start({ wait: false })
},
async getMaxSpendable(spendInfo: EdgeSpendInfo) {
return await getMaxSpendableInner(
spendInfo,
plugin,
engine,
config.allTokens,
walletInfo
)
},
async makeSpend(spendInfo: EdgeSpendInfo) {
return await engine.makeSpend(
spendInfo,
unsafeMakeSpend ? privateKeys : undefined
)
},
async signTx(tx: EdgeTransaction) {
return await engine.signTx(tx, privateKeys)
},
async broadcastTx(tx: EdgeTransaction) {
return await engine.broadcastTx(
tx,
unsafeBroadcastTx ? privateKeys : undefined
)
},
async saveTx() {},

async close() {
log.warn('killing memory wallet')
syncNetworkTask?.stop()
close(out)
await engine.killEngine()
}
})

updater.start({ wait: false })
return out
}
66 changes: 9 additions & 57 deletions src/core/currency/wallet/currency-wallet-api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { add, div, eq, lte, mul, sub } from 'biggystring'
import { div, eq, mul } from 'biggystring'
import { Disklet } from 'disklet'
import { bridgifyObject, onMethod, watchMethod } from 'yaob'

Expand Down Expand Up @@ -55,6 +55,7 @@ import {
} from './currency-wallet-files'
import { CurrencyWalletInput } from './currency-wallet-pixie'
import { MergedTransaction } from './currency-wallet-reducer'
import { getMaxSpendableInner } from './max-spend'
import { mergeMetadata } from './metadata'
import { upgradeMemos } from './upgrade-memos'

Expand Down Expand Up @@ -388,62 +389,13 @@ export function makeCurrencyWalletApi(
return await engine.broadcastTx(tx, { privateKeys })
},
async getMaxSpendable(spendInfo: EdgeSpendInfo): Promise<string> {
spendInfo = upgradeMemos(spendInfo, plugin.currencyInfo)
// Figure out which asset this is:
const upgradedCurrency = upgradeCurrencyCode({
allTokens: input.props.state.accounts[accountId].allTokens[pluginId],
currencyInfo: plugin.currencyInfo,
tokenId: spendInfo.tokenId
})

if (typeof engine.getMaxSpendable === 'function') {
// Only provide wallet info if currency requires it:
const privateKeys = unsafeMakeSpend ? walletInfo.keys : undefined

return await engine.getMaxSpendable(
{ ...spendInfo, ...upgradedCurrency },
{ privateKeys }
)
}

const { networkFeeOption, customNetworkFee } = spendInfo
const balance = engine.getBalance(upgradedCurrency)

// Copy all the spend targets, setting the amounts to 0
// but keeping all other information so we can get accurate fees:
const spendTargets = spendInfo.spendTargets.map(spendTarget => {
return { ...spendTarget, nativeAmount: '0' }
})

// The range of possible values includes `min`, but not `max`.
function getMax(min: string, max: string): Promise<string> {
const diff = sub(max, min)
if (lte(diff, '1')) {
return Promise.resolve(min)
}
const mid = add(min, div(diff, '2'))

// Try the average:
spendTargets[0].nativeAmount = mid

// Only provide wallet info if currency requires it:
const privateKeys = unsafeMakeSpend ? walletInfo.keys : undefined

return engine
.makeSpend(
{
...upgradedCurrency,
spendTargets,
networkFeeOption,
customNetworkFee
},
{ privateKeys }
)
.then(() => getMax(mid, max))
.catch(() => getMax(min, mid))
}

return await getMax('0', add(balance, '1'))
return await getMaxSpendableInner(
spendInfo,
plugin,
engine,
input.props.state.accounts[accountId].allTokens[pluginId],
walletInfo
)
},
async getPaymentProtocolInfo(
paymentProtocolUrl: string
Expand Down
Loading
Loading