zksync-sso
Version:
ZKsync Smart Sign On SDK
337 lines • 12.7 kB
JavaScript
import { createWalletClient, custom, http } from "viem";
import { createZksyncSessionClient } from "../client/index.js";
import { getTransactionWithPaymasterData } from "../paymaster/index.js";
import { StorageItem } from "../utils/storage.js";
import { parseSessionConfigJSON } from "./session/utils.js";
export class Signer {
constructor({ metadata, communicator, updateListener, session, chains, transports, paymasterHandler, onSessionStateChange, skipPreTransactionStateValidation, storage }) {
Object.defineProperty(this, "getMetadata", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "communicator", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "updateListener", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "chains", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "transports", {
enumerable: true,
configurable: true,
writable: true,
value: {}
});
Object.defineProperty(this, "sessionParameters", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "paymasterHandler", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "onSessionStateChange", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "skipPreTransactionStateValidation", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_account", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_chainsInfo", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "client", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "clearState", {
enumerable: true,
configurable: true,
writable: true,
value: () => {
this._account.remove();
this._chainsInfo.remove();
}
});
if (!chains.length)
throw new Error("At least one chain must be included in the config");
this.getMetadata = metadata;
this.communicator = communicator;
this.updateListener = updateListener;
this.sessionParameters = session;
this.chains = chains;
this.transports = transports || {};
this.paymasterHandler = paymasterHandler;
this.onSessionStateChange = onSessionStateChange;
this.skipPreTransactionStateValidation = skipPreTransactionStateValidation;
this._chainsInfo = new StorageItem(StorageItem.scopedStorageKey("chainsInfo"), [], { storage });
this._account = new StorageItem(StorageItem.scopedStorageKey("account"), null, {
onChange: (newValue) => {
if (newValue) {
this.updateListener.onAccountsUpdate([newValue.address]);
this.updateListener.onChainUpdate(newValue.activeChainId);
this.createWalletClient();
}
else {
this.updateListener.onAccountsUpdate([]);
}
},
storage,
});
try {
if (this.account)
this.createWalletClient();
}
catch (error) {
console.error("Failed to create wallet client", error);
console.error("Logging out to prevent crash loop");
this.clearState();
}
}
get walletClient() {
return this.client?.instance;
}
getClient(parameters) {
const chainId = parameters?.chainId || this.chain.id;
const chain = this.chains.find((e) => e.id === chainId);
if (!chain)
throw new Error(`Chain with id ${chainId} is not supported`);
if (!this.walletClient)
throw new Error("Wallet client is not created");
return this.walletClient;
}
get account() {
const account = this._account.get();
if (!account)
return null;
const chain = this.chains.find((e) => e.id === account.activeChainId);
return {
...account,
activeChainId: chain?.id || this.chains[0].id,
};
}
get session() { return this.account?.session; }
get chainsInfo() { return this._chainsInfo.get(); }
get accounts() { return this.account ? [this.account.address] : []; }
get chain() {
const chainId = this.account?.activeChainId || this.chains[0].id;
return this.chains.find((e) => e.id === chainId);
}
createWalletClient() {
const session = this.session;
const chain = this.chain;
const chainInfo = this.chainsInfo.find((e) => e.id === chain.id);
if (!this.account)
throw new Error("Account is not set");
if (!chainInfo)
throw new Error(`Chain info for ${chain} wasn't set during handshake`);
if (session) {
this.client = {
type: "session",
instance: createZksyncSessionClient({
address: this.account.address,
sessionKey: session.sessionKey,
sessionConfig: parseSessionConfigJSON(session.sessionConfig),
contracts: chainInfo.contracts,
chain,
transport: this.transports[chain.id] || http(),
paymasterHandler: this.paymasterHandler,
onSessionStateChange: (event) => {
if (!this.onSessionStateChange)
return;
this.onSessionStateChange({
state: event,
address: this.account.address,
chainId: chain.id,
});
},
skipPreTransactionStateValidation: this.skipPreTransactionStateValidation,
}),
};
}
else {
this.client = {
type: "auth-server",
instance: createWalletClient({
key: "zksync-sso-auth-server-wallet",
account: this.account.address,
chain,
transport: custom({
request: this.request.bind(this),
}),
}),
};
}
}
async handshake() {
let sessionPreferences;
let metadata = {
name: "Unknown DApp",
icon: null,
configData: {},
};
try {
metadata = this.getMetadata();
}
catch (error) {
console.error("Failed to get website metadata. Proceeding with default one.", error);
}
if (this.sessionParameters) {
try {
sessionPreferences = await this.sessionParameters();
}
catch (error) {
console.error("Failed to get session data. Proceeding connection with no session.", error);
}
}
const responseMessage = await this.sendRpcRequest({
method: "eth_requestAccounts",
params: {
metadata,
sessionPreferences,
},
});
const handshakeData = responseMessage.content.result;
this._chainsInfo.set(handshakeData.chainsInfo);
this._account.set({
address: handshakeData.account.address,
activeChainId: handshakeData.account.activeChainId || this.chain.id,
session: handshakeData.account.session,
});
return this.accounts;
}
switchChain(chainId) {
const chain = this.chains.find((chain) => chain.id === chainId);
const chainInfo = this.chainsInfo.find((e) => e.id === chainId);
if (!chainInfo) {
console.error(`Chain ${chainId} is not supported or chain info was not set during handshake`);
return false;
}
;
if (!chain) {
console.error(`Chain ${chainId} is missing in the configuration`);
return false;
}
;
if (chain.id === this.chain.id)
return true;
this._account.set({
...this.account,
activeChainId: chain.id,
});
return true;
}
async request(request) {
const localResult = await this.tryLocalHandling(request);
if (localResult !== undefined)
return localResult;
const response = await this.sendRpcRequest(request);
return response.content.result;
}
async disconnect() {
this.clearState();
}
async tryLocalHandling(request) {
const client = this.walletClient;
const originalClient = this.client;
switch (request.method) {
case "eth_estimateGas": {
if (!client)
return undefined;
const params = request.params;
const res = await client.request({ method: request.method, params: params });
return res;
}
case "eth_sendTransaction": {
if (originalClient?.type !== "session")
return undefined;
const params = request.params;
const transactionRequest = params[0];
const res = await originalClient.instance.sendTransaction(transactionRequest);
return res;
}
case "wallet_switchEthereumChain": {
throw new Error("Chain switching is not supported yet");
// const params = request.params as ExtractParams<"wallet_switchEthereumChain">;
// const chainId = params[0].chainId;
// const switched = this.switchChain(typeof chainId === "string" ? hexToNumber(chainId as Hash) : chainId);
// return switched ? (null as ExtractReturnType<TMethod>) : undefined;
}
case "wallet_getCapabilities": {
const chainInfo = this.chainsInfo.find((e) => e.id === this.chain.id);
if (!chainInfo)
throw new Error("Chain info is not set");
return { [this.chain.id]: chainInfo.capabilities };
}
case "eth_accounts": {
return this.accounts;
}
default:
return undefined;
}
}
async sendRpcRequest(request) {
// Open popup immediately to make sure popup won't be blocked by Safari
await this.communicator.ready();
if (request.method === "eth_sendTransaction") {
const params = request.params[0];
if (params) {
/* eslint-disable @typescript-eslint/no-unused-vars */
const { chainId: _, ...transaction } = await getTransactionWithPaymasterData(this.chain.id, params.from, params, this.paymasterHandler);
request = {
method: request.method,
params: [transaction],
};
}
}
const message = this.createRequestMessage({
action: request,
chainId: this.chain.id,
});
const response = await this.communicator.postRequestAndWaitForResponse(message);
const content = response.content;
if ("error" in content)
throw content.error;
return response;
}
createRequestMessage(content) {
return {
id: crypto.randomUUID(),
content,
};
}
}
//# sourceMappingURL=Signer.js.map