@unilogin/sdk
Version:
SDK is a JS library, that communicates with relayer. SDK allows managing contract, by creating basic contract-calling messages.
272 lines (232 loc) • 10.3 kB
text/typescript
import {ensure, ApplicationWallet, walletFromBrain, Procedure, ExecutionOptions, ensureNotFalsy, findGasOption, FAST_GAS_MODE_INDEX, ETHER_NATIVE_TOKEN, waitUntil} from '@unilogin/commons';
import UniLoginSdk from '../../api/sdk';
import {FutureWallet} from '../../api/wallet/FutureWallet';
import {DeployingWallet} from '../../api/wallet/DeployingWallet';
import {InvalidWalletState, InvalidPassphrase, WalletOverridden, TransactionHashNotFound} from '../utils/errors';
import {utils, Wallet} from 'ethers';
import {DeployedWallet, WalletStorage} from '../..';
import {map, State, Property} from 'reactive-properties';
import {WalletState} from '../models/WalletService';
import {IStorageService} from '../models/IStorageService';
import {WalletSerializer} from './WalletSerializer';
import {ConnectingWallet} from '../../api/wallet/ConnectingWallet';
import {NoopStorageService} from './NoopStorageService';
import {WalletStorageService} from './WalletStorageService';
type WalletFromBackupCodes = (username: string, password: string) => Promise<Wallet>;
interface WalletStorageWithMigration extends WalletStorage {
migrate?: () => Promise<void>;
}
export class WalletService {
private readonly walletSerializer: WalletSerializer;
private readonly walletStorage: WalletStorageWithMigration;
private readonly _stateProperty = new State<WalletState>({kind: 'None'});
readonly stateProperty: Property<WalletState> = this._stateProperty;
walletDeployed = this.stateProperty.pipe(map((state) => state.kind === 'Deployed'));
isAuthorized = this.walletDeployed;
get state() {
return this.stateProperty.get();
}
private setState(state: WalletState) {
this.saveToStorage(state);
this._stateProperty.set(state);
}
constructor(
public readonly sdk: UniLoginSdk,
private readonly walletFromPassphrase: WalletFromBackupCodes = walletFromBrain,
storageService: IStorageService = new NoopStorageService(),
) {
this.walletStorage = new WalletStorageService(storageService, sdk.config.network);
this.walletSerializer = new WalletSerializer(sdk);
}
getDeployedWallet(): DeployedWallet {
ensure(this.state.kind === 'Deployed', InvalidWalletState, 'Deployed', this.state.kind);
return this.state.wallet;
}
getFutureWallet() {
ensure(this.state.kind === 'Future', InvalidWalletState, 'Future', this.state.kind);
return this.state.wallet;
}
private getDeployingWallet(): DeployingWallet {
ensure(this.state.kind === 'Deploying', InvalidWalletState, 'Deploying', this.state.kind);
return this.state.wallet;
}
getConnectingWallet(): ConnectingWallet {
ensure(this.state.kind === 'Connecting', Error, 'Invalid state: expected connecting wallet');
return this.state.wallet;
}
async createDeployingWallet(name: string): Promise<DeployingWallet> {
const futureWallet = await this.sdk.createFutureWallet(name, '0', ETHER_NATIVE_TOKEN.address);
const deployingWallet = await futureWallet.deploy();
this.setDeploying(deployingWallet);
return deployingWallet;
}
async createFutureWallet(name: string, gasToken = ETHER_NATIVE_TOKEN.address): Promise<FutureWallet> {
const gasModes = await this.sdk.getGasModes();
const gasOption = findGasOption(gasModes[FAST_GAS_MODE_INDEX].gasOptions, gasToken);
const futureWallet = await this.sdk.createFutureWallet(name, gasOption.gasPrice.toString(), gasToken);
this.setFutureWallet(futureWallet, name);
return futureWallet;
}
async createWallet(name: string, gasToken?: string): Promise<FutureWallet | DeployingWallet> {
if (this.sdk.isRefundPaid()) {
return this.createDeployingWallet(name);
}
return this.createFutureWallet(name, gasToken);
}
async initDeploy() {
ensure(this.state.kind === 'Future', InvalidWalletState, 'Future', this.state.kind);
const {wallet: {deploy}} = this.state;
const deployingWallet = await deploy();
this.setState({kind: 'Deploying', wallet: deployingWallet});
return this.getDeployingWallet();
}
async waitForTransactionHash() {
if (this.state.kind === 'Deployed') {
return this.state.wallet;
}
const deployingWallet = this.getDeployingWallet();
const {transactionHash} = await deployingWallet.waitForTransactionHash();
ensureNotFalsy(transactionHash, TransactionHashNotFound);
this.setState({kind: 'Deploying', wallet: deployingWallet, transactionHash});
return deployingWallet;
}
async waitToBeSuccess() {
if (this.state.kind === 'Deployed') {
return this.state.wallet;
}
const deployingWallet = this.getDeployingWallet();
const deployedWallet = await deployingWallet.waitToBeSuccess();
this.setState({kind: 'Deployed', wallet: deployedWallet});
return deployedWallet;
}
async deployFutureWallet() {
await this.initDeploy();
await this.waitForTransactionHash();
return this.waitToBeSuccess();
}
setFutureWallet(wallet: FutureWallet, name: string) {
ensure(this.state.kind === 'None', WalletOverridden);
this.setState({kind: 'Future', name, wallet});
}
setDeployed() {
ensure(this.state.kind === 'Future', InvalidWalletState, 'Future', this.state.kind);
const {name, wallet: {contractAddress, privateKey}} = this.state;
const wallet = new DeployedWallet(contractAddress, name, privateKey, this.sdk);
this.setState({kind: 'Deployed', wallet});
}
setDeploying(wallet: DeployingWallet) {
ensure(this.state.kind === 'None', WalletOverridden);
this.setState({kind: 'Deploying', wallet});
}
setConnecting(wallet: ConnectingWallet) {
ensure(this.state.kind === 'None', WalletOverridden);
this._stateProperty.set({kind: 'Connecting', wallet});
}
setWallet(wallet: ApplicationWallet) {
ensure(this.state.kind === 'None' || this.state.kind === 'Connecting', WalletOverridden);
this.setState({
kind: 'Deployed',
wallet: new DeployedWallet(wallet.contractAddress, wallet.name, wallet.privateKey, this.sdk),
});
}
async recover(name: string, passphrase: string) {
const contractAddress = await this.sdk.getWalletContractAddress(name);
const wallet = await this.walletFromPassphrase(name, passphrase);
const deployedWallet = new DeployedWallet(contractAddress, name, wallet.privateKey, this.sdk);
ensure(await deployedWallet.keyExist(wallet.address), InvalidPassphrase);
this.setWallet(deployedWallet.asApplicationWallet);
}
async initializeConnection(name: string): Promise<number[]> {
const contractAddress = await this.sdk.getWalletContractAddress(name);
const {privateKey, securityCode} = await this.sdk.connect(contractAddress);
const connectingWallet: ConnectingWallet = new ConnectingWallet(contractAddress, name, privateKey, this.sdk);
this.setConnecting(connectingWallet);
this.setState({kind: 'Connecting', wallet: connectingWallet});
return securityCode;
}
async waitForConnection() {
if (this.state.kind === 'Deployed') return;
ensure(this.state.kind === 'Connecting', InvalidWalletState, 'Connecting', this.state.kind);
const connectingWallet = this.getConnectingWallet();
const filter = {
contractAddress: connectingWallet.contractAddress,
key: connectingWallet.publicKey,
};
const addKeyEvent = await this.sdk.walletContractService.getEventNameFor(connectingWallet.contractAddress, 'KeyAdded');
return new Promise((resolve, reject) => {
const setWallet = this.setWallet.bind(this);
const unsubscribe = this.sdk.subscribe(addKeyEvent, filter, () => {
setWallet(connectingWallet);
unsubscribe();
resolve();
});
connectingWallet.unsubscribe = unsubscribe;
});
}
async cancelWaitForConnection(tick = 500, timeout = 1500) {
if (this.state.kind === 'Deployed') return;
await waitUntil(() => !!this.getConnectingWallet().unsubscribe, tick, timeout);
this.getConnectingWallet().unsubscribe!();
this.disconnect();
}
async connect(name: string, callback: Procedure) {
const contractAddress = await this.sdk.getWalletContractAddress(name);
const {privateKey, securityCode} = await this.sdk.connect(contractAddress);
const connectingWallet: ConnectingWallet = new ConnectingWallet(contractAddress, name, privateKey, this.sdk);
this.setConnecting(connectingWallet);
this.setState({kind: 'Connecting', wallet: connectingWallet});
const filter = {
contractAddress,
key: utils.computeAddress(privateKey),
};
const addKeyEvent = await this.sdk.walletContractService.getEventNameFor(connectingWallet.contractAddress, 'KeyAdded');
const unsubscribe = this.sdk.subscribe(addKeyEvent, filter, () => {
this.setWallet(connectingWallet);
unsubscribe;
callback();
});
return {unsubscribe, securityCode};
}
async removeWallet(executionOptions: ExecutionOptions) {
if (this.state.kind !== 'Deployed') {
this.disconnect();
return;
}
const existingKeysCount = (await this.state.wallet.getKeys()).length;
if (existingKeysCount > 1) {
const execution = await this.state.wallet.removeCurrentKey(executionOptions);
execution.waitToBeSuccess().then(() => this.disconnect());
return execution;
}
this.disconnect();
}
disconnect(): void {
this.setState({kind: 'None'});
}
saveToStorage(state: WalletState) {
const serialized = this.walletSerializer.serialize(state);
if (serialized !== undefined) {
this.walletStorage.save(serialized);
}
}
async loadFromStorage() {
ensure(this.state.kind === 'None', WalletOverridden);
await this.walletStorage.migrate?.();
const state = this.walletStorage.load();
this._stateProperty.set(this.walletSerializer.deserialize(state));
}
finalize() {
this._stateProperty.set({kind: 'None'});
}
getRequiredDeploymentBalance() {
ensure(this.state.kind === 'Future', InvalidWalletState, 'Future', this.state.kind);
return this.state.wallet.getMinimalAmount();
}
isKind(kind: string) {
return this.state.kind === kind;
}
getContractAddress() {
ensure(this.state.kind !== 'None', InvalidWalletState, 'not None', this.state.kind);
return this.state.wallet.contractAddress;
}
}