zkverifyjs
Version:
Submit proofs to zkVerify and query proof state with ease using our npm package.
209 lines • 8.89 kB
JavaScript
import { startSession, startWalletSession } from "../../../api/start/index.js";
import { closeSession } from "../../../api/close/index.js";
import { accountInfo } from "../../../api/accountInfo/index.js";
import { canonicalAddress, deriveChildAt, setupAccount } from "../../../api/account/index.js";
import { checkReadOnly } from "../../../utils/helpers/index.js";
import { Mutex } from 'async-mutex';
import { SupportedNetwork } from "../../../config/index.js";
export class ConnectionManager {
constructor(connection, config) {
this.accountMutex = new Mutex();
this.connection = connection;
this.customNetwork = config.host === SupportedNetwork.Custom;
this.readOnly = !('accounts' in connection) && !('injector' in connection) && !('signer' in connection);
}
/**
* Creates a new session with a connection to the specified network.
* Supports multiple accounts for startSession if provided in options.
* @param {zkVerifySessionOptions} options - The session configuration options.
* @returns {Promise<ConnectionManager>} A promise resolving to a ConnectionManager instance.
*/
static async createSession(options) {
const connection = options.wallet ? await startWalletSession(options) : await startSession(options);
return new ConnectionManager(connection, options.networkConfig);
}
/**
* Closes the current session, disconnecting from the provider and cleaning up resources.
* @returns {Promise<void>} A promise that resolves when the session is closed.
*/
async close() {
return closeSession(this.connection.provider);
}
/**
* Retrieves account information for a specified address or all accounts.
* If no address is provided, returns an array of all account info objects.
*
* @param {string} [accountAddress] - The address of the account to fetch info for. If undefined, returns all accounts.
* @returns {Promise<AccountInfo[]>} A promise resolving to an array of account info objects.
* @throws Will throw an error if the account is not found.
*/
async getAccountInfo(accountAddress) {
checkReadOnly(this.connection);
const accountConnection = this.connection;
const accountList = Array.from(accountConnection.accounts.values());
if (accountAddress === undefined) {
return Promise.all(accountList.map(account => accountInfo(this.api, account)));
}
if (accountConnection.accounts.has(accountAddress)) {
return [await accountInfo(this.api, accountConnection.accounts.get(accountAddress))];
}
throw new Error(`Account ${accountAddress} not found in this session.`);
}
/**
* Adds a single account to the session in a thread-safe manner.
*
* @param {string} seedPhrase - The seed phrase used to generate the account.
* @returns {Promise<string>} A promise resolving to the account address.
* @throws {Error} If the account is already added.
*/
async addAccount(seedPhrase) {
return this.accountMutex.runExclusive(async () => {
const account = setupAccount(seedPhrase);
if (!('accounts' in this.connection)) {
this.connection = {
api: this.api,
provider: this.provider,
accounts: new Map()
};
}
const accountConnection = this.connection;
if (accountConnection.accounts.has(account.address)) {
throw new Error(`Account ${account.address} is already active.`);
}
accountConnection.accounts.set(account.address, account);
this.readOnly = false;
return account.address;
});
}
/**
* Adds multiple accounts to the session in a thread-safe manner.
*
* @param {string[]} seedPhrases - An array of seed phrases to generate accounts.
* @returns {Promise<string[]>} A promise resolving to an array of added account addresses.
* @throws {Error} If any of the accounts are already active.
*/
async addAccounts(seedPhrases) {
return Promise.all(seedPhrases.map(seedPhrase => this.addAccount(seedPhrase)));
}
/**
* Removes an account from the session in a thread-safe manner.
*
* @param {string} [address] - (Optional) The account address to remove.
* If omitted and only one account exists, that account will be removed.
* If omitted and no accounts exist, the method does nothing.
* @throws {Error} If a specified account is not found or the connection type does not support accounts.
*/
async removeAccount(address) {
await this.accountMutex.runExclusive(async () => {
if (!('accounts' in this.connection)) {
throw new Error('This connection type does not support accounts.');
}
const accountConnection = this.connection;
if (!address && accountConnection.accounts.size === 1) {
const firstAccount = [...accountConnection.accounts.keys()][0] ?? undefined;
if (firstAccount) {
address = firstAccount;
}
}
if (!address) {
return;
}
if (!accountConnection.accounts.has(address)) {
throw new Error(`Account ${address} not found.`);
}
accountConnection.accounts.delete(address);
if (accountConnection.accounts.size === 0) {
this.connection = {
api: this.api,
provider: this.provider
};
this.readOnly = true;
}
});
}
/**
* Getter for the API instance.
* @returns {ApiPromise} The Polkadot.js API instance.
*/
get api() {
return this.connection.api;
}
/**
* Getter for the provider.
* @returns {WsProvider} The WebSocket provider.
*/
get provider() {
return this.connection.provider;
}
/**
* Retrieves the account associated with the given address.
* If no address is provided, it returns the first available account.
*
* @param {string} [accountAddress] - The address of the account to retrieve. If not provided, the first account is returned.
* @returns {KeyringPair} The associated KeyringPair.
* @throws {Error} If no accounts exist in the session or the specified account is not found.
*/
getAccount(accountAddress) {
if (!('accounts' in this.connection)) {
throw new Error('This connection type does not support accounts.');
}
const accountConnection = this.connection;
if (accountConnection.accounts.size === 0) {
throw new Error('No accounts have been added to this session.');
}
if (!accountAddress) {
return Array.from(accountConnection.accounts.values())[0];
}
const account = accountConnection.accounts.get(accountAddress);
if (!account) {
throw new Error(`Account with address '${accountAddress}' not found in the session.`);
}
return account;
}
/**
* Retrieves the current connection details.
*
* @returns {AccountConnection | WalletConnection | EstablishedConnection} The current connection instance.
*/
get connectionDetails() {
return this.connection;
}
/**
* Adds `count` derived accounts to the session from an existing base account.
* Uses hard derivation paths (`//0`, `//1`, …) and skips any that already exist.
*
* @param {string} baseAddress - The address of an account already in the session to derive from.
* @param {number} count - The number of derived accounts to add (must be a positive integer).
* @returns {Promise<string[]>} A promise resolving to the list of SS58-encoded addresses that were added.
* @throws Will throw an error if the connection does not support accounts, the base account is not found, or `count` is invalid.
*/
async addDerivedAccounts(baseAddress, count) {
if (!Number.isInteger(count) || count <= 0) {
throw new Error('count must be a positive integer.');
}
return this.accountMutex.runExclusive(async () => {
if (!('accounts' in this.connection)) {
throw new Error('This connection type does not support accounts.');
}
const sessionAccountsMap = this.connection.accounts;
const baseAccountPair = sessionAccountsMap.get(baseAddress) ?? Array.from(sessionAccountsMap.values()).find(pair => pair.address === baseAddress || canonicalAddress(pair) === baseAddress);
if (!baseAccountPair) {
throw new Error(`Base account ${baseAddress} not found in this session.`);
}
const newlyAddedAddresses = [];
for (let childIndex = 0; newlyAddedAddresses.length < count; childIndex++) {
const {
pair: derivedChildPair,
address: derivedChildAddress
} = deriveChildAt(baseAccountPair, childIndex);
if (sessionAccountsMap.has(derivedChildAddress)) {
continue;
}
sessionAccountsMap.set(derivedChildAddress, derivedChildPair);
newlyAddedAddresses.push(derivedChildAddress);
}
this.readOnly = false;
return newlyAddedAddresses;
});
}
}