UNPKG

@arcana/ca-sdk

Version:

Arcana Network's chain abstraction SDK for unified balance in Web3 apps

404 lines (403 loc) 16.2 kB
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var _CA_cosmosWallet; import { createCosmosWallet, Environment } from "@arcana/ca-common"; import SafeEventEmitter from "@metamask/safe-event-emitter"; import { keyDerivation } from "@starkware-industries/starkware-crypto-utils"; import { Account, CHAIN_IDS, Provider, } from "fuels"; import { createWalletClient, custom } from "viem"; import { createSiweMessage } from "viem/siwe"; import { ChainList } from "./chains"; import { getNetworkConfig } from "./config"; import { FUEL_NETWORK_URL } from "./constants"; import { getLogger, LOG_LEVEL, setLogLevel } from "./logger"; import { AllowanceQuery, BridgeQuery, TransferQuery } from "./query"; import { fixTx } from "./requestHandlers/fuel/common"; import { getFuelProvider } from "./requestHandlers/fuel/provider"; import { createHandler } from "./requestHandlers/router"; import { cosmosFeeGrant, equalFold, fetchBalances, fetchMyIntents, getSDKConfig, getSupportedChains, getTxOptions, isArcanaWallet, isEVMTx, minutesToMs, refundExpiredIntents, } from "./utils"; setLogLevel(LOG_LEVEL.NOLOGS); const logger = getLogger(); var INIT_STATUS; (function (INIT_STATUS) { INIT_STATUS[INIT_STATUS["CREATED"] = 0] = "CREATED"; INIT_STATUS[INIT_STATUS["RUNNING"] = 1] = "RUNNING"; INIT_STATUS[INIT_STATUS["DONE"] = 2] = "DONE"; })(INIT_STATUS || (INIT_STATUS = {})); export class CA { constructor(config = { debug: false, network: Environment.CORAL }) { this.caEvents = new SafeEventEmitter(); _CA_cosmosWallet.set(this, void 0); this.hooks = { onAllowance: (data) => data.allow(data.sources.map(() => "max")), onIntent: (data) => data.allow(), }; this.initPromises = []; this.initStatus = INIT_STATUS.CREATED; this.isArcanaProvider = false; this.deinit = () => { __classPrivateFieldSet(this, _CA_cosmosWallet, undefined, "f"); if (this.evm) { this.evm.provider.removeListener("accountsChanged", this.onAccountsChanged); } if (this.refundInterval) { clearInterval(this.refundInterval); this.refundInterval = undefined; } this.initStatus = INIT_STATUS.CREATED; }; this.getEVMProviderWithCA = () => { if (!this.evm) { throw new Error("EVM provider is not set"); } return this.evm.modProvider; }; this.init = async () => { if (!this.evm) { throw new Error("use setEVMProvider before calling init()"); } if (this.initStatus === INIT_STATUS.CREATED) { this.initStatus = INIT_STATUS.RUNNING; try { const address = await this.getEVMAddress(); this.setProviderHooks(); if (!this.isArcanaProvider) { await this.createCosmosWallet(); this.checkPendingRefunds(); } this.initStatus = INIT_STATUS.DONE; this.resolveInitPromises(); this.caEvents.emit("accountsChanged", [address]); } catch (e) { this.initStatus = INIT_STATUS.CREATED; logger.error("Error initializing CA", e); throw new Error("Error initializing CA"); } } else if (this.initStatus === INIT_STATUS.RUNNING) { return await this.waitForInit(); } }; this.onAccountsChanged = (accounts) => { this.deinit(); if (accounts.length !== 0) { this.init(); } }; const c = getSDKConfig(config); this.networkConfig = getNetworkConfig(c.network); this.chainList = new ChainList(this.networkConfig.NETWORK_HINT); this.siweStatement = c.siweStatement; if (c.debug) { setLogLevel(LOG_LEVEL.DEBUG); } } allowance() { if (!this.evm) { throw new Error("EVM provider is not set"); } return new AllowanceQuery(this.evm.client, this.networkConfig, this.chainList); } async bridge(input) { const bq = new BridgeQuery(input, this.init, this.changeChain.bind(this), this.createEVMHandler.bind(this), this.createFuelHandler.bind(this), await this.getEVMAddress(), this.networkConfig.VSC_DOMAIN, this.chainList, this.fuel?.account); await bq.initHandler(); return { exec: bq.exec, simulate: bq.simulate }; } async getFuelWithCA() { if (!this.fuel) { throw new Error("Fuel connector is not set."); } return { connector: this.fuel.modConnector, provider: this.fuel.modProvider, }; } async getMyIntents(page = 1) { const wallet = await this.getCosmosWallet(); const address = (await wallet.getAccounts())[0].address; return fetchMyIntents(address, this.networkConfig.GRPC_URL, page); } async getUnifiedBalance(symbol) { const balances = await this.getUnifiedBalances(); return balances.find((s) => equalFold(s.symbol, symbol)); } async getUnifiedBalances() { if (this.initStatus !== INIT_STATUS.DONE) { throw new Error("CA not initialized"); } const balances = await fetchBalances(this.networkConfig.VSC_DOMAIN, await this.getEVMAddress(), this.chainList, this.fuel?.address); return balances.assets.data; } async handleEVMTx(args, options = {}) { const response = await this.createEVMHandler(args.params[0], getTxOptions(options)); if (response) { await response.handler?.process(); return response.processTx(); } } async setEVMProvider(provider) { if (this.evm?.provider === provider) { return; } this.evm = { client: createWalletClient({ transport: custom(provider), }), modProvider: Object.assign({}, provider, { request: async (args) => { if (args.method === "eth_sendTransaction") { if (!this.isArcanaProvider) { return this.handleEVMTx(args); } } return provider.request(args); }, }), provider, }; this.isArcanaProvider = isArcanaWallet(provider); } async setFuelConnector(connector) { if (this.fuel?.connector === connector) { return; } logger.debug("setFuelConnector", { connected: connector.connected, connector: connector, }); if (!(await connector.isConnected())) { await connector.connect(); } const address = await connector.currentAccount(); if (!address) { throw new Error("could not get current account from connector"); } const modProvider = getFuelProvider(this.getUnifiedBalances.bind(this), address, this.chainList.getChainByID(CHAIN_IDS.fuel.mainnet)); const provider = new Provider(FUEL_NETWORK_URL, { resourceCacheTTL: -1, }); const clone = Object.create(connector); clone.sendTransaction = async (_address, _transaction, _params) => { logger.debug("fuelClone:sendTransaction:1", { _address, _params, _transaction, }); const handlerResponse = await this.createFuelHandler(_transaction, { bridge: false, gas: 0n, }); if (handlerResponse) { await handlerResponse.handler?.process(); } logger.debug("fuelClone:sendTransaction:2", { request: Object.assign({ inputs: [], }, _transaction), }); const tx = await fixTx(_address, _transaction, provider); return connector.sendTransaction(_address, tx, _params); }; this.fuel = { account: new Account(address, modProvider, connector), address, connector: connector, modConnector: clone, modProvider, provider, }; } setOnAllowanceHook(hook) { this.hooks.onAllowance = hook; } setOnIntentHook(hook) { this.hooks.onIntent = hook; } async transfer(input) { const tq = new TransferQuery(input, this.init, this.changeChain.bind(this), this.createEVMHandler.bind(this), this.createFuelHandler.bind(this), await this.getEVMAddress(), this.chainList, this.fuel?.account); await tq.initHandler(); return { exec: tq.exec, simulate: tq.simulate }; } changeChain(chainID) { if (!this.evm) { throw new Error("EVM provider is not set"); } return this.evm.client.switchChain({ id: chainID }); } async checkPendingRefunds() { await this.init(); const account = await this.getEVMAddress(); try { await refundExpiredIntents(account, this.networkConfig.COSMOS_URL, __classPrivateFieldGet(this, _CA_cosmosWallet, "f")); this.refundInterval = window.setInterval(async () => { await refundExpiredIntents(account, this.networkConfig.COSMOS_URL, __classPrivateFieldGet(this, _CA_cosmosWallet, "f")); }, minutesToMs(10)); } catch (e) { logger.error("Error checking pending refunds", e); } } async createCosmosWallet() { const sig = await this.signatureForLogin(); const pvtKey = keyDerivation.getPrivateKeyFromEthSignature(sig); __classPrivateFieldSet(this, _CA_cosmosWallet, await createCosmosWallet(`0x${pvtKey.padStart(64, "0")}`), "f"); const address = (await __classPrivateFieldGet(this, _CA_cosmosWallet, "f").getAccounts())[0].address; await cosmosFeeGrant(this.networkConfig.COSMOS_URL, this.networkConfig.VSC_DOMAIN, address); } async createEVMHandler(tx, options = {}) { if (!this.evm) { throw new Error("EVM provider is not set"); } if (!isEVMTx(tx)) { logger.debug("invalid evm tx, returning", { tx }); return null; } const opt = getTxOptions(options); const chainId = await this.getChainID(); const chain = this.chainList.getChainByID(chainId); if (!chain) { logger.info("chain not supported, returning", { chainId, }); return null; } return createHandler({ chain, chainList: this.chainList, cosmosWallet: await this.getCosmosWallet(), evm: { address: await this.getEVMAddress(), client: this.evm.client, tx, }, fuel: this.fuel, hooks: this.hooks, options: { emit: this.caEvents.emit.bind(this.caEvents), networkConfig: this.networkConfig, ...opt, }, }); } async createFuelHandler(tx, options = {}) { const chain = this.chainList.getChainByID(CHAIN_IDS.fuel.mainnet); if (!chain) { throw new Error(`chain not found: ${CHAIN_IDS.fuel.mainnet}`); } if (!this.fuel) { throw new Error("Fuel provider is not connected"); } const address = await this.fuel.connector.currentAccount(); if (!address) { throw new Error("could not get current account from connector"); } const opt = getTxOptions(options); return createHandler({ chain, chainList: this.chainList, cosmosWallet: await this.getCosmosWallet(), evm: { address: await this.getEVMAddress(), client: this.evm.client, }, fuel: { address, connector: this.fuel.connector, provider: this.fuel.provider, tx, }, hooks: { onAllowance: this.hooks.onAllowance, onIntent: this.hooks.onIntent, }, options: { emit: this.caEvents.emit.bind(this.caEvents), networkConfig: this.networkConfig, ...opt, }, }); } getChainID() { if (!this.evm) { throw new Error("EVM provider is not set"); } return this.evm.client.getChainId(); } async getCosmosWallet() { if (!__classPrivateFieldGet(this, _CA_cosmosWallet, "f")) { await this.createCosmosWallet(); } return __classPrivateFieldGet(this, _CA_cosmosWallet, "f"); } async getEVMAddress() { if (!this.evm) { throw new Error("EVM provider is not set"); } return (await this.evm.client.requestAddresses())[0]; } resolveInitPromises() { const list = this.initPromises; this.initPromises = []; for (const r of list) { r(); } } async setProviderHooks() { if (!this.evm) { throw new Error("EVM provider is not set"); } if (this.evm.provider) { this.evm.provider.on("accountsChanged", this.onAccountsChanged); } } async signatureForLogin() { if (!this.evm) { throw new Error("EVM provider is not set"); } const scheme = window.location.protocol.slice(0, -1); const domain = window.location.host; const origin = window.location.origin; const address = await this.getEVMAddress(); const message = createSiweMessage({ address, chainId: 1, domain, issuedAt: new Date("2024-12-16T12:17:43.182Z"), // this remains same to arrive at same pvt key nonce: "iLjYWC6s8frYt4l8w", // maybe this can be shortened hash of address scheme, statement: this.siweStatement, uri: origin, version: "1", }); const currentChain = await this.getChainID(); try { await this.evm.client.switchChain({ id: 1 }); const res = await this.evm.client.signMessage({ account: address, message, }); return res; } finally { await this.evm.client.switchChain({ id: currentChain }); } } async waitForInit() { const promise = new Promise((resolve) => { this.initPromises.push(resolve); }); return await promise; } } _CA_cosmosWallet = new WeakMap(); CA.getSupportedChains = getSupportedChains;