Files
bls-wallet/extension/source/cells/FormulaCell.ts
Andrew Morris 1303ffea9b Add network switching (#273)
* Update config system in prep for network switching

* Move builtinNetworks into config

* Move currencyConversionConfig into config

* Select network in ui

* mixtureHasChanged

* Fix issue where ethers Web3Provider assumes network doesn't change, handle addresses changing per network

* Implement per-network information for wallets

* lookupAddress -> pkHashToAddress

* Fix duplication of getting bls network config

* Restore preferred nonce sourcing

* Fix global access of blsNetworksConfig

* Fix global config access

* Fix commented hasChanged

* Fix build failures

* Fix linting issues

* Update extension/config.release.json

Co-authored-by: Jacob Caban-Tomski <jacque006@users.noreply.github.com>

* Update with PR feedback

Switch $preferences to non-$ name.
Add hidden field to networks to hide from end users.
Refactor wallet network data generation. Needs one more pass.

* PR fixes

Fix trailing comma in config json.
Properly inject env vars into config file.

* Move MultiNetowrkConfig to bls-wallet-clients

Add MultiNetworkConfig to clients. Deprecate NetworkConfig.
Update deps in clients.
Add chai-as-promised, ts-node to clients.
Remove need for transpiliation before client tests.
Finish getAndUpdateWalletsNetworkData changes in extension.

* Remove .only from client tests

* Use MultiNetworkConfig from clients lib.

* Fix file misspelling

* Update bls-wallet-clients experimental with main

* Remove empty local.json from CI build

* Update setup script with new extension config

Add troubleshooting section for Deno version

* Update extension & aggregator configs.

Update extension configs to hide all non-deployed networks.
Update aggregator local config to use pks 0 & 1 from main hardhat mnemonic.
Add dangerous command to print private keys from main hardhat mnemonic.

* Default extension network to arbitrum goerli

* Revert changes in aggregator local env

Co-authored-by: Jacob Caban-Tomski <jacque006@users.noreply.github.com>
2022-09-13 20:57:07 -06:00

298 lines
7.8 KiB
TypeScript

import { EventEmitter } from 'events';
import TypedEmitter from 'typed-emitter';
import AsyncReturnType from '../types/AsyncReturnType';
import ExplicitAny from '../types/ExplicitAny';
import CellIterator from './CellIterator';
import { IReadableCell, ChangeEvent } from './ICell';
import recordKeys from '../helpers/recordKeys';
import MemoryCell from './MemoryCell';
import AsyncIteratee from './AsyncIteratee';
import Stoppable from './Stoppable';
import nextEvent from './nextEvent';
import prefixKeys, { PrefixKeys } from './prefixKeys';
import assert from '../helpers/assert';
import delay from '../helpers/delay';
import mixtureHasChanged from './mixtureHasChanged';
import mixtureCopy from './mixtureCopy';
type InputValues<InputCells extends Record<string, IReadableCell<unknown>>> = {
[K in keyof InputCells]: AsyncReturnType<InputCells[K]['read']>;
};
export class FormulaCell<
InputCells extends Record<string, IReadableCell<unknown>>,
T,
> implements IReadableCell<Awaited<T>>
{
events = new EventEmitter() as TypedEmitter<{
change(changeEvent: ChangeEvent<T>): void;
end(): void;
'first-iterator'(): void;
'zero-iterators'(): void;
}>;
lastProvidedValue?: Awaited<T>;
iterationCell?: MemoryCell<'pending' | { value: Awaited<T> }>;
ended = false;
iteratorCount = 0;
constructor(
public inputCells: InputCells,
public formula: (
inputValues: PrefixKeys<'$', InputValues<InputCells>>,
) => T,
public hasChanged: (
previous: Awaited<T> | undefined,
latest: Awaited<T>,
) => boolean = mixtureHasChanged,
) {}
end() {
this.events.emit('end');
this.ended = true;
}
async read(): Promise<Awaited<T>> {
const iterationCell = this.iterateInputs();
try {
for await (const iterationState of iterationCell) {
if (iterationState === 'pending') {
continue;
}
return iterationState.value;
}
} finally {
this.decrementIterators();
}
assert(false, () => new Error('Inputs ended'));
}
[Symbol.asyncIterator](): AsyncIterator<Awaited<T>> {
this.iterateInputs();
const iterator = new CellIterator<Awaited<T>>(this);
iterator.events.on('finished', () => {
this.decrementIterators();
});
return iterator;
}
async decrementIterators() {
// Delay 500ms first. In some access patterns (long polling), a new iterator
// is coming immediately after iteration stops. This prevents us from
// stopping our tracking too eagerly, so that if iteration is immediately
// resumed, we don't need to recalculate unnecessarily.
await delay(500);
this.iteratorCount -= 1;
if (this.iteratorCount === 0) {
this.events.emit('zero-iterators');
}
}
iterateInputs() {
this.iteratorCount += 1;
if (this.iteratorCount === 1) {
this.events.emit('first-iterator');
}
if (this.iterationCell) {
return this.iterationCell;
}
this.iterationCell = new MemoryCell<'pending' | { value: Awaited<T> }>(
'pending',
(previous, latest) => {
if (previous === undefined) {
return true;
}
if (previous === 'pending' || latest === 'pending') {
return previous !== latest;
}
return this.hasChanged(previous.value, latest.value);
},
);
const { iterationCell } = this;
(async () => {
const stoppableSequence = new Stoppable(
toIterableOfRecords(this.inputCells),
);
{
const handler = () => {
stoppableSequence.stop();
this.events.off('zero-iterators', handler);
this.events.off('end', handler);
};
this.events.once('zero-iterators', handler);
this.events.once('end', handler);
}
for await (const inputValues of stoppableSequence) {
if (inputValues === 'stopped') {
break;
}
const latest = mixtureCopy(
await this.formula(
prefixKeys('$', inputValues.value as InputValues<InputCells>),
),
);
iterationCell.write({ value: latest });
if (this.hasChanged(this.lastProvidedValue, latest)) {
this.events.emit('change', {
previous: this.lastProvidedValue,
latest,
});
this.lastProvidedValue = latest;
}
}
iterationCell.end();
this.iterationCell = undefined;
if (this.iteratorCount > 0) {
this.end();
}
})();
return iterationCell;
}
/** Creates an ICell for the subscript of another ICell. */
static Sub<Input extends Record<string, unknown>, K extends keyof Input>(
input: IReadableCell<Input>,
key: K,
hasChanged: (
previous: Input[K] | undefined,
latest: Input[K],
) => boolean = mixtureHasChanged,
): IReadableCell<Input[K]> {
return new FormulaCell({ input }, ({ $input }) => $input[key], hasChanged);
}
/** Like Sub, but also maps undefined|null to defaultValue. */
static SubWithDefault<
Input extends Record<string, unknown>,
K extends keyof Input,
>(
input: IReadableCell<Input>,
key: K,
defaultValue: Exclude<Input[K], undefined | null>,
hasChanged: (
previous: Input[K] | undefined,
latest: Input[K],
) => boolean = mixtureHasChanged,
): IReadableCell<Exclude<Input[K], undefined | null>> {
return new FormulaCell(
{ input },
({ $input }) => $input[key] ?? defaultValue,
hasChanged,
);
}
}
function toIterableOfRecords<R extends Record<string, AsyncIterable<unknown>>>(
recordOfIterables: R,
): AsyncIterable<{ [K in keyof R]: AsyncIteratee<R[K]> }> {
return {
[Symbol.asyncIterator]() {
const latest = {} as { [K in keyof R]: AsyncIteratee<R[K]> };
let latestVersion = 0;
let providedVersion = 0;
let keysFilled = 0;
const keysNeeded = recordKeys(recordOfIterables).length;
let cleanup = () => {};
const events = new EventEmitter() as TypedEmitter<{
updated(): void;
end(): void;
}>;
const endedPromise = nextEvent(events, 'end');
function end() {
events.emit('end');
cleanup();
}
for (const key of recordKeys(recordOfIterables)) {
// eslint-disable-next-line no-loop-func
(async () => {
const stoppableSequence = new Stoppable(recordOfIterables[key]);
endedPromise.then(() => stoppableSequence.stop());
for await (const maybe of stoppableSequence) {
if (maybe === 'stopped') {
break;
}
if (!(key in latest)) {
keysFilled += 1;
}
latest[key] = maybe.value as ExplicitAny;
if (keysFilled === keysNeeded) {
latestVersion += 1;
events.emit('updated');
}
}
})();
}
return {
async next() {
if (latestVersion > providedVersion) {
providedVersion = latestVersion;
return { value: latest };
}
return new Promise<IteratorResult<typeof latest>>((resolve) => {
const updatedHandler = () => {
cleanup();
providedVersion = latestVersion;
resolve({ value: latest });
};
events.on('updated', updatedHandler);
const endHandler = () => {
cleanup();
resolve({ value: undefined, done: true });
};
events.on('end', endHandler);
cleanup = () => {
events.off('updated', updatedHandler);
events.off('end', endHandler);
};
});
},
async return(): Promise<IteratorResult<typeof latest>> {
end();
return { value: undefined, done: true };
},
};
},
};
}