diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1e7ca650a2..d27a86416a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,10 +27,6 @@ jobs: - run: bun lint - run: bun type-check:ci - - name: Test - if: github.ref_name == 'nightly' - run: bun test:ci - publish: needs: [build-lint-test] runs-on: ubuntu-latest diff --git a/bun.lock b/bun.lock index 34cdced0a5..c7a58c00dc 100644 --- a/bun.lock +++ b/bun.lock @@ -75,9 +75,11 @@ "@swapkit/plugins": "workspace:*", "@swapkit/toolboxes": "workspace:*", "@swapkit/wallets": "workspace:*", + "cosmjs-types": "0.10.1", "ts-pattern": "^5.9.0", }, "devDependencies": { + "cosmjs-types": "0.10.1", "ts-pattern": "5.9.0", }, }, @@ -105,6 +107,8 @@ "name": "@swapkit/plugins", "version": "4.2.9", "dependencies": { + "@mysten/sui": "1.44.0", + "@near-js/transactions": "2.5.0", "@near-js/utils": "~2.5.0", "@polkadot/keyring": "~13.5.7", "@polkadot/util": "~13.5.7", @@ -131,6 +135,7 @@ "@swapkit/server": "workspace:*", "@swapkit/toolboxes": "workspace:*", "@swapkit/wallets": "workspace:*", + "cosmjs-types": "0.10.1", }, }, "packages/server": { @@ -293,6 +298,7 @@ "@swapkit/wallets": "workspace:*", "class-variance-authority": "0.7.1", "clsx": "2.1.1", + "cosmjs-types": "0.10.1", "lucide-react": "0.552.0", "react": "19.1.1", "react-hook-form": "7.65.0", @@ -324,6 +330,7 @@ "@swapkit/helpers": "workspace:*", "@swapkit/toolboxes": "workspace:*", "@swapkit/wallet-core": "workspace:*", + "cosmjs-types": "0.10.1", "ethers": "^6.14.0", "sats-connect": "~1.0.0", "ts-pattern": "~5.9.0", @@ -334,6 +341,7 @@ "@near-js/crypto": "2.5.0", "@near-js/transactions": "2.5.0", "@solana/web3.js": "1.98.4", + "cosmjs-types": "0.10.1", "ethers": "6.15.0", "sats-connect": "1.0.0", "ts-pattern": "5.9.0", @@ -3741,7 +3749,7 @@ "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], - "oniguruma-to-es": ["oniguruma-to-es@4.3.3", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg=="], + "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], "open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], @@ -5337,8 +5345,6 @@ "abi-wan-kanabi/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], - "ajv-formats/ajv": ["ajv@8.11.2", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg=="], - "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "aria-hidden/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], diff --git a/package.json b/package.json index ded3ac3eb0..db2f5fd391 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "xrpl": "4.4.2" }, "name": "swapkit-monorepo", - "packageManager": "bun@^1.3.1", + "packageManager": "bun@^1.3.2", "private": true, "scripts": { "bootstrap": "bun clean --force; bun build:ci", diff --git a/packages/core/package.json b/packages/core/package.json index 50b5424c8e..43ff1e8964 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -5,10 +5,11 @@ "@swapkit/plugins": "workspace:*", "@swapkit/toolboxes": "workspace:*", "@swapkit/wallets": "workspace:*", + "cosmjs-types": "0.10.1", "ts-pattern": "^5.9.0" }, "description": "SwapKit - Core", - "devDependencies": { "ts-pattern": "5.9.0" }, + "devDependencies": { "cosmjs-types": "0.10.1", "ts-pattern": "5.9.0" }, "exports": { ".": { "bun": "./src/index.ts", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 827c7caf50..97d4dd97ab 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -15,7 +15,9 @@ import { type SKConfigState, SwapKitError, type SwapParams, + supportsV3SwapFlow, UTXOChains, + type WalletOption, } from "@swapkit/helpers"; import type { EVMTransaction, QuoteResponseRoute } from "@swapkit/helpers/api"; import type { createPlugin } from "@swapkit/plugins"; @@ -202,6 +204,23 @@ export function SwapKit< } function swap({ route, pluginName, ...rest }: SwapParams) { + const fromChain = AssetValue.from({ asset: route.sellAsset }).chain; + const wallet = getWallet(fromChain); + + if (!wallet) { + throw new SwapKitError("core_wallet_connection_not_found"); + } + + const useV3Flow = supportsV3SwapFlow(wallet.walletType as WalletOption, fromChain) && route.tx; + + if (useV3Flow) { + const plugin = getSwapKitPlugin("trading"); + if ("swap" in plugin) { + // @ts-expect-error TODO: fix this + return plugin.swap({ ...rest, route }); + } + } + const plugin = getSwapKitPlugin(pluginName || route.providers[0]); if ("swap" in plugin) { diff --git a/packages/helpers/src/api/swapkitApi/endpoints.ts b/packages/helpers/src/api/swapkitApi/endpoints.ts index f95ee8f5fa..118d21a8de 100644 --- a/packages/helpers/src/api/swapkitApi/endpoints.ts +++ b/packages/helpers/src/api/swapkitApi/endpoints.ts @@ -1,4 +1,5 @@ import { + AssetValue, type Chain, type EVMChain, EVMChains, @@ -10,6 +11,7 @@ import { } from "@swapkit/helpers"; import { match, P } from "ts-pattern"; import { + type ApproveResponse, type BalanceResponse, type BrokerDepositChannelParams, type DepositChannelResponse, @@ -122,6 +124,26 @@ export async function getChainBalance({ return scamFilter ? filterAssets(balances) : balances; } +export function getTokenApproval( + params: { spender: string; userWallet: string; assetValue: AssetValue } | { routeId: string; spender: string }, +) { + return match(params) + .with({ routeId: P.string, spender: P.string }, ({ routeId, spender }) => { + const url = getApiUrl(`/approve?routeId=${routeId}&sourceAddress=${spender}`); + return SKRequestClient.get(url); + }) + .with( + { assetValue: P.instanceOf(AssetValue), spender: P.string, userWallet: P.string }, + ({ spender, userWallet, assetValue }) => { + const url = getApiUrl( + `/approve?tokenIdentifier=${assetValue.toString()}&userWalletAddress=${userWallet}&spender=${spender}&amount=${assetValue.getValue("string")}`, + ); + return SKRequestClient.get(url); + }, + ) + .exhaustive(); +} + export function getTokenListProviders() { const url = getApiUrl("/providers"); return SKRequestClient.get(url); diff --git a/packages/helpers/src/api/swapkitApi/types.ts b/packages/helpers/src/api/swapkitApi/types.ts index 52bca6e53b..a8b70d35e7 100644 --- a/packages/helpers/src/api/swapkitApi/types.ts +++ b/packages/helpers/src/api/swapkitApi/types.ts @@ -446,6 +446,21 @@ export const EVMTransactionSchema = object({ export type EVMTransaction = z.infer; +export const TronTransactionSchema = z.object({ + raw_data: z.object({ + contract: z.any(), + expiration: z.number(), + ref_block_bytes: z.string(), + ref_block_hash: z.string(), + timestamp: z.number(), + }), + raw_data_hex: z.string(), + txID: z.string(), + visible: z.boolean(), +}); + +export type TronTransaction = z.infer; + export const EVMTransactionDetailsParamsSchema = array( union([ string(), @@ -468,7 +483,36 @@ export const EVMTransactionDetailsSchema = object({ export type EVMTransactionDetails = z.infer; -const EncodeObjectSchema = object({ typeUrl: string(), value: unknown() }); +const ThorchainDepositMsgSchema = object({ + typeUrl: string("/types.MsgDeposit"), + value: object({ + coins: array( + object({ + amount: string(), + asset: object({ chain: string(), symbol: string(), synth: boolean(), ticker: string() }), + }), + ), + memo: string(), + signer: string(), + }), +}); + +export type ThorchainDepositMsg = z.infer; + +const CosmosSendMsgSchema = object({ + typeUrl: string("/types.MsgSend"), + value: object({ + amount: array(object({ amount: string(), denom: string() })), + fromAddress: string(), + toAddress: string(), + }), +}); + +export type CosmosSendMsg = z.infer; + +const EncodeObjectSchema = object({ typeUrl: string(), value: CosmosSendMsgSchema.or(ThorchainDepositMsgSchema) }); + +export type APICosmosEncodedObject = z.infer; const FeeSchema = object({ amount: array(object({ amount: string(), denom: string() })), gas: string() }); @@ -572,7 +616,7 @@ export const QuoteResponseRouteItem = object({ sourceAddress: string().describe("Source address"), targetAddress: optional(string().describe("Target address")), totalSlippageBps: number().describe("Total slippage in bps"), - tx: optional(union([EVMTransactionSchema, CosmosTransactionSchema, string()])), + tx: optional(union([EVMTransactionSchema, CosmosTransactionSchema, TronTransactionSchema, string()])), txType: optional(z.enum(RouteQuoteTxType)), warnings: RouteQuoteWarningSchema, }); @@ -622,3 +666,34 @@ const BalanceResponseSchema = array( ); export type BalanceResponse = z.infer; + +export const ApproveRequestParams = z.union([ + z.object({ amount: z.string(), spender: z.string(), tokenIdentifier: z.string(), userWalletAddress: z.string() }), + z.object({ + amount: z.string(), + chainId: z.enum(ChainId), + spender: z.string(), + tokenContractAddress: z.string(), + userWalletAddress: z.string(), + }), + z.object({ routeId: z.string() }), +]); + +export type ApproveRequest = z.infer; + +export const ApproveResponseSchema = z.object({ + approvalTransaction: z.optional( + z.object({ + data: z.string(), + from: z.string(), + gasLimit: z.optional(z.string()), + gasPrice: z.optional(z.string()), + to: z.string(), + value: z.string(), + }), + ), + approvedAmount: z.string(), + isApproved: z.boolean(), +}); + +export type ApproveResponse = z.infer; diff --git a/packages/helpers/src/modules/swapKitConfig.ts b/packages/helpers/src/modules/swapKitConfig.ts index 1daac680c4..561dc904ba 100644 --- a/packages/helpers/src/modules/swapKitConfig.ts +++ b/packages/helpers/src/modules/swapKitConfig.ts @@ -12,6 +12,15 @@ import type { BalanceResponse, QuoteRequest, QuoteResponse, QuoteResponseRoute } import { WalletOption } from "../types"; import type { FeeMultiplierConfig } from "./feeMultiplier"; +export type V3SwapFlowConfig = { + /** + * Global flag to enable/disable V3 swap flow (raw tx signing from API). + * When disabled, always falls back to named plugins. + * @default true + */ + enabled: boolean; +}; + export type SKConfigIntegrations = { chainflip?: { useSDKBroker?: boolean; brokerUrl: string }; coinbase?: { @@ -80,6 +89,7 @@ const initialState = { requestOptions: { retry: { backoffMultiplier: 2, baseDelay: 300, maxDelay: 5000, maxRetries: 3 }, timeoutMs: 30000 }, rpcUrls, + v3SwapFlow: { enabled: true } as V3SwapFlowConfig, wallets: Object.values(WalletOption), }; type SKState = typeof initialState; @@ -89,10 +99,11 @@ export type SKConfigState = { chains?: SKState["chains"]; endpoints?: Partial; envs?: Partial; + feeMultipliers?: FeeMultiplierConfig; integrations?: Partial; rpcUrls?: Partial; + v3SwapFlow?: Partial; wallets?: SKState["wallets"]; - feeMultipliers?: FeeMultiplierConfig; }; type SwapKitConfigStore = SKState & { @@ -122,6 +133,7 @@ export const useSwapKitStore = create((set) => ({ feeMultipliers: config?.feeMultipliers || s.feeMultipliers, integrations: { ...s.integrations, ...config?.integrations }, rpcUrls: { ...s.rpcUrls, ...config?.rpcUrls }, + v3SwapFlow: { ...s.v3SwapFlow, ...config?.v3SwapFlow }, wallets: s.wallets.concat(config?.wallets || []), })), setEndpoint: (key, endpoint) => set((s) => ({ endpoints: { ...s.endpoints, [key]: endpoint } })), @@ -149,6 +161,7 @@ export const useSwapKitConfig = () => feeMultipliers: state?.feeMultipliers, integrations: state?.integrations, rpcUrls: state?.rpcUrls, + v3SwapFlow: state?.v3SwapFlow, wallets: state?.wallets, })), ); diff --git a/packages/helpers/src/modules/swapKitError.ts b/packages/helpers/src/modules/swapKitError.ts index 5113648dee..2eb42a983f 100644 --- a/packages/helpers/src/modules/swapKitError.ts +++ b/packages/helpers/src/modules/swapKitError.ts @@ -409,6 +409,10 @@ const errorCodes = { * Garden Plugin */ plugin_garden_missing_data: 42001, + /** + * Swap Plugin + */ + plugin_generic_swap_invalid_data: 43001, /** * SwapKit API */ diff --git a/packages/helpers/src/types/sdk.ts b/packages/helpers/src/types/sdk.ts index 74ae0523d0..9b125b65ff 100644 --- a/packages/helpers/src/types/sdk.ts +++ b/packages/helpers/src/types/sdk.ts @@ -8,7 +8,10 @@ export type GenericSwapParams = { route: T; }; -export type SwapParams = GenericSwapParams & { pluginName?: PluginNames }; +export type SwapParams = GenericSwapParams & { + pluginName?: PluginNames; + useApiTx?: boolean; +}; export enum FeeOption { Average = "average", diff --git a/packages/helpers/src/types/wallet.ts b/packages/helpers/src/types/wallet.ts index abc93e7ccf..f642f6110a 100644 --- a/packages/helpers/src/types/wallet.ts +++ b/packages/helpers/src/types/wallet.ts @@ -1,4 +1,5 @@ -import type { Chain, getChainConfig } from "@swapkit/types"; +import type { getChainConfig } from "@swapkit/types"; +import { Chain } from "@swapkit/types"; import type { BrowserProvider, Eip1193Provider } from "ethers"; import type { AssetValue } from "../modules/assetValue"; @@ -101,7 +102,6 @@ export type EIP6963ProviderDetail = { info: EIP6963ProviderInfo; provider: Eip11 export type EIP6963Provider = { info: EIP6963ProviderInfo; provider: Eip1193Provider }; -// This type represents the structure of an event dispatched by a wallet to announce its presence based on EIP-6963. export type EIP6963AnnounceProviderEvent = Event & { detail: EIP6963Provider }; export type ChainSigner = { @@ -122,3 +122,96 @@ export type GenericCreateTransactionParams = Omit> = { + // EVM Chains - all EVM wallets support via eth_sendTransaction + [Chain.Arbitrum]: EVMWallets, + [Chain.Aurora]: EVMWallets, + [Chain.Avalanche]: EVMWallets, + [Chain.Base]: EVMWallets, + [Chain.Berachain]: EVMWallets, + [Chain.BinanceSmartChain]: EVMWallets, + [Chain.Botanix]: EVMWallets, + [Chain.Chainflip]: EVMWallets, + [Chain.Core]: EVMWallets, + [Chain.Corn]: EVMWallets, + [Chain.Cronos]: EVMWallets, + [Chain.Ethereum]: EVMWallets, + [Chain.Gnosis]: EVMWallets, + [Chain.Hyperevm]: EVMWallets, + [Chain.MegaETH]: EVMWallets, + [Chain.Monad]: EVMWallets, + [Chain.Optimism]: EVMWallets, + [Chain.Polygon]: EVMWallets, + [Chain.Sonic]: EVMWallets, + [Chain.Unichain]: EVMWallets, + [Chain.XLayer]: EVMWallets, + + // UTXO Chains - only wallets with PSBT signing + [Chain.Bitcoin]: [ + WalletOption.BITGET, + WalletOption.EXODUS, + WalletOption.KEYSTORE, + WalletOption.OKX, + WalletOption.ONEKEY, + WalletOption.PASSKEYS, + WalletOption.PHANTOM, + ], + [Chain.BitcoinCash]: [WalletOption.KEYSTORE], + [Chain.Dash]: [WalletOption.KEYSTORE], + [Chain.Dogecoin]: [WalletOption.KEYSTORE], + [Chain.Litecoin]: [WalletOption.KEYSTORE], + [Chain.Zcash]: [WalletOption.KEYSTORE], + + // Cosmos Chains - only wallets with proto signing + [Chain.Cosmos]: [WalletOption.KEYSTORE], + [Chain.Kujira]: [WalletOption.KEYSTORE], + [Chain.Maya]: [WalletOption.KEYSTORE], + [Chain.Noble]: [WalletOption.KEYSTORE], + [Chain.THORChain]: [WalletOption.KEYSTORE], + + // Other Chains + [Chain.Cardano]: [WalletOption.KEYSTORE], + [Chain.Near]: [WalletOption.KEYSTORE, WalletOption.LEDGER], + [Chain.Polkadot]: [WalletOption.KEYSTORE], + [Chain.Ripple]: [WalletOption.KEYSTORE, WalletOption.LEDGER], + [Chain.Solana]: [WalletOption.KEYSTORE, WalletOption.PASSKEYS, WalletOption.EXODUS, WalletOption.PHANTOM], + [Chain.Sui]: [WalletOption.KEYSTORE], + [Chain.Ton]: [WalletOption.KEYSTORE], + [Chain.Tron]: [WalletOption.KEYSTORE, WalletOption.LEDGER], +}; diff --git a/packages/helpers/src/utils/wallets.ts b/packages/helpers/src/utils/wallets.ts index 9015251e73..224e61a852 100644 --- a/packages/helpers/src/utils/wallets.ts +++ b/packages/helpers/src/utils/wallets.ts @@ -1,11 +1,13 @@ import { type Chain, getChainConfig } from "@swapkit/types"; import type { BrowserProvider, JsonRpcProvider } from "ethers"; +import { SKConfig } from "../modules/swapKitConfig"; import { SwapKitError } from "../modules/swapKitError"; import { type EIP6963AnnounceProviderEvent, type EIP6963Provider, type EthereumWindowProvider, type NetworkParams, + V3SwapFlowSupport, WalletOption, } from "../types"; import { warnOnce } from "./others"; @@ -235,3 +237,28 @@ export function providerRequest({ const providerParams = params ? (Array.isArray(params) ? params : [params]) : []; return provider.send(method, providerParams); } + +/** + * Check if a wallet supports V3 swap flow (raw tx signing from API) for a specific chain. + * + * The V3 swap flow allows signing raw transactions returned by the API instead of + * having plugins build transactions themselves. This is more efficient but requires + * wallet support for signing arbitrary transactions. + * + * Takes into account: + * 1. Global v3SwapFlow.enabled flag + * 2. Per-chain capability in V3SwapFlowSupport registry + * + * @param walletType - The wallet type (e.g., WalletOption.KEYSTORE) + * @param chain - The chain to check support for + * @returns true if the wallet supports V3 flow for the chain, false otherwise + */ +export function supportsV3SwapFlow(walletType: WalletOption, chain: Chain): boolean { + const config = SKConfig.get("v3SwapFlow"); + + if (!config?.enabled) { + return false; + } + + return V3SwapFlowSupport[chain]?.includes(walletType) ?? false; +} diff --git a/packages/plugins/package.json b/packages/plugins/package.json index fa417c653f..c136dfc9f0 100644 --- a/packages/plugins/package.json +++ b/packages/plugins/package.json @@ -1,6 +1,8 @@ { "author": "swapkit-oss", "dependencies": { + "@mysten/sui": "1.44.0", + "@near-js/transactions": "2.5.0", "@near-js/utils": "~2.5.0", "@polkadot/keyring": "~13.5.7", "@polkadot/util": "~13.5.7", @@ -20,51 +22,57 @@ "exports": { ".": { "bun": "./src/index.ts", - "default": "./dist/index.js", - "require": "./dist/index.cjs", + "default": "./dist/src/index.js", + "require": "./dist/src/index.cjs", "types": "./dist/types/index.d.ts" }, "./chainflip": { "bun": "./src/chainflip/index.ts", - "default": "./dist/chainflip/index.js", - "require": "./dist/chainflip/index.cjs", + "default": "./dist/src/chainflip/index.js", + "require": "./dist/src/chainflip/index.cjs", "types": "./dist/types/chainflip/index.d.ts" }, "./evm": { "bun": "./src/evm/index.ts", - "default": "./dist/evm/index.js", - "require": "./dist/evm/index.cjs", + "default": "./dist/src/evm/index.js", + "require": "./dist/src/evm/index.cjs", "types": "./dist/types/evm/index.d.ts" }, "./garden": { "bun": "./src/garden/index.ts", - "default": "./dist/garden/index.js", - "require": "./dist/garden/index.cjs", + "default": "./dist/src/garden/index.js", + "require": "./dist/src/garden/index.cjs", "types": "./dist/types/garden/index.d.ts" }, "./near": { "bun": "./src/near/index.ts", - "default": "./dist/near/index.js", - "require": "./dist/near/index.cjs", + "default": "./dist/src/near/index.js", + "require": "./dist/src/near/index.cjs", "types": "./dist/types/near/index.d.ts" }, "./radix": { "bun": "./src/radix/index.ts", - "default": "./dist/radix/index.js", - "require": "./dist/radix/index.cjs", + "default": "./dist/src/radix/index.js", + "require": "./dist/src/radix/index.cjs", "types": "./dist/types/radix/index.d.ts" }, "./solana": { "bun": "./src/solana/index.ts", - "default": "./dist/solana/index.js", - "require": "./dist/solana/index.cjs", + "default": "./dist/src/solana/index.js", + "require": "./dist/src/solana/index.cjs", "types": "./dist/types/solana/index.d.ts" }, "./thorchain": { "bun": "./src/thorchain/index.ts", - "default": "./dist/thorchain/index.js", - "require": "./dist/thorchain/index.cjs", + "default": "./dist/src/thorchain/index.js", + "require": "./dist/src/thorchain/index.cjs", "types": "./dist/types/thorchain/index.d.ts" + }, + "./trading": { + "bun": "./src/trading/index.ts", + "default": "./dist/src/trading/index.js", + "require": "./dist/src/trading/index.cjs", + "types": "./dist/types/trading/index.d.ts" } }, "files": ["dist/", "src/"], diff --git a/packages/plugins/src/index.ts b/packages/plugins/src/index.ts index 9f4b6d50d0..3ee1d6e4c4 100644 --- a/packages/plugins/src/index.ts +++ b/packages/plugins/src/index.ts @@ -27,6 +27,10 @@ export async function loadPlugin

(pluginName: P) { const { SolanaPlugin } = await import("./solana"); return SolanaPlugin; }) + .with("trading", async () => { + const { TradingPlugin } = await import("./trading"); + return TradingPlugin; + }) .with("near", async () => { const { NearPlugin } = await import("./near"); return NearPlugin; diff --git a/packages/plugins/src/near/plugin.ts b/packages/plugins/src/near/plugin.ts index bb2dba73a7..61b66afa54 100644 --- a/packages/plugins/src/near/plugin.ts +++ b/packages/plugins/src/near/plugin.ts @@ -194,7 +194,7 @@ export const NearPlugin = createPlugin({ sender: wallet.address, }); - return wallet.signAndSendTransaction(unsignedTransaction); + return wallet.signAndBroadcastTransaction(unsignedTransaction); } if (!wallet) { diff --git a/packages/plugins/src/trading/index.ts b/packages/plugins/src/trading/index.ts new file mode 100644 index 0000000000..14b07d679e --- /dev/null +++ b/packages/plugins/src/trading/index.ts @@ -0,0 +1 @@ +export { TradingPlugin } from "./plugin"; diff --git a/packages/plugins/src/trading/plugin.ts b/packages/plugins/src/trading/plugin.ts new file mode 100644 index 0000000000..eaf4e0fbd5 --- /dev/null +++ b/packages/plugins/src/trading/plugin.ts @@ -0,0 +1,113 @@ +import type { ZcashPsbt } from "@bitgo/utxo-lib/dist/src/bitgo"; +import { + ApproveMode, + AssetValue, + Chain, + CosmosChains, + EVMChains, + SwapKitError, + type SwapParams, +} from "@swapkit/helpers"; +import { + CosmosTransactionSchema, + EVMTransactionSchema, + type QuoteResponseRoute, + TronTransactionSchema, +} from "@swapkit/helpers/api"; +import { match, P } from "ts-pattern"; +import { approve, createPlugin } from "../utils"; + +const isEVMTransaction = (tx: unknown) => EVMTransactionSchema.safeParse(tx).success; +const isTronTransaction = (tx: unknown) => TronTransactionSchema.safeParse(tx).success; +const isCosmosTransaction = (tx: unknown) => CosmosTransactionSchema.safeParse(tx).success; + +export const TradingPlugin = createPlugin({ + methods: ({ getWallet }) => ({ + approveAssetValue: approve({ approveMode: ApproveMode.Approve, getWallet }), + isAssetValueApproved: approve({ approveMode: ApproveMode.CheckOnly, getWallet }), + swap: function swap({ route }: SwapParams<"trading", QuoteResponseRoute>) { + const { sellAsset, tx } = route; + const sellAssetValue = AssetValue.from({ asset: sellAsset }); + const chain = sellAssetValue.chain; + + return match({ chain, tx }) + .returnType>() + .with( + { chain: P.union(Chain.Bitcoin, Chain.Dogecoin, Chain.Litecoin, Chain.Dash), tx: P.string }, + async ({ chain, tx }) => { + const { Psbt } = await import("bitcoinjs-lib"); + const wallet = await getWallet(chain); + const psbt = Psbt.fromBase64(tx); + if (chain === Chain.Dogecoin) psbt.setMaximumFeeRate(650000000); + + return wallet.signAndBroadcastTransaction(psbt); + }, + ) + .with({ chain: Chain.BitcoinCash, tx: P.string }, async ({ chain, tx }) => { + const { UtxoPsbt } = await import("@bitgo/utxo-lib/dist/src/bitgo"); + const { networks } = await import("@bitgo/utxo-lib"); + const wallet = await getWallet(chain); + const psbt = UtxoPsbt.fromBuffer(Buffer.from(tx, "base64"), { network: networks.bitcoincash }); + + return wallet.signAndBroadcastTransaction(psbt); + }) + .with({ chain: Chain.Zcash, tx: P.string }, async ({ chain, tx }) => { + const { ZcashPsbt } = await import("@bitgo/utxo-lib/dist/src/bitgo"); + const { networks } = await import("@bitgo/utxo-lib"); + const wallet = await getWallet(chain); + + const psbt = ZcashPsbt.fromBuffer(Buffer.from(tx, "base64"), { network: networks.zcash }); + + return wallet.signAndBroadcastTransaction(psbt as ZcashPsbt); + }) + .with({ chain: P.union(...EVMChains), tx: P.when(isEVMTransaction) }, async ({ chain, tx }) => { + const wallet = await getWallet(chain); + const transaction = EVMTransactionSchema.parse(tx); + + return wallet.sendTransaction({ ...transaction, value: BigInt(transaction.value || "0") }); + }) + .with({ chain: Chain.Solana, tx: P.string }, async ({ chain, tx }) => { + const { VersionedTransaction } = await import("@solana/web3.js"); + const wallet = await getWallet(chain); + + const transaction = VersionedTransaction.deserialize(Buffer.from(tx, "base64")); + return wallet.signAndBroadcastTransaction(transaction); + }) + .with({ chain: P.union(...CosmosChains), tx: P.when(isCosmosTransaction) }, async ({ chain, tx }) => { + const wallet = await getWallet(chain); + const transaction = CosmosTransactionSchema.parse(tx); + + return wallet.signAndBroadcastTransaction(transaction); + }) + .with({ chain: Chain.Near, tx: P.string }, async ({ chain, tx }) => { + const { Transaction } = await import("@near-js/transactions"); + const wallet = await getWallet(chain); + + const transaction = Transaction.decode(Buffer.from(tx, "base64")); + + return wallet.signAndBroadcastTransaction(transaction); + }) + .with({ chain: Chain.Ripple, tx: P.string }, async ({ chain, tx }) => { + const wallet = await getWallet(chain); + + return wallet.signAndBroadcastTransaction(JSON.parse(tx)); + }) + .with({ chain: Chain.Tron, tx: P.when(isTronTransaction) }, async ({ chain, tx }) => { + const wallet = await getWallet(chain); + const transaction = TronTransactionSchema.parse(tx); + + return wallet.signAndBroadcastTransaction(transaction); + }) + .with({ chain: Chain.Sui, tx: P.string }, async ({ chain, tx }) => { + const wallet = await getWallet(chain); + + return wallet.signAndBroadcastTransaction(tx); + }) + .otherwise(() => { + throw new SwapKitError("plugin_generic_swap_invalid_data", { chain, tx }); + }); + }, + }), + name: "trading", + properties: { supportedSwapkitProviders: [] }, +}); diff --git a/packages/plugins/src/types.ts b/packages/plugins/src/types.ts index 5266abfdc5..73c3600308 100644 --- a/packages/plugins/src/types.ts +++ b/packages/plugins/src/types.ts @@ -6,6 +6,7 @@ import type { NearPlugin } from "./near"; import type { RadixPlugin } from "./radix"; import type { SolanaPlugin } from "./solana/plugin"; import type { ThorchainPlugin } from "./thorchain"; +import type { TradingPlugin } from "./trading"; export type * from "./chainflip/types"; export type * from "./thorchain/types"; @@ -15,7 +16,8 @@ export type SKPlugins = typeof ChainflipPlugin & typeof RadixPlugin & typeof SolanaPlugin & typeof EVMPlugin & - typeof NearPlugin; + typeof NearPlugin & + typeof TradingPlugin; export type PluginName = keyof SKPlugins; diff --git a/packages/plugins/src/utils.ts b/packages/plugins/src/utils.ts index b8f5affd72..3ababc890c 100644 --- a/packages/plugins/src/utils.ts +++ b/packages/plugins/src/utils.ts @@ -1,5 +1,7 @@ import type { ApproveMode, ApproveReturnType, EVMChain, ProviderName } from "@swapkit/helpers"; -import { type AssetValue, EVMChains, SwapKitError } from "@swapkit/helpers"; +import { AssetValue, EVMChains, SwapKitError } from "@swapkit/helpers"; +import { type QuoteResponseRoute, SwapKitApi } from "@swapkit/helpers/api"; +import { match, P } from "ts-pattern"; import type { SwapKitPluginParams } from "./types"; export function createPlugin< @@ -15,28 +17,57 @@ export function createPlugin< } export function approve({ approveMode, getWallet }: { approveMode: T } & SwapKitPluginParams) { - return function approve({ assetValue, spenderAddress }: { spenderAddress: string; assetValue: AssetValue }) { - const evmChain = assetValue.chain as EVMChain; - const isEVMChain = EVMChains.includes(evmChain); - const isNativeEVM = isEVMChain && assetValue.isGasAsset; - - if (isNativeEVM || !isEVMChain || assetValue.isSynthetic) { - const isApproved = approveMode === "checkOnly" || "approved"; - return Promise.resolve(isApproved) as ApproveReturnType; - } - - const wallet = getWallet(evmChain); - const walletAction = approveMode === "checkOnly" ? wallet.isApproved : wallet.approve; - - if (!(assetValue.address && wallet.address)) { - throw new SwapKitError("core_approve_asset_address_or_from_not_found"); - } - - return walletAction({ - amount: assetValue.getBaseValue("bigint"), - assetAddress: assetValue.address, - from: wallet.address, - spenderAddress, - }); + return function approve(params: { spenderAddress: string; assetValue: AssetValue; route?: QuoteResponseRoute }) { + return match(params) + .with({ route: P.not(P.nullish), spenderAddress: P.string }, async ({ route, spenderAddress }) => { + const assetValue = AssetValue.from({ asset: route.sellAsset, value: route.sellAmount }); + const isEVMChain = EVMChains.includes(assetValue.chain as EVMChain); + const isNativeEVM = isEVMChain && assetValue.isGasAsset; + if (isNativeEVM || !isEVMChain || assetValue.isSynthetic || approveMode === "checkOnly") { + return true; + } + + const response = await SwapKitApi.getTokenApproval({ routeId: route.routeId, spender: spenderAddress }); + if (approveMode === "checkOnly") { + return response.isApproved; + } + + if (!response.approvalTransaction) { + throw new SwapKitError("core_approve_asset_target_invalid"); + } + + const wallet = getWallet(assetValue.chain as EVMChain); + return wallet.sendTransaction({ + data: response.approvalTransaction.data, + from: response.approvalTransaction.from, + to: response.approvalTransaction.to, + value: BigInt(assetValue.getBaseValue("bigint") || "0"), + }); + }) + .otherwise(({ spenderAddress, assetValue }) => { + const evmChain = assetValue.chain as EVMChain; + const isEVMChain = EVMChains.includes(evmChain); + const isNativeEVM = isEVMChain && assetValue.isGasAsset; + + if (isNativeEVM || !isEVMChain || assetValue.isSynthetic) { + const isApproved = approveMode === "checkOnly" ? true : "approved"; + return Promise.resolve(isApproved) as ApproveReturnType; + } + + const wallet = getWallet(evmChain); + + const walletAction = approveMode === "checkOnly" ? wallet.isApproved : wallet.approve; + + if (!(assetValue.address && wallet.address)) { + throw new SwapKitError("core_approve_asset_address_or_from_not_found"); + } + + return walletAction({ + amount: assetValue.getBaseValue("bigint"), + assetAddress: assetValue.address, + from: wallet.address, + spenderAddress, + }); + }); }; } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 974d07b20d..9ddc20f2ac 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -6,7 +6,8 @@ "@swapkit/plugins": "workspace:*", "@swapkit/server": "workspace:*", "@swapkit/toolboxes": "workspace:*", - "@swapkit/wallets": "workspace:*" + "@swapkit/wallets": "workspace:*", + "cosmjs-types": "0.10.1" }, "description": "SwapKit - SDK", "exports": { diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 580e586973..4b10834ffc 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -11,6 +11,7 @@ import { NearPlugin } from "@swapkit/plugins/near"; import { RadixPlugin } from "@swapkit/plugins/radix"; import { SolanaPlugin } from "@swapkit/plugins/solana"; import { MayachainPlugin, ThorchainPlugin } from "@swapkit/plugins/thorchain"; +import { TradingPlugin } from "@swapkit/plugins/trading"; import type { createWallet } from "@swapkit/wallets"; import { bitgetWallet } from "@swapkit/wallets/bitget"; @@ -92,6 +93,7 @@ export const defaultPlugins = { ...SolanaPlugin, ...NearPlugin, ...GardenPlugin, + ...TradingPlugin, }; export const defaultWallets = { diff --git a/packages/toolboxes/src/cosmos/toolbox/cosmos.ts b/packages/toolboxes/src/cosmos/toolbox/cosmos.ts index aaa4c96fcc..cc98634477 100644 --- a/packages/toolboxes/src/cosmos/toolbox/cosmos.ts +++ b/packages/toolboxes/src/cosmos/toolbox/cosmos.ts @@ -21,7 +21,8 @@ import { type TCLikeChain, updateDerivationPath, } from "@swapkit/helpers"; -import { SwapKitApi } from "@swapkit/helpers/api"; +import { type CosmosTransaction, SwapKitApi } from "@swapkit/helpers/api"; +import type { TxRaw } from "cosmjs-types/cosmos/tx/v1beta1/tx"; import { match, P } from "ts-pattern"; import type { CosmosToolboxParams } from "../types"; import { @@ -136,54 +137,85 @@ export async function createCosmosToolbox({ chain, ...toolboxParams }: CosmosToo return base64.encode(account?.pubkey); } - async function signTransaction({ - recipient, - assetValue, - memo = "", - feeRate, - feeOptionKey = FeeOption.Fast, - }: GenericTransferParams) { + async function signTransaction(transaction: CosmosTransaction): Promise { const from = await getAddress(); if (!(signer && from)) { throw new SwapKitError("toolbox_cosmos_signer_not_defined"); } - const feeAssetValue = AssetValue.from({ chain }); - const assetDenom = getDenomWithChain(feeAssetValue); - const txFee = feeRate || feeToStdFee((await getFees(chain, SafeDefaultFeeValues[chain]))[feeOptionKey], assetDenom); - const signingClient = await createSigningStargateClient(rpcUrl, signer); - const denom = getMsgSendDenom(assetValue.symbol); - const amount = assetValue.getBaseValue("string"); - const { TxRaw } = await import("cosmjs-types/cosmos/tx/v1beta1/tx"); + const txRaw = await signingClient.sign(from, transaction.msgs, transaction.fee, transaction.memo, { + accountNumber: transaction.accountNumber, + chainId: transaction.chainId, + sequence: transaction.sequence, + }); + + return txRaw; + } + + async function signAndBroadcastTransaction(transaction: CosmosTransaction) { + const from = await getAddress(); + + if (!(signer && from)) { + throw new SwapKitError("toolbox_cosmos_signer_not_defined"); + } + + const signingClient = await createSigningStargateClient(rpcUrl, signer); - const msgSend = { amount: [{ amount, denom }], fromAddress: from, toAddress: recipient }; + const result = await signingClient.signAndBroadcast(from, transaction.msgs, transaction.fee, transaction.memo); - const txRaw = await signingClient.sign( - from, - [{ typeUrl: "/cosmos.bank.v1beta1.MsgSend", value: msgSend }], - txFee as StdFee, - memo, - ); + if (result.code !== 0) { + throw new SwapKitError("core_swap_transaction_error", { code: result.code, message: result.rawLog }); + } - const txBytes = TxRaw.encode(txRaw).finish(); - return Buffer.from(txBytes).toString("hex"); + return result.transactionHash; } + // async function transfer({ + // recipient, + // assetValue, + // memo = "", + // feeRate, + // feeOptionKey = FeeOption.Fast, + // }: GenericTransferParams) { + // const from = await getAddress(); + + // if (!(signer && from)) { + // throw new SwapKitError("toolbox_cosmos_signer_not_defined"); + // } + + // const feeAssetValue = AssetValue.from({ chain }); + // const assetDenom = getDenomWithChain(feeAssetValue); + // const txFee = feeRate || feeToStdFee((await getFees(chain, SafeDefaultFeeValues[chain]))[feeOptionKey], assetDenom); + + // const signingClient = await createSigningStargateClient(rpcUrl, signer); + // const denom = getMsgSendDenom(assetValue.symbol); + // const amount = assetValue.getBaseValue("string"); + + // const { TxRaw } = await import("cosmjs-types/cosmos/tx/v1beta1/tx"); + + // const msgSend = { amount: [{ amount, denom }], fromAddress: from, toAddress: recipient }; + + // const txRaw = await signingClient.sign( + // from, + // [{ typeUrl: "/cosmos.bank.v1beta1.MsgSend", value: msgSend }], + // txFee as StdFee, + // memo, + // ); + + // const txBytes = TxRaw.encode(txRaw).finish(); + // return Buffer.from(txBytes).toString("hex"); + // } + async function transfer({ recipient, assetValue, memo = "", feeRate, feeOptionKey = FeeOption.Fast, - dryRun = false, - }: GenericTransferParams & { dryRun?: boolean }) { - if (dryRun) { - return signTransaction({ assetValue, feeOptionKey, feeRate, memo, recipient }); - } - + }: GenericTransferParams) { const from = await getAddress(); if (!(signer && from)) { @@ -246,6 +278,9 @@ export async function createCosmosToolbox({ chain, ...toolboxParams }: CosmosToo importedSigning.DirectSecp256k1Wallet ?? importedSigning.default?.DirectSecp256k1Wallet; return DirectSecp256k1Wallet.fromKey(privateKey, chainPrefix); }, + + signAndBroadcastTransaction, + signer, signTransaction, transfer, validateAddress: getCosmosValidateAddress(chain), diff --git a/packages/toolboxes/src/cosmos/toolbox/thorchain.ts b/packages/toolboxes/src/cosmos/toolbox/thorchain.ts index 9d288a4024..a618b9c207 100644 --- a/packages/toolboxes/src/cosmos/toolbox/thorchain.ts +++ b/packages/toolboxes/src/cosmos/toolbox/thorchain.ts @@ -242,6 +242,7 @@ export async function createThorchainToolbox({ chain, ...toolboxParams }: Cosmos derivationPath: derivationPathToString(derivationPath), prefix: chainPrefix, }), + signer, signMultisigTx: signMultisigTx(chain), signWithPrivateKey, transfer, diff --git a/packages/toolboxes/src/near/toolbox.ts b/packages/toolboxes/src/near/toolbox.ts index 5e8097e6be..7206fb93f5 100644 --- a/packages/toolboxes/src/near/toolbox.ts +++ b/packages/toolboxes/src/near/toolbox.ts @@ -116,7 +116,6 @@ export async function getNearToolbox(toolboxParams?: NearToolboxParams) { const { assetValue, recipient, memo } = params; const sender = await getAddress(); - // Handle NEP-141 token transfers - check if recipient needs storage if (!assetValue.isGasAsset && assetValue.address) { const storageBalance = await checkStorageBalance({ accountId: recipient, contractId: assetValue.address }); @@ -125,9 +124,8 @@ export async function getNearToolbox(toolboxParams?: NearToolboxParams) { } } - // Standard transfer (native NEAR or token with registered storage) const transaction = await createTransaction({ ...params, sender }); - return signAndSendTransaction(transaction); + return signAndBroadcastTransaction(transaction); } async function createTransaction(params: NearCreateTransactionParams) { @@ -236,7 +234,7 @@ export async function getNearToolbox(toolboxParams?: NearToolboxParams) { return result.transaction.hash; } - async function signAndSendTransaction(transaction: Transaction) { + async function signAndBroadcastTransaction(transaction: Transaction) { if (!signer) { throw new SwapKitError("toolbox_near_no_signer"); } @@ -413,7 +411,8 @@ export async function getNearToolbox(toolboxParams?: NearToolboxParams) { getSignerFromPrivateKey: getNearSignerFromPrivateKey, provider, serializeTransaction, - signAndSendTransaction, + signAndBroadcastTransaction, + signer, signTransaction, transfer, validateAddress: await getValidateNearAddress(), diff --git a/packages/toolboxes/src/near/types/index.ts b/packages/toolboxes/src/near/types/index.ts new file mode 100644 index 0000000000..d14bed5046 --- /dev/null +++ b/packages/toolboxes/src/near/types/index.ts @@ -0,0 +1,3 @@ +export * from "./contract"; +export * from "./nep141"; +export * from "./toolbox"; diff --git a/packages/toolboxes/src/ripple/index.ts b/packages/toolboxes/src/ripple/index.ts index 75c130ef0f..a2952e0a02 100644 --- a/packages/toolboxes/src/ripple/index.ts +++ b/packages/toolboxes/src/ripple/index.ts @@ -149,6 +149,16 @@ export const getRippleToolbox = async (params: RippleToolboxParams = {}) => { throw new SwapKitError({ errorKey: "toolbox_ripple_broadcast_error", info: { chain: Chain.Ripple } }); }; + const signAndBroadcastTransaction = async (tx: Transaction) => { + try { + const signedTx = await signTransaction(tx); + + return await broadcastTransaction(signedTx.tx_blob); + } catch (error) { + throw new SwapKitError({ errorKey: "toolbox_ripple_broadcast_error", info: { chain: Chain.Ripple, error } }); + } + }; + const transfer = async (params: GenericTransferParams) => { if (!signer) { throw new SwapKitError({ errorKey: "toolbox_ripple_signer_not_found" }); @@ -163,15 +173,14 @@ export const getRippleToolbox = async (params: RippleToolboxParams = {}) => { return { broadcastTransaction, - createSigner, // Expose the helper + createSigner, createTransaction, disconnect, estimateTransactionFee, - // Core methods getAddress, getBalance, - // Signer related - signer, // Expose the signer instance if created/provided + signAndBroadcastTransaction, + signer, signTransaction, transfer, validateAddress: rippleValidateAddress, diff --git a/packages/toolboxes/src/solana/index.ts b/packages/toolboxes/src/solana/index.ts index 3464027692..1ed96609ff 100644 --- a/packages/toolboxes/src/solana/index.ts +++ b/packages/toolboxes/src/solana/index.ts @@ -2,18 +2,6 @@ import type { PublicKey, Transaction, VersionedTransaction } from "@solana/web3. import type { GenericCreateTransactionParams, GenericTransferParams } from "@swapkit/helpers"; import type { getSolanaToolbox } from "./toolbox"; -// type DisplayEncoding = "utf8" | "hex"; - -// type PhantomRequestMethod = -// | "connect" -// | "disconnect" -// | "signAndSendTransaction" -// | "signAndSendTransactionV0" -// | "signAndSendTransactionV0WithLookupTable" -// | "signTransaction" -// | "signAllTransactions" -// | "signMessage"; - interface ConnectOpts { onlyIfTrusted: boolean; } diff --git a/packages/toolboxes/src/solana/toolbox.ts b/packages/toolboxes/src/solana/toolbox.ts index 35df116606..336c0505eb 100644 --- a/packages/toolboxes/src/solana/toolbox.ts +++ b/packages/toolboxes/src/solana/toolbox.ts @@ -143,6 +143,11 @@ export async function getSolanaToolbox( getBalance, getConnection, getPubkeyFromAddress, + signAndBroadcastTransaction: async (transaction: Transaction | VersionedTransaction) => { + const signedTx = await signTransaction(getConnection, signer)(transaction); + return broadcastTransaction(getConnection)(signedTx); + }, + signer, signTransaction: signTransaction(getConnection, signer), transfer: transfer(getConnection, signer), }; @@ -360,10 +365,10 @@ function broadcastTransaction(getConnection: () => Promise) { function signTransaction(getConnection: () => Promise, signer?: SolanaSigner) { return async (transaction: Transaction | VersionedTransaction) => { - const { VersionedTransaction } = await import("@solana/web3.js"); if (!signer) { throw new SwapKitError("toolbox_solana_no_signer"); } + const { VersionedTransaction } = await import("@solana/web3.js"); if (!(transaction instanceof VersionedTransaction)) { const connection = await getConnection(); diff --git a/packages/toolboxes/src/sui/toolbox.ts b/packages/toolboxes/src/sui/toolbox.ts index 72797a1e74..786e88707c 100644 --- a/packages/toolboxes/src/sui/toolbox.ts +++ b/packages/toolboxes/src/sui/toolbox.ts @@ -1,3 +1,4 @@ +import type { Transaction } from "@mysten/sui/transactions"; import { AssetValue, Chain, getChainConfig, SwapKitError } from "@swapkit/helpers"; import { match, P } from "ts-pattern"; import type { SuiCreateTransactionParams, SuiToolboxParams, SuiTransferParams } from "./types"; @@ -154,11 +155,37 @@ export async function getSuiToolbox({ provider: providerParam, ...signerParams } return txHash; } + async function broadcastTransaction(signedTransaction: { bytes: string; signature: string }) { + const suiClient = await getSuiClient(); + const { digest: txHash } = await suiClient.executeTransactionBlock({ + signature: signedTransaction.signature, + transactionBlock: signedTransaction.bytes, + }); + + return txHash; + } + + async function signAndBroadcastTransaction(transaction: Transaction | Uint8Array | string) { + if (!signer) { + throw new SwapKitError("toolbox_sui_no_signer"); + } + + const suiClient = await getSuiClient(); + + const txBytes = typeof transaction === "string" ? Uint8Array.from(Buffer.from(transaction, "base64")) : transaction; + + const { digest: txHash } = await suiClient.signAndExecuteTransaction({ signer, transaction: txBytes }); + + return txHash; + } + return { + broadcastTransaction, createTransaction, estimateTransactionFee, getAddress, getBalance, + signAndBroadcastTransaction, signTransaction, transfer, validateAddress, diff --git a/packages/toolboxes/src/tron/toolbox.ts b/packages/toolboxes/src/tron/toolbox.ts index ff26008462..dd822c9d50 100644 --- a/packages/toolboxes/src/tron/toolbox.ts +++ b/packages/toolboxes/src/tron/toolbox.ts @@ -117,6 +117,8 @@ export const createTronToolbox = async ( transfer: (params: TronTransferParams) => Promise; estimateTransactionFee: (params: TronTransferParams & { sender?: string }) => Promise; createTransaction: (params: TronCreateTransactionParams) => Promise; + signAndBroadcastTransaction: (transaction: TronTransaction) => Promise; + signer: TronSigner | undefined; signTransaction: (transaction: TronTransaction) => Promise; broadcastTransaction: (signedTransaction: TronSignedTransaction) => Promise; approve: (params: TronApproveParams) => Promise; @@ -504,6 +506,11 @@ export const createTronToolbox = async ( return txid; }; + const signAndBroadcastTransaction = async (transaction: TronTransaction) => { + const signedTx = await signTransaction(transaction); + return await broadcastTransaction(signedTx); + }; + const getApprovedAmount = async ({ assetAddress, spenderAddress, from }: TronApprovedParams) => { try { const contract = tronWeb.contract(trc20ABI, assetAddress); @@ -577,6 +584,8 @@ export const createTronToolbox = async ( getApprovedAmount, getBalance, isApproved, + signAndBroadcastTransaction, + signer, signTransaction, transfer, tronWeb, diff --git a/packages/toolboxes/src/utxo/toolbox/bitcoinCash.ts b/packages/toolboxes/src/utxo/toolbox/bitcoinCash.ts index 2932b6dc55..9db542a96e 100644 --- a/packages/toolboxes/src/utxo/toolbox/bitcoinCash.ts +++ b/packages/toolboxes/src/utxo/toolbox/bitcoinCash.ts @@ -1,9 +1,5 @@ -import { - address as bchAddress, - Transaction, - TransactionBuilder, - // @ts-expect-error -} from "@psf/bitcoincashjs-lib"; +import { bitgo, networks } from "@bitgo/utxo-lib"; +import type { UtxoPsbt } from "@bitgo/utxo-lib/dist/src/bitgo"; import { Chain, type ChainSigner, @@ -14,45 +10,39 @@ import { SwapKitError, updateDerivationPath, } from "@swapkit/helpers"; -import { Psbt } from "bitcoinjs-lib"; -import { accumulative, compileMemo, getUtxoApi, getUtxoNetwork, toCashAddress, toLegacyAddress } from "../helpers"; -import type { - BchECPair, - TargetOutput, - TransactionBuilderType, - TransactionType, - UTXOBuildTxParams, - UTXOTransferParams, - UTXOType, -} from "../types"; + +import { accumulative, compileMemo, getUtxoApi, toCashAddress, toLegacyAddress } from "../helpers"; +import type { TargetOutput, UTXOBuildTxParams, UTXOTransferParams, UTXOType } from "../types"; import type { UtxoToolboxParams } from "./params"; -import { createUTXOToolbox, getCreateKeysForPath } from "./utxo"; +import { addressFromKeysGetter, createUTXOToolbox, getCreateKeysForPath } from "./utxo"; import { bchValidateAddress, stripPrefix } from "./validators"; +type Psbt = UtxoPsbt; + const chain = Chain.BitcoinCash; +const network = networks.bitcoincash; export function stripToCashAddress(address: string) { return stripPrefix(toCashAddress(address)); } -function createSignerWithKeys(keys: BchECPair) { - function signTransaction({ builder, utxos }: { builder: TransactionBuilderType; utxos: UTXOType[] }) { - utxos.forEach((utxo, index) => { - builder.sign(index, keys, undefined, 0x41, utxo.witnessUtxo?.value); - }); +async function createSignerWithKeys({ phrase, derivationPath }: { phrase: string; derivationPath: string }) { + const keyPair = (await getCreateKeysForPath(chain))({ derivationPath, phrase }); - return builder.build(); + async function signTransaction(psbt: Psbt) { + await psbt.signAllInputs(keyPair); + return psbt; } - const getAddress = () => { - const address = keys.getAddress(0); - return Promise.resolve(stripToCashAddress(address)); - }; + async function getAddress() { + const addressGetter = await addressFromKeysGetter(chain); + return addressGetter(keyPair); + } return { getAddress, signTransaction }; } -export async function createBCHToolbox( +export async function createBCHToolbox( toolboxParams: UtxoToolboxParams[T] | { phrase?: string; derivationPath?: DerivationPathArray; index?: number }, ) { const phrase = "phrase" in toolboxParams ? toolboxParams.phrase : undefined; @@ -65,9 +55,11 @@ export async function createBCHToolbox( : updateDerivationPath(NetworkDerivationPath[chain], { index }), ); - const keys = phrase ? (await getCreateKeysForPath(chain))({ derivationPath, phrase }) : undefined; - - const signer = keys ? createSignerWithKeys(keys) : "signer" in toolboxParams ? toolboxParams.signer : undefined; + const signer = phrase + ? await createSignerWithKeys({ derivationPath, phrase }) + : "signer" in toolboxParams + ? toolboxParams.signer + : undefined; function getAddress() { return Promise.resolve(signer?.getAddress()); @@ -79,15 +71,31 @@ export async function createBCHToolbox( return getBalance(stripPrefix(toCashAddress(address))); } + async function signTransaction(psbt: Psbt) { + if (!signer) throw new SwapKitError("toolbox_utxo_no_signer"); + const signedTx = await signer.signTransaction(psbt); + return signedTx; + } + + async function signAndBroadcastTransaction(psbt: Psbt): Promise { + if (!signer) throw new SwapKitError("toolbox_utxo_no_signer"); + const signedTx = await signer.signTransaction(psbt); + const txHex = signedTx.toHex(); + return broadcastTx(txHex); + } + return { ...toolbox, broadcastTx, - buildTx, + // buildTx, createTransaction, getAddress, getAddressFromKeys, getBalance: handleGetBalance, getFeeRates, + signAndBroadcastTransaction, + signer, + signTransaction, stripPrefix, stripToCashAddress, transfer: transfer({ broadcastTx, getFeeRates, signer }), @@ -95,54 +103,6 @@ export async function createBCHToolbox( }; } -async function createTransaction({ assetValue, recipient, memo, feeRate, sender }: UTXOBuildTxParams) { - if (!bchValidateAddress(recipient)) throw new SwapKitError("toolbox_utxo_invalid_address", { address: recipient }); - - // Overestimate by 7500 byte * feeRate to ensure we have enough UTXOs for fees and change - const targetValue = Math.ceil(assetValue.getBaseValue("number") + feeRate * 7500); - - const utxos = await getUtxoApi(chain).getUtxos({ - address: stripToCashAddress(sender), - fetchTxHex: true, - targetValue, - }); - - const compiledMemo = memo ? await compileMemo(memo) : null; - - const targetOutputs: TargetOutput[] = []; - // output to recipient - targetOutputs.push({ address: recipient, value: assetValue.getBaseValue("number") }); - const { inputs, outputs } = accumulative({ chain, feeRate, inputs: utxos, outputs: targetOutputs }); - - // .inputs and .outputs will be undefined if no solution was found - if (!(inputs && outputs)) throw new SwapKitError("toolbox_utxo_insufficient_balance", { assetValue, sender }); - const getNetwork = await getUtxoNetwork(); - const builder = new TransactionBuilder(getNetwork(chain)) as TransactionBuilderType; - - await Promise.all( - inputs.map(async (utxo: UTXOType) => { - const txHex = await getUtxoApi(chain).getRawTx(utxo.hash); - - builder.addInput(Transaction.fromBuffer(Buffer.from(txHex, "hex")), utxo.index); - }), - ); - - for (const output of outputs) { - const address = "address" in output && output.address ? output.address : toLegacyAddress(sender); - const getNetwork = await getUtxoNetwork(); - const outputScript = bchAddress.toOutputScript(toLegacyAddress(address), getNetwork(chain)); - - builder.addOutput(outputScript, output.value); - } - - // add output for memo - if (compiledMemo) { - builder.addOutput(compiledMemo, 0); // Add OP_RETURN {script, value} - } - - return { builder, utxos: inputs }; -} - function transfer({ broadcastTx, getFeeRates, @@ -150,7 +110,7 @@ function transfer({ }: { broadcastTx: (txHash: string) => Promise; getFeeRates: () => Promise>; - signer?: ChainSigner<{ builder: TransactionBuilderType; utxos: UTXOType[] }, TransactionType>; + signer?: ChainSigner; }) { return async function transfer({ recipient, @@ -166,68 +126,68 @@ function transfer({ const feeRate = rest.feeRate || (await getFeeRates())[feeOptionKey]; // try out if psbt tx is faster/better/nicer - const { builder, utxos } = await createTransaction({ ...rest, assetValue, feeRate, recipient, sender: from }); + const { psbt } = await createTransaction({ ...rest, assetValue, feeRate, recipient, sender: from }); - const tx = await signer.signTransaction({ builder, utxos }); + const tx = await signer.signTransaction(psbt); const txHex = tx.toHex(); return broadcastTx(txHex); }; } -async function buildTx({ +async function createTransaction({ assetValue, recipient, memo, feeRate, sender, - setSigHashType, + setSigHashType = true, }: UTXOBuildTxParams & { setSigHashType?: boolean }) { const recipientCashAddress = toCashAddress(recipient); if (!bchValidateAddress(recipientCashAddress)) throw new SwapKitError("toolbox_utxo_invalid_address", { address: recipientCashAddress }); - // Overestimate by 7500 byte * feeRate to ensure we have enough UTXOs for fees and change const targetValue = Math.ceil(assetValue.getBaseValue("number") + feeRate * 7500); const utxos = await getUtxoApi(chain).getUtxos({ address: stripToCashAddress(sender), - fetchTxHex: false, + // Correctly fetch txHex for nonWitnessUtxo + fetchTxHex: true, targetValue, }); const feeRateWhole = Number(feeRate.toFixed(0)); const compiledMemo = memo ? await compileMemo(memo) : null; - const targetOutputs = [] as TargetOutput[]; - // output to recipient targetOutputs.push({ address: toLegacyAddress(recipient), value: assetValue.getBaseValue("number") }); - //2. add output memo to targets (optional) if (compiledMemo) { targetOutputs.push({ script: compiledMemo, value: 0 }); } const { inputs, outputs } = accumulative({ chain, feeRate: feeRateWhole, inputs: utxos, outputs: targetOutputs }); - // .inputs and .outputs will be undefined if no solution was found if (!(inputs && outputs)) throw new SwapKitError("toolbox_utxo_insufficient_balance", { assetValue, sender }); - const getNetwork = await getUtxoNetwork(); - const psbt = new Psbt({ network: getNetwork(chain) }); // Network-specific + + const psbt = new bitgo.UtxoPsbt({ network }); // Network-specific for (const { hash, index, witnessUtxo } of inputs) { - psbt.addInput({ hash, index, sighashType: setSigHashType ? 0x41 : undefined, witnessUtxo }); + psbt.addInput({ + hash, + index, + sighashType: setSigHashType + ? bitgo.UtxoTransaction.SIGHASH_ALL | bitgo.UtxoTransaction.SIGHASH_FORKID + : undefined, + ...(witnessUtxo && { witnessUtxo: { ...witnessUtxo, value: BigInt(witnessUtxo?.value) } }), + }); } - // Outputs for (const output of outputs) { - const address = "address" in output && output.address ? output.address : toLegacyAddress(sender); + const outAddress = "address" in output && output.address ? output.address : toLegacyAddress(sender); const params = output.script - ? compiledMemo - ? { script: compiledMemo, value: 0 } - : undefined - : { address, value: output.value }; + ? { script: output.script, value: 0n } + : { address: outAddress, value: BigInt(output.value) }; if (params) { psbt.addOutput(params); diff --git a/packages/toolboxes/src/utxo/toolbox/params.ts b/packages/toolboxes/src/utxo/toolbox/params.ts index 3455539f7c..8815491b00 100644 --- a/packages/toolboxes/src/utxo/toolbox/params.ts +++ b/packages/toolboxes/src/utxo/toolbox/params.ts @@ -3,13 +3,12 @@ * These are not exported from the package to avoid leaking third-party library types. */ -import type { ZcashPsbt } from "@bitgo/utxo-lib/dist/src/bitgo"; +import type { UtxoPsbt, ZcashPsbt } from "@bitgo/utxo-lib/dist/src/bitgo"; import { Chain, type ChainSigner } from "@swapkit/helpers"; import type { Psbt } from "bitcoinjs-lib"; -import type { TransactionBuilderType, TransactionType, UTXOType } from "../types"; export type UtxoToolboxParams = { - [Chain.BitcoinCash]: { signer: ChainSigner<{ builder: TransactionBuilderType; utxos: UTXOType[] }, TransactionType> }; + [Chain.BitcoinCash]: { signer: ChainSigner }; [Chain.Bitcoin]: { signer: ChainSigner }; [Chain.Dogecoin]: { signer: ChainSigner }; [Chain.Litecoin]: { signer: ChainSigner }; diff --git a/packages/toolboxes/src/utxo/toolbox/utxo.ts b/packages/toolboxes/src/utxo/toolbox/utxo.ts index 8e62021e27..b89b2028cd 100644 --- a/packages/toolboxes/src/utxo/toolbox/utxo.ts +++ b/packages/toolboxes/src/utxo/toolbox/utxo.ts @@ -1,6 +1,4 @@ import secp256k1 from "@bitcoinerlab/secp256k1"; -// @ts-expect-error -import { ECPair, HDNode } from "@psf/bitcoincashjs-lib"; import { HDKey } from "@scure/bip32"; import { mnemonicToSeedSync } from "@scure/bip39"; import { @@ -8,7 +6,6 @@ import { applyFeeMultiplier, Chain, type ChainSigner, - DerivationPath, type DerivationPathArray, derivationPathToString, FeeOption, @@ -162,6 +159,40 @@ async function createSignerWithKeys({ return { getAddress, signTransaction }; } +function getSignTransaction({ chain, signer }: { chain: UTXOChain; signer?: ChainSigner }) { + return async function signTransaction(psbt: Psbt): Promise { + if (!signer) throw new SwapKitError("toolbox_utxo_no_signer"); + // Check if this is a standard PSBT signer (not BCH) + if (chain !== Chain.BitcoinCash) { + const signedPsbt = await signer.signTransaction(psbt); + return signedPsbt; + } + // BCH uses a different transaction type, so we can't support PSBT signing + throw new SwapKitError("toolbox_utxo_invalid_params", { + chain, + error: "PSBT signing is not supported for BitcoinCash. Use the BCH-specific signTransaction method.", + }); + }; +} + +function getSignAndBroadcastTransaction({ chain, signer }: { chain: UTXOChain; signer?: ChainSigner }) { + return async function signAndBroadcastTransaction(psbt: Psbt): Promise { + if (!signer) throw new SwapKitError("toolbox_utxo_no_signer"); + // Check if this is a standard PSBT signer (not BCH) + if (chain !== Chain.BitcoinCash) { + const signedPsbt = await signer.signTransaction(psbt); + signedPsbt.finalizeAllInputs(); + const txHex = signedPsbt.extractTransaction().toHex(); + return getUtxoApi(chain).broadcastTx(txHex); + } + // BCH uses a different transaction type, so we can't support PSBT signing + throw new SwapKitError("toolbox_utxo_invalid_params", { + chain, + error: "PSBT signing is not supported for BitcoinCash. Use the BCH-specific signTransaction method.", + }); + }; +} + export async function createUTXOToolbox({ chain, ...toolboxParams @@ -208,6 +239,9 @@ export async function createUTXOToolbox({ const keys = createKeysForPath(params); return keys.toWIF(); }, + signAndBroadcastTransaction: getSignAndBroadcastTransaction({ chain, signer: signer as ChainSigner }), + signer, + signTransaction: getSignTransaction({ chain, signer: signer as ChainSigner }), transfer: transfer(signer as UtxoToolboxParams["BTC"]["signer"]), validateAddress: (address: string) => validateAddress({ address, chain }), }; @@ -295,7 +329,7 @@ function estimateTransactionFee(chain: UTXOChain) { } type CreateKeysForPathReturnType = { - [Chain.BitcoinCash]: BchECPair; + [Chain.BitcoinCash]: ECPairInterface; [Chain.Bitcoin]: ECPairInterface; [Chain.Dash]: ECPairInterface; [Chain.Dogecoin]: ECPairInterface; @@ -309,30 +343,8 @@ export async function getCreateKeysForPath CreateKeysForPathReturnType[T]; - } case Chain.Bitcoin: + case Chain.BitcoinCash: case Chain.Dogecoin: case Chain.Litecoin: case Chain.Zcash: diff --git a/packages/toolboxes/src/utxo/toolbox/zcash.ts b/packages/toolboxes/src/utxo/toolbox/zcash.ts index 3fcb787c6a..4e72db659b 100644 --- a/packages/toolboxes/src/utxo/toolbox/zcash.ts +++ b/packages/toolboxes/src/utxo/toolbox/zcash.ts @@ -231,11 +231,32 @@ export async function createZcashToolbox( return keys.toWIF(); } + function getSignTransaction(signer?: ZcashSigner) { + return async function signTransaction(psbt: ZcashPsbt): Promise { + if (!signer) throw new SwapKitError("toolbox_utxo_no_signer"); + const signedPsbt = await signer.signTransaction(psbt); + return signedPsbt; + }; + } + + function getSignAndBroadcastTransaction(signer?: ZcashSigner) { + return async function signAndBroadcastTransaction(psbt: ZcashPsbt): Promise { + if (!signer) throw new SwapKitError("toolbox_utxo_no_signer"); + const signedPsbt = await signer.signTransaction(psbt); + signedPsbt.finalizeAllInputs(); + const txHex = signedPsbt.extractTransaction().toHex(); + return baseToolbox.broadcastTx(txHex); + }; + } + return { ...baseToolbox, createKeysForPath, createTransaction, getPrivateKeyFromMnemonic, + signAndBroadcastTransaction: getSignAndBroadcastTransaction(signer), + signer, + signTransaction: getSignTransaction(signer), transfer, validateAddress: validateZcashAddress, }; diff --git a/packages/ui/package.json b/packages/ui/package.json index 056678dcba..403cf8d878 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -22,6 +22,7 @@ "@swapkit/wallets": "workspace:*", "class-variance-authority": "0.7.1", "clsx": "2.1.1", + "cosmjs-types": "0.10.1", "lucide-react": "0.552.0", "react": "19.1.1", "react-hook-form": "7.65.0", diff --git a/packages/ui/tailwind.config.d.ts b/packages/ui/tailwind.config.d.ts deleted file mode 100644 index cb10a37e5f..0000000000 --- a/packages/ui/tailwind.config.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -declare const config: { - content: string[]; - corePlugins: { preflight: false }; - darkMode: "class"; - plugins: { handler: () => void }[]; - prefix: string; - theme: { - container: { center: true; padding: string; screens: { "2xl": string } }; - extend: { - animation: { "accordion-down": string; "accordion-up": string }; - colors: { - background: string; - foreground: string; - primary: { DEFAULT: string; foreground: string }; - secondary: { DEFAULT: string; foreground: string }; - tertiary: { DEFAULT: string; foreground: string }; - accent: { DEFAULT: string; foreground: string }; - muted: { DEFAULT: string; foreground: string }; - destructive: { DEFAULT: string; foreground: string }; - success: { DEFAULT: string; foreground: string }; - border: string; - ring: string; - input: string; - card: { DEFAULT: string; foreground: string }; - popover: { DEFAULT: string; foreground: string }; - }; - keyframes: { - "accordion-down": { from: { height: string }; to: { height: string } }; - "accordion-up": { from: { height: string }; to: { height: string } }; - }; - }; - }; -}; -export default config; -//# sourceMappingURL=tailwind.config.d.ts.map diff --git a/packages/ui/tailwind.config.d.ts.map b/packages/ui/tailwind.config.d.ts.map deleted file mode 100644 index 2ec6150731..0000000000 --- a/packages/ui/tailwind.config.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"tailwind.config.d.ts","sourceRoot":"","sources":["tailwind.config.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCM,CAAC;AAEnB,eAAe,MAAM,CAAC"} \ No newline at end of file diff --git a/packages/ui/tailwind.config.ts b/packages/ui/tailwind.config.ts deleted file mode 100644 index d83da7da67..0000000000 --- a/packages/ui/tailwind.config.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { Config } from "tailwindcss"; - -import tailwindCssAnimatePlugin from "tailwindcss-animate"; - -const config = { - content: ["./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}"], - corePlugins: { preflight: false }, - darkMode: "class", - plugins: [tailwindCssAnimatePlugin], - prefix: "sk-ui-", - theme: { - container: { center: true, padding: "2rem", screens: { "2xl": "1400px" } }, - extend: { - animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out" }, - // biome-ignore assist/source/useSortedKeys: sort by use case, not alphabetically - colors: { - background: "hsl(var(--sk-ui-background))", - foreground: "hsl(var(--sk-ui-foreground))", - - primary: { DEFAULT: "hsl(var(--sk-ui-primary))", foreground: "hsl(var(--sk-ui-primary-foreground))" }, - secondary: { DEFAULT: "hsl(var(--sk-ui-secondary))", foreground: "hsl(var(--sk-ui-secondary-foreground))" }, - tertiary: { DEFAULT: "hsl(var(--sk-ui-tertiary))", foreground: "hsl(var(--sk-ui-tertiary-foreground))" }, - - accent: { DEFAULT: "hsl(var(--sk-ui-accent))", foreground: "hsl(var(--sk-ui-accent-foreground))" }, - muted: { DEFAULT: "hsl(var(--sk-ui-muted))", foreground: "hsl(var(--sk-ui-muted-foreground))" }, - - destructive: { - DEFAULT: "hsl(var(--sk-ui-destructive))", - foreground: "hsl(var(--sk-ui-destructive-foreground))", - }, - success: { DEFAULT: "hsl(var(--sk-ui-success))", foreground: "hsl(var(--sk-ui-success-foreground))" }, - - border: "hsl(var(--sk-ui-border))", - ring: "hsl(var(--sk-ui-ring))", - input: "hsl(var(--sk-ui-input))", - - card: { DEFAULT: "hsl(var(--sk-ui-card))", foreground: "hsl(var(--sk-ui-card-foreground))" }, - popover: { DEFAULT: "hsl(var(--sk-ui-popover))", foreground: "hsl(var(--sk-ui -popover-foreground))" }, - }, - keyframes: { - "accordion-down": { from: { height: "0" }, to: { height: "var(--radix-accordion-content-height)" } }, - "accordion-up": { from: { height: "var(--radix-accordion-content-height)" }, to: { height: "0" } }, - }, - }, - }, -} satisfies Config; - -export default config; diff --git a/packages/wallet-extensions/package.json b/packages/wallet-extensions/package.json index 28387f818b..e1eaf0d1c8 100644 --- a/packages/wallet-extensions/package.json +++ b/packages/wallet-extensions/package.json @@ -9,6 +9,7 @@ "@swapkit/helpers": "workspace:*", "@swapkit/toolboxes": "workspace:*", "@swapkit/wallet-core": "workspace:*", + "cosmjs-types": "0.10.1", "ethers": "^6.14.0", "sats-connect": "~1.0.0", "ts-pattern": "~5.9.0" @@ -20,6 +21,7 @@ "@near-js/crypto": "2.5.0", "@near-js/transactions": "2.5.0", "@solana/web3.js": "1.98.4", + "cosmjs-types": "0.10.1", "ethers": "6.15.0", "sats-connect": "1.0.0", "ts-pattern": "5.9.0" diff --git a/packages/wallet-extensions/src/phantom/index.ts b/packages/wallet-extensions/src/phantom/index.ts index 7be5544106..d1ea53259b 100644 --- a/packages/wallet-extensions/src/phantom/index.ts +++ b/packages/wallet-extensions/src/phantom/index.ts @@ -48,8 +48,20 @@ async function getWalletMethods(chain: PhantomSupportedChain) { } const { getUtxoToolbox } = await import("@swapkit/toolboxes/utxo"); + const { Psbt } = await import("bitcoinjs-lib"); const [{ address }] = await provider.requestAccounts(); - const toolbox = await getUtxoToolbox(chain); + + async function signTransaction(psbt: InstanceType) { + const psbtBytes = psbt.toBuffer(); + const signedPsbtBytes = await provider.signPSBT(psbtBytes, { + inputsToSign: [{ address, signingIndexes: psbt.txInputs.map((_, index) => index) }], + }); + + return Psbt.fromBuffer(Buffer.from(signedPsbtBytes)); + } + + const signer = { getAddress: () => Promise.resolve(address), signTransaction }; + const toolbox = await getUtxoToolbox(chain, { signer }); return { ...toolbox, address }; } diff --git a/packages/wallet-hardware/src/keepkey/chains/utxo.ts b/packages/wallet-hardware/src/keepkey/chains/utxo.ts index d79da6dce9..c9f3e0d587 100644 --- a/packages/wallet-hardware/src/keepkey/chains/utxo.ts +++ b/packages/wallet-hardware/src/keepkey/chains/utxo.ts @@ -31,7 +31,7 @@ export async function utxoWalletMethods({ chain: Exclude; derivationPath?: DerivationPathArray; }): Promise< - UTXOToolboxes[UTXOChain] & { + Omit & { address: string; signTransaction: (psbt: Psbt, inputs: KeepKeyInputObject[], memo?: string) => Promise; } @@ -105,10 +105,7 @@ export async function utxoWalletMethods({ if (!recipient) throw new SwapKitError("wallet_keepkey_invalid_params", { reason: "Recipient address must be provided" }); - const createTxMethod = - chain === Chain.BitcoinCash - ? (toolbox as UTXOToolboxes["BCH"]).buildTx - : (toolbox as UTXOToolboxes["BTC"]).createTransaction; + const createTxMethod = (toolbox as UTXOToolboxes["BTC"]).createTransaction; const { psbt, inputs: rawInputs } = await createTxMethod({ ...rest, diff --git a/packages/wallet-hardware/src/trezor/index.ts b/packages/wallet-hardware/src/trezor/index.ts index 045bd2e6e0..68c40f0f8c 100644 --- a/packages/wallet-hardware/src/trezor/index.ts +++ b/packages/wallet-hardware/src/trezor/index.ts @@ -265,10 +265,7 @@ async function getTrezorWallet({ const feeRate = paramFeeRate || (await toolbox.getFeeRates())[feeOptionKey || FeeOption.Fast]; - const createTxMethod = - chain === Chain.BitcoinCash - ? (toolbox as UTXOToolboxes["BCH"]).buildTx - : (toolbox as UTXOToolboxes["BTC"]).createTransaction; + const createTxMethod = (toolbox as UTXOToolboxes["BTC"]).createTransaction; const { psbt, inputs } = await createTxMethod({ ...rest, diff --git a/playgrounds/vite/src/Swap/index.tsx b/playgrounds/vite/src/Swap/index.tsx index 38821259f4..7495d60e13 100644 --- a/playgrounds/vite/src/Swap/index.tsx +++ b/playgrounds/vite/src/Swap/index.tsx @@ -23,6 +23,7 @@ export default function Swap({ const txHash = await skClient.swap({ feeOptionKey: FeeOption.Fast, route, + // useApiTx: true, // enable this to use the API transaction instead of the plugin transactionF ...(isChainflipBoost ? { maxBoostFeeBps: 10 } : {}), });