Skip to content

Commit

Permalink
Improve Restoration and Update process (#460)
Browse files Browse the repository at this point in the history
* add custom restoring loader in onboarding/end-of-flow page

* add spinner in onboarding/end-of-flow

* assetRegistry port

* fix onboarding mnemonic input trim issue

* faster StorageContext cache

* add Presenter

* present update

* let marina-logo click triggers a store update

* Presenter fix: do not update utxos set onNewTransaction event

* sortAssets in Presenter

* fix tx flow computing & uncomfirmed event

* always add unblinded input

* use prevoutInputIndex to fetch blinding data while computing txflow

* add "WalletAssets" member in PresentationCache
  • Loading branch information
louisinger authored Apr 7, 2023
1 parent f0aa852 commit bed2c19
Show file tree
Hide file tree
Showing 36 changed files with 1,304 additions and 619 deletions.
7 changes: 4 additions & 3 deletions src/application/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,11 @@ export class AccountFactory {

// Account is a readonly way to interact with the account data (transactions, utxos, scripts, etc.)
export class Account {
private network: networks.Network;
private node: BIP32Interface;
private blindingKeyNode: Slip77Interface;
private walletRepository: WalletRepository;
private _cacheAccountType: AccountType | undefined;
readonly network: networks.Network;
readonly name: string;

static BASE_DERIVATION_PATH = "m/84'/1776'/0'";
Expand Down Expand Up @@ -241,6 +241,7 @@ export class Account {
gapLimit = GAP_LIMIT,
start?: { internal: number; external: number }
): Promise<{
txIDsFromChain: string[];
next: { internal: number; external: number };
}> {
const type = await this.getAccountType();
Expand Down Expand Up @@ -272,7 +273,6 @@ export class Account {

const scripts = scriptsWithDetails.map(([script]) => h2b(script));
const histories = await chainSource.fetchHistories(scripts);

for (const [index, history] of histories.entries()) {
tempRestoredScripts[scriptsWithDetails[index][0]] = scriptsWithDetails[index][1];
if (history.length > 0) {
Expand Down Expand Up @@ -313,6 +313,7 @@ export class Account {
]);

return {
txIDsFromChain: Array.from(historyTxsId),
next: {
internal: indexes.internal,
external: indexes.external,
Expand Down Expand Up @@ -428,7 +429,7 @@ export class Account {
return results;
}

private async getNextIndexes(): Promise<{ internal: number; external: number }> {
async getNextIndexes(): Promise<{ internal: number; external: number }> {
if (!this.walletRepository || !this.name) return { internal: 0, external: 0 };
const { [this.name]: accountDetails } = await this.walletRepository.getAccountDetails(
this.name
Expand Down
14 changes: 8 additions & 6 deletions src/application/blinder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ export class BlinderService {

async blindPset(pset: Pset): Promise<Pset> {
const ownedInputs: OwnedInput[] = [];
for (const [inputIndex, input] of pset.inputs.entries()) {
const unblindOutput = await this.walletRepository.getOutputBlindingData(
Buffer.from(input.previousTxid).reverse().toString('hex'),
input.previousTxIndex
);

const inputsBlindingData = await this.walletRepository.getOutputBlindingData(
...pset.inputs.map(({ previousTxIndex, previousTxid }) => ({
txID: Buffer.from(previousTxid).reverse().toString('hex'),
vout: previousTxIndex,
}))
);
for (const inputIndex of pset.inputs.keys()) {
const unblindOutput = inputsBlindingData.at(inputIndex);
if (!unblindOutput || !unblindOutput.blindingData) continue;
ownedInputs.push({
asset: AssetHash.fromHex(unblindOutput.blindingData.asset).bytesWithoutPrefix,
Expand Down
313 changes: 313 additions & 0 deletions src/application/presenter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
import type { Asset, NetworkString } from 'marina-provider';
import type { LoadingValue, PresentationCache, Presenter } from '../domain/presenter';
import type {
AppRepository,
AssetRepository,
BlockheadersRepository,
WalletRepository,
} from '../domain/repository';
import type { TxDetails } from '../domain/transaction';
import { computeBalances, computeTxDetailsExtended } from '../domain/transaction';
import type { BlockHeader } from '../domain/chainsource';
import { MainAccount, MainAccountLegacy, MainAccountTest } from './account';

function createLoadingValue<T>(value: T): LoadingValue<T> {
return {
value,
loading: false,
};
}

const setValue = <T>(value: T): LoadingValue<T> => ({
loading: false,
value,
});

const setLoading = <T>(loadingValue: LoadingValue<T>): LoadingValue<T> => ({
...loadingValue,
loading: true,
});

export class PresenterImpl implements Presenter {
private state: PresentationCache = {
network: 'liquid',
authenticated: createLoadingValue(false),
balances: createLoadingValue({}),
utxos: createLoadingValue([]),
assetsDetails: createLoadingValue<Record<string, Asset>>({}),
transactions: createLoadingValue([]),
blockHeaders: createLoadingValue<Record<number, BlockHeader>>({}),
walletAssets: createLoadingValue(new Set()),
};
private closeFunction: (() => void) | null = null;

constructor(
private appRepository: AppRepository,
private walletRepository: WalletRepository,
private assetsRepository: AssetRepository,
private blockHeadersRepository: BlockheadersRepository
) {}

stop() {
if (this.closeFunction) {
this.closeFunction();
this.closeFunction = null;
}
}

async present(emits: (cache: PresentationCache) => void) {
this.state = {
...this.state,
authenticated: setLoading(this.state.authenticated),
balances: setLoading(this.state.balances),
utxos: setLoading(this.state.utxos),
assetsDetails: setLoading(this.state.assetsDetails),
transactions: setLoading(this.state.transactions),
walletAssets: setLoading(this.state.walletAssets),
blockHeaders: setLoading(this.state.blockHeaders),
};
emits(this.state);

this.state = await this.updateNetwork();
this.state = await this.updateAuthenticated();
emits(this.state);
this.state = await this.updateUtxos();
this.state = await this.updateBalances();
emits(this.state);
this.state = await this.updateAssets();
emits(this.state);
this.state = await this.updateTransactions();
emits(this.state);
this.state = await this.updateBlockHeaders();
emits(this.state);

const closeFns: (() => void)[] = [];

closeFns.push(
this.blockHeadersRepository.onNewBlockHeader((network, blockHeader) => {
if (network !== this.state.network) return Promise.resolve();
this.state = {
...this.state,
blockHeaders: setValue({
...this.state.blockHeaders.value,
[blockHeader.height]: blockHeader,
}),
};
emits(this.state);
return Promise.resolve();
})
);

closeFns.push(
this.appRepository.onNetworkChanged(async () => {
this.state = await this.updateNetwork();
this.state = {
...this.state,
balances: setLoading(this.state.balances),
utxos: setLoading(this.state.utxos),
assetsDetails: setLoading(this.state.assetsDetails),
blockHeaders: setLoading(this.state.blockHeaders),
transactions: setLoading(this.state.transactions),
walletAssets: setLoading(this.state.walletAssets),
};
emits(this.state);

this.state = await this.updateUtxos();
this.state = await this.updateBalances();
emits(this.state);
this.state = await this.updateAssets();
emits(this.state);
this.state = await this.updateTransactions();
emits(this.state);
this.state = await this.updateBlockHeaders();
emits(this.state);
})
);

closeFns.push(
this.appRepository.onIsAuthenticatedChanged(async (authenticated) => {
this.state = {
...this.state,
authenticated: setValue(authenticated),
};
emits(this.state);
return Promise.resolve();
})
);

closeFns.push(
this.walletRepository.onNewTransaction(async (_, details: TxDetails) => {
if (!this.state.authenticated.value) return;
const scripts = await this.walletRepository.getAccountScripts(
this.state.network,
MainAccountLegacy,
this.state.network === 'liquid' ? MainAccount : MainAccountTest
);
const extendedTxDetails = await computeTxDetailsExtended(
this.appRepository,
this.walletRepository,
scripts
)(details);
this.state = {
...this.state,
transactions: setValue(
[extendedTxDetails, ...this.state.transactions.value].sort(sortTxDetails)
),
walletAssets: setValue(
new Set([...this.state.walletAssets.value, ...Object.keys(extendedTxDetails.txFlow)])
),
};
})
);

closeFns.push(
this.assetsRepository.onNewAsset((asset) => {
if (!this.state.authenticated.value) return Promise.resolve();
this.state = {
...this.state,
assetsDetails: setValue({ ...this.state.assetsDetails.value, [asset.assetHash]: asset }),
};
emits(this.state);
return Promise.resolve();
})
);

closeFns.push(
...['liquid', 'testnet', 'regtest']
.map((network) => [
this.walletRepository.onNewUtxo(network as NetworkString)(
async ({ txID, vout, blindingData }) => {
if (!this.state.authenticated.value) return;
if (network !== this.state.network) return;
this.state = {
...this.state,
utxos: setValue([...this.state.utxos.value, { txID, vout, blindingData }]),
};
this.state = await this.updateBalances();
emits(this.state);
}
),
this.walletRepository.onDeleteUtxo(network as NetworkString)(async ({ txID, vout }) => {
if (!this.state.authenticated.value) return;
if (network !== this.state.network) return;
this.state = {
...this.state,
utxos: setValue(
this.state.utxos.value.filter((utxo) => utxo.txID !== txID || utxo.vout !== vout)
),
};
this.state = await this.updateBalances();
emits(this.state);
}),
])
.flat()
);

closeFns.push(
this.walletRepository.onUnblindingEvent(async ({ txID, vout, blindingData }) => {
if (!this.state.authenticated.value) return;
if (this.state.utxos.value.find((utxo) => utxo.txID === txID && utxo.vout === vout)) {
this.state = {
...this.state,
utxos: setValue(
this.state.utxos.value.map((utxo) =>
utxo.txID === txID && utxo.vout === vout ? { txID, vout, blindingData } : utxo
)
),
};
emits(this.state);
this.state = await this.updateBalances();
emits(this.state);
}
this.state = await this.updateTransactions();
emits(this.state);
})
);

this.closeFunction = () => {
closeFns.forEach((fn) => fn());
};
}

private async updateNetwork(): Promise<PresentationCache> {
const network = await this.appRepository.getNetwork();
if (!network) return this.state;
return {
...this.state,
network,
};
}

private async updateAuthenticated(): Promise<PresentationCache> {
const { isAuthenticated } = await this.appRepository.getStatus();
return {
...this.state,
authenticated: setValue(isAuthenticated),
};
}

private async updateUtxos(): Promise<PresentationCache> {
const utxos = await this.walletRepository.getUtxos(this.state.network);
return {
...this.state,
utxos: setValue(utxos),
};
}

private async updateBalances(): Promise<PresentationCache> {
return Promise.resolve({
...this.state,
balances: setValue(computeBalances(this.state.utxos.value)),
});
}

private async updateAssets(): Promise<PresentationCache> {
const assets = await this.assetsRepository.getAllAssets(this.state.network);
return {
...this.state,
assetsDetails: setValue(Object.fromEntries(assets.map((asset) => [asset.assetHash, asset]))),
};
}

private async updateTransactions(): Promise<PresentationCache> {
const transactions = await this.walletRepository.getTransactions(this.state.network);
const details = await this.walletRepository.getTxDetails(...transactions);
const scripts = await this.walletRepository.getAccountScripts(
this.state.network,
MainAccountLegacy,
this.state.network === 'liquid' ? MainAccount : MainAccountTest
);
const extendedTxDetails = await Promise.all(
Object.values(details).map(
computeTxDetailsExtended(this.appRepository, this.walletRepository, scripts)
)
);
const assetsInTransactions = extendedTxDetails.reduce(
(acc, tx) => [...acc, ...Object.keys(tx.txFlow)],
[] as string[]
);
return {
...this.state,
transactions: setValue(extendedTxDetails.sort(sortTxDetails)),
walletAssets: setValue(new Set(assetsInTransactions)),
};
}

private async updateBlockHeaders(): Promise<PresentationCache> {
const blockHeaders = await this.blockHeadersRepository.getAllBlockHeaders(this.state.network);

return {
...this.state,
blockHeaders: setValue(blockHeaders),
};
}
}

// sort function for txDetails, use the height member to sort
// put unconfirmed txs first and then sort by height (desc)
function sortTxDetails(a: TxDetails, b: TxDetails): number {
if (a.height === b.height) return 0;
if (!a.height || a.height === -1) return -1;
if (!b.height || b.height === -1) return 1;
return b.height - a.height;
}
Loading

0 comments on commit bed2c19

Please sign in to comment.