@arcana/ca-sdk
Version:
Arcana Network's chain abstraction SDK for unified balance in Web3 apps
407 lines (406 loc) • 16.3 kB
JavaScript
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, switchChain, } 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();
}
};
this.config = getSDKConfig(config);
this.networkConfig = getNetworkConfig(this.config.network);
this.chainList = new ChainList(this.networkConfig.NETWORK_HINT);
if (this.config.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");
}
const chain = this.chainList.getChainByID(chainID);
if (!chain) {
throw new Error("chain not supported");
}
return switchChain(this.evm.client, chain);
}
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.config.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;