diff --git a/.github/workflows/push-webapp.yml b/.github/workflows/push-webapp.yml index bfb2461861..8aa2e812a3 100644 --- a/.github/workflows/push-webapp.yml +++ b/.github/workflows/push-webapp.yml @@ -9,6 +9,7 @@ on: - webapp - webapp-dev - webapp-demo + - webapp-ca jobs: master: @@ -45,9 +46,9 @@ jobs: with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - projectName: ${{ vars.CLOUDFLARE_PAGES_PROJECT_NAME }} + projectName: ${{ github.ref_name == 'webapp-ca' && 'chain-abstraction' || vars.CLOUDFLARE_PAGES_PROJECT_NAME }} gitHubToken: ${{ secrets.GH_AUTOMATION_TOKEN }} - branch: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || github.ref_name }} + branch: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || github.ref_name }} # Branch webapp & webapp-ca will be production branch directory: './packages/webapp/build' wranglerVersion: '3' - if: "github.ref_name == 'webapp-dev' && github.event_name == 'push'" diff --git a/README.md b/README.md index d6081c4d63..b4d580ff43 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ Cronjob is define in folder `packages/extension-koni-base/src/cron`. ### Add new redux store - Subwallet extension use [redux-tookit](https://redux-toolkit.js.org/) to generate store. - Define redux store reducers and state into separate file by method `createSlice` of redux toolkit. -- Map reducer into root store in file index.ts +- Map reducer into root store in file klaster.ts ### Add new message caller Read "Add a message handle" diff --git a/package.json b/package.json index e59f4498b2..a77197e972 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,8 @@ "@polkadot/types-support": "^12.0.2", "@polkadot/util": "^12.6.2", "@polkadot/util-crypto": "^12.6.2", - "@subwallet/chain-list": "0.2.87", + "@uniswap/v3-periphery": "1.4.4", + "@subwallet/chain-list": "0.2.89-beta.5", "@subwallet/keyring": "^0.1.6", "@subwallet/react-ui": "5.1.2-b79", "@subwallet/ui-keyring": "^0.1.6", diff --git a/packages/extension-base/package.json b/packages/extension-base/package.json index cfef86b1fc..26cf8452eb 100644 --- a/packages/extension-base/package.json +++ b/packages/extension-base/package.json @@ -35,6 +35,7 @@ "@metamask/safe-event-emitter": "^2.0.0", "@metaverse-network-sdk/type-definitions": "^0.0.1-13", "@oak-foundation/types": "^0.0.23", + "@particle-network/aa": "^2.0.2", "@polkadot-api/merkleize-metadata": "^1.1.0", "@polkadot/api": "^11.0.3", "@polkadot/api-base": "^10.11.2", @@ -55,13 +56,16 @@ "@reduxjs/toolkit": "^1.9.1", "@sora-substrate/type-definitions": "^1.17.7", "@substrate/connect": "^0.8.9", - "@subwallet/chain-list": "0.2.87", + "@subwallet/chain-list": "0.2.89-beta.5", "@subwallet/extension-base": "^1.2.30-0", "@subwallet/extension-chains": "^1.2.30-0", "@subwallet/extension-dapp": "^1.2.30-0", "@subwallet/extension-inject": "^1.2.30-0", "@subwallet/keyring": "^0.1.6", "@subwallet/ui-keyring": "^0.1.6", + "@uniswap/sdk-core": "^5.3.1", + "@uniswap/v3-periphery": "^1.4.4", + "@uniswap/v3-sdk": "^3.13.1", "@walletconnect/keyvaluestorage": "^1.1.1", "@walletconnect/sign-client": "^2.14.0", "@walletconnect/types": "^2.14.0", @@ -85,12 +89,14 @@ "is-buffer": "^2.0.5", "joi": "^17.13.3", "json-rpc-engine": "^6.1.0", + "klaster-sdk": "^0.5.11", "manta-extension-sdk": "^1.1.0", "moment": "^2.29.4", "protobufjs": "^7.2.4", "rxjs": "^7.8.1", "sails-js": "^0.1.6", "uuid": "^9.0.0", + "viem": "^2.21.2", "web3": "^1.10.0", "web3-core": "^1.10.0", "web3-core-helpers": "^1.10.0", diff --git a/packages/extension-base/src/background/KoniTypes.ts b/packages/extension-base/src/background/KoniTypes.ts index 6f223885da..9cb9322b61 100644 --- a/packages/extension-base/src/background/KoniTypes.ts +++ b/packages/extension-base/src/background/KoniTypes.ts @@ -6,6 +6,7 @@ import { TransactionError } from '@subwallet/extension-base/background/errors/Tr import { AuthUrls, Resolver } from '@subwallet/extension-base/background/handlers/State'; import { AccountAuthType, AccountJson, AddressJson, AuthorizeRequest, ConfirmationRequestBase, RequestAccountList, RequestAccountSubscribe, RequestAccountUnsubscribe, RequestAuthorizeCancel, RequestAuthorizeReject, RequestAuthorizeSubscribe, RequestAuthorizeTab, RequestCurrentAccountAddress, ResponseAuthorizeList, ResponseJsonGetAccountInfo, SeedLengths } from '@subwallet/extension-base/background/types'; import { RequestOptimalTransferProcess } from '@subwallet/extension-base/services/balance-service/helpers'; +import { BridgeProvider, CAProvider } from '@subwallet/extension-base/services/chain-abstraction-service/helper/util'; import { _CHAIN_VALIDATION_ERROR } from '@subwallet/extension-base/services/chain-service/handler/types'; import { _ChainState, _EvmApi, _NetworkUpsertParams, _SubstrateApi, _ValidateCustomAssetRequest, _ValidateCustomAssetResponse, EnableChainParams, EnableMultiChainParams } from '@subwallet/extension-base/services/chain-service/types'; import { AppBannerData, AppConfirmationData, AppPopupData } from '@subwallet/extension-base/services/mkt-campaign-service/types'; @@ -445,7 +446,7 @@ export interface UiSettings { unlockType: WalletUnlockType; enableChainPatrol: boolean; // On-ramp service account reference - walletReference: string; + walletReference: string } export type RequestSettingsType = UiSettings; @@ -708,6 +709,7 @@ export interface TransactionHistoryItem Promise); let eventsHandler: undefined | ((eventEmitter: TransactionEmitter) => void); + const smartAccountOwner = getEthereumSmartAccountOwner(from); + const provider = await this.#koniState.settingService.getCASettings(); + + const originChainInfo = chainInfoMap[originNetworkKey]; + const destChainInfo = chainInfoMap[destinationNetworkKey]; + + if (smartAccountOwner && destinationTokenInfo) { + let res: SWAATransaction['transaction']; + + if (provider.caProvider === CAProvider.KLASTER) { + const klasterService = new KlasterService(); + + await klasterService.init(smartAccountOwner.owner); + + res = await klasterService.getBridgeTx(originTokenInfo, destinationTokenInfo, originChainInfo, destChainInfo, value); + } else { + const evmApi = this.#koniState.getEvmApi(originNetworkKey); + const [feeResp, bridgeTxConfig] = await getAcrossBridgeData({ + amount: BigInt(value), + destAccount: to, + destinationChainId: _getEvmChainId(destChainInfo) as number, + destinationTokenContract: _getContractAddressOfToken(destinationTokenInfo), + sourceChainId: _getEvmChainId(originChainInfo) as number, + sourceTokenContract: _getContractAddressOfToken(originTokenInfo), + srcAccount: from, + isTestnet: originChainInfo.isTestnet + }); + + const spendingApprovalTxConfig = await getERC20SpendingApprovalTx(feeResp.spokePoolAddress, from, _getContractAddressOfToken(originTokenInfo), evmApi); + + res = await ParticleAAHandler.createUserOperation(_getEvmChainId(originChainInfo) as number, smartAccountOwner, [spendingApprovalTxConfig, bridgeTxConfig]); + } + + return this.#koniState.transactionService.handleAATransaction({ + address: from, + chain: originNetworkKey, + chainType: ChainType.EVM, + transferNativeAmount: _isNativeToken(originTokenInfo) ? value : '0', + transaction: res, + data: inputData, + extrinsicType: ExtrinsicType.TRANSFER_XCM, + ignoreWarnings: true, + isTransferAll: false, + provider: provider.caProvider + }); + } + if (fromKeyPair && destinationTokenInfo) { if (isSnowBridgeEvmTransfer) { const evmApi = this.#koniState.getEvmApi(originNetworkKey); @@ -3989,14 +4081,14 @@ export default class KoniExtension { } /// Inject account - private addInjects (request: RequestAddInjectedAccounts): boolean { - this.#koniState.keyringService.addInjectAccounts(request.accounts); + private async addInjects (request: RequestAddInjectedAccounts): Promise { + await this.#koniState.keyringService.addInjectAccounts(request.accounts); return true; } - private removeInjects (request: RequestRemoveInjectedAccounts): boolean { - this.#koniState.keyringService.removeInjectAccounts(request.addresses); + private async removeInjects (request: RequestRemoveInjectedAccounts): Promise { + await this.#koniState.keyringService.removeInjectAccounts(request.addresses); return true; } @@ -4502,15 +4594,34 @@ export default class KoniExtension { .generateBeforeHandleResponseErrors(swapValidations); } + const caSetting = await this.#koniState.settingService.getCASettings(); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { chainType, extrinsic, extrinsicType, transferNativeAmount, txChain, txData } = await this.#koniState.swapService.handleSwapProcess(inputData); + const { chainType, extrinsic, extrinsicType, transferNativeAmount, txChain, txData } = await this.#koniState.swapService.handleSwapProcess({ + ...inputData, + caProvider: caSetting.caProvider + }); // const chosenFeeToken = process.steps.findIndex((step) => step.type === SwapStepType.SET_FEE_TOKEN) > -1; // const allowSkipValidation = [ExtrinsicType.SET_FEE_TOKEN, ExtrinsicType.SWAP].includes(extrinsicType); + if (chainType === ChainType.EVM) { + return await this.#koniState.transactionService.handleAATransaction({ + address, + chain: txChain, + chainType: ChainType.EVM, + transferNativeAmount, + transaction: extrinsic as UserOpBundle | QuoteResponse, + data: inputData, + extrinsicType: ExtrinsicType.EVM_EXECUTE, + ignoreWarnings: true, + isTransferAll: false, + provider: caSetting.caProvider + }); + } + return await this.#koniState.transactionService.handleTransaction({ address, chain: txChain, - transaction: extrinsic, + transaction: extrinsic as TransactionData, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment data: txData, extrinsicType, // change this depends on step @@ -4542,6 +4653,14 @@ export default class KoniExtension { /* Ledger */ + private setCASetting (setting: CaSetting) { + this.#koniState.settingService.setCASettings(setting); + } + + private async getCASetting (): Promise { + return this.#koniState.settingService.getCASettings(); + } + // -------------------------------------------------------------- // eslint-disable-next-line @typescript-eslint/require-await public async handle (id: string, type: TMessageType, request: RequestTypes[TMessageType], port: chrome.runtime.Port): Promise> { @@ -5143,6 +5262,10 @@ export default class KoniExtension { case 'pri(ledger.generic.allow)': return this.subscribeLedgerGenericAllowChains(id, port); /* Ledger */ + case 'pri(ca.setConfig)': + return this.setCASetting(request as CaSetting); + case 'pri(ca.getConfig)': + return this.getCASetting(); // Default default: throw new Error(`Unable to handle message of type ${type}`); diff --git a/packages/extension-base/src/koni/background/handlers/State.ts b/packages/extension-base/src/koni/background/handlers/State.ts index 631a37177c..55a10ae683 100644 --- a/packages/extension-base/src/koni/background/handlers/State.ts +++ b/packages/extension-base/src/koni/background/handlers/State.ts @@ -154,12 +154,12 @@ export default class KoniState { this.eventService = new EventService(); this.dbService = new DatabaseService(this.eventService); - this.keyringService = new KeyringService(this.eventService); this.notificationService = new NotificationService(); this.chainService = new ChainService(this.dbService, this.eventService); this.subscanService = SubscanService.getInstance(); this.settingService = new SettingService(); + this.keyringService = new KeyringService(this.eventService, this.settingService); this.requestService = new RequestService(this.chainService, this.settingService, this.keyringService); this.priceService = new PriceService(this.dbService, this.eventService, this.chainService); this.balanceService = new BalanceService(this); @@ -1444,7 +1444,8 @@ export default class KoniState { estimateFee: { value: transactionValidated.estimateGas, symbol: token.symbol, - decimals: token.decimals || 18 + decimals: token.decimals || 18, + feeTokenSlug: token.slug }, id }); diff --git a/packages/extension-base/src/services/chain-abstraction-service/helper/tx-encoder.ts b/packages/extension-base/src/services/chain-abstraction-service/helper/tx-encoder.ts new file mode 100644 index 0000000000..8dbd77e68a --- /dev/null +++ b/packages/extension-base/src/services/chain-abstraction-service/helper/tx-encoder.ts @@ -0,0 +1,122 @@ +// Copyright 2019-2022 @subwallet/extension-base authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { encodeFunctionData, Hex, parseAbi } from 'viem'; +import { TransactionConfig } from 'web3-core'; + +export interface AcrossSuggestedFeeResp { + totalRelayFee: { + pct: string, + total: string + }, + relayerCapitalFee: { + pct: string, + total: string + }, + relayerGasFee: { + pct: string, + total: string + }, + lpFee: { + pct: string, + total: string + }, + timestamp: string, + isAmountTooLow: boolean, + quoteBlock: string, + spokePoolAddress: string, + exclusiveRelayer: string, + exclusivityDeadline: string, + expectedFillTimeSec: string +} + +export interface BridgeParams { + sourceTokenContract: string; + destinationTokenContract: string; + sourceChainId: number; + destinationChainId: number; + amount: bigint; + srcAccount: string; + destAccount: string; + isTestnet?: boolean; +} + +const amountMap: Record = { + default: '1000000000', + '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1': '10000000000000000000000', + '0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14': '100000000000000000', + '0x4200000000000000000000000000000000000006': '100000000000000000' +}; + +export async function getAcrossSuggestedFee (data: BridgeParams): Promise { + const url = (data.isTestnet ? 'https://testnet.across.to/api/suggested-fees?' : 'https://across.to/api/suggested-fees?') + new URLSearchParams({ + originChainId: data.sourceChainId.toString(), + destinationChainId: data.destinationChainId.toString(), + inputToken: data.sourceTokenContract, + outputToken: data.destinationTokenContract, + amount: amountMap[data.destinationTokenContract] || amountMap.default + }).toString(); + + console.log('url', url); + + try { + const result = fetch(url, { + method: 'GET' + }); + + return await result.then((res) => res.json()) as AcrossSuggestedFeeResp; + } catch (e) { + console.log('e', e); + throw Error('Sent amount is too low relative to fees'); + } +} + +export function encodeAcrossCallData (data: BridgeParams, fees: AcrossSuggestedFeeResp): Hex { + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const abi = parseAbi([ + 'function depositV3(address depositor, address recipient, address inputToken, address outputToken, uint256 inputAmount, uint256 outputAmount, uint256 destinationChainId, address exclusiveRelayer, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityDeadline, bytes calldata message) external' + ]); + const outputAmount = data.amount - BigInt(fees.totalRelayFee.total); + const fillDeadline = Math.round(Date.now() / 1000) + 900; + + // const [srcAddress, destAddress] = data.account.getAddresses([data.sourceChainId, data.destinationChainId]); + + // if (!srcAddress || !destAddress) { + // throw Error(`Can't fetch address from multichain account for ${data.sourceChainId} or ${data.destinationChainId}`); + // } + + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return encodeFunctionData({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + abi: abi, + functionName: 'depositV3', + args: [ + data.srcAccount, + data.destAccount, + data.sourceTokenContract, + data.destinationTokenContract, + data.amount, + outputAmount > 0 ? outputAmount : 0n, + BigInt(data.destinationChainId), + fees.exclusiveRelayer, + parseInt(fees.timestamp), + fillDeadline, + parseInt(fees.exclusivityDeadline), + '0x' + ] + }); +} + +export async function getAcrossBridgeData (data: BridgeParams): Promise<[AcrossSuggestedFeeResp, TransactionConfig]> { + const feeResponse = await getAcrossSuggestedFee(data); + + return [ + feeResponse, + { + to: feeResponse.spokePoolAddress as `0x${string}`, + data: encodeAcrossCallData(data, feeResponse) + } as TransactionConfig + ]; +} diff --git a/packages/extension-base/src/services/chain-abstraction-service/helper/util.ts b/packages/extension-base/src/services/chain-abstraction-service/helper/util.ts new file mode 100644 index 0000000000..329fb3bead --- /dev/null +++ b/packages/extension-base/src/services/chain-abstraction-service/helper/util.ts @@ -0,0 +1,17 @@ +// Copyright 2019-2022 @subwallet/extension-base authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export enum CAProvider { + KLASTER = 'KLASTER', + PARTICLE = 'PARTICLE', + ORBY = 'ORBY', + ARCANA = 'ARCANA', + NEAR = 'NEAR', + POLYGON = 'POLYGON', +} + +export enum BridgeProvider { + ACROSS = 'ACROSS', + WORMHOLE = 'WORMHOLE', + LAYER_ZERO = 'LAYER_ZERO', +} diff --git a/packages/extension-base/src/services/chain-abstraction-service/klaster.ts b/packages/extension-base/src/services/chain-abstraction-service/klaster.ts new file mode 100644 index 0000000000..95628b907b --- /dev/null +++ b/packages/extension-base/src/services/chain-abstraction-service/klaster.ts @@ -0,0 +1,131 @@ +// Copyright 2019-2022 @subwallet/extension-base authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; +import { _getContractAddressOfToken, _getEvmChainId } from '@subwallet/extension-base/services/chain-service/utils'; +import { batchTx, BiconomyV2AccountInitData, BridgePlugin, BridgePluginParams, buildItx, encodeApproveTx, initKlaster, klasterNodeHost, KlasterSDK, loadBicoV2Account, QuoteResponse, rawTx, TransactionBatch } from 'klaster-sdk'; + +import { getAcrossBridgeData } from './helper/tx-encoder'; + +export class KlasterService { + public sdk: KlasterSDK; + private readonly bridgePlugin: BridgePlugin; + private isInit = false; + static chainTestnetMap: Record = {}; + + static async getSmartAccount (ownerAddress: string): Promise { + const klasterSdk = await initKlaster({ + accountInitData: loadBicoV2Account({ + owner: ownerAddress as `0x${string}` + }), + nodeUrl: klasterNodeHost.default + }); + + return klasterSdk.account.getAddress(1) as string; + } + + constructor () { + this.bridgePlugin = async (data: BridgePluginParams) => { + const [srcAddress, destAddress] = data.account.getAddresses([data.sourceChainId, data.destinationChainId]); + + const [feeResponse, bridgeTxConfig] = await getAcrossBridgeData({ + amount: data.amount, + destAccount: destAddress as string, + destinationChainId: data.destinationChainId, + destinationTokenContract: data.destinationToken, + sourceChainId: data.sourceChainId, + sourceTokenContract: data.sourceToken, + srcAccount: srcAddress as string, + isTestnet: KlasterService.chainTestnetMap[data.sourceChainId] + }); + + const outputAmount = data.amount - BigInt(feeResponse.totalRelayFee.total); + const acrossApproveTx = encodeApproveTx({ + tokenAddress: data.sourceToken, + amount: 10000000000000000000000n, + recipient: feeResponse.spokePoolAddress as `0x${string}` + }); + + const acrossCallTx = rawTx({ + to: feeResponse.spokePoolAddress as `0x${string}`, + data: bridgeTxConfig.data as `0x${string}`, + gasLimit: BigInt(250_000) + }); + + return { + receivedOnDestination: outputAmount, + txBatch: batchTx(data.sourceChainId, [acrossApproveTx, acrossCallTx]) + }; + }; + } + + async init (ownerAddress: string) { + if (!this.isInit) { + this.sdk = await initKlaster({ + accountInitData: loadBicoV2Account({ + owner: ownerAddress as `0x${string}` + }), + nodeUrl: klasterNodeHost.default + }); + + this.isInit = true; + } + } + + static updateChainMap (chainInfo: _ChainInfo) { + const chainId = _getEvmChainId(chainInfo) as number; + + if (chainId in KlasterService.chainTestnetMap) { + return; + } + + KlasterService.chainTestnetMap[chainId] = chainInfo.isTestnet; + } + + async getBridgeTx (srcToken: _ChainAsset, destToken: _ChainAsset, srcChain: _ChainInfo, destChain: _ChainInfo, value: string, previousTx?: TransactionBatch): Promise { + KlasterService.updateChainMap(srcChain); + KlasterService.updateChainMap(destChain); + + const sourceChainId = _getEvmChainId(srcChain) as number; + + const res = await this.bridgePlugin({ + account: this.sdk.account, + amount: BigInt(value), + sourceChainId: sourceChainId, + destinationChainId: _getEvmChainId(destChain) as number, + sourceToken: _getContractAddressOfToken(srcToken) as `0x${string}`, + destinationToken: _getContractAddressOfToken(destToken) as `0x${string}` + }); + + const steps = [res.txBatch]; + + previousTx && steps.unshift(previousTx); + + const iTx = buildItx({ + steps, + feeTx: this.sdk.encodePaymentFee(sourceChainId, 'USDC') + }); + + const quote = await this.sdk.getQuote(iTx); + + console.debug(quote); + + return quote; + } + + async buildTx (srcChain: _ChainInfo, txs: TransactionBatch[]): Promise { + KlasterService.updateChainMap(srcChain); + const sourceChainId = _getEvmChainId(srcChain) as number; + + const iTx = buildItx({ + steps: txs, + feeTx: this.sdk.encodePaymentFee(sourceChainId, 'USDC') + }); + + const quote = await this.sdk.getQuote(iTx); + + console.debug(quote); + + return quote; + } +} diff --git a/packages/extension-base/src/services/chain-abstraction-service/particle.ts b/packages/extension-base/src/services/chain-abstraction-service/particle.ts new file mode 100644 index 0000000000..ec3baab8e2 --- /dev/null +++ b/packages/extension-base/src/services/chain-abstraction-service/particle.ts @@ -0,0 +1,96 @@ +// Copyright 2019-2022 @subwallet/extension-base authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { AccountContract, SmartAccount, SmartAccountConfig, Transaction, UserOp, UserOpBundle } from '@particle-network/aa'; +import { SmartAccountData } from '@subwallet/extension-base/background/types'; +import { anyNumberToBN } from '@subwallet/extension-base/utils'; +import { createMockParticleProvider } from '@subwallet/extension-base/utils/mock/provider/particle'; +import { TransactionConfig } from 'web3-core'; + +export const ParticleContract: AccountContract = { + name: 'BICONOMY', + version: '2.0.0' +}; + +const config: SmartAccountConfig = { + projectId: '80d48082-7a98-41e0-8eb5-571e2e00cc7f', + clientKey: 'cSuNrTTMf0d2Vp3l9aaAyvGm2UzRjywnNK0duRHN', + appId: 'ca41cf00-9bac-4980-aef5-f521925f3de7', + aaOptions: { + accountContracts: { // 'BICONOMY', 'CYBERCONNECT', 'SIMPLE', 'LIGHT', 'XTERIO' + BICONOMY: [ + { + version: '1.0.0', + chainIds: [1] + }, + { + version: '2.0.0', + chainIds: [1, 11155111, 8453, 84532, 42161] + } + ], + CYBERCONNECT: [ + { + version: '1.0.0', + chainIds: [1] + } + ], + SIMPLE: [ + { + version: '1.0.0', + chainIds: [1] + } + ] + } + // paymasterApiKeys: [{ // Optional + // chainId: 1, + // apiKey: 'Biconomy Paymaster API Key', + // }] + } +}; + +export class ParticleAAHandler { + static getSmartAccount = async (account: SmartAccountData): Promise => { + const provider = createMockParticleProvider(1, account.owner); + + const smartAccount = new SmartAccount(provider, config); + + smartAccount.setSmartAccountContract(account.provider || ParticleContract); + + return smartAccount.getAddress(); + }; + + static createUserOperation = async (chainId: number, account: SmartAccountData, _txList: TransactionConfig[]): Promise => { + const provider = createMockParticleProvider(chainId, account.owner); + + const smartAccount = new SmartAccount(provider, config); + + smartAccount.setSmartAccountContract(account.provider || ParticleContract); + + const txList: Transaction[] = []; + + for (const _tx of _txList) { + const tx: Transaction = { + data: _tx.data, + value: anyNumberToBN(_tx.value).toString(), + to: _tx.to || '', + gasLimit: _tx.gas + }; + + txList.push(tx); + } + + console.debug('quote', await smartAccount.getFeeQuotes(txList)); + + return await smartAccount.buildUserOperation({ tx: txList }); + }; + + static sendSignedUserOperation = async (chainId: number, account: SmartAccountData, userOp: UserOp): Promise => { + const provider = createMockParticleProvider(chainId, account.owner); + + const smartAccount = new SmartAccount(provider, config); + + smartAccount.setSmartAccountContract(account.provider || ParticleContract); + + return await smartAccount.sendSignedUserOperation(userOp); + }; +} diff --git a/packages/extension-base/src/services/keyring-service/index.ts b/packages/extension-base/src/services/keyring-service/index.ts index 4a344259bf..d8a104d707 100644 --- a/packages/extension-base/src/services/keyring-service/index.ts +++ b/packages/extension-base/src/services/keyring-service/index.ts @@ -3,14 +3,20 @@ import { CurrentAccountInfo, KeyringState } from '@subwallet/extension-base/background/KoniTypes'; import { ALL_ACCOUNT_KEY } from '@subwallet/extension-base/constants'; +import { CAProvider } from '@subwallet/extension-base/services/chain-abstraction-service/helper/util'; +import { KlasterService } from '@subwallet/extension-base/services/chain-abstraction-service/klaster'; +import { ParticleAAHandler, ParticleContract } from '@subwallet/extension-base/services/chain-abstraction-service/particle'; import { EventService } from '@subwallet/extension-base/services/event-service'; +import SettingService from '@subwallet/extension-base/services/setting-service/SettingService'; import { CurrentAccountStore } from '@subwallet/extension-base/stores'; import { InjectedAccountWithMeta } from '@subwallet/extension-inject/types'; import { keyring } from '@subwallet/ui-keyring'; import { SubjectInfo } from '@subwallet/ui-keyring/observable/types'; +import { InjectAccount } from '@subwallet/ui-keyring/types'; import { BehaviorSubject } from 'rxjs'; import { stringShorten } from '@polkadot/util'; +import { isEthereumAddress } from '@polkadot/util-crypto'; export class KeyringService { private readonly currentAccountStore = new CurrentAccountStore(); @@ -27,13 +33,14 @@ export class KeyringService { isLocked: false }); - constructor (private eventService: EventService) { + constructor (private eventService: EventService, private settingService: SettingService) { this.injected = false; this.eventService.waitCryptoReady.then(() => { this.currentAccountStore.get('CurrentAccountInfo', (rs) => { rs && this.currentAccountSubject.next(rs); }); this.subscribeAccounts().catch(console.error); + this.subscribeCASetting().catch(console.error); }).catch(console.error); } @@ -70,6 +77,46 @@ export class KeyringService { }); } + private async subscribeCASetting () { + await this.eventService.waitInjectReady; + const subject = this.settingService.getCASubject(); + let currentProvider = (await this.settingService.getCASettings()).caProvider; + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + subject.asObservable().subscribe(async ({ caProvider }) => { + if (currentProvider !== caProvider) { + const oldProvider = currentProvider; + + currentProvider = caProvider; + + const oldPairs = keyring.getPairs(); + const accounts = oldPairs.filter(({ meta }) => meta.aaSdk === oldProvider && meta.isInjected); + const currentAddress = this.currentAccountSubject.value.address; + const currentAccountOwner = accounts.find(({ address }) => address === currentAddress)?.meta.smartAccountOwner; + + await this.addInjectAccounts(accounts.map(({ meta, type }): InjectedAccountWithMeta => ({ + address: meta.smartAccountOwner as string, + meta: { + source: meta.source as string, + name: meta.name as string + }, + type + })), caProvider); + + if (currentAccountOwner) { + const newPairs = keyring.getPairs(); + const newPair = newPairs.find(({ meta }) => meta.smartAccountOwner === currentAccountOwner && meta.aaSdk === caProvider); + + if (newPair) { + this.setCurrentAccount({ address: newPair.address, currentGenesisHash: null }); + } + } + + keyring.removeInjects(accounts.map(({ address }) => address)); + } + }); + } + get keyringState () { return this.keyringStateSubject.value; } @@ -114,7 +161,37 @@ export class KeyringService { /* Inject */ - public addInjectAccounts (accounts: InjectedAccountWithMeta[]) { + public async addInjectAccounts (_accounts: InjectedAccountWithMeta[], _caProvider?: CAProvider) { + const caProvider = _caProvider || (await this.settingService.getCASettings()).caProvider; + const accounts: InjectAccount[] = await Promise.all(_accounts.map(async (acc): Promise => { + const isEthereum = isEthereumAddress(acc.address); + + if (isEthereum) { + let smartAddress: string; + + if (caProvider === CAProvider.KLASTER) { + smartAddress = await KlasterService.getSmartAccount(acc.address); + } else { + smartAddress = await ParticleAAHandler.getSmartAccount({ owner: acc.address, provider: ParticleContract }); + } + + return { + ...acc, + address: smartAddress, + meta: { + ...acc.meta, + isSmartAccount: true, + smartAccountOwner: acc.address, + aaSdk: caProvider, + aaProvider: ParticleContract + } + }; + } + + return acc; + }) + ); + keyring.addInjects(accounts.map((account) => { const name = account.meta.name || stringShorten(account.address); @@ -153,14 +230,30 @@ export class KeyringService { } } - public removeInjectAccounts (_addresses: string[]) { - const addresses = _addresses.map((address) => { + public async removeInjectAccounts (_addresses: string[]) { + const caProvider = (await this.settingService.getCASettings()).caProvider; + const convertedAddresses = await Promise.all(_addresses.map(async (address) => { + const isEthereum = isEthereumAddress(address); + + if (isEthereum) { + if (caProvider === CAProvider.KLASTER) { + return await KlasterService.getSmartAccount(address); + } else { + return await ParticleAAHandler.getSmartAccount({ owner: address }); + } + } + + return address; + })); + + const addresses = convertedAddresses.map((address) => { try { return keyring.getPair(address).address; } catch (error) { return address; } }); + const currentAddress = this.currentAccountSubject.value.address; const afterAccounts = Object.keys(this.accounts).filter((address) => (addresses.indexOf(address) < 0)); diff --git a/packages/extension-base/src/services/setting-service/SettingService.ts b/packages/extension-base/src/services/setting-service/SettingService.ts index a515a9ee3b..573b180338 100644 --- a/packages/extension-base/src/services/setting-service/SettingService.ts +++ b/packages/extension-base/src/services/setting-service/SettingService.ts @@ -1,9 +1,11 @@ // Copyright 2019-2022 @subwallet/extension-base authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { LanguageType, PassPhishing, RequestSettingsType, UiSettings } from '@subwallet/extension-base/background/KoniTypes'; +import { CaSetting, LanguageType, PassPhishing, RequestSettingsType, UiSettings } from '@subwallet/extension-base/background/KoniTypes'; import { LANGUAGE } from '@subwallet/extension-base/constants'; +import { BridgeProvider, CAProvider } from '@subwallet/extension-base/services/chain-abstraction-service/helper/util'; import { SWStorage } from '@subwallet/extension-base/storage'; +import CASettingStore from '@subwallet/extension-base/stores/CASettingStore'; import PassPhishingStore from '@subwallet/extension-base/stores/PassPhishingStore'; import SettingsStore from '@subwallet/extension-base/stores/Settings'; import { Subject } from 'rxjs'; @@ -14,6 +16,7 @@ import { DEFAULT_SETTING } from './constants'; export default class SettingService { private readonly settingsStore = new SettingsStore(); private readonly passPhishingStore = new PassPhishingStore(); + private readonly caSettingStore = new CASettingStore(); constructor () { this.initSetting().catch(console.error); @@ -22,6 +25,15 @@ export default class SettingService { private async initSetting () { let old = (await SWStorage.instance.getItem(LANGUAGE) || 'en') as LanguageType; + const currentCASetting = await this.getCASettings(); + + if (!currentCASetting || Object.keys(currentCASetting).length === 0) { + this.setCASettings({ + bridgeProvider: BridgeProvider.ACROSS, + caProvider: CAProvider.KLASTER + }); + } + const updateLanguage = ({ language }: UiSettings) => { if (language !== old) { old = language; @@ -52,6 +64,18 @@ export default class SettingService { this.settingsStore.set('Settings', data, callback); } + public setCASettings (data: CaSetting) { + this.caSettingStore.set('CASetting', data); + } + + public async getCASettings () { + return await this.caSettingStore.asyncGet('CASetting'); + } + + public getCASubject () { + return this.caSettingStore.getSubject(); + } + public passPhishingSubject (): Subject> { return this.passPhishingStore.getSubject(); } diff --git a/packages/extension-base/src/services/subscan-service/index.spec.ts b/packages/extension-base/src/services/subscan-service/index.spec.ts index 696d940e38..7799e0bf26 100644 --- a/packages/extension-base/src/services/subscan-service/index.spec.ts +++ b/packages/extension-base/src/services/subscan-service/index.spec.ts @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 -// Test for index.ts +// Test for klaster.ts import { SUBSCAN_API_CHAIN_MAP } from '@subwallet/extension-base/services/subscan-service/subscan-chain-map'; diff --git a/packages/extension-base/src/services/swap-service/handler/uniswap-handler.ts b/packages/extension-base/src/services/swap-service/handler/uniswap-handler.ts new file mode 100644 index 0000000000..814c0f3613 --- /dev/null +++ b/packages/extension-base/src/services/swap-service/handler/uniswap-handler.ts @@ -0,0 +1,251 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { UserOpBundle } from '@particle-network/aa'; +import { SwapError } from '@subwallet/extension-base/background/errors/SwapError'; +import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; +import { BasicTxErrorType, ChainType, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; +import { SmartAccountData } from '@subwallet/extension-base/background/types'; +import { getERC20SpendingApprovalTx } from '@subwallet/extension-base/koni/api/contract-handler/evm/web3'; +import { BalanceService } from '@subwallet/extension-base/services/balance-service'; +import { getAcrossBridgeData } from '@subwallet/extension-base/services/chain-abstraction-service/helper/tx-encoder'; +import { CAProvider } from '@subwallet/extension-base/services/chain-abstraction-service/helper/util'; +import { KlasterService } from '@subwallet/extension-base/services/chain-abstraction-service/klaster'; +import { ParticleAAHandler } from '@subwallet/extension-base/services/chain-abstraction-service/particle'; +import { ChainService } from '@subwallet/extension-base/services/chain-service'; +import { _getChainNativeTokenSlug, _getContractAddressOfToken, _getEvmChainId } from '@subwallet/extension-base/services/chain-service/utils'; +import { SwapBaseHandler, SwapBaseInterface } from '@subwallet/extension-base/services/swap-service/handler/base-handler'; +import { calculateSwapRate, handleUniswapQuote, SWAP_QUOTE_TIMEOUT_MAP } from '@subwallet/extension-base/services/swap-service/utils'; +import { BaseStepDetail, CommonOptimalPath, CommonStepFeeInfo, CommonStepType, DEFAULT_FIRST_STEP, MOCK_STEP_FEE } from '@subwallet/extension-base/types/service-base'; +import { OptimalSwapPathParams, SwapEarlyValidation, SwapFeeType, SwapProviderId, SwapQuote, SwapRequest, SwapStepType, SwapSubmitParams, SwapSubmitStepData, ValidateSwapProcessParams } from '@subwallet/extension-base/types/swap'; +import { getEthereumSmartAccountOwner } from '@subwallet/extension-base/utils'; +import { batchTx, encodeApproveTx, QuoteResponse, rawTx } from 'klaster-sdk'; +import { TransactionConfig } from 'web3-core'; + +export class UniswapHandler implements SwapBaseInterface { + providerSlug: SwapProviderId; + private swapBaseHandler: SwapBaseHandler; + private readonly isTestnet: boolean = true; + + constructor (chainService: ChainService, balanceService: BalanceService, isTestnet = true) { + this.swapBaseHandler = new SwapBaseHandler({ + balanceService, + chainService, + providerName: 'Uniswap', + providerSlug: isTestnet ? SwapProviderId.UNISWAP_SEPOLIA : SwapProviderId.UNISWAP_ETHEREUM + }); + this.providerSlug = isTestnet ? SwapProviderId.UNISWAP_SEPOLIA : SwapProviderId.UNISWAP_ETHEREUM; + + this.isTestnet = isTestnet; + } + + get providerInfo () { + return this.swapBaseHandler.providerInfo; + } + + generateOptimalProcess (params: OptimalSwapPathParams): Promise { + const res: CommonOptimalPath = { + totalFee: [ + MOCK_STEP_FEE, + params.selectedQuote?.feeInfo || MOCK_STEP_FEE + ], + steps: [ + DEFAULT_FIRST_STEP, + { + id: 1, + name: 'Swap', + type: SwapStepType.SWAP + } + ] + }; + + return Promise.resolve(res); + } + + getSubmitStep (params: OptimalSwapPathParams): Promise<[BaseStepDetail, CommonStepFeeInfo] | undefined> { + return Promise.resolve(undefined); + } + + async getSwapQuote (request: SwapRequest): Promise { + const { from, to: _to } = request.pair; + let to = _to; + + if (to === 'base_sepolia-ERC20-WETH-0x4200000000000000000000000000000000000006') { + to = 'sepolia_ethereum-ERC20-WETH-0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14'; + } else if (to === 'arbitrum_one-ERC20-USDC-0xaf88d065e77c8cC2239327C5EDb3A432268e5831') { + to = 'base_mainnet-ERC20-USDC-0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; + } else if (to === 'base_mainnet-ERC20-USDC-0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913') { + to = 'arbitrum_one-ERC20-USDC-0xaf88d065e77c8cC2239327C5EDb3A432268e5831'; + } else if (to === 'optimism-ERC20-DAI-0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1') { + if (from === 'base_mainnet-ERC20-USDC-0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913') { + to = 'base_mainnet-ERC20-DAI-0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb'; + } else { + to = 'arbitrum_one-ERC20-DAI-0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1'; + } + } + + const fromToken = this.swapBaseHandler.chainService.getAssetBySlug(from); + const toToken = this.swapBaseHandler.chainService.getAssetBySlug(to); + const fromChain = this.swapBaseHandler.chainService.getChainInfoByKey(fromToken.originChain); + + const { quote: availQuote } = await handleUniswapQuote(request, this.swapBaseHandler.chainService.getEvmApi(fromChain.slug), this.swapBaseHandler.chainService); + const result: SwapQuote = { + pair: request.pair, + fromAmount: request.fromAmount, + toAmount: availQuote.toString(), + rate: calculateSwapRate(request.fromAmount, availQuote.toString(), fromToken, toToken), + provider: this.providerInfo, + aliveUntil: +Date.now() + SWAP_QUOTE_TIMEOUT_MAP.default, + feeInfo: { + feeComponent: [ + { + feeType: SwapFeeType.NETWORK_FEE, + amount: '1000000', + tokenSlug: fromToken.slug + } + ], + defaultFeeToken: _getChainNativeTokenSlug(fromChain), + feeOptions: [_getChainNativeTokenSlug(fromChain), fromToken.slug], + selectedFeeToken: fromToken.slug + }, + route: { + path: [fromToken.slug, toToken.slug] + } + } as SwapQuote; + + return Promise.resolve(result); + } + + async handleSubmitStep (params: SwapSubmitParams): Promise { + const request: SwapRequest = { + address: params.address, + fromAmount: params.quote.fromAmount, + pair: params.quote.pair, + slippage: 0 + }; + const fromTokenSlug = params.quote.pair.from; + const toTokenSlug = params.quote.pair.to; + const fromToken = this.swapBaseHandler.chainService.getAssetBySlug(fromTokenSlug); + const toToken = this.swapBaseHandler.chainService.getAssetBySlug(toTokenSlug); + let bridgeTokenSlug = '0x'; + const fromChain = this.swapBaseHandler.chainService.getChainInfoByKey(fromToken.originChain); + const chainId = _getEvmChainId(fromChain) || 0; + + if (toTokenSlug === 'base_sepolia-ERC20-WETH-0x4200000000000000000000000000000000000006') { + bridgeTokenSlug = 'sepolia_ethereum-ERC20-WETH-0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14'; + } else if (toTokenSlug === 'arbitrum_one-ERC20-USDC-0xaf88d065e77c8cC2239327C5EDb3A432268e5831') { + bridgeTokenSlug = 'base_mainnet-ERC20-USDC-0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; + } else if (toTokenSlug === 'base_mainnet-ERC20-USDC-0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913') { + bridgeTokenSlug = 'arbitrum_one-ERC20-USDC-0xaf88d065e77c8cC2239327C5EDb3A432268e5831'; + } else if (toTokenSlug === 'optimism-ERC20-DAI-0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1') { + if (fromTokenSlug === 'base_mainnet-ERC20-USDC-0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913') { + bridgeTokenSlug = 'base_mainnet-ERC20-DAI-0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb'; + } else { + bridgeTokenSlug = 'arbitrum_one-ERC20-DAI-0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1'; + } + } + + // const toAddress = SWAP_ROUTER_02_ADDRESSES(chainId); + + const evmApi = this.swapBaseHandler.chainService.getEvmApi(fromToken.originChain); + const { callData, routerAddress: toAddress } = await handleUniswapQuote(request, evmApi, this.swapBaseHandler.chainService); + + const owner = getEthereumSmartAccountOwner(request.address); + let tx: UserOpBundle | QuoteResponse; + + if (params.caProvider === CAProvider.KLASTER) { + const approveSwapTx = encodeApproveTx({ + tokenAddress: _getContractAddressOfToken(fromToken) as `0x${string}`, + amount: 10000000000000000000000n, + recipient: toAddress as `0x${string}` + }); + + const swapTx = rawTx({ + to: toAddress as `0x${string}`, + data: callData as `0x${string}`, + gasLimit: BigInt(250_000) + }); + const txBatch = batchTx(chainId, [approveSwapTx, swapTx]); + + const klasterService = new KlasterService(); + + await klasterService.init(owner?.owner as string); + + if (bridgeTokenSlug === '0x') { + tx = await klasterService.buildTx(fromChain, [txBatch]); + } else { + const bridgeOriginToken = this.swapBaseHandler.chainService.getAssetBySlug(bridgeTokenSlug); + const bridgeOriginChain = this.swapBaseHandler.chainService.getChainInfoByKey(bridgeOriginToken.originChain); + const bridgeDestChain = this.swapBaseHandler.chainService.getChainInfoByKey(toToken.originChain); + + tx = await klasterService.getBridgeTx(bridgeOriginToken, toToken, bridgeOriginChain, bridgeDestChain, params.quote.toAmount, txBatch); + } + } else { + const swapApprovalTxConfig = await getERC20SpendingApprovalTx(toAddress, params.address, _getContractAddressOfToken(fromToken), evmApi); + + const swapTxConfig: TransactionConfig = { + ...swapApprovalTxConfig, + gas: 250_000, + data: callData as `0x${string}`, + to: toAddress as `0x${string}` + }; + + if (bridgeTokenSlug === '0x') { + tx = await ParticleAAHandler.createUserOperation(_getEvmChainId(fromChain) as number, owner as SmartAccountData, [swapApprovalTxConfig, swapTxConfig]); + } else { + const bridgeOriginToken = this.swapBaseHandler.chainService.getAssetBySlug(bridgeTokenSlug); + const bridgeOriginChain = this.swapBaseHandler.chainService.getChainInfoByKey(bridgeOriginToken.originChain); + const bridgeDestChain = this.swapBaseHandler.chainService.getChainInfoByKey(toToken.originChain); + const [feeResp, bridgeTxConfig] = await getAcrossBridgeData({ + amount: BigInt(params.quote.toAmount), + srcAccount: request.address, + sourceChainId: _getEvmChainId(bridgeOriginChain) as number, + sourceTokenContract: _getContractAddressOfToken(bridgeOriginToken), + destAccount: request.address, + destinationChainId: _getEvmChainId(bridgeDestChain) as number, + destinationTokenContract: _getContractAddressOfToken(toToken), + isTestnet: this.isTestnet + }); + const bridgeApprovalTxConfig = await getERC20SpendingApprovalTx(feeResp.spokePoolAddress, params.address, _getContractAddressOfToken(bridgeOriginToken), evmApi); + + tx = await ParticleAAHandler.createUserOperation(_getEvmChainId(fromChain) as number, owner as SmartAccountData, [ + swapApprovalTxConfig, + swapTxConfig, + bridgeApprovalTxConfig, + bridgeTxConfig + ]); + } + } + + return Promise.resolve({ + txChain: fromToken.originChain, + extrinsic: tx, + txData: undefined, + transferNativeAmount: '0', + extrinsicType: ExtrinsicType.SWAP, + chainType: ChainType.EVM + }); + } + + handleSwapProcess (params: SwapSubmitParams): Promise { + const { currentStep, process } = params; + const type = process.steps[currentStep].type; + + switch (type) { + case CommonStepType.DEFAULT: + return Promise.reject(new TransactionError(BasicTxErrorType.UNSUPPORTED)); + case SwapStepType.SWAP: + return this.handleSubmitStep(params); + default: + return this.handleSubmitStep(params); + } + } + + validateSwapProcess (params: ValidateSwapProcessParams): Promise { + return Promise.resolve([]); + } + + validateSwapRequest (request: SwapRequest): Promise { + return Promise.resolve({}); + } +} diff --git a/packages/extension-base/src/services/swap-service/index.ts b/packages/extension-base/src/services/swap-service/index.ts index 0e01cb99d8..c2e7acad19 100644 --- a/packages/extension-base/src/services/swap-service/index.ts +++ b/packages/extension-base/src/services/swap-service/index.ts @@ -12,6 +12,7 @@ import { AssetHubSwapHandler } from '@subwallet/extension-base/services/swap-ser import { SwapBaseInterface } from '@subwallet/extension-base/services/swap-service/handler/base-handler'; import { ChainflipSwapHandler } from '@subwallet/extension-base/services/swap-service/handler/chainflip-handler'; import { HydradxHandler } from '@subwallet/extension-base/services/swap-service/handler/hydradx-handler'; +import { UniswapHandler } from '@subwallet/extension-base/services/swap-service/handler/uniswap-handler'; import { _PROVIDER_TO_SUPPORTED_PAIR_MAP, getSwapAltToken, SWAP_QUOTE_TIMEOUT_MAP } from '@subwallet/extension-base/services/swap-service/utils'; import { CommonOptimalPath, DEFAULT_FIRST_STEP, MOCK_STEP_FEE } from '@subwallet/extension-base/types/service-base'; import { _SUPPORTED_SWAP_PROVIDERS, OptimalSwapPathParams, QuoteAskResponse, SwapErrorType, SwapPair, SwapProviderId, SwapQuote, SwapQuoteResponse, SwapRequest, SwapRequestResult, SwapStepType, SwapSubmitParams, SwapSubmitStepData, ValidateSwapProcessParams } from '@subwallet/extension-base/types/swap'; @@ -182,6 +183,12 @@ export class SwapService implements ServiceWithProcessInterface, StoppableServic case SwapProviderId.ROCOCO_ASSET_HUB: this.handlers[providerId] = new AssetHubSwapHandler(this.chainService, this.state.balanceService, 'rococo_assethub'); break; + case SwapProviderId.UNISWAP_SEPOLIA: + this.handlers[providerId] = new UniswapHandler(this.chainService, this.state.balanceService); + break; + case SwapProviderId.UNISWAP_ETHEREUM: + this.handlers[providerId] = new UniswapHandler(this.chainService, this.state.balanceService, false); + break; default: throw new Error('Unsupported provider'); diff --git a/packages/extension-base/src/services/swap-service/utils.ts b/packages/extension-base/src/services/swap-service/utils.ts index bb279b7169..5236090caf 100644 --- a/packages/extension-base/src/services/swap-service/utils.ts +++ b/packages/extension-base/src/services/swap-service/utils.ts @@ -3,11 +3,18 @@ import { Asset, Assets, Chain, Chains } from '@chainflip/sdk/swap'; import { COMMON_ASSETS, COMMON_CHAIN_SLUGS } from '@subwallet/chain-list'; -import { _ChainAsset } from '@subwallet/chain-list/types'; -import { _getAssetDecimals } from '@subwallet/extension-base/services/chain-service/utils'; +import { _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; +import { ChainService } from '@subwallet/extension-base/services/chain-service'; +import { _EvmApi } from '@subwallet/extension-base/services/chain-service/types'; +import { _getAssetDecimals, _getAssetName, _getAssetSymbol, _getContractAddressOfToken, _getEvmChainId } from '@subwallet/extension-base/services/chain-service/utils'; import { CHAINFLIP_BROKER_API } from '@subwallet/extension-base/services/swap-service/handler/chainflip-handler'; -import { SwapPair, SwapProviderId } from '@subwallet/extension-base/types/swap'; +import { SwapPair, SwapProviderId, SwapRequest } from '@subwallet/extension-base/types/swap'; +import { CurrencyAmount, Percent, QUOTER_ADDRESSES, SWAP_ROUTER_02_ADDRESSES, Token, TradeType, V3_CORE_FACTORY_ADDRESSES } from '@uniswap/sdk-core'; +// @ts-ignore +import IUniswapV3PoolABI from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json'; +import { computePoolAddress, FeeAmount, Pool as PoolV3, Route as RouteV3, SwapQuoter, SwapRouter as SwapRouterV3, Trade as TradeV3 } from '@uniswap/v3-sdk'; import BigN from 'bignumber.js'; +import { ethers } from 'ethers'; export const CHAIN_FLIP_TESTNET_EXPLORER = 'https://blocks-perseverance.chainflip.io'; export const CHAIN_FLIP_MAINNET_EXPLORER = 'https://scan.chainflip.io'; @@ -47,7 +54,9 @@ export const _PROVIDER_TO_SUPPORTED_PAIR_MAP: Record = { [SwapProviderId.CHAIN_FLIP_TESTNET]: [COMMON_CHAIN_SLUGS.CHAINFLIP_POLKADOT, COMMON_CHAIN_SLUGS.ETHEREUM_SEPOLIA], [SwapProviderId.POLKADOT_ASSET_HUB]: [COMMON_CHAIN_SLUGS.POLKADOT_ASSET_HUB], [SwapProviderId.KUSAMA_ASSET_HUB]: [COMMON_CHAIN_SLUGS.KUSAMA_ASSET_HUB], - [SwapProviderId.ROCOCO_ASSET_HUB]: [COMMON_CHAIN_SLUGS.ROCOCO_ASSET_HUB] + [SwapProviderId.ROCOCO_ASSET_HUB]: [COMMON_CHAIN_SLUGS.ROCOCO_ASSET_HUB], + [SwapProviderId.UNISWAP_SEPOLIA]: [COMMON_CHAIN_SLUGS.ETHEREUM_SEPOLIA, 'base_sepolia'], + [SwapProviderId.UNISWAP_ETHEREUM]: [COMMON_CHAIN_SLUGS.ETHEREUM, COMMON_CHAIN_SLUGS.ARBITRUM, 'base_mainnet'] }; export function getSwapAlternativeAsset (swapPair: SwapPair): string | undefined { @@ -103,3 +112,181 @@ export function getChainflipBroker (isTestnet: boolean) { // noted: currently no }; } } + +export interface UniSwapPoolInfo { + token0: string + token1: string + fee: number + sqrtPriceX96: string, + liquidity: string, + tick: number +} + +interface HandlerUniswapFunctionProps { + fromToken: _ChainAsset; + toToken: _ChainAsset; + fromChain: _ChainInfo; + recipient: string; + fromAmount: string; + web3Api: _EvmApi; +} + +interface HandlerUniswapFunctionResult { + quote: string; + callData: string; + routerAddress: string; +} + + +const handleUniswapV3 = async ({ fromAmount, fromChain, fromToken, recipient, toToken, web3Api }: HandlerUniswapFunctionProps): Promise => { + const fromContract = _getContractAddressOfToken(fromToken); + const toContract = _getContractAddressOfToken(toToken); + const chainId: number = _getEvmChainId(fromChain) || 0; + const useV2 = ['base_sepolia', 'base_mainnet', COMMON_CHAIN_SLUGS.ETHEREUM_SEPOLIA].includes(fromChain.slug); + const swaptoDAI = _getAssetSymbol(toToken) === 'DAI'; + + const fromTokenStruct = new Token( + chainId, + fromContract, + _getAssetDecimals(fromToken), + _getAssetSymbol(fromToken), + _getAssetName(fromToken) + ); + + const toTokenStruct = new Token( + chainId, + toContract, + _getAssetDecimals(toToken), + _getAssetSymbol(toToken), + _getAssetName(toToken) + ); + + const currentPoolAddress = computePoolAddress({ + fee: swaptoDAI ? FeeAmount.LOW : FeeAmount.HIGH, + tokenA: fromTokenStruct, + tokenB: toTokenStruct, + factoryAddress: V3_CORE_FACTORY_ADDRESSES[chainId] + }); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access + const poolContract = new web3Api.api.eth.Contract(IUniswapV3PoolABI.abi, currentPoolAddress); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access + // const quoterContract = new web3Api.api.eth.Contract(Quoter.abi, QUOTER_ADDRESSES[chainId as number]); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const [fee, liquidity, slot0] = await Promise.all([ + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access + poolContract.methods.fee().call(), + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access + poolContract.methods.liquidity().call(), + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access + poolContract.methods.slot0().call() + ]); + + const provider = new ethers.JsonRpcProvider(web3Api.apiUrl); + + const poolInfo: UniSwapPoolInfo = { + token0: fromContract, + token1: toContract, + fee: parseInt(fee as string), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access + sqrtPriceX96: slot0[0].toString(), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access + liquidity: liquidity.toString(), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument + tick: parseInt(slot0[1]) + }; + + const pool = new PoolV3( + fromTokenStruct, + toTokenStruct, + poolInfo.fee, + poolInfo.sqrtPriceX96.toString(), + poolInfo.liquidity.toString(), + poolInfo.tick + ); + + const swapRoute = new RouteV3( + [pool], + fromTokenStruct, + toTokenStruct + ); + + const { calldata } = SwapQuoter.quoteCallParameters( + swapRoute, + CurrencyAmount.fromRawAmount( + fromTokenStruct, + fromAmount + ), + TradeType.EXACT_INPUT, + { + useQuoterV2: useV2 + } + ); + + const quoteCallReturnData = await provider.call({ + to: QUOTER_ADDRESSES[chainId], + data: calldata + }); + + const availQuote = web3Api.api.eth.abi.decodeParameter('uint256', quoteCallReturnData); + + const uncheckedTrade = TradeV3.createUncheckedTrade({ + route: swapRoute, + inputAmount: CurrencyAmount.fromRawAmount( + fromTokenStruct, + fromAmount + ), + outputAmount: CurrencyAmount.fromRawAmount( + toTokenStruct, + availQuote.toString() + ), + tradeType: TradeType.EXACT_INPUT + }); + + const methodParameters = SwapRouterV3.swapCallParameters([uncheckedTrade], { + slippageTolerance: new Percent(500, 10_000), + deadline: Math.floor(Date.now() / 1000) + 60 * 20, + recipient + }); + const routerAddress = SWAP_ROUTER_02_ADDRESSES(chainId) + + return { + quote: availQuote.toString(), + callData: methodParameters.calldata, + routerAddress + }; +}; + +export async function handleUniswapQuote (request: SwapRequest, web3Api: _EvmApi, chainService: ChainService): Promise { + const { from, to: _to } = request.pair; + let to = _to; + + if (to === 'base_sepolia-ERC20-WETH-0x4200000000000000000000000000000000000006') { + to = 'sepolia_ethereum-ERC20-WETH-0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14'; + } else if (to === 'arbitrum_one-ERC20-USDC-0xaf88d065e77c8cC2239327C5EDb3A432268e5831') { + to = 'base_mainnet-ERC20-USDC-0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; + } else if (to === 'base_mainnet-ERC20-USDC-0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913') { + to = 'arbitrum_one-ERC20-USDC-0xaf88d065e77c8cC2239327C5EDb3A432268e5831'; + } else if (to === 'optimism-ERC20-DAI-0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1') { + if (from === 'base_mainnet-ERC20-USDC-0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913') { + to = 'base_mainnet-ERC20-DAI-0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb'; + } else { + to = 'arbitrum_one-ERC20-DAI-0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1'; + } + } + + const fromToken = chainService.getAssetBySlug(from); + const toToken = chainService.getAssetBySlug(to); + + const fromChain = chainService.getChainInfoByKey(fromToken.originChain); + + return await handleUniswapV3({ + fromAmount: request.fromAmount, + fromChain, + fromToken, + recipient: request.address, + toToken, + web3Api + }); +} diff --git a/packages/extension-base/src/services/transaction-service/index.ts b/packages/extension-base/src/services/transaction-service/index.ts index 135988f0fd..df294c158d 100644 --- a/packages/extension-base/src/services/transaction-service/index.ts +++ b/packages/extension-base/src/services/transaction-service/index.ts @@ -1,27 +1,31 @@ // Copyright 2019-2022 @subwallet/extension-base authors & contributors // SPDX-License-Identifier: Apache-2.0 +import { UserOpBundle } from '@particle-network/aa'; import { EvmProviderError } from '@subwallet/extension-base/background/errors/EvmProviderError'; import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; -import { AmountData, BasicTxErrorType, ChainType, EvmProviderErrorType, EvmSendTransactionRequest, ExtrinsicStatus, ExtrinsicType, NotificationType, TransactionAdditionalInfo, TransactionDirection, TransactionHistoryItem } from '@subwallet/extension-base/background/KoniTypes'; -import { AccountJson } from '@subwallet/extension-base/background/types'; +import { AmountData, BaseRequestSign, BasicTxErrorType, ChainType, EvmProviderErrorType, EvmSendTransactionRequest, EvmSignatureRequest, ExtrinsicStatus, ExtrinsicType, NotificationType, TransactionAdditionalInfo, TransactionDirection, TransactionHistoryItem } from '@subwallet/extension-base/background/KoniTypes'; +import { AccountJson, SmartAccountData } from '@subwallet/extension-base/background/types'; import { ALL_ACCOUNT_KEY } from '@subwallet/extension-base/constants'; import { checkBalanceWithTransactionFee, checkSigningAccountForTransaction, checkSupportForTransaction, estimateFeeForTransaction } from '@subwallet/extension-base/core/logic-validation/transfer'; import KoniState from '@subwallet/extension-base/koni/background/handlers/State'; +import { CAProvider } from '@subwallet/extension-base/services/chain-abstraction-service/helper/util'; +import { KlasterService } from '@subwallet/extension-base/services/chain-abstraction-service/klaster'; +import { ParticleAAHandler } from '@subwallet/extension-base/services/chain-abstraction-service/particle'; import { ChainService } from '@subwallet/extension-base/services/chain-service'; -import { _getAssetDecimals, _getAssetSymbol, _getChainNativeTokenBasicInfo, _getChainNativeTokenSlug, _getEvmChainId, _isChainEvmCompatible } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getAssetDecimals, _getAssetSymbol, _getChainNativeTokenBasicInfo, _getEvmChainId, _isChainEvmCompatible } from '@subwallet/extension-base/services/chain-service/utils'; import { EventService } from '@subwallet/extension-base/services/event-service'; import { HistoryService } from '@subwallet/extension-base/services/history-service'; import { EXTENSION_REQUEST_URL } from '@subwallet/extension-base/services/request-service/constants'; import { TRANSACTION_TIMEOUT } from '@subwallet/extension-base/services/transaction-service/constants'; import { parseLiquidStakingEvents, parseLiquidStakingFastUnstakeEvents, parseTransferEventLogs, parseXcmEventLogs } from '@subwallet/extension-base/services/transaction-service/event-parser'; import { getBaseTransactionInfo, getTransactionId, isSubstrateTransaction } from '@subwallet/extension-base/services/transaction-service/helpers'; -import { SWTransaction, SWTransactionInput, SWTransactionResponse, TransactionEmitter, TransactionEventMap, TransactionEventResponse, ValidateTransactionResponseInput } from '@subwallet/extension-base/services/transaction-service/types'; +import { SWAATransaction, SWTransaction, SWTransactionAAInput, SWTransactionInput, SWTransactionResponse, TransactionEmitter, TransactionEventMap, TransactionEventResponse, ValidateTransactionResponseInput } from '@subwallet/extension-base/services/transaction-service/types'; import { getExplorerLink, parseTransactionData } from '@subwallet/extension-base/services/transaction-service/utils'; import { isWalletConnectRequest } from '@subwallet/extension-base/services/wallet-connect-service/helpers'; import { Web3Transaction } from '@subwallet/extension-base/signers/types'; import { LeavePoolAdditionalData, RequestStakePoolingBonding, RequestYieldStepSubmit, SpecialYieldPoolInfo, YieldPoolType } from '@subwallet/extension-base/types'; -import { _isRuntimeUpdated, anyNumberToBN, reformatAddress } from '@subwallet/extension-base/utils'; +import { _isRuntimeUpdated, anyNumberToBN, getEthereumSmartAccountOwner, reformatAddress } from '@subwallet/extension-base/utils'; import { mergeTransactionAndSignature } from '@subwallet/extension-base/utils/eth/mergeTransactionAndSignature'; import { isContractAddress, parseContractInput } from '@subwallet/extension-base/utils/eth/parseTransaction'; import { BN_ZERO } from '@subwallet/extension-base/utils/number'; @@ -30,6 +34,7 @@ import { addHexPrefix } from 'ethereumjs-util'; import { ethers, TransactionLike } from 'ethers'; import EventEmitter from 'eventemitter3'; import { t } from 'i18next'; +import { QuoteResponse } from 'klaster-sdk'; import { BehaviorSubject, interval as rxjsInterval, Subscription } from 'rxjs'; import { TransactionConfig, TransactionReceipt } from 'web3-core'; @@ -236,6 +241,94 @@ export default class TransactionService { return validatedTransaction; } + public async handleAATransaction (transaction: SWTransactionAAInput): Promise { + const validatedTransaction: SWAATransaction = { + transaction: transaction.transaction, + provider: transaction.provider, + chain: transaction.chain, + chainType: transaction.chainType, + address: transaction.address, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + data: transaction.data, + status: ExtrinsicStatus.QUEUED, + extrinsicHash: '', + createdAt: new Date().getTime(), + updatedAt: new Date().getTime(), + extrinsicType: transaction.extrinsicType, + id: transaction.id || getTransactionId(transaction.chainType, transaction.chain, false, false), + errors: transaction.errors || [], + warnings: transaction.warnings || [] + }; + + const emitter = this.sendAATransaction(validatedTransaction); + + await new Promise((resolve, reject) => { + // TODO + emitter.on('signed', (data: TransactionEventResponse) => { + validatedTransaction.id = data.id; + // validatedTransaction.extrinsicHash = data.extrinsicHash; + resolve(); + }); + + emitter.on('error', (data: TransactionEventResponse) => { + if (data.errors.length > 0) { + validatedTransaction.errors.push(...data.errors); + resolve(); + } + }); + }); + + // @ts-ignore + 'transaction' in validatedTransaction && delete validatedTransaction.transaction; + + return validatedTransaction; + } + + public addAATransaction (inputTransaction: SWTransactionAAInput): TransactionEmitter { + const transactions = this.transactions; + // Fill transaction default info + const transaction = this.fillTransactionDefaultInfo(inputTransaction as SWTransactionInput); + + // Add Transaction + transactions[transaction.id] = transaction; + this.transactionSubject.next({ ...transactions }); + + return this.signAndSendEvmAATransaction(transaction as unknown as SWAATransaction); + } + + private sendAATransaction (transaction: SWAATransaction): TransactionEmitter { + // Send Transaction + const emitter = this.addAATransaction(transaction); + + emitter.on('signed', (data: TransactionEventResponse) => { + this.onSigned(data); + }); + + emitter.on('send', (data: TransactionEventResponse) => { + this.onSend(data); + }); + + emitter.on('extrinsicHash', (data: TransactionEventResponse) => { + this.onHasTransactionHash(data); + }); + // + emitter.on('success', (data: TransactionEventResponse) => { + this.handlePostProcessing(data.id); + this.onSuccess(data); + }); + + emitter.on('error', (data: TransactionEventResponse) => { + // this.handlePostProcessing(data.id); // might enable this later + this.onFailed({ ...data, errors: [...data.errors, new TransactionError(BasicTxErrorType.INTERNAL_ERROR)] }); + }); + + emitter.on('timeout', (data: TransactionEventResponse) => { + this.onTimeOut({ ...data, errors: [...data.errors, new TransactionError(BasicTxErrorType.TIMEOUT)] }); + }); + + return emitter; + } + private async sendTransaction (transaction: SWTransaction): Promise { // Send Transaction const emitter = await (transaction.chainType === 'substrate' ? this.signAndSendSubstrateTransaction(transaction) : this.signAndSendEvmTransaction(transaction)); @@ -324,7 +417,8 @@ export default class TransactionService { blockNumber: 0, // Will be added in next step blockHash: '', // Will be added in next step nonce: nonce ?? 0, - startBlock: startBlock || 0 + startBlock: startBlock || 0, + caProvider: transaction.provider }; const nativeAsset = _getChainNativeTokenBasicInfo(chainInfo); @@ -1163,6 +1257,110 @@ export default class TransactionService { return emitter; } + private signAndSendEvmAATransaction ({ address, + chain, + id, + provider, + transaction }: SWAATransaction): TransactionEmitter { + const chainInfo = this.state.chainService.getChainInfoByKey(chain); + const chainId = _getEvmChainId(chainInfo) as number; + const accountPair = keyring.getPair(address); + const account: AccountJson = { address, ...accountPair.meta }; + const owner = getEthereumSmartAccountOwner(address) as SmartAccountData; + + const emitter = new EventEmitter(); + + let _payload: EvmSignatureRequest; + + if (provider === CAProvider.PARTICLE) { + const { userOpHash } = transaction as UserOpBundle; + + _payload = { + account, + payload: userOpHash, + hashPayload: userOpHash, + type: 'personal_sign', + canSign: true, + id + }; + } else { + const { itxHash } = transaction as QuoteResponse; + + console.log('ok', itxHash); + _payload = { + account, + payload: itxHash, + hashPayload: itxHash, + type: 'personal_sign', + canSign: true, + id + }; + } + + const eventData: TransactionEventResponse = { + id, + errors: [], + warnings: [], + extrinsicHash: id, + eventLogs: [] + }; + + this.state.requestService.addConfirmation(id, EXTENSION_REQUEST_URL, 'evmSignatureRequest', _payload) + .then(async ({ isApproved, payload: signature }) => { + if (isApproved) { + if (!signature) { + throw new EvmProviderError(EvmProviderErrorType.UNAUTHORIZED, 'Bad signature'); + } + + // Emit signed event + emitter.emit('signed', eventData); + + // Add start info + emitter.emit('send', eventData); // This event is needed after sending transaction with queue + + try { + if (provider === CAProvider.PARTICLE) { + const userOp = (transaction as UserOpBundle).userOp; + + userOp.signature = signature; + + eventData.extrinsicHash = await ParticleAAHandler.sendSignedUserOperation(chainId, owner, userOp); + } else { + const klasterService = new KlasterService(); + + const owner = getEthereumSmartAccountOwner(address); + + await klasterService.init(owner?.owner as string); + const result = await klasterService.sdk.execute(transaction as QuoteResponse, signature); + + eventData.extrinsicHash = result.itxHash; + } + + emitter.emit('extrinsicHash', eventData); + emitter.emit('success', eventData); + } catch (_e) { + const e = _e as Error; + + eventData.errors.push(new TransactionError(BasicTxErrorType.UNABLE_TO_SEND, e.message)); + emitter.emit('error', eventData); + } + } else { + this.removeTransaction(id); + eventData.errors.push(new TransactionError(BasicTxErrorType.USER_REJECT_REQUEST)); + emitter.emit('error', eventData); + } + }) + .catch((e: Error) => { + this.removeTransaction(id); + // TODO: Change type + eventData.errors.push(new TransactionError(BasicTxErrorType.UNABLE_TO_SIGN, e.message)); + + emitter.emit('error', eventData); + }); + + return emitter; + } + private handleTransactionTimeout (emitter: EventEmitter, eventData: TransactionEventResponse): void { const timeout = setTimeout(() => { const transaction = this.getTransaction(eventData.id); diff --git a/packages/extension-base/src/services/transaction-service/types.ts b/packages/extension-base/src/services/transaction-service/types.ts index 7b0cb7d144..84bc1299e4 100644 --- a/packages/extension-base/src/services/transaction-service/types.ts +++ b/packages/extension-base/src/services/transaction-service/types.ts @@ -1,8 +1,11 @@ // Copyright 2019-2022 @subwallet/extension-base authors & contributors // SPDX-License-Identifier: Apache-2.0 +import { UserOpBundle } from '@particle-network/aa'; import { BaseRequestSign, ChainType, ExtrinsicDataTypeMap, ExtrinsicStatus, ExtrinsicType, FeeData, ValidateTransactionResponse } from '@subwallet/extension-base/background/KoniTypes'; +import { CAProvider } from '@subwallet/extension-base/services/chain-abstraction-service/helper/util'; import EventEmitter from 'eventemitter3'; +import { QuoteResponse } from 'klaster-sdk'; import { TransactionConfig } from 'web3-core'; import { SubmittableExtrinsic } from '@polkadot/api/promise/types'; @@ -25,6 +28,12 @@ export interface SWTransaction extends ValidateTransactionResponse, Partial Promise; eventsHandler?: (eventEmitter: TransactionEmitter) => void; + provider?: CAProvider; +} + +export interface SWAATransaction extends Pick { + transaction: UserOpBundle | QuoteResponse; + provider: CAProvider; } export type SWTransactionResult = Omit @@ -43,6 +52,11 @@ export interface SWTransactionInput extends SwInputBase, Partial { + transaction: SWAATransaction['transaction']; + provider: CAProvider; +} + export type SWTransactionResponse = SwInputBase & Pick & Partial>; export type ValidateTransactionResponseInput = SWTransactionInput; diff --git a/packages/extension-base/src/services/transaction-service/utils.ts b/packages/extension-base/src/services/transaction-service/utils.ts index 14c837010c..0c7e6c08e9 100644 --- a/packages/extension-base/src/services/transaction-service/utils.ts +++ b/packages/extension-base/src/services/transaction-service/utils.ts @@ -81,3 +81,7 @@ export function getChainflipExplorerLink (data: ChainflipSwapTxData, chainInfo: return `${chainflipDomain}/channels/${data.depositChannelId}`; } + +export function getKlasterExplorerLink (iTxHash: string) { + return `https://explorer.klaster.io/details/${iTxHash}`; +} diff --git a/packages/extension-base/src/stores/CASettingStore.ts b/packages/extension-base/src/stores/CASettingStore.ts new file mode 100644 index 0000000000..85a64ee069 --- /dev/null +++ b/packages/extension-base/src/stores/CASettingStore.ts @@ -0,0 +1,12 @@ +// Copyright 2019-2022 @subwallet/extension-koni authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { CaSetting } from '@subwallet/extension-base/background/KoniTypes'; +import { EXTENSION_PREFIX } from '@subwallet/extension-base/defaults'; +import SubscribableStore from '@subwallet/extension-base/stores/SubscribableStore'; + +export default class CASettingStore extends SubscribableStore { + constructor () { + super(EXTENSION_PREFIX ? `${EXTENSION_PREFIX}subwallet-ca-store` : null); + } +} diff --git a/packages/extension-base/src/types/index.ts b/packages/extension-base/src/types/index.ts index 7ba49ba379..fdf3889b80 100644 --- a/packages/extension-base/src/types/index.ts +++ b/packages/extension-base/src/types/index.ts @@ -19,5 +19,7 @@ export * from './common'; export * from './fee'; export * from './metadata'; export * from './ordinal'; +export * from './service-base'; +export * from './swap'; export * from './transaction'; export * from './yield'; diff --git a/packages/extension-base/src/types/swap/index.ts b/packages/extension-base/src/types/swap/index.ts index c8eccfde80..f27cc343f4 100644 --- a/packages/extension-base/src/types/swap/index.ts +++ b/packages/extension-base/src/types/swap/index.ts @@ -7,6 +7,9 @@ import { AmountData, ChainType, ExtrinsicType } from '@subwallet/extension-base/ import { TransactionData } from '@subwallet/extension-base/types'; import { BaseStepDetail, CommonOptimalPath, CommonStepFeeInfo } from '@subwallet/extension-base/types/service-base'; import BigN from 'bignumber.js'; +import {UserOpBundle} from "@particle-network/aa"; +import {QuoteResponse} from "klaster-sdk"; +import {CAProvider} from "@subwallet/extension-base/services/chain-abstraction-service/helper/util"; // core export type SwapRate = number; @@ -67,6 +70,8 @@ export enum SwapProviderId { POLKADOT_ASSET_HUB = 'POLKADOT_ASSET_HUB', KUSAMA_ASSET_HUB = 'KUSAMA_ASSET_HUB', ROCOCO_ASSET_HUB = 'ROCOCO_ASSET_HUB', + UNISWAP_SEPOLIA = 'UNISWAP_SEPOLIA', + UNISWAP_ETHEREUM = 'UNISWAP_ETHEREUM' } export const _SUPPORTED_SWAP_PROVIDERS: SwapProviderId[] = [ @@ -76,7 +81,9 @@ export const _SUPPORTED_SWAP_PROVIDERS: SwapProviderId[] = [ SwapProviderId.HYDRADX_TESTNET, SwapProviderId.POLKADOT_ASSET_HUB, SwapProviderId.KUSAMA_ASSET_HUB, - SwapProviderId.ROCOCO_ASSET_HUB + SwapProviderId.ROCOCO_ASSET_HUB, + SwapProviderId.UNISWAP_SEPOLIA, + SwapProviderId.UNISWAP_ETHEREUM ]; export interface SwapProvider { @@ -168,12 +175,13 @@ export interface SwapSubmitParams { address: string; slippage: number; // Example: 0.01 for 1% recipient?: string; + caProvider?: CAProvider; } export interface SwapSubmitStepData { txChain: string; txData: any; - extrinsic: TransactionData; + extrinsic: TransactionData | UserOpBundle | QuoteResponse; transferNativeAmount: string; extrinsicType: ExtrinsicType; chainType: ChainType diff --git a/packages/extension-base/src/utils/account.ts b/packages/extension-base/src/utils/account.ts index 9ce347edd8..0736e3620f 100644 --- a/packages/extension-base/src/utils/account.ts +++ b/packages/extension-base/src/utils/account.ts @@ -1,8 +1,10 @@ // Copyright 2019-2022 @subwallet/extension-base authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { AddressJson } from '@subwallet/extension-base/background/types'; +import { AccountContract } from '@particle-network/aa'; +import { AddressJson, SmartAccountData } from '@subwallet/extension-base/background/types'; import { reformatAddress } from '@subwallet/extension-base/utils/index'; +import { keyring } from '@subwallet/ui-keyring'; import { SubjectInfo } from '@subwallet/ui-keyring/observable/types'; import { decodeAddress, encodeAddress, isAddress, isEthereumAddress } from '@polkadot/util-crypto'; @@ -26,3 +28,22 @@ export function quickFormatAddressToCompare (address?: string) { export const convertSubjectInfoToAddresses = (subjectInfo: SubjectInfo): AddressJson[] => { return Object.values(subjectInfo).map((info): AddressJson => ({ address: info.json.address, type: info.type, ...info.json.meta })); }; + +export const getEthereumSmartAccountOwner = (address: string): SmartAccountData | undefined => { + try { + const pair = keyring.getPair(address); + + const owner = pair.meta.smartAccountOwner; + + if (owner) { + return { + owner: owner as string, + provider: pair.meta.aaProvider as AccountContract + }; + } else { + return undefined; + } + } catch (error) { + return undefined; + } +}; diff --git a/packages/extension-base/src/utils/mock/provider/particle.ts b/packages/extension-base/src/utils/mock/provider/particle.ts new file mode 100644 index 0000000000..b4683b2898 --- /dev/null +++ b/packages/extension-base/src/utils/mock/provider/particle.ts @@ -0,0 +1,28 @@ +// Copyright 2019-2022 @subwallet/extension-base authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import SafeEventEmitter from '@metamask/safe-event-emitter'; +import { IEthereumProvider, JsonRpcRequest } from '@particle-network/aa'; + +class MockParticleProvider extends SafeEventEmitter implements IEthereumProvider { + constructor (private chainId: number, private address: string) { + super(); + } + + request (request: Partial): Promise { + const { method } = request; + + switch (method) { + case 'eth_chainId': + return Promise.resolve(this.chainId); + case 'eth_accounts': + return Promise.resolve([this.address]); + } + + return Promise.resolve(undefined); + } +} + +export const createMockParticleProvider = (chainId: number, address: string): IEthereumProvider => { + return new MockParticleProvider(chainId, address); +}; diff --git a/packages/extension-base/src/utils/request.ts b/packages/extension-base/src/utils/request.ts index 7ba461d794..b9c98e277a 100644 --- a/packages/extension-base/src/utils/request.ts +++ b/packages/extension-base/src/utils/request.ts @@ -1,8 +1,14 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 +import { UserOpBundle } from '@particle-network/aa'; import { EXTENSION_REQUEST_URL } from '@subwallet/extension-base/services/request-service/constants'; +import { QuoteResponse } from 'klaster-sdk'; export function isInternalRequest (url: string): boolean { return url === EXTENSION_REQUEST_URL; } + +export const isParticleOP = (transaction: UserOpBundle | QuoteResponse): transaction is UserOpBundle => { + return (transaction as UserOpBundle).userOp !== undefined; +}; diff --git a/packages/extension-koni-ui/package.json b/packages/extension-koni-ui/package.json index 038802a712..3523c414c9 100644 --- a/packages/extension-koni-ui/package.json +++ b/packages/extension-koni-ui/package.json @@ -34,7 +34,7 @@ "@polkadot/util-crypto": "^12.6.2", "@ramonak/react-progress-bar": "^5.0.3", "@reduxjs/toolkit": "^1.9.1", - "@subwallet/chain-list": "0.2.87", + "@subwallet/chain-list": "0.2.89-beta.5", "@subwallet/extension-base": "^1.2.30-0", "@subwallet/extension-chains": "^1.2.30-0", "@subwallet/extension-dapp": "^1.2.30-0", diff --git a/packages/extension-koni-ui/src/Popup/Account/ConnectLedger.tsx b/packages/extension-koni-ui/src/Popup/Account/ConnectLedger.tsx index 40dc423bc8..227c66d7fe 100644 --- a/packages/extension-koni-ui/src/Popup/Account/ConnectLedger.tsx +++ b/packages/extension-koni-ui/src/Popup/Account/ConnectLedger.tsx @@ -307,6 +307,7 @@ const Component: React.FC = (props: Props) => { <>
= (props: Props) => { width={52} /> )} - innerSize={52} sizeLinkIcon={36} sizeSquircleBorder={108} /> diff --git a/packages/extension-koni-ui/src/components/Logo/DualLogo.tsx b/packages/extension-koni-ui/src/components/Logo/DualLogo.tsx index 8ba5122b2e..55b694ceec 100644 --- a/packages/extension-koni-ui/src/components/Logo/DualLogo.tsx +++ b/packages/extension-koni-ui/src/components/Logo/DualLogo.tsx @@ -33,13 +33,19 @@ const defaultLogo = ; const Component = ({ className, leftLogo = defaultLogo, linkIcon = defaultLinkIcon, rightLogo = defaultLogo, sizeSquircleBorder, innerSize }: Props) => { return (
- + {leftLogo}
{linkIcon}
- + {rightLogo}
diff --git a/packages/extension-koni/public/images/projects/uniswap.png b/packages/extension-koni/public/images/projects/uniswap.png new file mode 100644 index 0000000000..76ecd7a65d Binary files /dev/null and b/packages/extension-koni/public/images/projects/uniswap.png differ diff --git a/packages/extension-web-ui/package.json b/packages/extension-web-ui/package.json index 50868abee0..131d5801ef 100644 --- a/packages/extension-web-ui/package.json +++ b/packages/extension-web-ui/package.json @@ -35,7 +35,7 @@ "@polkadot/util-crypto": "^12.6.2", "@ramonak/react-progress-bar": "^5.0.3", "@reduxjs/toolkit": "^1.9.1", - "@subwallet/chain-list": "0.2.87", + "@subwallet/chain-list": "0.2.89-beta.5", "@subwallet/extension-base": "^1.2.30-0", "@subwallet/extension-chains": "^1.2.30-0", "@subwallet/extension-dapp": "^1.2.30-0", diff --git a/packages/extension-web-ui/src/Popup/Confirmations/index.tsx b/packages/extension-web-ui/src/Popup/Confirmations/index.tsx index 31a5b64c26..9bb3b139c6 100644 --- a/packages/extension-web-ui/src/Popup/Confirmations/index.tsx +++ b/packages/extension-web-ui/src/Popup/Confirmations/index.tsx @@ -119,7 +119,7 @@ const Component = function ({ className }: Props) { } } - if (confirmation.item.isInternal && confirmation.type !== 'connectWCRequest') { + if (confirmation.item.isInternal && !['connectWCRequest', 'evmSignatureRequest'].includes(confirmation.type)) { return ( = (props: Props) => { let promise: Promise<`0x${string}`>; if (isMessage) { - promise = evmWallet.request<`0x${string}`>({ method: payload.payload.type, params: [account.address, payload.payload.payload] }); + const address = account.smartAccountOwner || account.address; + + promise = evmWallet.request<`0x${string}`>({ method: payload.payload.type, params: [address, payload.payload.payload] }); } else { promise = new Promise<`0x${string}`>((resolve, reject) => { const { account, canSign, estimateGas, hashPayload, isToContract, parseData, ...transactionConfig } = payload.payload; @@ -214,7 +216,7 @@ const Component: React.FC = (props: Props) => { setLoading(false); }); } - }, [account.address, chainId, evmWallet, isMessage, notify, onApproveSignature, onCancel, payload.payload]); + }, [account.address, account.smartAccountOwner, chainId, evmWallet, isMessage, notify, onApproveSignature, onCancel, payload.payload]); const onConfirm = useCallback(() => { removeTransactionPersist(extrinsicType); diff --git a/packages/extension-web-ui/src/Popup/Home/History/Detail/index.tsx b/packages/extension-web-ui/src/Popup/Home/History/Detail/index.tsx index e2b0057df3..a127ab0fc7 100644 --- a/packages/extension-web-ui/src/Popup/Home/History/Detail/index.tsx +++ b/packages/extension-web-ui/src/Popup/Home/History/Detail/index.tsx @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import { ExtrinsicType, TransactionAdditionalInfo } from '@subwallet/extension-base/background/KoniTypes'; -import { getChainflipExplorerLink, getExplorerLink } from '@subwallet/extension-base/services/transaction-service/utils'; +import { CAProvider } from '@subwallet/extension-base/services/chain-abstraction-service/helper/util'; +import { getChainflipExplorerLink, getExplorerLink, getKlasterExplorerLink } from '@subwallet/extension-base/services/transaction-service/utils'; import { ChainflipSwapTxData, SwapProviderId, SwapTxData } from '@subwallet/extension-base/types/swap'; import { InfoItemBase } from '@subwallet/extension-web-ui/components'; import { BaseModal } from '@subwallet/extension-web-ui/components/Modal/BaseModal'; @@ -67,6 +68,12 @@ function Component ({ className = '', data, onCancel }: Props): React.ReactEleme } } + const caProvider = data.caProvider; + + if (caProvider === CAProvider.KLASTER) { + link = getKlasterExplorerLink(data.extrinsicHash); + } + return (