UNPKG

saepenatus

Version:

Web3-Onboard makes it simple to connect Ethereum hardware and software wallets to your dapp. Features standardised spec compliant web3 providers for all supported wallets, framework agnostic modern javascript UI with code splitting, CSS customization, mul

321 lines (287 loc) 8.92 kB
import { firstValueFrom, Subject } from 'rxjs' import { ProviderRpcError, ProviderRpcErrorCode, SofiaProLight, SofiaProRegular } from '@web3-onboard/common' import type { PatchedEIP1193Provider, TransactionPreviewInitOptions, TransactionPreviewModule, TransactionPreviewAPI, TransactionPreviewOptions, TransactionForSim, FullPreviewOptions } from './types.js' import type { EIP1193Provider } from '@web3-onboard/common' import type { InternalTransaction, MultiSimOutput, NetBalanceChange } from 'bnc-sdk' import initI18N from './i18n/index.js' import { validateTPInit, validateTPOptions } from './validation' import simulateTransactions from './simulateTransactions.js' // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore import TransactionPreview from './views/Index.svelte' export * from './types.js' const approved$ = new Subject<boolean>() let options: FullPreviewOptions let optionalSettings: TransactionPreviewOptions let app: TransactionPreview const destroyApp = () => { app.$destroy() } const handleRequireApproval = async ( app: TransactionPreview, fullProviderRequest: EIP1193Provider['request'], req: { method: string params?: Array<unknown> } ) => { const approved = await firstValueFrom(approved$) app.$destroy() if (!approved) { throw new ProviderRpcError({ code: ProviderRpcErrorCode.ACCOUNT_ACCESS_REJECTED, message: 'User rejected the transaction' }) } fullProviderRequest(req) } const netBalanceChangesExist = (simResp: MultiSimOutput): boolean => { if ( simResp && simResp.netBalanceChanges && simResp.netBalanceChanges.length ) { return simResp.netBalanceChanges.some((balChange: NetBalanceChange[]) => { return balChange.length && balChange.length > 0 }) } return false } export const patchProvider = ( walletProvider: PatchedEIP1193Provider ): PatchedEIP1193Provider => { if (!walletProvider) { throw new Error( `An EIP 1193 wallet provider is required to preform patching and watch for transactions e.g. an injected wallet using window.ethereum` ) } if (walletProvider.simPatched) return walletProvider as PatchedEIP1193Provider const fullProviderRequest = walletProvider.request const patchedProvider = walletProvider as PatchedEIP1193Provider const request: EIP1193Provider['request'] = async (req: { method: string params?: Array<unknown> }): Promise<any> => { if ( req.method === 'eth_sendTransaction' && req.params && req.params.length ) { const transactionParams = req.params as TransactionForSim[] await handlePreview(transactionParams, fullProviderRequest, req) } else { return fullProviderRequest(req) } } try { patchedProvider.request = request patchedProvider.simPatched = true } catch (err) { console.error( `There was an error patching the passed in wallet provider. The provider may be read only and may be incompatible with Transaction Preview` ) } return patchedProvider } const handlePreview = async ( transaction: TransactionForSim[], fullProviderRequest?: PatchedEIP1193Provider['request'], req?: { method: string params?: Array<unknown> } ): Promise<void | unknown> => { try { if (!options) { throw new Error( `Please initialize Transaction Preview package prior to previewing a transaction. You can do this by calling the init function with the appropriate params` ) } const preview = await simulateTransactions(options, transaction) if (!preview) { throw new Error(`An error ocurred while simulating the transaction, please see the console for more details`) } if (preview.error.length) { fullProviderRequest(req) handleTPErrors(preview) } if (preview.status !== 'simulated' || !netBalanceChangesExist(preview)) { // If transaction simulation was unsuccessful or balanceChanges do // not exist do not create DOM el return fullProviderRequest(req) } if (app) app.$destroy() app = mountTransactionPreview(preview) options.requireTransactionApproval ? handleRequireApproval(app, fullProviderRequest, req) : fullProviderRequest(req) .then(() => { app.$destroy() }) .catch(() => app.$destroy()) } catch (e) { fullProviderRequest(req) if (app) app.$destroy() throw new Error(`${e}`) } } export const previewTransaction = async ( transaction: TransactionForSim[] ): Promise<MultiSimOutput> => { try { if (!options) { throw new Error( `Please initialize Transaction Preview package prior to previewing a transaction. You can do this by calling the init function with the appropriate params` ) } const preview = await simulateTransactions(options, transaction) if (!preview) { throw new Error(`An error ocurred while simulating the transaction, please see the console for more details`) } if (preview.error.length) { handleTPErrors(preview) } if (preview.status !== 'simulated' || !netBalanceChangesExist(preview)) { // If transaction simulation was unsuccessful or balanceChanges do // not exist do not create DOM el console.error('No net balance changes ocurred from this simulation') } if (app) app.$destroy() app = mountTransactionPreview(preview) return preview } catch (e) { if (app) app.$destroy() throw new Error(`${e}`) } } const handleTPErrors = (preview: MultiSimOutput) => { let internalErrs = preview.internalTransactions.reduce( (acc: string[], tx: InternalTransaction[]) => { if (tx.length) { tx.forEach((t: InternalTransaction) => { if (t.errorReason) acc.push(t.errorReason) }) } return acc }, [] ) internalErrs = [...preview.error, ...internalErrs] throw new Error( `An error occurred during transaction simulation: ${internalErrs.join( ' - ' )}` ) } const transactionPreview: TransactionPreviewModule = ( tpOptions: TransactionPreviewOptions ): TransactionPreviewAPI => { if (tpOptions) { const error = validateTPOptions(tpOptions) if (error) { throw error } } // defaults requireTransactionApproval to true optionalSettings = { requireTransactionApproval: true, ...tpOptions } initI18N((tpOptions && tpOptions.i18n) || {}) return { patchProvider, init, previewTransaction } } const init = (initOptions: TransactionPreviewInitOptions): void => { if (initOptions) { const error = validateTPInit(initOptions) if (error) { throw error } } options = { ...initOptions, ...optionalSettings } } const mountTransactionPreview = (simResponse: MultiSimOutput) => { class TransactionPreviewEl extends HTMLElement { constructor() { super() } } if (!customElements.get('transaction-preview')) { customElements.define('transaction-preview', TransactionPreviewEl) } // Add Fonts to main page const styleEl = document.createElement('style') styleEl.innerHTML = ` ${SofiaProRegular} ${SofiaProLight} ` document.body.appendChild(styleEl) // add to DOM const transactionPreviewDomElement = document.createElement( 'transaction-preview' ) const target = transactionPreviewDomElement.attachShadow({ mode: 'open' }) transactionPreviewDomElement.style.all = 'initial' const getW3OEl = document.querySelector('onboard-v2') const containerElementQuery = options.containerElement || 'body' let containerEl: Element | null // If Onboard present copy stylesheets over to TransactionPreview shadow DOM if (getW3OEl && getW3OEl.shadowRoot) { const w3OStyleSheets = getW3OEl.shadowRoot.styleSheets const transactionPreviewStyleSheet = new CSSStyleSheet() Object.values(w3OStyleSheets).forEach(sheet => { const styleRules = Object.values(sheet.cssRules) styleRules.forEach(rule => transactionPreviewStyleSheet.insertRule(rule.cssText) ) }) // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore target.adoptedStyleSheets = [transactionPreviewStyleSheet] containerEl = getW3OEl.shadowRoot.querySelector(containerElementQuery) } else { containerEl = document.querySelector(containerElementQuery) } if (!containerEl) { throw new Error( `Element with query ${containerElementQuery} does not exist.` ) } containerEl.appendChild(transactionPreviewDomElement) const { requireTransactionApproval } = options const app = new TransactionPreview({ target, intro: true, props: { simResponse, requireTransactionApproval, approved$, destroyApp } }) return app } export default transactionPreview