@0xsequence/connect
Version:
Connect package for Sequence Web SDK
407 lines âĒ 20.6 kB
JavaScript
import { allNetworks } from '@0xsequence/network';
import { SequenceWaaS, WebrpcEndpointError } from '@0xsequence/waas';
import { ethers } from 'ethers';
import { v4 as uuidv4 } from 'uuid';
import { getAddress, InternalRpcError, ProviderDisconnectedError, toHex, TransactionRejectedRpcError, UserRejectedRequestError, zeroAddress } from 'viem';
import { createConnector } from 'wagmi';
import { LocalStorageKey } from '../../constants/localStorage.js';
import { normalizeChainId } from '../../utils/helpers.js';
import { getPkcePair, getXOauthUrl } from '../X/XAuth.js';
sequenceWaasWallet.type = 'sequence-waas';
export function sequenceWaasWallet(params) {
const nodesUrl = params.nodesUrl ?? 'https://nodes.sequence.app';
const showConfirmationModal = params.enableConfirmationModal ?? false;
const sequenceWaas = new SequenceWaaS({
waasConfigKey: params.waasConfigKey,
projectAccessKey: params.projectAccessKey,
network: params.network ?? 137
});
const sequenceWaasProvider = new SequenceWaasProvider(sequenceWaas, showConfirmationModal, nodesUrl);
return createConnector(config => ({
id: `sequence-waas`,
name: 'Sequence WaaS',
type: sequenceWaasWallet.type,
sequenceWaas,
sequenceWaasProvider,
params,
async setup() {
if (typeof window !== 'object') {
// (for SSR) only run in browser client
return;
}
if (params.googleClientId) {
await config.storage?.setItem(LocalStorageKey.WaasGoogleClientID, params.googleClientId);
}
if (params.appleClientId) {
await config.storage?.setItem(LocalStorageKey.WaasAppleClientID, params.appleClientId);
}
if (params.appleRedirectURI) {
await config.storage?.setItem(LocalStorageKey.WaasAppleRedirectURI, params.appleRedirectURI);
}
if (params.epicAuthUrl) {
await config.storage?.setItem(LocalStorageKey.WaasEpicAuthUrl, params.epicAuthUrl);
}
if (params.XClientId && params.XRedirectURI) {
const { code_challenge, code_verifier } = await getPkcePair();
const authUrl = await getXOauthUrl(params.XClientId, params.XRedirectURI, code_challenge);
await config.storage?.setItem(LocalStorageKey.WaasXAuthUrl, authUrl);
await config.storage?.setItem(LocalStorageKey.WaasXClientID, params.XClientId);
await config.storage?.setItem(LocalStorageKey.WaasXRedirectURI, params.XRedirectURI);
await config.storage?.setItem(LocalStorageKey.WaasXCodeVerifier, code_verifier);
}
sequenceWaasProvider.on('error', error => {
if (isSessionInvalidOrNotFoundError(error)) {
this.disconnect();
}
});
},
async connect(_connectInfo) {
const provider = await this.getProvider();
const isSignedIn = await provider.sequenceWaas.isSignedIn();
if (!isSignedIn) {
const googleIdToken = await config.storage?.getItem(LocalStorageKey.WaasGoogleIdToken);
const emailIdToken = await config.storage?.getItem(LocalStorageKey.WaasEmailIdToken);
const appleIdToken = await config.storage?.getItem(LocalStorageKey.WaasAppleIdToken);
const epicIdToken = await config.storage?.getItem(LocalStorageKey.WaasEpicIdToken);
const xIdToken = await config.storage?.getItem(LocalStorageKey.WaasXIdToken);
let idToken;
if (params.loginType === 'google' && googleIdToken) {
idToken = googleIdToken;
}
else if (params.loginType === 'email' && emailIdToken) {
idToken = emailIdToken;
}
else if (params.loginType === 'apple' && appleIdToken) {
idToken = appleIdToken;
}
else if (params.loginType === 'epic' && epicIdToken) {
idToken = epicIdToken;
}
else if (params.loginType === 'X' && xIdToken) {
idToken = xIdToken;
}
await config.storage?.removeItem(LocalStorageKey.WaasGoogleIdToken);
await config.storage?.removeItem(LocalStorageKey.WaasEmailIdToken);
await config.storage?.removeItem(LocalStorageKey.WaasAppleIdToken);
await config.storage?.removeItem(LocalStorageKey.WaasEpicIdToken);
await config.storage?.removeItem(LocalStorageKey.WaasXIdToken);
if (idToken) {
try {
let signInResponse;
if (params.loginType === 'X') {
signInResponse = await provider.sequenceWaas.signIn({ xAccessToken: idToken }, randomName());
}
else {
signInResponse = await provider.sequenceWaas.signIn({ idToken }, randomName());
}
if (signInResponse?.email) {
await config.storage?.setItem(LocalStorageKey.WaasSignInEmail, signInResponse.email);
}
}
catch (e) {
console.log(e);
await this.disconnect();
throw e;
}
}
}
const accounts = await this.getAccounts();
if (accounts.length) {
await config.storage?.setItem(LocalStorageKey.WaasActiveLoginType, params.loginType);
}
else {
throw new Error('No accounts found');
}
return {
accounts,
chainId: await this.getChainId()
};
},
async disconnect() {
const provider = await this.getProvider();
try {
await provider.sequenceWaas.dropSession({ sessionId: await provider.sequenceWaas.getSessionId(), strict: false });
}
catch (e) {
console.log(e);
}
await config.storage?.removeItem(LocalStorageKey.WaasActiveLoginType);
await config.storage?.removeItem(LocalStorageKey.WaasSignInEmail);
config.emitter.emit('disconnect');
},
async getAccounts() {
const provider = await this.getProvider();
try {
const isSignedIn = await provider.sequenceWaas.isSignedIn();
if (isSignedIn) {
const address = await provider.sequenceWaas.getAddress();
return [getAddress(address)];
}
}
catch (err) {
return [];
}
return [];
},
async getProvider() {
return sequenceWaasProvider;
},
async isAuthorized() {
const provider = await this.getProvider();
const activeWaasOption = await config.storage?.getItem(LocalStorageKey.WaasActiveLoginType);
if (params.loginType !== activeWaasOption) {
return false;
}
try {
return await provider.sequenceWaas.isSignedIn();
}
catch (e) {
return false;
}
},
async switchChain({ chainId }) {
const provider = await this.getProvider();
const chain = config.chains.find(c => c.id === chainId) || config.chains[0];
await provider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: toHex(chainId) }]
});
config.emitter.emit('change', { chainId });
return chain;
},
async getChainId() {
const provider = await this.getProvider();
return Number(provider.getChainId());
},
async onAccountsChanged(accounts) {
return { account: accounts[0] };
},
async onChainChanged(chain) {
config.emitter.emit('change', { chainId: normalizeChainId(chain) });
},
async onConnect(_connectInfo) { },
async onDisconnect() {
await this.disconnect();
}
}));
}
export class SequenceWaasProvider extends ethers.AbstractProvider {
sequenceWaas;
showConfirmation;
nodesUrl;
jsonRpcProvider;
requestConfirmationHandler;
feeConfirmationHandler;
currentNetwork;
constructor(sequenceWaas, showConfirmation, nodesUrl) {
super(sequenceWaas.config.network);
this.sequenceWaas = sequenceWaas;
this.showConfirmation = showConfirmation;
this.nodesUrl = nodesUrl;
const initialChain = sequenceWaas.config.network;
const initialChainName = allNetworks.find(n => n.chainId === initialChain || n.name === initialChain)?.name;
const initialJsonRpcProvider = new ethers.JsonRpcProvider(`${nodesUrl}/${initialChainName}/${sequenceWaas.config.projectAccessKey}`);
this.jsonRpcProvider = initialJsonRpcProvider;
this.currentNetwork = ethers.Network.from(sequenceWaas.config.network);
}
async request({ method, params }) {
if (method === 'wallet_switchEthereumChain') {
const chainId = normalizeChainId(params?.[0].chainId);
const networkName = allNetworks.find(n => n.chainId === chainId)?.name;
const jsonRpcProvider = new ethers.JsonRpcProvider(`${this.nodesUrl}/${networkName}/${this.sequenceWaas.config.projectAccessKey}`);
this.jsonRpcProvider = jsonRpcProvider;
this.currentNetwork = ethers.Network.from(chainId);
return null;
}
if (method === 'eth_chainId') {
return toHex(this.currentNetwork.chainId);
}
if (method === 'eth_accounts') {
const address = await this.sequenceWaas.getAddress();
const account = getAddress(address);
return [account];
}
if (method === 'eth_sendTransaction') {
const txns = await ethers.resolveProperties(params?.[0]);
const chainId = this.getChainId();
let feeOptionsResponse;
try {
feeOptionsResponse = await this.checkTransactionFeeOptions({ transactions: [txns], chainId });
}
catch (error) {
if (isSessionInvalidOrNotFoundError(error)) {
await this.emit('error', error);
throw new ProviderDisconnectedError(new Error('Provider is not connected'));
}
else {
const message = typeof error === 'object' && error !== null && 'cause' in error
? String(error.cause) || 'Failed to check transaction fee options'
: 'Failed to check transaction fee options';
throw new InternalRpcError(new Error(message));
}
}
const feeOptions = feeOptionsResponse?.feeOptions;
let selectedFeeOption;
if (!feeOptionsResponse?.isSponsored && feeOptions && feeOptions.length > 0) {
if (!this.feeConfirmationHandler) {
throw new TransactionRejectedRpcError(new Error('Unable to send transaction: please use useWaasFeeOptions hook and pick a fee option'));
}
const id = uuidv4();
const confirmation = await this.feeConfirmationHandler.confirmFeeOption(id, feeOptions, txns, chainId);
if (!confirmation.confirmed) {
throw new UserRejectedRequestError(new Error('User rejected send transaction request'));
}
if (id !== confirmation.id) {
throw new UserRejectedRequestError(new Error('User confirmation ids do not match'));
}
selectedFeeOption = feeOptions.find(feeOption => {
// Handle the case where feeTokenAddress is ZeroAddress and contractAddress is null
if (confirmation.feeTokenAddress === zeroAddress && feeOption.token.contractAddress === null) {
return true;
}
return feeOption.token.contractAddress === confirmation.feeTokenAddress;
});
}
if (this.requestConfirmationHandler && this.showConfirmation) {
const id = uuidv4();
const confirmation = await this.requestConfirmationHandler.confirmSignTransactionRequest(id, txns, chainId);
if (!confirmation.confirmed) {
throw new UserRejectedRequestError(new Error('User rejected send transaction request'));
}
if (id !== confirmation.id) {
throw new UserRejectedRequestError(new Error('User confirmation ids do not match'));
}
}
let response;
try {
response = await this.sequenceWaas.sendTransaction({
transactions: [await ethers.resolveProperties(params?.[0])],
network: chainId,
transactionsFeeOption: selectedFeeOption,
transactionsFeeQuote: feeOptionsResponse?.feeQuote
});
}
catch (error) {
if (isSessionInvalidOrNotFoundError(error)) {
await this.emit('error', error);
throw new ProviderDisconnectedError(new Error('Provider is not connected'));
}
else {
const message = typeof error === 'object' && error !== null && 'cause' in error
? String(error.cause) || 'Failed to send transaction'
: 'Failed to send transaction';
throw new InternalRpcError(new Error(message));
}
}
if (response.code === 'transactionFailed') {
// Failed
throw new TransactionRejectedRpcError(new Error(`Unable to send transaction: ${response.data.error}`));
}
if (response.code === 'transactionReceipt') {
// Success
const { txHash } = response.data;
return txHash;
}
}
if (method === 'eth_sign' || method === 'personal_sign') {
if (this.requestConfirmationHandler && this.showConfirmation) {
const id = uuidv4();
const confirmation = await this.requestConfirmationHandler.confirmSignMessageRequest(id, params?.[0], Number(this.currentNetwork.chainId));
if (!confirmation.confirmed) {
throw new UserRejectedRequestError(new Error('User rejected sign message request'));
}
if (id !== confirmation.id) {
throw new UserRejectedRequestError(new Error('User confirmation ids do not match'));
}
}
let sig;
try {
sig = await this.sequenceWaas.signMessage({ message: params?.[0], network: Number(this.currentNetwork.chainId) });
}
catch (error) {
if (isSessionInvalidOrNotFoundError(error)) {
await this.emit('error', error);
throw new ProviderDisconnectedError(new Error('Provider is not connected'));
}
else {
const message = typeof error === 'object' && error !== null && 'cause' in error
? String(error.cause) || 'Failed to sign message'
: 'Failed to sign message';
throw new InternalRpcError(new Error(message));
}
}
return sig.data.signature;
}
if (method === 'eth_signTypedData' || method === 'eth_signTypedData_v4') {
if (this.requestConfirmationHandler && this.showConfirmation) {
const id = uuidv4();
const confirmation = await this.requestConfirmationHandler.confirmSignMessageRequest(id, JSON.stringify(JSON.parse(params?.[1]), null, 2), // Pretty print the typed data for confirmation
Number(this.currentNetwork.chainId));
if (!confirmation.confirmed) {
throw new UserRejectedRequestError(new Error('User rejected sign typed data request'));
}
if (id !== confirmation.id) {
throw new UserRejectedRequestError(new Error('User confirmation ids do not match'));
}
}
let sig;
try {
sig = await this.sequenceWaas.signTypedData({
typedData: JSON.parse(params?.[1]),
network: Number(this.currentNetwork.chainId)
});
}
catch (error) {
if (isSessionInvalidOrNotFoundError(error)) {
await this.emit('error', error);
throw new ProviderDisconnectedError(new Error('Provider is not connected'));
}
else {
const message = typeof error === 'object' && error !== null && 'cause' in error
? String(error.cause) || 'Failed to sign typed data'
: 'Failed to sign typed data';
throw new InternalRpcError(new Error(message));
}
}
return sig.data.signature;
}
return await this.jsonRpcProvider.send(method, params ?? []);
}
async getTransaction(txHash) {
return await this.jsonRpcProvider.getTransaction(txHash);
}
detectNetwork() {
return Promise.resolve(this.currentNetwork);
}
getChainId() {
return Number(this.currentNetwork.chainId);
}
async checkTransactionFeeOptions({ transactions, chainId }) {
const resp = await this.sequenceWaas.feeOptions({
transactions: transactions,
network: chainId
});
if (resp.data.feeQuote && resp.data.feeOptions) {
return { feeQuote: resp.data.feeQuote, feeOptions: resp.data.feeOptions, isSponsored: false };
}
return { feeQuote: resp.data.feeQuote, feeOptions: resp.data.feeOptions, isSponsored: true };
}
}
const DEVICE_EMOJIS = [
// 256 emojis for unsigned byte range 0 - 255
...'ðķðąððđð°ðĶðŧðžðĻðŊðĶðŪð·ð―ðļðĩðððððð§ðĶðĪðĢðĨðĶðĶ
ðĶðĶðšððīðĶðððĶððððĶðĶð·ðļðĶðĒððĶðĶðĶððĶðĶðĶðĶðĄð ððŽðģððĶðð
ððĶðĶðͧððĶðĶðŠðŦðĶðĶððððððððĶððĶððĐðĶŪðððĶðĶðĶðĶĒðĶĐðððĶðĶĻðĶĄðĶĶðĶĨðððŋðĶðūððēðĩððēðģðīðąðŋðððððĢðððððūðð·ðđðĨðšðļðžðŧðððððððððððððĨððĨĨðĨð
ðĨðĨĶðĨŽðĨðķð―ðĨð§ð§
ðĨð ðĨðĨŊððĨðĨĻð§ðĨðģð§ðĨð§ðĨðĨĐðððĶīðððððĨŠðĨð§ðŪðŊðĨðĨðĨŦðððēððĢðąðĨðĶŠðĪððððĨðĨ ðĨŪðĒðĄð§ðĻðĶðĨ§ð§ð°ððŪððŽðŦðŋðĐðŠð°ðĨððððð
ðððððððððððððððŊð°ðąðēðģðūðŊðšðŧð―ððð'
];
// Generate a random name for the session, using a single random emoji and 2 random words
// from the list of words of ethers
export function randomName() {
const wordlistSize = 2048;
const words = ethers.wordlists.en;
const randomEmoji = DEVICE_EMOJIS[Math.floor(Math.random() * DEVICE_EMOJIS.length)];
const randomWord1 = words.getWord(Math.floor(Math.random() * wordlistSize));
const randomWord2 = words.getWord(Math.floor(Math.random() * wordlistSize));
return `${randomEmoji} ${randomWord1} ${randomWord2}`;
}
function isSessionInvalidOrNotFoundError(error) {
return error instanceof WebrpcEndpointError && error.cause === 'session invalid or not found';
}
//# sourceMappingURL=sequenceWaasConnector.js.map