diff --git a/bun.lock b/bun.lock index 1a503915e8..f216942fc1 100644 --- a/bun.lock +++ b/bun.lock @@ -255,6 +255,7 @@ "@ledgerhq/wallet-api-client": "~1.9.0", "@swapkit/helpers": "workspace:*", "@swapkit/toolboxes": "workspace:*", + "@trezor/connect-web": "~9.6.0", "ethers": "^6.14.0", "ts-pattern": "^5.7.0", }, @@ -271,6 +272,7 @@ "@ledgerhq/hw-transport": "6.31.6", "@ledgerhq/hw-transport-webusb": "6.29.9", "@ledgerhq/wallet-api-client": "1.9.1", + "@trezor/connect-web": "~9.6.0", "ethers": "6.15.0", "ts-pattern": "5.8.0", }, @@ -302,7 +304,7 @@ "@cosmjs/proto-signing": "~0.33.0", "@keplr-wallet/types": "~0.12.238", "@passkeys/core": "^4.0.0", - "@passkeys/react": "^3.0.0", + "@passkeys/react": "^3.0.1", "@radixdlt/babylon-gateway-api-sdk": "~1.10.0", "@radixdlt/radix-dapp-toolkit": "~2.2.0", "@scure/base": "~1.2.0", @@ -313,7 +315,6 @@ "@swapkit/wallet-core": "workspace:*", "@swapkit/wallet-hardware": "workspace:*", "@swapkit/wallet-keystore": "workspace:*", - "@trezor/connect-web": "~9.6.0", "@walletconnect/modal": "~2.7.0", "@walletconnect/sign-client": "~2.21.0", "bitcoinjs-lib": "~6.1.0", @@ -339,7 +340,6 @@ "@solana/web3.js": "1.98.4", "@swapkit/helpers": "workspace:*", "@swapkit/toolboxes": "workspace:*", - "@trezor/connect-web": "9.6.2", "@walletconnect/logger": "2.1.2", "@walletconnect/modal": "2.7.0", "@walletconnect/sign-client": "2.21.8", diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 2bd4431377..5efd79c382 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -2,15 +2,9 @@ "author": "swapkit-oss", "description": "SwapKit - Contracts", "exports": { - ".": { - "import": "./dist/index.js", - "require": "./dist/index.cjs", - "types": "./dist/types/index.d.ts" - } + ".": { "import": "./dist/index.js", "require": "./dist/index.cjs", "types": "./dist/types/index.d.ts" } }, - "files": [ - "dist/" - ], + "files": ["dist/"], "homepage": "https://github.com/swapkit/SwapKit", "license": "Apache-2.0", "name": "@swapkit/contracts", diff --git a/packages/helpers/src/api/swapkitApi/types.ts b/packages/helpers/src/api/swapkitApi/types.ts index a7a2e3bd91..13ecf95a3a 100644 --- a/packages/helpers/src/api/swapkitApi/types.ts +++ b/packages/helpers/src/api/swapkitApi/types.ts @@ -403,6 +403,20 @@ export const EVMTransactionSchema = object({ export type EVMTransaction = z.infer; +export const NEARTransactionSchema = z.object({ + details: z.object({ + blockHash: z.string().describe("Hash of the block"), + nonce: z.number().describe("Nonce of the transaction"), + signerId: z.string().describe("ID of the signer"), + }), + gas: z.string().describe("Gas limit for the transaction"), + gasPrice: z.string().describe("Gas price for the transaction"), + publicKey: z.string().describe("Public key of the signer"), + serialized: z.string().describe("Serialized transaction"), +}); + +export type NEARTransaction = z.infer; + export const EVMTransactionDetailsParamsSchema = array( union([ string(), @@ -412,6 +426,21 @@ export const EVMTransactionDetailsParamsSchema = array( ]), ); +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 type EVMTransactionDetailsParams = z.infer; export const EVMTransactionDetailsSchema = object({ @@ -425,7 +454,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() }); @@ -527,7 +585,9 @@ 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, NEARTransactionSchema, TronTransactionSchema, string()]), + ), txType: optional(z.enum(RouteQuoteTxType)), warnings: RouteQuoteWarningSchema, }); diff --git a/packages/helpers/src/modules/swapKitError.ts b/packages/helpers/src/modules/swapKitError.ts index 21c3a2d82a..d949f8960f 100644 --- a/packages/helpers/src/modules/swapKitError.ts +++ b/packages/helpers/src/modules/swapKitError.ts @@ -1,335 +1,241 @@ +// biome-ignore assist/source/useSortedKeys: keys are sorted by topic const errorCodes = { - api_v2_invalid_method_key_hash: 60003, - /** - * SwapKit API - */ - api_v2_invalid_response: 60001, - api_v2_server_error: 60002, - chainflip_broker_fund_invalid_address: 30107, - chainflip_broker_fund_only_flip_supported: 30106, - /** - * Chainflip - Broker - */ - chainflip_broker_invalid_params: 30101, - chainflip_broker_recipient_error: 30102, - chainflip_broker_register: 30103, - chainflip_broker_tx_error: 30104, - chainflip_broker_withdraw: 30105, - - /** - * Chainflip - */ - chainflip_channel_error: 30001, - chainflip_unknown_asset: 30002, - core_approve_asset_address_or_from_not_found: 10004, - core_approve_asset_target_invalid: 10007, - core_chain_halted: 10010, /** * Core */ core_estimated_max_spendable_chain_not_supported: 10001, - core_explorer_unsupported_chain: 10008, core_extend_error: 10002, core_inbound_data_not_found: 10003, + core_approve_asset_address_or_from_not_found: 10004, core_plugin_not_found: 10005, core_plugin_swap_not_found: 10006, - core_swap_asset_not_recognized: 10203, - core_swap_contract_not_found: 10204, - core_swap_contract_not_supported: 10206, + core_approve_asset_target_invalid: 10007, + core_explorer_unsupported_chain: 10008, + core_verify_message_not_supported: 10009, + core_chain_halted: 10010, + /** + * Core - Wallet + */ + core_wallet_connection_not_found: 10101, + core_wallet_ctrl_not_installed: 10102, + core_wallet_evmwallet_not_installed: 10103, + core_wallet_walletconnect_not_installed: 10104, + core_wallet_keystore_not_installed: 10105, + core_wallet_ledger_not_installed: 10106, + core_wallet_trezor_not_installed: 10107, + core_wallet_keplr_not_installed: 10108, + core_wallet_okx_not_installed: 10109, + core_wallet_keepkey_not_installed: 10110, + core_wallet_talisman_not_installed: 10111, + core_wallet_not_keypair_wallet: 10112, + core_wallet_sign_message_not_supported: 10113, + core_wallet_connection_failed: 10114, /** * Core - Swap */ core_swap_invalid_params: 10201, - core_swap_quote_mode_not_supported: 10208, core_swap_route_not_complete: 10202, + core_swap_asset_not_recognized: 10203, + core_swap_contract_not_found: 10204, core_swap_route_transaction_not_found: 10205, + core_swap_contract_not_supported: 10206, core_swap_transaction_error: 10207, - core_transaction_add_liquidity_asset_error: 10308, - core_transaction_add_liquidity_base_address: 10306, - core_transaction_add_liquidity_base_error: 10307, - core_transaction_add_liquidity_invalid_params: 10305, - core_transaction_create_liquidity_asset_error: 10303, - core_transaction_create_liquidity_base_error: 10302, - core_transaction_create_liquidity_invalid_params: 10304, + core_swap_quote_mode_not_supported: 10208, + core_swap_transfer_failed: 10209, + core_wallet_incompatible: 10210, /** * Core - Transaction */ core_transaction_deposit_error: 10301, - core_transaction_deposit_gas_error: 10312, + core_transaction_create_liquidity_base_error: 10302, + core_transaction_create_liquidity_asset_error: 10303, + core_transaction_create_liquidity_invalid_params: 10304, + core_transaction_add_liquidity_invalid_params: 10305, + core_transaction_add_liquidity_base_address: 10306, + core_transaction_add_liquidity_base_error: 10307, + core_transaction_add_liquidity_asset_error: 10308, + core_transaction_withdraw_error: 10309, + core_transaction_deposit_to_pool_error: 10310, core_transaction_deposit_insufficient_funds_error: 10311, + core_transaction_deposit_gas_error: 10312, + core_transaction_invalid_sender_address: 10313, core_transaction_deposit_server_error: 10314, - core_transaction_deposit_to_pool_error: 10310, + core_transaction_user_rejected: 10315, core_transaction_failed: 10316, core_transaction_invalid_recipient_address: 10317, - core_transaction_invalid_sender_address: 10313, - core_transaction_user_rejected: 10315, - core_transaction_withdraw_error: 10309, - core_verify_message_not_supported: 10009, - core_wallet_connection_failed: 10114, /** - * Core - Wallet + * Wallets - General */ - core_wallet_connection_not_found: 10101, - core_wallet_ctrl_not_installed: 10102, - core_wallet_evmwallet_not_installed: 10103, - core_wallet_keepkey_not_installed: 10110, - core_wallet_keplr_not_installed: 10108, - core_wallet_keystore_not_installed: 10105, - core_wallet_ledger_not_installed: 10106, - core_wallet_not_keypair_wallet: 10112, - core_wallet_okx_not_installed: 10109, - core_wallet_sign_message_not_supported: 10113, - core_wallet_talisman_not_installed: 10111, - core_wallet_trezor_not_installed: 10107, - core_wallet_walletconnect_not_installed: 10104, - helpers_chain_not_supported: 70009, - helpers_failed_to_switch_network: 70007, - helpers_invalid_asset_identifier: 70005, - helpers_invalid_asset_url: 70004, - helpers_invalid_identifier: 70003, - helpers_invalid_memo_type: 70006, + wallet_connection_rejected_by_user: 20001, + wallet_missing_api_key: 20002, + wallet_chain_not_supported: 20003, + wallet_missing_params: 20004, + wallet_provider_not_found: 20005, + wallet_failed_to_add_or_switch_network: 20006, + wallet_locked: 20007, /** - * Helpers + * Wallets - Ledger */ - helpers_invalid_number_different_decimals: 70001, - helpers_invalid_number_of_years: 70002, - helpers_invalid_params: 70010, - helpers_invalid_response: 70011, - helpers_not_found_provider: 70008, + wallet_ledger_connection_error: 20101, + wallet_ledger_connection_claimed: 20102, + wallet_ledger_get_address_error: 20103, + wallet_ledger_device_not_found: 20104, + wallet_ledger_device_locked: 20105, + wallet_ledger_transport_error: 20106, + wallet_ledger_public_key_error: 20107, + wallet_ledger_derivation_path_error: 20108, + wallet_ledger_signing_error: 20109, + wallet_ledger_app_not_open: 20110, + wallet_ledger_invalid_response: 20111, + wallet_ledger_method_not_supported: 20112, + wallet_ledger_invalid_params: 20113, + wallet_ledger_invalid_signature: 20114, + wallet_ledger_no_provider: 20115, + wallet_ledger_pubkey_not_found: 20116, + wallet_ledger_transport_not_defined: 20117, + wallet_ledger_webusb_not_supported: 20118, + wallet_ledger_chain_not_supported: 20119, + wallet_ledger_invalid_asset: 20120, + wallet_ledger_invalid_account: 20121, + wallet_ledger_address_not_found: 20122, + wallet_ledger_failed_to_get_address: 20123, /** - * Anything else + * Wallets - Phantom */ - not_implemented: 99999, + wallet_phantom_not_found: 20201, /** - * NEAR Plugin + * Wallets - Ctrl */ - plugin_near_invalid_name: 41001, - plugin_near_name_unavailable: 41003, - plugin_near_no_connection: 41002, - plugin_near_registration_failed: 41004, - plugin_near_transfer_failed: 41005, - thorchain_asset_is_not_tcy: 40003, + wallet_ctrl_not_found: 20301, + wallet_ctrl_send_transaction_no_address: 20302, + wallet_ctrl_contract_address_not_provided: 20303, + wallet_ctrl_asset_not_defined: 20304, + wallet_ctrl_sign_transaction_not_supported: 20305, + wallet_ctrl_transaction_missing_data: 20306, /** - * THORChain + * Wallets - WalletConnect */ - thorchain_chain_halted: 40001, - thorchain_preferred_asset_payout_required: 40105, - thorchain_swapin_memo_required: 40103, + wallet_walletconnect_project_id_not_specified: 20401, + wallet_walletconnect_connection_not_established: 20402, + wallet_walletconnect_namespace_not_supported: 20403, + wallet_walletconnect_chain_not_supported: 20404, + wallet_walletconnect_invalid_method: 20405, + wallet_walletconnect_method_not_supported: 20406, /** - * THORChain - Swap + * Wallets - Trezor */ - thorchain_swapin_router_required: 40101, - thorchain_swapin_token_required: 40104, - thorchain_swapin_vault_required: 40102, - thorchain_trading_halted: 40002, + wallet_trezor_failed_to_sign_transaction: 20501, + wallet_trezor_derivation_path_not_supported: 20502, + wallet_trezor_failed_to_get_address: 20503, + wallet_trezor_transport_error: 20504, + wallet_trezor_method_not_supported: 20505, /** - * Toolboxes - Cosmos + * Wallets - Talisman */ - toolbox_cosmos_account_not_found: 50101, - toolbox_cosmos_invalid_fee: 50102, - toolbox_cosmos_invalid_params: 50103, - toolbox_cosmos_no_signer: 50104, - toolbox_cosmos_not_supported: 50105, - toolbox_cosmos_signer_not_defined: 50106, - toolbox_cosmos_validate_address_prefix_not_found: 50107, - toolbox_cosmos_verify_signature_no_pubkey: 50108, + wallet_talisman_not_enabled: 20601, + wallet_talisman_not_found: 20602, /** - * Toolboxes - EVM + * Wallets - Polkadot */ - toolbox_evm_error_estimating_gas_limit: 50201, - toolbox_evm_error_sending_transaction: 50202, - toolbox_evm_gas_estimation_error: 50203, - toolbox_evm_invalid_gas_asset_address: 50204, - toolbox_evm_invalid_params: 50205, - toolbox_evm_invalid_transaction: 50206, - toolbox_evm_no_abi_fragment: 50207, - toolbox_evm_no_contract_address: 50208, - toolbox_evm_no_fee_data: 50209, - toolbox_evm_no_from_address: 50210, - toolbox_evm_no_gas_price: 50211, - toolbox_evm_no_signer: 50213, - toolbox_evm_no_signer_address: 50212, - toolbox_evm_no_to_address: 50214, - toolbox_evm_not_supported: 50215, - toolbox_evm_provider_not_eip1193_compatible: 50216, - toolbox_near_access_key_error: 90605, - toolbox_near_balance_failed: 90608, - toolbox_near_empty_batch: 90607, - toolbox_near_invalid_address: 90602, - toolbox_near_invalid_amount: 90603, - toolbox_near_invalid_gas_params: 90612, - toolbox_near_invalid_name: 90609, - toolbox_near_missing_contract_address: 90610, - toolbox_near_no_account: 90611, - toolbox_near_no_rpc_url: 90606, + wallet_polkadot_not_found: 20701, /** - * Toolboxes - Near + * Wallets - Radix */ - toolbox_near_no_signer: 90601, - toolbox_near_transfer_failed: 90604, + wallet_radix_not_found: 20801, + wallet_radix_transaction_failed: 20802, + wallet_radix_invalid_manifest: 20803, + wallet_radix_method_not_supported: 20804, + wallet_radix_no_account: 20805, /** - * Toolboxes - General + * Wallets - KeepKey */ - toolbox_not_supported: 59901, + wallet_keepkey_not_found: 20901, + wallet_keepkey_asset_not_defined: 20902, + wallet_keepkey_contract_address_not_provided: 20903, + wallet_keepkey_send_transaction_no_address: 20904, + wallet_keepkey_derivation_path_error: 20905, + wallet_keepkey_signing_error: 20906, + wallet_keepkey_transport_error: 20907, + wallet_keepkey_unsupported_chain: 20908, + wallet_keepkey_invalid_response: 20909, + wallet_keepkey_chain_not_supported: 20910, + wallet_keepkey_signer_not_found: 20911, + wallet_keepkey_no_accounts: 20912, + wallet_keepkey_method_not_supported: 20913, + wallet_keepkey_invalid_params: 20914, + wallet_keepkey_config_not_found: 20915, + wallet_keepkey_no_provider: 20916, + wallet_keepkey_account_not_found: 20917, /** - * Toolboxes - Radix + * Wallets - BitKeep/BitGet */ - toolbox_radix_method_not_supported: 50601, - toolbox_ripple_asset_not_supported: 50704, - toolbox_ripple_broadcast_error: 50705, + wallet_bitkeep_not_found: 21001, + wallet_bitkeep_failed_to_switch_network: 21002, + wallet_bitkeep_no_accounts: 21003, /** - * Toolboxes - Ripple - */ - toolbox_ripple_get_balance_error: 50701, - toolbox_ripple_rpc_not_configured: 50702, - toolbox_ripple_signer_not_found: 50703, - toolbox_solana_fee_estimation_failed: 50402, - /** - * Toolboxes - Solana - */ - toolbox_solana_no_signer: 50401, - /** - * Toolboxes - Substrate - */ - toolbox_substrate_not_supported: 50501, - toolbox_tron_allowance_check_failed: 50809, - toolbox_tron_approve_failed: 50807, - toolbox_tron_fee_estimation_failed: 50805, - toolbox_tron_invalid_token_contract: 50808, - toolbox_tron_invalid_token_identifier: 50802, - /** - * Toolboxes - Tron + * Wallets - Exodus */ - toolbox_tron_no_signer: 50801, - toolbox_tron_token_transfer_failed: 50803, - toolbox_tron_transaction_creation_failed: 50804, - toolbox_tron_trongrid_api_error: 50806, + wallet_exodus_sign_transaction_error: 21101, + wallet_exodus_not_found: 21102, + wallet_exodus_no_address: 21103, + wallet_exodus_request_canceled: 21104, + wallet_exodus_signature_canceled: 21105, + wallet_exodus_failed_to_switch_network: 21106, + wallet_exodus_chain_not_supported: 21107, + wallet_exodus_instance_missing: 21108, /** - * Toolboxes - UTXO + * Wallets - OneKey */ - toolbox_utxo_api_error: 50301, - toolbox_utxo_broadcast_failed: 50302, - toolbox_utxo_insufficient_balance: 50303, - toolbox_utxo_invalid_address: 50304, - toolbox_utxo_invalid_params: 50305, - toolbox_utxo_invalid_transaction: 50306, - toolbox_utxo_no_signer: 50307, - toolbox_utxo_not_supported: 50308, - wallet_bitkeep_failed_to_switch_network: 21002, - wallet_bitkeep_no_accounts: 21003, + wallet_onekey_not_found: 21201, + wallet_onekey_sign_transaction_error: 21202, /** - * Wallets - BitKeep/BitGet + * Wallets - OKX */ - wallet_bitkeep_not_found: 21001, - wallet_chain_not_supported: 20003, - wallet_coinbase_chain_not_supported: 21702, - wallet_coinbase_method_not_supported: 21703, - wallet_coinbase_no_accounts: 21704, + wallet_okx_not_found: 21301, + wallet_okx_chain_not_supported: 21302, + wallet_okx_failed_to_switch_network: 21303, + wallet_okx_no_accounts: 21304, /** - * Wallets - Coinbase + * Wallets - Keplr */ - wallet_coinbase_not_found: 21701, + wallet_keplr_not_found: 21401, + wallet_keplr_chain_not_supported: 21402, + wallet_keplr_signer_not_found: 21403, + wallet_keplr_no_accounts: 21404, /** - * Wallets - General + * Wallets - Cosmostation */ - wallet_connection_rejected_by_user: 20001, + wallet_cosmostation_not_found: 21501, wallet_cosmostation_chain_not_supported: 21502, wallet_cosmostation_evm_provider_not_found: 21503, wallet_cosmostation_keplr_provider_not_found: 21504, wallet_cosmostation_no_accounts: 21505, wallet_cosmostation_no_evm_accounts: 21506, wallet_cosmostation_no_evm_address: 21507, + wallet_cosmostation_signer_not_found: 21508, /** - * Wallets - Cosmostation + * Wallets - XDefi */ - wallet_cosmostation_not_found: 21501, - wallet_cosmostation_signer_not_found: 21508, - wallet_ctrl_asset_not_defined: 20304, - wallet_ctrl_contract_address_not_provided: 20303, + wallet_xdefi_not_found: 21601, + wallet_xdefi_chain_not_supported: 21602, /** - * Wallets - Ctrl + * Wallets - Coinbase */ - wallet_ctrl_not_found: 20301, - wallet_ctrl_send_transaction_no_address: 20302, + wallet_coinbase_not_found: 21701, + wallet_coinbase_chain_not_supported: 21702, + wallet_coinbase_method_not_supported: 21703, + wallet_coinbase_no_accounts: 21704, /** * Wallets - EVM Extensions */ wallet_evm_extensions_failed_to_switch_network: 21801, wallet_evm_extensions_no_provider: 21802, wallet_evm_extensions_not_found: 21803, - wallet_exodus_chain_not_supported: 21107, - wallet_exodus_failed_to_switch_network: 21106, - wallet_exodus_instance_missing: 21108, - wallet_exodus_no_address: 21103, - wallet_exodus_not_found: 21102, - wallet_exodus_request_canceled: 21104, - /** - * Wallets - Exodus - */ - wallet_exodus_sign_transaction_error: 21101, - wallet_exodus_signature_canceled: 21105, - wallet_failed_to_add_or_switch_network: 20006, - wallet_keepkey_account_not_found: 20917, - wallet_keepkey_asset_not_defined: 20902, - wallet_keepkey_chain_not_supported: 20910, - wallet_keepkey_config_not_found: 20915, - wallet_keepkey_contract_address_not_provided: 20903, - wallet_keepkey_derivation_path_error: 20905, - wallet_keepkey_invalid_params: 20914, - wallet_keepkey_invalid_response: 20909, - wallet_keepkey_method_not_supported: 20913, - wallet_keepkey_no_accounts: 20912, - wallet_keepkey_no_provider: 20916, - /** - * Wallets - KeepKey - */ - wallet_keepkey_not_found: 20901, - wallet_keepkey_send_transaction_no_address: 20904, - wallet_keepkey_signer_not_found: 20911, - wallet_keepkey_signing_error: 20906, - wallet_keepkey_transport_error: 20907, - wallet_keepkey_unsupported_chain: 20908, - wallet_keplr_chain_not_supported: 21402, - wallet_keplr_no_accounts: 21404, - /** - * Wallets - Keplr - */ - wallet_keplr_not_found: 21401, - wallet_keplr_signer_not_found: 21403, /** * Wallets - Keystore */ wallet_keystore_invalid_password: 21901, wallet_keystore_unsupported_version: 21902, - wallet_ledger_address_not_found: 20122, - wallet_ledger_app_not_open: 20110, - wallet_ledger_chain_not_supported: 20119, - wallet_ledger_connection_claimed: 20102, - /** - * Wallets - Ledger - */ - wallet_ledger_connection_error: 20101, - wallet_ledger_derivation_path_error: 20108, - wallet_ledger_device_locked: 20105, - wallet_ledger_device_not_found: 20104, - wallet_ledger_failed_to_get_address: 20123, - wallet_ledger_get_address_error: 20103, - wallet_ledger_invalid_account: 20121, - wallet_ledger_invalid_asset: 20120, - wallet_ledger_invalid_params: 20113, - wallet_ledger_invalid_response: 20111, - wallet_ledger_invalid_signature: 20114, - wallet_ledger_method_not_supported: 20112, - wallet_ledger_no_provider: 20115, - wallet_ledger_pubkey_not_found: 20116, - wallet_ledger_public_key_error: 20107, - wallet_ledger_signing_error: 20109, - wallet_ledger_transport_error: 20106, - wallet_ledger_transport_not_defined: 20117, - wallet_ledger_webusb_not_supported: 20118, - wallet_locked: 20007, - wallet_missing_api_key: 20002, - wallet_missing_params: 20004, /** * Wallets - Near Extensions */ @@ -337,80 +243,186 @@ const errorCodes = { wallet_near_extensions_no_provider: 22002, wallet_near_extensions_not_found: 22003, wallet_near_method_not_supported: 22003, - wallet_okx_chain_not_supported: 21302, - wallet_okx_failed_to_switch_network: 21303, - wallet_okx_no_accounts: 21304, + /** - * Wallets - OKX + * Wallets - Vultisig */ - wallet_okx_not_found: 21301, + wallet_vultisig_not_found: 22101, + wallet_vultisig_contract_address_not_provided: 22102, + wallet_vultisig_asset_not_defined: 22103, + wallet_vultisig_send_transaction_no_address: 22104, + /** - * Wallets - OneKey + * Wallets - Xaman */ - wallet_onekey_not_found: 21201, - wallet_onekey_sign_transaction_error: 21202, + wallet_xaman_not_configured: 23001, + wallet_xaman_not_connected: 23002, + wallet_xaman_auth_failed: 23003, + wallet_xaman_connection_failed: 23004, + wallet_xaman_transaction_failed: 23005, + wallet_xaman_monitoring_failed: 23006, + /** - * Wallets - Phantom + * Chainflip */ - wallet_phantom_not_found: 20201, + chainflip_channel_error: 30001, + chainflip_unknown_asset: 30002, /** - * Wallets - Polkadot + * Chainflip - Broker */ - wallet_polkadot_not_found: 20701, - wallet_provider_not_found: 20005, - wallet_radix_invalid_manifest: 20803, - wallet_radix_method_not_supported: 20804, - wallet_radix_no_account: 20805, + chainflip_broker_invalid_params: 30101, + chainflip_broker_recipient_error: 30102, + chainflip_broker_register: 30103, + chainflip_broker_tx_error: 30104, + chainflip_broker_withdraw: 30105, + chainflip_broker_fund_only_flip_supported: 30106, + chainflip_broker_fund_invalid_address: 30107, /** - * Wallets - Radix + * THORChain */ - wallet_radix_not_found: 20801, - wallet_radix_transaction_failed: 20802, + thorchain_chain_halted: 40001, + thorchain_trading_halted: 40002, + thorchain_asset_is_not_tcy: 40003, /** - * Wallets - Talisman + * THORChain - Swap */ - wallet_talisman_not_enabled: 20601, - wallet_talisman_not_found: 20602, - wallet_trezor_derivation_path_not_supported: 20502, - wallet_trezor_failed_to_get_address: 20503, + thorchain_swapin_router_required: 40101, + thorchain_swapin_vault_required: 40102, + thorchain_swapin_memo_required: 40103, + thorchain_swapin_token_required: 40104, + thorchain_preferred_asset_payout_required: 40105, /** - * Wallets - Trezor + * Toolboxes - Cosmos */ - wallet_trezor_failed_to_sign_transaction: 20501, - wallet_trezor_method_not_supported: 20505, - wallet_trezor_transport_error: 20504, - wallet_vultisig_asset_not_defined: 22103, - wallet_vultisig_contract_address_not_provided: 22102, - + toolbox_cosmos_account_not_found: 50101, + toolbox_cosmos_invalid_fee: 50102, + toolbox_cosmos_invalid_params: 50103, + toolbox_cosmos_no_signer: 50104, + toolbox_cosmos_not_supported: 50105, + toolbox_cosmos_signer_not_defined: 50106, + toolbox_cosmos_validate_address_prefix_not_found: 50107, + toolbox_cosmos_verify_signature_no_pubkey: 50108, /** - * Wallets - Vultisig + * Toolboxes - EVM */ - wallet_vultisig_not_found: 22101, - wallet_vultisig_send_transaction_no_address: 22104, - wallet_walletconnect_chain_not_supported: 20404, - wallet_walletconnect_connection_not_established: 20402, - wallet_walletconnect_invalid_method: 20405, - wallet_walletconnect_method_not_supported: 20406, - wallet_walletconnect_namespace_not_supported: 20403, + toolbox_evm_error_estimating_gas_limit: 50201, + toolbox_evm_error_sending_transaction: 50202, + toolbox_evm_gas_estimation_error: 50203, + toolbox_evm_invalid_gas_asset_address: 50204, + toolbox_evm_invalid_params: 50205, + toolbox_evm_invalid_transaction: 50206, + toolbox_evm_no_abi_fragment: 50207, + toolbox_evm_no_contract_address: 50208, + toolbox_evm_no_fee_data: 50209, + toolbox_evm_no_from_address: 50210, + toolbox_evm_no_gas_price: 50211, + toolbox_evm_no_signer_address: 50212, + toolbox_evm_no_signer: 50213, + toolbox_evm_no_to_address: 50214, + toolbox_evm_not_supported: 50215, + toolbox_evm_provider_not_eip1193_compatible: 50216, /** - * Wallets - WalletConnect + * Toolboxes - UTXO */ - wallet_walletconnect_project_id_not_specified: 20401, - wallet_xaman_auth_failed: 23003, - wallet_xaman_connection_failed: 23004, - wallet_xaman_monitoring_failed: 23006, - + toolbox_utxo_api_error: 50301, + toolbox_utxo_broadcast_failed: 50302, + toolbox_utxo_insufficient_balance: 50303, + toolbox_utxo_invalid_address: 50304, + toolbox_utxo_invalid_params: 50305, + toolbox_utxo_invalid_transaction: 50306, + toolbox_utxo_no_signer: 50307, + toolbox_utxo_not_supported: 50308, /** - * Wallets - Xaman + * Toolboxes - Solana */ - wallet_xaman_not_configured: 23001, - wallet_xaman_not_connected: 23002, - wallet_xaman_transaction_failed: 23005, - wallet_xdefi_chain_not_supported: 21602, + toolbox_solana_no_signer: 50401, + toolbox_solana_fee_estimation_failed: 50402, /** - * Wallets - XDefi + * Toolboxes - Substrate */ - wallet_xdefi_not_found: 21601, + toolbox_substrate_not_supported: 50501, + toolbox_substrate_transfer_error: 50502, + toolbox_substrate_no_signer: 50503, + /** + * Toolboxes - Radix + */ + toolbox_radix_method_not_supported: 50601, + /** + * Toolboxes - Ripple + */ + toolbox_ripple_get_balance_error: 50701, + toolbox_ripple_rpc_not_configured: 50702, + toolbox_ripple_signer_not_found: 50703, + toolbox_ripple_asset_not_supported: 50704, + toolbox_ripple_broadcast_error: 50705, + toolbox_ripple_no_signer: 50706, + /** + * Toolboxes - Tron + */ + toolbox_tron_no_signer: 50801, + toolbox_tron_invalid_token_identifier: 50802, + toolbox_tron_token_transfer_failed: 50803, + toolbox_tron_transaction_creation_failed: 50804, + toolbox_tron_fee_estimation_failed: 50805, + toolbox_tron_trongrid_api_error: 50806, + toolbox_tron_approve_failed: 50807, + toolbox_tron_invalid_token_contract: 50808, + toolbox_tron_allowance_check_failed: 50809, + /** + * Toolboxes - Near + */ + toolbox_near_no_signer: 90601, + toolbox_near_invalid_address: 90602, + toolbox_near_invalid_amount: 90603, + toolbox_near_transfer_failed: 90604, + toolbox_near_access_key_error: 90605, + toolbox_near_no_rpc_url: 90606, + toolbox_near_empty_batch: 90607, + toolbox_near_balance_failed: 90608, + toolbox_near_invalid_name: 90609, + toolbox_near_missing_contract_address: 90610, + toolbox_near_no_account: 90611, + toolbox_near_invalid_gas_params: 90612, + /** + * Toolboxes - General + */ + toolbox_not_supported: 59901, + /** + * NEAR Plugin + */ + plugin_near_invalid_name: 41001, + plugin_near_no_connection: 41002, + plugin_near_name_unavailable: 41003, + plugin_near_registration_failed: 41004, + plugin_near_transfer_failed: 41005, + /** + * SwapKit Plugin + */ + plugin_swapkit_invalid_tx_data: 59801, + /** + * SwapKit API + */ + api_v2_invalid_response: 60001, + api_v2_server_error: 60002, + api_v2_invalid_method_key_hash: 60003, + /** + * Helpers + */ + helpers_invalid_number_different_decimals: 70001, + helpers_invalid_number_of_years: 70002, + helpers_invalid_identifier: 70003, + helpers_invalid_asset_url: 70004, + helpers_invalid_asset_identifier: 70005, + helpers_invalid_memo_type: 70006, + helpers_failed_to_switch_network: 70007, + helpers_not_found_provider: 70008, + helpers_chain_not_supported: 70009, + helpers_invalid_params: 70010, + helpers_invalid_response: 70011, + /** + * Anything else + */ + not_implemented: 99999, } as const; export type ErrorKeys = keyof typeof errorCodes; diff --git a/packages/helpers/src/types/chains.ts b/packages/helpers/src/types/chains.ts index 50cb291937..b0cbcced91 100644 --- a/packages/helpers/src/types/chains.ts +++ b/packages/helpers/src/types/chains.ts @@ -270,7 +270,7 @@ export const RPC_URLS: Record = { [Chain.Radix]: "https://radix-mainnet.rpc.grove.city/v1/326002fc/core", [Chain.Ripple]: "wss://xrpl.ws/", [Chain.Solana]: "https://solana-rpc.publicnode.com", - [Chain.THORChain]: "https://rpc.thorswap.net", + [Chain.THORChain]: "https://rpc.ninerealms.com", [Chain.Tron]: "https://tron-rpc.publicnode.com", [Chain.Zcash]: "https://api.tatum.io/v3/blockchain/node/zcash-mainnet/t-6894a2ae7fc90cccfd3ce71b-2fce88aa7f4a41a5b1e93874", @@ -318,7 +318,7 @@ export const FALLBACK_URLS: Record = { [Chain.Ripple]: ["wss://s1.ripple.com/", "wss://s2.ripple.com/"], [Chain.THORChain]: ["https://thornode.ninerealms.com", NODE_URLS[Chain.THORChain]], [StagenetChain.THORChain]: [], - [Chain.Solana]: ["https://api.mainnet-beta.solana.com", "https://solana-mainnet.rpc.extrnode.com"], + [Chain.Solana]: [], [Chain.Tron]: ["https://api.tronstack.io", "https://api.tron.network"], [Chain.Zcash]: [], }; diff --git a/packages/plugins/src/chainflip/broker.ts b/packages/plugins/src/chainflip/broker.ts index 8df3b7e513..9b52cc0798 100644 --- a/packages/plugins/src/chainflip/broker.ts +++ b/packages/plugins/src/chainflip/broker.ts @@ -1,4 +1,6 @@ +import type { SubmittableExtrinsic } from "@polkadot/api/types"; import { decodeAddress } from "@polkadot/keyring"; +import type { Callback, ISubmittableResult } from "@polkadot/types/types"; import { isHex, u8aToHex } from "@polkadot/util"; import { AssetValue, Chain, SwapKitError, wrapWithThrow } from "@swapkit/helpers"; import type { getEvmToolbox } from "@swapkit/toolboxes/evm"; @@ -6,7 +8,17 @@ import type { getSubstrateToolbox } from "@swapkit/toolboxes/substrate"; import type { WithdrawFeeResponse } from "./types"; -type ChainflipToolbox = Awaited>>; +type ChainflipToolbox = Awaited>> & { + signAndBroadcastTransaction: ({ + tx, + callback, + address, + }: { + tx: SubmittableExtrinsic<"promise">; + callback?: Callback; + address?: string; + }) => Promise; +}; export const assetIdentifierToChainflipTicker = new Map([ ["ARB.ETH", "ArbEth"], @@ -28,7 +40,7 @@ const registerAsBroker = (toolbox: ChainflipToolbox) => () => { throw new SwapKitError("chainflip_broker_register"); } - return toolbox.signAndBroadcast({ address: toolbox.getAddress(), tx: extrinsic }); + return toolbox.signAndBroadcastTransaction({ address: toolbox.getAddress(), tx: extrinsic }); }; const withdrawFee = @@ -49,7 +61,7 @@ const withdrawFee = throw new SwapKitError("chainflip_broker_withdraw"); } - toolbox.signAndBroadcast({ + toolbox.signAndBroadcastTransaction({ callback: (result) => { if (!result.status?.isFinalized) { return; diff --git a/packages/plugins/src/chainflip/plugin.ts b/packages/plugins/src/chainflip/plugin.ts index 21c48ff1ce..28a75ea8e1 100644 --- a/packages/plugins/src/chainflip/plugin.ts +++ b/packages/plugins/src/chainflip/plugin.ts @@ -1,4 +1,4 @@ -import { AssetValue, type CryptoChain, ProviderName, SwapKitError } from "@swapkit/helpers"; +import { AssetValue, type Chain, type CryptoChain, ProviderName, SwapKitError } from "@swapkit/helpers"; import { SwapKitApi } from "@swapkit/helpers/api"; import { createPlugin } from "../utils"; import type { RequestSwapDepositAddressParams } from "./types"; @@ -27,7 +27,7 @@ export const ChainflipPlugin = createPlugin({ const sellAsset = await AssetValue.from({ asset: sellAssetString, asyncTokenLookup: true, value: sellAmount }); - const wallet = getWallet(sellAsset.chain as CryptoChain); + const wallet = getWallet(sellAsset.chain as Exclude); if (!wallet) { throw new SwapKitError("core_wallet_connection_not_found"); @@ -39,16 +39,13 @@ export const ChainflipPlugin = createPlugin({ maxBoostFeeBps: maxBoostFeeBps || chainflip.maxBoostFeeBps, }); - // @ts-expect-error TODO: right now it's inferred from toolboxes - // we need to simplify this to one object params const tx = await wallet.transfer({ assetValue: sellAsset, isProgramDerivedAddress: true, recipient: depositAddress, - sender: wallet.address, }); - return tx as string; + return tx; }, }), name: "chainflip", diff --git a/packages/plugins/src/index.ts b/packages/plugins/src/index.ts index 9f4b6d50d0..0398da7025 100644 --- a/packages/plugins/src/index.ts +++ b/packages/plugins/src/index.ts @@ -31,6 +31,10 @@ export async function loadPlugin

(pluginName: P) { const { NearPlugin } = await import("./near"); return NearPlugin; }) + .with("swapkit", async () => { + const { SwapKitPlugin } = await import("./swapkit"); + return SwapKitPlugin; + }) .exhaustive(); return plugin as unknown as SKPlugins[P]; diff --git a/packages/plugins/src/near/plugin.ts b/packages/plugins/src/near/plugin.ts index 4eaf3ed324..8bcd59081e 100644 --- a/packages/plugins/src/near/plugin.ts +++ b/packages/plugins/src/near/plugin.ts @@ -152,16 +152,10 @@ export const NearPlugin = createPlugin({ }, async swap(swapParams: SwapParams<"near", QuoteResponseRoute>) { const { - route: { - buyAsset: buyAssetString, - sellAsset: sellAssetString, - inboundAddress, - sellAmount, - meta: { near }, - }, + route: { buyAsset: buyAssetString, sellAsset: sellAssetString, inboundAddress, sellAmount }, } = swapParams; - if (!(sellAssetString && buyAssetString && near?.sellAsset)) { + if (!(sellAssetString && buyAssetString)) { throw new SwapKitError("core_swap_asset_not_recognized"); } diff --git a/packages/plugins/src/swapkit/index.ts b/packages/plugins/src/swapkit/index.ts new file mode 100644 index 0000000000..7941ca330a --- /dev/null +++ b/packages/plugins/src/swapkit/index.ts @@ -0,0 +1,2 @@ +export * from "./plugin"; +export * from "./types"; diff --git a/packages/plugins/src/swapkit/plugin.ts b/packages/plugins/src/swapkit/plugin.ts new file mode 100644 index 0000000000..f2ff429e9e --- /dev/null +++ b/packages/plugins/src/swapkit/plugin.ts @@ -0,0 +1,274 @@ +import { bitgo, networks } from "@bitgo/utxo-lib"; +import type { ZcashPsbt } from "@bitgo/utxo-lib/dist/src/bitgo"; +import { Transaction } from "@near-js/transactions"; +import { VersionedTransaction } from "@solana/web3.js"; +import { + AssetValue, + Chain, + type CosmosChain, + // type CosmosChain, + CosmosChains, + type CryptoChain, + type EVMChain, + EVMChains, + ProviderName, + SwapKitError, +} from "@swapkit/helpers"; +import { + CosmosTransactionSchema, + type EVMTransaction, + EVMTransactionSchema, + NEARTransactionSchema, + type QuoteResponseRoute, + SwapKitApi, + TronTransactionSchema, +} from "@swapkit/helpers/api"; +import type { FullWallet } from "@swapkit/toolboxes"; +import { Psbt } from "bitcoinjs-lib"; +import { match } from "ts-pattern"; +import type { SwapKitPluginParams } from "../types"; +import { createPlugin } from "../utils"; +import type { SwapKitQuoteParams, SwapKitSwapParams } from "./types"; + +export function walletHasWorkingSigner(wallet: FullWallet[CryptoChain]): boolean { + return !!wallet?.signer; +} + +export const SwapKitPlugin = createPlugin({ + methods: (params: SwapKitPluginParams) => { + const { getWallet } = params; + + /** + * Get a quote from the SwapKit API + */ + async function quote(quoteParams: SwapKitQuoteParams): Promise { + try { + const response = await SwapKitApi.getSwapQuote({ + affiliate: quoteParams.affiliate, + affiliateFee: quoteParams.affiliateBasisPoints, + buyAsset: quoteParams.buyAsset, + destinationAddress: quoteParams.destinationAddress, + providers: quoteParams.providers, + sellAmount: quoteParams.sellAmount || "0", + sellAsset: quoteParams.sellAsset, + slippage: quoteParams.slippage, + sourceAddress: quoteParams.sourceAddress, + }); + + if (!response?.routes || response.routes.length === 0) { + throw new SwapKitError("core_swap_invalid_params", { error: "No routes available for this swap" }); + } + + // Return the best route (first one) + const route = response.routes[0]; + if (!route) { + throw new SwapKitError("core_swap_invalid_params", { error: "No valid route found" }); + } + return route; + } catch (error) { + throw new SwapKitError("core_swap_invalid_params", { error }); + } + } + + /** + * Execute a swap using the SwapKit API route + */ + async function swap(swapParams: SwapKitSwapParams): Promise { + const { + route: { tx, ...route }, + } = swapParams; + + // Determine the source chain from the sell asset + const sellAsset = await AssetValue.from({ asset: route.sellAsset, asyncTokenLookup: true }); + const chain = sellAsset.chain; + + try { + if (!walletHasWorkingSigner(getWallet(chain as CryptoChain))) { + match(chain as CryptoChain) + .returnType>() + .with(...EVMChains, async () => { + const wallet = await getWallet(chain as EVMChain); + if (!wallet) { + throw new SwapKitError("core_wallet_connection_not_found", { chain }); + } + const { from, to, data, value } = tx as EVMTransaction; + return await wallet.sendTransaction({ data, from, to, value: BigInt(value || "0") }); + }) + .with(Chain.Radix, async () => { + const wallet = await getWallet(chain as Chain.Radix); + if (!wallet) { + throw new SwapKitError("core_wallet_connection_not_found", { chain }); + } + return wallet.signAndBroadcast({ manifest: tx as string }); + }) + .otherwise(async () => { + const { targetAddress, sellAmount, memo } = route; + + if (!targetAddress) { + throw new SwapKitError("core_swap_invalid_params", { missing: ["targetAddress"] }); + } + + const assetValue = await AssetValue.from({ + asset: route.sellAsset, + asyncTokenLookup: true, + value: sellAmount, + }); + + const wallet = await getWallet(chain as CryptoChain); + + if (!wallet) { + throw new SwapKitError("core_wallet_connection_not_found", { chain }); + } + + const txHash = await wallet.transfer({ + assetValue, + isProgramDerivedAddress: true, + memo, + recipient: targetAddress, + }); + + return txHash as string; + }); + } + + // Try to use signer-based transaction execution first + return await match(chain as CryptoChain) + .returnType>() + .with(Chain.BitcoinCash, async () => { + if (typeof tx !== "string") { + throw new SwapKitError("plugin_swapkit_invalid_tx_data", { chain, tx }); + } + const wallet = await getWallet(chain as Chain.BitcoinCash); + + if (walletHasWorkingSigner(wallet)) { + return wallet.signAndBroadcastTransaction( + bitgo.UtxoPsbt.fromBuffer(Buffer.from(tx, "base64"), { network: networks.bitcoincash }), + ); + } + throw new Error("No signer available in wallet"); + }) + .with(Chain.Zcash, async () => { + if (typeof tx !== "string") { + throw new SwapKitError("plugin_swapkit_invalid_tx_data", { chain, tx }); + } + const wallet = await getWallet(chain as Chain.Zcash); + + if (walletHasWorkingSigner(wallet)) { + return wallet.signAndBroadcastTransaction( + bitgo.ZcashPsbt.fromBuffer(Buffer.from(tx, "base64"), { network: networks.zcash }) as ZcashPsbt, + ); + } + throw new Error("No signer available in wallet"); + }) + .with(Chain.Bitcoin, Chain.Dogecoin, Chain.Litecoin, async () => { + if (typeof tx !== "string") { + throw new SwapKitError("plugin_swapkit_invalid_tx_data", { chain, tx }); + } + const wallet = await getWallet(chain as Chain.Bitcoin | Chain.Dogecoin | Chain.Litecoin | Chain.Dash); + + if (walletHasWorkingSigner(wallet)) { + const psbt = Psbt.fromBase64(tx); + return wallet.signAndBroadcastTransaction(psbt); + } + throw new Error("No signer available in wallet"); + }) + .with(...EVMChains, async () => { + const transaction = EVMTransactionSchema.safeParse(tx); + if (!transaction.success) { + throw new SwapKitError("plugin_swapkit_invalid_tx_data", { chain, tx }); + } + const wallet = await getWallet(chain as EVMChain); + + if (walletHasWorkingSigner(wallet)) { + return wallet.signAndBroadcastTransaction(transaction.data); + } + throw new Error("No signer available in wallet"); + }) + .with(Chain.Solana, async () => { + if (typeof tx !== "string") { + throw new SwapKitError("plugin_swapkit_invalid_tx_data", { chain, tx }); + } + const wallet = await getWallet(chain as Chain.Solana); + + if (walletHasWorkingSigner(wallet)) { + const transaction = VersionedTransaction.deserialize(Buffer.from(tx, "base64")); + return wallet.signAndBroadcastTransaction(transaction); + } + throw new Error("No signer available in wallet"); + }) + .with(...CosmosChains, async () => { + const transaction = CosmosTransactionSchema.safeParse(tx); + if (!transaction.success) { + throw new SwapKitError("plugin_swapkit_invalid_tx_data", { chain, tx }); + } + const wallet = await getWallet(chain as CosmosChain); + + if (walletHasWorkingSigner(wallet)) { + return wallet.signAndBroadcastTransaction(transaction.data); + } + throw new Error("No signer available in wallet"); + }) + .with(Chain.Near, async () => { + const transaction = NEARTransactionSchema.safeParse(tx); + if (!transaction.success) { + throw new SwapKitError("plugin_swapkit_invalid_tx_data", { chain, tx }); + } + const wallet = await getWallet(chain as Chain.Near); + + if (walletHasWorkingSigner(wallet)) { + return wallet.signAndBroadcastTransaction( + Transaction.decode(Buffer.from(transaction.data.serialized, "base64")), + ); + } + throw new Error("No signer available in wallet"); + }) + .with(Chain.Radix, async () => { + if (typeof tx !== "string") { + throw new SwapKitError("plugin_swapkit_invalid_tx_data", { chain, tx }); + } + const wallet = await getWallet(chain as Chain.Radix); + + if (walletHasWorkingSigner(wallet)) { + return wallet.signAndBroadcast({ manifest: tx }); + } + throw new Error("No signer available in wallet"); + }) + .with(Chain.Ripple, async () => { + if (typeof tx !== "string") { + throw new SwapKitError("plugin_swapkit_invalid_tx_data", { chain, tx }); + } + const wallet = await getWallet(chain as Chain.Ripple); + + if (walletHasWorkingSigner(wallet)) { + return wallet.signAndBroadcastTransaction(JSON.parse(tx)); + } + throw new Error("No signer available in wallet"); + }) + .with(Chain.Tron, async () => { + const transaction = TronTransactionSchema.safeParse(tx); + if (!transaction.success) { + throw new SwapKitError("plugin_swapkit_invalid_tx_data", { chain, tx }); + } + const wallet = await getWallet(chain as Chain.Tron); + + if (walletHasWorkingSigner(wallet)) { + return wallet.signAndBroadcastTransaction(transaction.data); + } + throw new Error("No signer available in wallet"); + }) + .otherwise(() => { + throw new SwapKitError("plugin_swapkit_invalid_tx_data", { + error: `Chain ${chain} is not supported for swaps`, + }); + }); + } catch (error) { + if (error instanceof SwapKitError) throw error; + throw new SwapKitError("core_swap_invalid_params", { error }); + } + } + + return { quote, swap }; + }, + name: "swapkit", + properties: { supportedSwapkitProviders: Object.values(ProviderName) }, +}); diff --git a/packages/plugins/src/swapkit/types.ts b/packages/plugins/src/swapkit/types.ts new file mode 100644 index 0000000000..1a10c06aed --- /dev/null +++ b/packages/plugins/src/swapkit/types.ts @@ -0,0 +1,40 @@ +import type { AssetValue, Chain, FeeOption, GenericTransferParams } from "@swapkit/helpers"; +import type { QuoteResponseRoute } from "@swapkit/helpers/api"; + +export type SwapKitQuoteParams = { + sellAsset: string; + buyAsset: string; + sellAmount?: string; + buyAmount?: string; + sourceAddress?: string; + destinationAddress?: string; + slippage?: number; + providers?: string[]; + affiliate?: string; + affiliateBasisPoints?: number; +}; + +export type SwapKitSwapParams = { route: QuoteResponseRoute; recipient?: string; feeOptionKey?: FeeOption }; + +export type SwapKitTransactionParams = { + // Transaction data from SwapKit API + data?: string; + to: string; + value?: string; + gas?: string; + gasPrice?: string; + + // PSBT for UTXO chains + psbt?: string; + + // Solana transaction + transaction?: string; + + // Cosmos transaction + msg?: any; + + // Fallback transfer params + transferParams?: GenericTransferParams & { assetValue: AssetValue; memo?: string }; +}; + +export type WalletCapabilities = { supportsSignAndBroadcast: boolean; walletType: string; chain: Chain }; diff --git a/packages/plugins/src/types.ts b/packages/plugins/src/types.ts index ad3b37b7ec..688870c5f8 100644 --- a/packages/plugins/src/types.ts +++ b/packages/plugins/src/types.ts @@ -5,15 +5,18 @@ import type { EVMPlugin } from "./evm"; import type { NearPlugin } from "./near"; import type { RadixPlugin } from "./radix"; import type { SolanaPlugin } from "./solana/plugin"; +import type { SwapKitPlugin } from "./swapkit"; import type { ThorchainPlugin } from "./thorchain"; export type * from "./chainflip/types"; +export type * from "./swapkit/types"; export type * from "./thorchain/types"; export type SKPlugins = typeof ChainflipPlugin & typeof ThorchainPlugin & typeof RadixPlugin & typeof SolanaPlugin & + typeof SwapKitPlugin & typeof EVMPlugin & typeof NearPlugin; diff --git a/packages/plugins/src/utils.ts b/packages/plugins/src/utils.ts index 31c563af35..4779d0e247 100644 --- a/packages/plugins/src/utils.ts +++ b/packages/plugins/src/utils.ts @@ -1,5 +1,5 @@ import type { ProviderName } from "@swapkit/helpers"; -import type { SwapKitPluginParams } from "./types"; +import type { PluginName, SKPlugins, SwapKitPluginParams } from "./types"; export function createPlugin< const Name extends string, @@ -12,3 +12,40 @@ export function createPlugin< return { [name]: plugin } as { [key in Name]: typeof plugin }; } + +export async function loadPlugin

(pluginName: P) { + const { match } = await import("ts-pattern"); + + const plugin = await match(pluginName as PluginName) + .with("chainflip", async () => { + const { ChainflipPlugin } = await import("./chainflip"); + return ChainflipPlugin; + }) + .with("thorchain", async () => { + const { ThorchainPlugin } = await import("./thorchain"); + return ThorchainPlugin; + }) + .with("radix", async () => { + const { RadixPlugin } = await import("./radix"); + return RadixPlugin; + }) + .with("evm", async () => { + const { EVMPlugin } = await import("./evm"); + return EVMPlugin; + }) + .with("solana", async () => { + const { SolanaPlugin } = await import("./solana"); + return SolanaPlugin; + }) + .with("swapkit", async () => { + const { SwapKitPlugin } = await import("./swapkit"); + return SwapKitPlugin; + }) + .with("near", async () => { + const { NearPlugin } = await import("./near"); + return NearPlugin; + }) + .exhaustive(); + + return plugin as unknown as SKPlugins[P]; +} diff --git a/packages/toolboxes/src/cosmos/thorchainUtils/registry.ts b/packages/toolboxes/src/cosmos/thorchainUtils/registry.ts index 77ff8f5f5e..2e346ca149 100644 --- a/packages/toolboxes/src/cosmos/thorchainUtils/registry.ts +++ b/packages/toolboxes/src/cosmos/thorchainUtils/registry.ts @@ -19,12 +19,13 @@ export async function createDefaultAminoTypes(chain: Chain.THORChain | Chain.May const imported = await import("@cosmjs/stargate"); const AminoTypes = imported.AminoTypes ?? imported.default?.AminoTypes; const aminoTypePrefix = chain === Chain.THORChain ? "thorchain" : "mayachain"; + const addressPrefix = chain === Chain.THORChain ? "thor" : "maya"; return new AminoTypes({ "/types.MsgDeposit": { aminoType: `${aminoTypePrefix}/MsgDeposit`, fromAmino: ({ signer, ...rest }: any) => ({ ...rest, signer: bech32ToBase64(signer) }), - toAmino: ({ signer, ...rest }: any) => ({ ...rest, signer: base64ToBech32(signer) }), + toAmino: ({ signer, ...rest }: any) => ({ ...rest, signer: base64ToBech32(signer, addressPrefix) }), }, "/types.MsgSend": { aminoType: `${aminoTypePrefix}/MsgSend`, @@ -35,8 +36,8 @@ export async function createDefaultAminoTypes(chain: Chain.THORChain | Chain.May }), toAmino: ({ fromAddress, toAddress, ...rest }: any) => ({ ...rest, - from_address: base64ToBech32(fromAddress), - to_address: base64ToBech32(toAddress), + from_address: base64ToBech32(fromAddress, addressPrefix), + to_address: base64ToBech32(toAddress, addressPrefix), }), }, }); diff --git a/packages/toolboxes/src/cosmos/toolbox/cosmos.ts b/packages/toolboxes/src/cosmos/toolbox/cosmos.ts index 2d68d780f8..646b3799ce 100644 --- a/packages/toolboxes/src/cosmos/toolbox/cosmos.ts +++ b/packages/toolboxes/src/cosmos/toolbox/cosmos.ts @@ -22,9 +22,9 @@ import { SwapKitNumber, updateDerivationPath, } from "@swapkit/helpers"; -import { SwapKitApi } from "@swapkit/helpers/api"; +import { type CosmosTransaction, SwapKitApi } from "@swapkit/helpers/api"; import { match, P } from "ts-pattern"; -import type { CosmosToolboxParams } from "../types"; +import type { CosmosSignedTransaction, CosmosToolboxParams } from "../types"; import { cosmosCreateTransaction, createSigningStargateClient, @@ -136,6 +136,61 @@ export async function createCosmosToolbox({ chain, ...toolboxParams }: CosmosToo return base64.encode(account?.pubkey); } + async function signTransaction(transaction: CosmosTransaction): Promise { + const from = await getAddress(); + + if (!(signer && from)) { + throw new SwapKitError("toolbox_cosmos_signer_not_defined"); + } + + const signingClient = await createSigningStargateClient(rpcUrl, signer); + + // Sign the transaction without broadcasting + const txRaw = await signingClient.sign(from, transaction.msgs, transaction.fee, transaction.memo, { + accountNumber: transaction.accountNumber, + chainId: transaction.chainId, + sequence: transaction.sequence, + }); + + // Import TxRaw for encoding + const { TxRaw } = await import("cosmjs-types/cosmos/tx/v1beta1/tx"); + const txBytes = TxRaw.encode(txRaw).finish(); + + // Calculate transaction hash + const importedCrypto = await import("@cosmjs/crypto"); + const sha256 = importedCrypto.sha256 ?? importedCrypto.default?.sha256; + const importedEncoding = await import("@cosmjs/encoding"); + const toHex = importedEncoding.toHex ?? importedEncoding.default?.toHex; + const txHash = toHex(sha256(txBytes)).toUpperCase(); + + return { txBytes, txHash }; + } + + async function signAndBroadcastTransaction(transaction: CosmosTransaction): Promise { + const from = await getAddress(); + + if (!(signer && from)) { + throw new SwapKitError("toolbox_cosmos_signer_not_defined"); + } + + const signingClient = await createSigningStargateClient(rpcUrl, signer); + + // Use signAndBroadcast for atomic operation + const result = await signingClient.signAndBroadcast( + from, + transaction.msgs, + transaction.fee, + transaction.memo, + undefined, // timeoutHeight + ); + + if (result.code !== 0) { + throw new SwapKitError("core_swap_transaction_error", { code: result.code, message: result.rawLog }); + } + + return result.transactionHash; + } + async function transfer({ recipient, assetValue, @@ -200,6 +255,9 @@ export async function createCosmosToolbox({ chain, ...toolboxParams }: CosmosToo importedSigning.DirectSecp256k1Wallet ?? importedSigning.default?.DirectSecp256k1Wallet; return DirectSecp256k1Wallet.fromKey(privateKey, chainPrefix); }, + signAndBroadcastTransaction, + signer, + signTransaction, transfer, validateAddress: getCosmosValidateAddress(chainPrefix), verifySignature: verifySignature(getAccount), diff --git a/packages/toolboxes/src/cosmos/toolbox/thorchain.ts b/packages/toolboxes/src/cosmos/toolbox/thorchain.ts index b6d922f8d2..4941f5cc3e 100644 --- a/packages/toolboxes/src/cosmos/toolbox/thorchain.ts +++ b/packages/toolboxes/src/cosmos/toolbox/thorchain.ts @@ -244,6 +244,7 @@ export async function createThorchainToolbox({ derivationPath: derivationPathToString(derivationPath), prefix: chainPrefix, }), + signer, signMultisigTx: signMultisigTx(chain), signWithPrivateKey, transfer, diff --git a/packages/toolboxes/src/cosmos/types.ts b/packages/toolboxes/src/cosmos/types.ts index 538faf5dca..e84fd4cbbf 100644 --- a/packages/toolboxes/src/cosmos/types.ts +++ b/packages/toolboxes/src/cosmos/types.ts @@ -31,6 +31,12 @@ export type MultisigTx = { export type CosmosSigner = DirectSecp256k1HdWallet | OfflineDirectSigner | OfflineAminoSigner; +export type CosmosSignedTransaction = { + txBytes: Uint8Array; + txHash: string; + bodyBytes?: Uint8Array; // For THORChain multisig support +}; + export type CosmosToolboxParams = { chain: T } & ( | { signer?: CosmosSigner } | { phrase?: string; derivationPath?: DerivationPathArray; index?: number } diff --git a/packages/toolboxes/src/evm/toolbox/baseEVMToolbox.ts b/packages/toolboxes/src/evm/toolbox/baseEVMToolbox.ts index 332580a346..b2d6947af1 100644 --- a/packages/toolboxes/src/evm/toolbox/baseEVMToolbox.ts +++ b/packages/toolboxes/src/evm/toolbox/baseEVMToolbox.ts @@ -24,6 +24,7 @@ import { type JsonRpcSigner, type Provider, type Signer, + type TransactionRequest, } from "ethers"; import { toHexString } from "../helpers"; import type { @@ -47,6 +48,44 @@ type ToolboxWrapParams

= T & { export const MAX_APPROVAL = BigInt("0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); +function getSignTransaction({ signer }: { signer?: Signer }) { + return async function signTransaction(tx: TransactionRequest): Promise { + if (!signer) throw new SwapKitError("toolbox_evm_no_signer"); + + // Sign the transaction without broadcasting + const signedTx = await signer.signTransaction(tx); + return signedTx; + }; +} + +function getSignAndBroadcastTransaction({ + chain, + provider, + signer, +}: { + chain: EVMChain; + provider: Provider | BrowserProvider; + signer?: Signer; +}) { + return async function signAndBroadcastTransaction({ from, to, data, value }: TransactionRequest): Promise { + if (!signer) throw new SwapKitError("toolbox_evm_no_signer"); + + const tx = { data, from, to, value }; + + const gasLimit = await provider.estimateGas(tx); + const gasPrices = getEstimateGasPrices({ chain, provider })(); + + const transaction = { ...tx, ...gasPrices, gasLimit }; + + // Sign the transaction + const signedTx = await signer.signTransaction(transaction); + + // Broadcast the signed transaction + const response = await provider.broadcastTransaction(signedTx); + return response.hash; + }; +} + export function BaseEVMToolbox< P extends Provider | BrowserProvider, S extends (ChainSigner & Signer) | JsonRpcSigner | HDNodeWallet | undefined, @@ -81,7 +120,10 @@ export function BaseEVMToolbox< }, isApproved: getIsApproved({ chain, provider }), sendTransaction: getSendTransaction({ chain, isEIP1559Compatible, provider, signer }), + signAndBroadcastTransaction: getSignAndBroadcastTransaction({ chain, provider, signer }), + signer, signMessage: signer?.signMessage, + signTransaction: getSignTransaction({ signer }), transfer: getTransfer({ chain, isEIP1559Compatible, provider, signer }), validateAddress: (address: string) => evmValidateAddress({ address }), }; diff --git a/packages/toolboxes/src/near/toolbox.ts b/packages/toolboxes/src/near/toolbox.ts index f1c3002f3a..7dce972915 100644 --- a/packages/toolboxes/src/near/toolbox.ts +++ b/packages/toolboxes/src/near/toolbox.ts @@ -225,6 +225,38 @@ export async function getNearToolbox(toolboxParams?: NearToolboxParams): Promise return result.transaction.hash; } + async function signAndBroadcastTransaction(transaction: Transaction): Promise { + if (!signer) { + throw new SwapKitError("toolbox_near_no_signer"); + } + + try { + // Sign the transaction + const signedTransaction = await signTransaction(transaction); + + // Broadcast the signed transaction + return await broadcastTransaction(signedTransaction); + } catch (error) { + throw new SwapKitError("toolbox_near_transfer_failed", { error }); + } + } + + async function signMessage(message: string): Promise { + if (!signer?.sign) { + throw new SwapKitError("toolbox_near_no_signer"); + } + + try { + // Sign the message using the key pair + const signature = await signer.sign(message); + + // Return the signature as a base64 string + return Buffer.from(signature).toString("base64"); + } catch (error) { + throw new SwapKitError("toolbox_near_transfer_failed", { error }); + } + } + async function estimateTransactionFee(params: NearTransferParams | NearGasEstimateParams) { if ("assetValue" in params) { const baseTransferCost = "115123062500"; // gas units for transfer @@ -444,6 +476,9 @@ export async function getNearToolbox(toolboxParams?: NearToolboxParams): Promise getSignerFromPrivateKey: getNearSignerFromPrivateKey, nep141, provider, + signAndBroadcastTransaction, + signer, + signMessage, signTransaction, transfer, validateAddress: await getValidateNearAddress(), diff --git a/packages/toolboxes/src/near/types/toolbox.ts b/packages/toolboxes/src/near/types/toolbox.ts index 5e496e235b..9a39a66e78 100644 --- a/packages/toolboxes/src/near/types/toolbox.ts +++ b/packages/toolboxes/src/near/types/toolbox.ts @@ -46,6 +46,9 @@ export interface NearToolbox { estimateTransactionFee: (params: NearTransferParams | NearGasEstimateParams) => Promise; broadcastTransaction: (signedTransaction: SignedTransaction) => Promise; signTransaction: (transaction: Transaction) => Promise; + signAndBroadcastTransaction: (transaction: Transaction) => Promise; + signer: NearSigner | undefined; + signMessage: (message: string) => Promise; getBalance: (address: string) => Promise; validateAddress: (address: string) => boolean; getSignerFromPhrase: (params: GetSignerFromPhraseParams) => Promise; diff --git a/packages/toolboxes/src/radix/index.ts b/packages/toolboxes/src/radix/index.ts index 78ac0ff13a..f294317713 100644 --- a/packages/toolboxes/src/radix/index.ts +++ b/packages/toolboxes/src/radix/index.ts @@ -127,6 +127,10 @@ export const RadixToolbox = async ({ dappConfig }: { dappConfig?: SKConfigIntegr signAndBroadcast: (() => { throw new SwapKitError("toolbox_radix_method_not_supported", { method: "signAndBroadcast" }); }) as (params: any) => Promise, + signer: undefined, + transfer: (() => { + throw new SwapKitError("toolbox_radix_method_not_supported", { method: "transfer" }); + }) as (params: any) => Promise, validateAddress: radixValidateAddress, }; }; diff --git a/packages/toolboxes/src/ripple/index.ts b/packages/toolboxes/src/ripple/index.ts index 4601a23cda..2761394efd 100644 --- a/packages/toolboxes/src/ripple/index.ts +++ b/packages/toolboxes/src/ripple/index.ts @@ -21,11 +21,8 @@ const RIPPLE_ERROR_CODES = { ACCOUNT_NOT_FOUND: 19 } as const; function createSigner(phrase: string): ChainSigner { const wallet = Wallet.fromMnemonic(phrase); return { - // publicKey: wallet.publicKey, - // Address is sync, but interface requires async getAddress: () => Promise.resolve(wallet.address), - // Signing is sync, but interface requires async - signTransaction: (tx: Transaction) => Promise.resolve(wallet.sign(tx as Transaction)), // Cast needed as Wallet.sign expects Transaction + signTransaction: (tx: Transaction) => Promise.resolve(wallet.sign(tx as Transaction)), }; } @@ -35,7 +32,7 @@ export function rippleValidateAddress(address: string) { type RippleToolboxParams = | { phrase?: string } - | { signer?: ChainSigner }; + | { signer?: ChainSigner & { wallet?: Wallet } }; export const getRippleToolbox = async (params: RippleToolboxParams = {}) => { const signer = @@ -145,6 +142,20 @@ export const getRippleToolbox = async (params: RippleToolboxParams = {}) => { throw new SwapKitError({ errorKey: "toolbox_ripple_broadcast_error", info: { chain: Chain.Ripple } }); }; + const signAndBroadcastTransaction = async (tx: Transaction): Promise => { + if (!signer) { + throw new SwapKitError({ errorKey: "toolbox_ripple_signer_not_found" }); + } + + 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" }); @@ -159,15 +170,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 3bc08b6f36..3464027692 100644 --- a/packages/toolboxes/src/solana/index.ts +++ b/packages/toolboxes/src/solana/index.ts @@ -1,20 +1,18 @@ -import type { PublicKey, SendOptions, Transaction, VersionedTransaction } from "@solana/web3.js"; +import type { PublicKey, Transaction, VersionedTransaction } from "@solana/web3.js"; import type { GenericCreateTransactionParams, GenericTransferParams } from "@swapkit/helpers"; import type { getSolanaToolbox } from "./toolbox"; -type DisplayEncoding = "utf8" | "hex"; +// type DisplayEncoding = "utf8" | "hex"; -type PhantomEvent = "connect" | "disconnect" | "accountChanged"; - -type PhantomRequestMethod = - | "connect" - | "disconnect" - | "signAndSendTransaction" - | "signAndSendTransactionV0" - | "signAndSendTransactionV0WithLookupTable" - | "signTransaction" - | "signAllTransactions" - | "signMessage"; +// type PhantomRequestMethod = +// | "connect" +// | "disconnect" +// | "signAndSendTransaction" +// | "signAndSendTransactionV0" +// | "signAndSendTransactionV0WithLookupTable" +// | "signTransaction" +// | "signAllTransactions" +// | "signMessage"; interface ConnectOpts { onlyIfTrusted: boolean; @@ -27,19 +25,8 @@ export type SolanaWallet = Awaited>; export interface SolanaProvider { connect: (opts?: Partial) => Promise<{ publicKey: PublicKey }>; disconnect: () => Promise; - getAddress: () => Promise; - isConnected: boolean | null; - isPhantom: boolean; - on: (event: PhantomEvent, handler: (args: any) => void) => void; publicKey: PublicKey | null; - request: (method: PhantomRequestMethod, params: any) => Promise; - signMessage: (message: Uint8Array | string, display?: DisplayEncoding) => Promise; - signAndSendTransaction: ( - transaction: Transaction | VersionedTransaction, - opts?: SendOptions, - ) => Promise<{ signature: string; publicKey: PublicKey }>; signTransaction: (transaction: T) => Promise; - signAllTransactions: (transactions: T[]) => Promise; } export type SolanaCreateTransactionParams = Omit & { diff --git a/packages/toolboxes/src/solana/toolbox.ts b/packages/toolboxes/src/solana/toolbox.ts index 049c252a8b..3445f68998 100644 --- a/packages/toolboxes/src/solana/toolbox.ts +++ b/packages/toolboxes/src/solana/toolbox.ts @@ -130,6 +130,11 @@ export async function getSolanaToolbox( }, 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), }; @@ -328,7 +333,7 @@ function transfer(getConnection: () => Promise, signer?: SolanaSigne sender, }); - if ("connect" in signer) { + if ("signTransaction" in signer) { const signedTransaction = await signer.signTransaction(transaction); return broadcastTransaction(getConnection)(signedTransaction); } diff --git a/packages/toolboxes/src/substrate/substrate.ts b/packages/toolboxes/src/substrate/substrate.ts index 82499aa13f..59b00a31aa 100644 --- a/packages/toolboxes/src/substrate/substrate.ts +++ b/packages/toolboxes/src/substrate/substrate.ts @@ -82,11 +82,10 @@ const transfer = async ( { recipient, assetValue, sender }: SubstrateTransferParams, ) => { const transfer = createTransaction(api, { assetValue, recipient }); + if (!transfer) throw new SwapKitError("toolbox_substrate_transfer_error"); const isKeyring = isKeyringPair(signer); - if (!transfer) return; - const address = isKeyring ? (signer as IKeyringPair).address : sender; if (!address) throw new SwapKitError("core_transaction_invalid_sender_address"); @@ -97,7 +96,7 @@ const transfer = async ( signer: isKeyring ? undefined : signer, }); - return tx?.toString(); + return tx.toString(); }; const estimateTransactionFee = async ( @@ -191,37 +190,8 @@ export const BaseSubstrateToolbox = ({ gasAsset: AssetValue; signer?: IKeyringPair | Signer; chain?: SubstrateChain; -}) => ({ - api, - broadcast, - convertAddress, - createKeyring: (phrase: string) => createKeyring(phrase, network.prefix), - createTransaction: (params: GenericCreateTransactionParams) => createTransaction(api, params), - decodeAddress, - encodeAddress, - estimateTransactionFee: (params: SubstrateTransferParams) => { - if (!signer) throw new SwapKitError("core_wallet_not_keypair_wallet"); - return estimateTransactionFee(api, signer, gasAsset, params); - }, - gasAsset, - getAddress: (keyring?: IKeyringPair | Signer) => { - const keyringPair = keyring || signer; - if (!keyringPair) throw new SwapKitError("core_wallet_not_keypair_wallet"); - - return isKeyringPair(keyringPair) ? keyringPair.address : undefined; - }, - getBalance: createBalanceGetter(chain || Chain.Polkadot, api), - network, - sign: (tx: SubmittableExtrinsic<"promise">) => { - if (!signer) throw new SwapKitError("core_wallet_not_keypair_wallet"); - if (isKeyringPair(signer)) return sign(signer, tx); - - throw new SwapKitError( - "core_wallet_not_keypair_wallet", - "Signer does not have keyring pair capabilities required for signing.", - ); - }, - signAndBroadcast: ({ +}) => { + function signAndBroadcastTransaction({ tx, callback, address, @@ -229,7 +199,7 @@ export const BaseSubstrateToolbox = ({ tx: SubmittableExtrinsic<"promise">; callback?: Callback; address?: string; - }) => { + }) { if (!signer) throw new SwapKitError("core_wallet_not_keypair_wallet"); if (isKeyringPair(signer)) return signAndBroadcastKeyring(signer, tx, callback); @@ -241,13 +211,47 @@ export const BaseSubstrateToolbox = ({ "core_wallet_not_keypair_wallet", "Signer does not have keyring pair capabilities required for signing.", ); - }, - transfer: (params: SubstrateTransferParams) => { - if (!signer) throw new SwapKitError("core_wallet_not_keypair_wallet"); - return transfer(api, signer, params); - }, - validateAddress: (address: string) => validateAddress(address, network.prefix), -}); + } + + return { + api, + broadcast, + convertAddress, + createKeyring: (phrase: string) => createKeyring(phrase, network.prefix), + createTransaction: (params: GenericCreateTransactionParams) => createTransaction(api, params), + decodeAddress, + encodeAddress, + estimateTransactionFee: (params: SubstrateTransferParams) => { + if (!signer) throw new SwapKitError("core_wallet_not_keypair_wallet"); + return estimateTransactionFee(api, signer, gasAsset, params); + }, + gasAsset, + getAddress: (keyring?: IKeyringPair | Signer) => { + const keyringPair = keyring || signer; + if (!keyringPair) throw new SwapKitError("core_wallet_not_keypair_wallet"); + + return isKeyringPair(keyringPair) ? keyringPair.address : undefined; + }, + getBalance: createBalanceGetter(chain || Chain.Polkadot, api), + network, + sign, + /** + * @deprecated @V4 Use signAndBroadcastTransaction instead - will be removed in next major + */ + signAndBroadcast: signAndBroadcastTransaction, + signAndBroadcastTransaction, + signer, + signTransaction: (tx: SubmittableExtrinsic<"promise">) => { + if (!signer || !isKeyringPair(signer)) throw new SwapKitError("toolbox_substrate_no_signer"); + return sign(signer, tx); + }, + transfer: (params: SubstrateTransferParams) => { + if (!signer) throw new SwapKitError("core_wallet_not_keypair_wallet"); + return transfer(api, signer, params); + }, + validateAddress: (address: string) => validateAddress(address, network.prefix), + }; +}; export const substrateValidateAddress = ({ address, diff --git a/packages/toolboxes/src/tron/toolbox.ts b/packages/toolboxes/src/tron/toolbox.ts index 9a87a8689f..ff9316b6f0 100644 --- a/packages/toolboxes/src/tron/toolbox.ts +++ b/packages/toolboxes/src/tron/toolbox.ts @@ -116,6 +116,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; @@ -516,6 +518,11 @@ export const createTronToolbox = async ( return txid; }; + const signAndBroadcastTransaction = async (transaction: TronTransaction) => { + const signedTx = await signTransaction(transaction); + return await broadcastTransaction(signedTx); + }; + /** * Check the current allowance for a spender on a token */ @@ -600,6 +607,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 29d149e4a7..3735484656 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,40 +10,33 @@ 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 "./index"; -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 }; } @@ -65,9 +54,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 +70,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 +102,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 +109,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 +125,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/index.ts b/packages/toolboxes/src/utxo/toolbox/index.ts index 389d7de6b3..3f9d9b124e 100644 --- a/packages/toolboxes/src/utxo/toolbox/index.ts +++ b/packages/toolboxes/src/utxo/toolbox/index.ts @@ -1,7 +1,6 @@ -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, type DerivationPathArray, SwapKitError, type UTXOChain } from "@swapkit/helpers"; import type { Psbt } from "bitcoinjs-lib"; -import type { TransactionBuilderType, TransactionType, UTXOType } from "../types"; import { createBCHToolbox } from "./bitcoinCash"; import { createUTXOToolbox } from "./utxo"; import { createZcashToolbox } from "./zcash"; @@ -26,7 +25,7 @@ export type UTXOWallets = { }; 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 5083be4bd9..95ae24e576 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,7 @@ import { applyFeeMultiplier, Chain, type ChainSigner, - DerivationPath, + // DerivationPath, type DerivationPathArray, derivationPathToString, FeeOption, @@ -147,7 +145,7 @@ async function createSignerWithKeys({ phrase: string; derivationPath: string; }) { - const keyPair = (await getCreateKeysForPath(chain as Chain.Bitcoin))({ derivationPath, phrase }); + const keyPair = (await getCreateKeysForPath(chain))({ derivationPath, phrase }); async function signTransaction(psbt: Psbt) { await psbt.signAllInputs(keyPair); @@ -162,6 +160,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 @@ -209,6 +241,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 }), }; @@ -296,7 +331,7 @@ function estimateTransactionFee(chain: UTXOChain) { } type CreateKeysForPathReturnType = { - [Chain.BitcoinCash]: BchECPair; + [Chain.BitcoinCash]: ECPairInterface; [Chain.Bitcoin]: ECPairInterface; [Chain.Dash]: ECPairInterface; [Chain.Dogecoin]: ECPairInterface; @@ -310,30 +345,32 @@ export async function getCreateKeysForPath CreateKeysForPathReturnType[T]; + // } - return keyPair as BchECPair; - } as (params: { wif?: string; phrase?: string; derivationPath?: string }) => 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 177f2574fe..d9ed634686 100644 --- a/packages/toolboxes/src/utxo/toolbox/zcash.ts +++ b/packages/toolboxes/src/utxo/toolbox/zcash.ts @@ -234,11 +234,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/wallet-hardware/package.json b/packages/wallet-hardware/package.json index c94ce2f1e9..e0d33ac436 100644 --- a/packages/wallet-hardware/package.json +++ b/packages/wallet-hardware/package.json @@ -15,6 +15,7 @@ "@ledgerhq/wallet-api-client": "~1.9.0", "@swapkit/helpers": "workspace:*", "@swapkit/toolboxes": "workspace:*", + "@trezor/connect-web": "~9.6.0", "ethers": "^6.14.0", "ts-pattern": "^5.7.0" }, @@ -32,6 +33,7 @@ "@ledgerhq/hw-transport": "6.31.6", "@ledgerhq/hw-transport-webusb": "6.29.9", "@ledgerhq/wallet-api-client": "1.9.1", + "@trezor/connect-web": "~9.6.0", "ethers": "6.15.0", "ts-pattern": "5.8.0" }, diff --git a/packages/wallet-hardware/src/keepkey/chains/utxo.ts b/packages/wallet-hardware/src/keepkey/chains/utxo.ts index 7f591f045e..aaf470b258 100644 --- a/packages/wallet-hardware/src/keepkey/chains/utxo.ts +++ b/packages/wallet-hardware/src/keepkey/chains/utxo.ts @@ -1,27 +1,17 @@ +import type { UtxoPsbt } from "@bitgo/utxo-lib/dist/src/bitgo"; import type { KeepKeySdk } from "@keepkey/keepkey-sdk"; import { Chain, DerivationPath, type DerivationPathArray, derivationPathToString, - FeeOption, - type GenericTransferParams, SwapKitError, type UTXOChain, } from "@swapkit/helpers"; -import type { UTXOToolboxes } from "@swapkit/toolboxes/utxo"; -import type { Psbt } from "bitcoinjs-lib"; +import { stripToCashAddress } from "@swapkit/toolboxes/utxo"; +import { type Psbt, script, Transaction } from "bitcoinjs-lib"; import { bip32ToAddressNList, ChainToKeepKeyName } from "../coins"; -interface KeepKeyInputObject { - addressNList: number[]; - scriptType: string; - amount: string; - vout: number; - txid: string; - hex: string; -} - export const utxoWalletMethods = async ({ sdk, chain, @@ -33,7 +23,6 @@ export const utxoWalletMethods = async ({ }) => { const { getUtxoToolbox } = await import("@swapkit/toolboxes/utxo"); // This might not work for BCH - const toolbox = await getUtxoToolbox(chain); const scriptType = [Chain.Bitcoin, Chain.Litecoin].includes(chain) ? ("p2wpkh" as const) : ("p2pkh" as const); const derivationPathString = derivationPath ? derivationPathToString(derivationPath) : `${DerivationPath[chain]}/0`; @@ -46,7 +35,57 @@ export const utxoWalletMethods = async ({ const walletAddress: string = (await sdk.address.utxoGetAddress(addressInfo)).address; - const signTransaction = async (psbt: Psbt, inputs: KeepKeyInputObject[], memo = "") => { + function psbtToKeepKeyParams(psbt: Psbt | UtxoPsbt) { + // 1. Map Inputs (logic remains the same) + const inputs = psbt.data.inputs.map((input, index) => { + const txInput = psbt.txInputs[index]; + let utxoValue: number; + let utxoHex: string; + + // ---- THIS IS THE CRITICAL LOGIC ---- + if (input.witnessUtxo) { + // Use this path for SegWit inputs (BTC, LTC) + utxoValue = Number(input.witnessUtxo.value); + + // KeepKey's `hex` field requires the full transaction. If the PSBT creator + // only provided a witnessUtxo, we may need to fetch the full transaction hex here. + // A well-formed PSBT for KeepKey should probably include it anyway. + if (input.nonWitnessUtxo) { + utxoHex = input.nonWitnessUtxo.toString("hex"); + } else { + // You would need a helper here to fetch the raw transaction from a block explorer + utxoHex = ""; + } + } else if (input.nonWitnessUtxo) { + // Use this path for Non-SegWit inputs (BCH, DOGE, legacy BTC/LTC) + const prevTx = Transaction.fromBuffer(input.nonWitnessUtxo); + utxoValue = prevTx.outs[txInput?.index || 0]?.value || 0; + utxoHex = input.nonWitnessUtxo.toString("hex"); + } else { + // This is an invalid PSBT for signing; it's missing the necessary UTXO info. + throw new Error(`PSBT input ${index} is missing both witnessUtxo and nonWitnessUtxo.`); + } + // ------------------------------------ + + const bip32Derivation = input.bip32Derivation?.[0]; + if (!bip32Derivation) { + throw new Error(`PSBT input ${index} is missing derivation path required by KeepKey.`); + } + + // ... logic to determine scriptType from the UTXO's script ... + + return { + addressNList: addressInfo.address_n, + amount: utxoValue.toString(), + hex: utxoHex, + scriptType, + txid: txInput?.hash.toString("hex"), + vout: txInput?.index, + }; + }); + + // 2. MAP OUTPUTS & SEPARATE THE MEMO (OP_RETURN) + let opReturnData = ""; const outputs = psbt.txOutputs .map((output) => { const { value, address, change } = output as { @@ -56,21 +95,23 @@ export const utxoWalletMethods = async ({ change?: boolean; }; - const outputAddress = - // @ts-expect-error - stripToCashAddress is not defined in the UTXO toolbox just only on BCH - chain === Chain.BitcoinCash ? toolbox.stripToCashAddress(address) : address; + const outputAddress = chain === Chain.BitcoinCash ? stripToCashAddress(address) : address; + + if (output.script && output.script[0] === 0x6a) { + // An OP_RETURN script always starts with the byte 0x6a. + // ---- THIS IS THE CORRECTED LOGIC ---- + // KeepKey expects a clear string, so we decode the buffer as UTF-8. + opReturnData = output.script.slice(1).toString("utf8"); + // ------------------------------------ + return null; // Exclude OP_RETURN from outputs + } if (change || address === walletAddress) { - return { - addressNList: addressInfo.address_n, - addressType: "change", - amount: value, - isChange: true, - scriptType, - }; + return { address: output.address, addressType: "change", amount: value, isChange: true, scriptType }; } if (outputAddress) { + // This is a RECIPIENT output return { address: outputAddress, addressType: "spend", amount: value }; } @@ -78,6 +119,48 @@ export const utxoWalletMethods = async ({ }) .filter(Boolean); + // 3. Return the final object for KeepKey + return { + inputs, + opReturnData, // This is now a clear string, e.g., "for my friend" + outputs, + }; + } + + const signTransaction = async (psbt: T) => { + const { inputs, opReturnData: memo, outputs } = psbtToKeepKeyParams(psbt); + + // const outputs = psbt.txOutputs + // .map((output) => { + // const { value, address, change } = output as { + // address: string; + // script: Buffer; + // value: number; + // change?: boolean; + // }; + + // const outputAddress = + // // @ts-expect-error - stripToCashAddress is not defined in the UTXO toolbox just only on BCH + // chain === Chain.BitcoinCash ? toolbox.stripToCashAddress(address) : address; + + // if (change || address === walletAddress) { + // return { + // addressNList: addressInfo.address_n, + // addressType: "change", + // amount: value, + // isChange: true, + // scriptType, + // }; + // } + + // if (outputAddress) { + // return { address: outputAddress, addressType: "spend", amount: value }; + // } + + // return null; + // }) + // .filter(Boolean); + const removeNullAndEmptyObjectsFromArray = (arr: any[]) => { return arr.filter((item) => item !== null && typeof item === "object" && Object.keys(item).length > 0); }; @@ -89,42 +172,87 @@ export const utxoWalletMethods = async ({ outputs: removeNullAndEmptyObjectsFromArray(outputs), }); - return responseSign.serializedTx?.toString(); - }; + if (responseSign.serializedTx) { + // 3. PARSE the finalized transaction hex back into a transaction object + const finalTx = Transaction.fromHex(responseSign.serializedTx.toString()); - const transfer = async ({ recipient, feeOptionKey, feeRate, memo, ...rest }: GenericTransferParams) => { - if (!walletAddress) - throw new SwapKitError("wallet_keepkey_invalid_params", { reason: "From address must be provided" }); - 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 { psbt, inputs: rawInputs } = await createTxMethod({ - ...rest, - feeRate: feeRate || (await toolbox.getFeeRates())[feeOptionKey || FeeOption.Fast], - fetchTxHex: true, - memo, - recipient, - sender: walletAddress, - }); + // 4. EXTRACT the signatures and UPDATE the original PSBT + psbt.data.inputs.forEach((input, index) => { + const finalTxInput = finalTx.ins[index]; + + if (!finalTxInput || !finalTxInput.script) { + throw new SwapKitError("wallet_keepkey_signing_error", { + error: `Could not find a valid signature script in the final transaction for input ${index}`, + }); + } + + if (!input.bip32Derivation || !input.bip32Derivation[0]) { + return; // Cannot add signature without pubkey info from original PSBT + } + + // The finalTxInput.script contains the fully assembled scriptSig. + // We can pass this directly to the psbt's finalizer methods, + // or for consistency, add it to the partialSig map. + // NOTE: For a simple P2PKH, the script is [signature, pubkey]. + // The `psbt.updateInput` is the cleanest way to add this back. + // We are essentially taking the "answer" from the final tx and putting it + // into our PSBT's "worksheet". + + const pubkey = input.bip32Derivation[0].pubkey; + + // Decompile the script to extract just the signature + // Note: This can be complex. For P2PKH, the first push is the signature. + const chunks = script.decompile(finalTxInput.script); + if (!chunks || chunks.length < 2) { + throw new SwapKitError("wallet_keepkey_signing_error", { + error: `Unexpected script format in final transaction for input ${index}`, + }); + } + const signature = chunks[0] as Buffer; - const inputs = rawInputs.map(({ value, index, hash, txHex }) => ({ - //@TODO don't hardcode master, lookup on blockbook what input this is for and what path that address is! - addressNList: addressInfo.address_n, - amount: value.toString(), - hex: txHex || "", - scriptType, - txid: hash, - vout: index, - })); - - const txHex = await signTransaction(psbt, inputs, memo); - return toolbox.broadcastTx(txHex); + psbt.updateInput(index, { partialSig: [{ pubkey, signature }] }); + }); + + // 5. Return the updated PSBT, which now contains KeepKey's signature + return psbt; + } + throw new SwapKitError("wallet_keepkey_signing_error", { + error: "KeepKey did not return a serialized transaction", + }); }; - return { ...toolbox, address: walletAddress, signTransaction, transfer }; + const signer = { getAddress: async () => walletAddress, signTransaction: signTransaction }; + + const toolbox = await getUtxoToolbox>(chain, { signer }); + + // const signAndBroadcastTransaction = async (psbt: Psbt) => { + // const txHex = await signTransaction(psbt as Psbt); + // return toolbox.broadcastTx(txHex); + // }; + + // const transfer = async ({ recipient, feeOptionKey, feeRate, memo, ...rest }: GenericTransferParams) => { + // if (!walletAddress) + // throw new SwapKitError("wallet_keepkey_invalid_params", { reason: "From address must be provided" }); + // if (!recipient) + // throw new SwapKitError("wallet_keepkey_invalid_params", { reason: "Recipient address must be provided" }); + + // const createTxMethod = + // chain === Chain.BitcoinCash + // ? (toolbox as UTXOToolboxes["BCH"]).createTransaction + // : (toolbox as UTXOToolboxes["BTC"]).createTransaction; + + // const { psbt } = await createTxMethod({ + // ...rest, + // feeRate: feeRate || (await toolbox.getFeeRates())[feeOptionKey || FeeOption.Fast], + // fetchTxHex: true, + // memo, + // recipient, + // sender: walletAddress, + // }); + + // const txHex = await signTransaction(psbt as Psbt); + // return toolbox.broadcastTx(txHex); + // }; + + return { ...toolbox, address: walletAddress }; }; diff --git a/packages/wallet-hardware/src/ledger/clients/utxo.ts b/packages/wallet-hardware/src/ledger/clients/utxo.ts index 3dc1d21580..9eaa881cb0 100644 --- a/packages/wallet-hardware/src/ledger/clients/utxo.ts +++ b/packages/wallet-hardware/src/ledger/clients/utxo.ts @@ -6,36 +6,6 @@ import { type Psbt, Transaction } from "bitcoinjs-lib"; import { getLedgerTransport } from "../helpers/getLedgerTransport"; -type Params = { psbt: Psbt; inputUtxos: UTXOType[]; btcApp: any; derivationPath: string }; - -const signUTXOTransaction = ( - { psbt, inputUtxos, btcApp, derivationPath }: Params, - options?: Partial, -) => { - const inputs = inputUtxos.map((item) => { - const utxoTx = Transaction.fromHex(item.txHex || ""); - const splitTx = btcApp.splitTransaction(utxoTx.toHex(), utxoTx.hasWitnesses()); - - return [splitTx, item.index, undefined as string | null | undefined, undefined as number | null | undefined] as any; - }); - - const newTxHex = psbt.data.globalMap.unsignedTx.toBuffer().toString("hex"); - - const splitNewTx = btcApp.splitTransaction(newTxHex, true); - const outputScriptHex = btcApp.serializeTransactionOutputs(splitNewTx).toString("hex"); - - const params: CreateTransactionArg = { - additionals: ["bech32"], - associatedKeysets: inputs.map(() => derivationPath), - inputs, - outputScriptHex, - segwit: true, - useTrustedInputForSegwit: true, - }; - - return btcApp.createPaymentTransaction({ ...params, ...options }); -}; - const BaseLedgerUTXO = ({ chain, additionalSignParams, @@ -56,13 +26,6 @@ const BaseLedgerUTXO = ({ transport ||= await getLedgerTransport(); } - async function createTransportWebUSB() { - transport = await getLedgerTransport(); - const BitcoinApp = (await import("@ledgerhq/hw-app-btc")).default; - - btcApp = new BitcoinApp({ currency: chain, transport }); - } - return (derivationPathArray?: DerivationPathArray | string) => { const derivationPath = typeof derivationPathArray === "string" @@ -101,9 +64,35 @@ const BaseLedgerUTXO = ({ return btcApp.getWalletXpub({ path, xpubVersion }); }, signTransaction: async (psbt: Psbt, inputUtxos: UTXOType[]) => { - await createTransportWebUSB(); + await checkBtcAppAndCreateTransportWebUSB(false); - return signUTXOTransaction({ btcApp, derivationPath, inputUtxos, psbt }, additionalSignParams); + const inputs = inputUtxos.map((item) => { + const utxoTx = Transaction.fromHex(item.txHex || ""); + const splitTx = btcApp.splitTransaction(utxoTx.toHex(), utxoTx.hasWitnesses()); + + return [ + splitTx, + item.index, + undefined as string | null | undefined, + undefined as number | null | undefined, + ] as any; + }); + + const newTxHex = psbt.data.globalMap.unsignedTx.toBuffer().toString("hex"); + + const splitNewTx = btcApp.splitTransaction(newTxHex, true); + const outputScriptHex = btcApp.serializeTransactionOutputs(splitNewTx).toString("hex"); + + const params: CreateTransactionArg = { + additionals: ["bech32"], + associatedKeysets: inputs.map(() => derivationPath), + inputs, + outputScriptHex, + segwit: true, + useTrustedInputForSegwit: true, + }; + + return btcApp.createPaymentTransaction({ ...params, ...additionalSignParams }); }, }; }; diff --git a/packages/wallet-hardware/src/ledger/types.ts b/packages/wallet-hardware/src/ledger/types.ts index 3cb225d6c6..98077f2cc4 100644 --- a/packages/wallet-hardware/src/ledger/types.ts +++ b/packages/wallet-hardware/src/ledger/types.ts @@ -12,13 +12,14 @@ import type { } from "./clients/evm"; import type { THORChainLedger } from "./clients/thorchain"; import type { TronLedger } from "./clients/tron"; -import type { BitcoinCashLedger, BitcoinLedger, DogecoinLedger, LitecoinLedger } from "./clients/utxo"; +import type { BitcoinCashLedger, BitcoinLedger, DogecoinLedger, LitecoinLedger, ZcashLedger } from "./clients/utxo"; export type UTXOLedgerClients = | ReturnType | ReturnType | ReturnType - | ReturnType; + | ReturnType + | ReturnType; export type CosmosLedgerClients = CosmosLedger | THORChainLedger; export type EVMLedgerClients = | ReturnType diff --git a/packages/wallet-hardware/src/trezor/index.ts b/packages/wallet-hardware/src/trezor/index.ts index 3854e5e16e..c519e1bb9c 100644 --- a/packages/wallet-hardware/src/trezor/index.ts +++ b/packages/wallet-hardware/src/trezor/index.ts @@ -1,4 +1,4 @@ -import type { ZcashPsbt } from "@bitgo/utxo-lib/dist/src/bitgo"; +import type { UtxoPsbt, ZcashPsbt } from "@bitgo/utxo-lib/dist/src/bitgo"; import { Chain, type DerivationPathArray, @@ -8,11 +8,12 @@ import { type GenericTransferParams, SKConfig, SwapKitError, + type UTXOChain, WalletOption, } from "@swapkit/helpers"; -import type { UTXOToolboxes, UTXOType } from "@swapkit/toolboxes/utxo"; +import { stripPrefix } from "@swapkit/toolboxes/utxo"; import { createWallet, getWalletSupportedChains } from "@swapkit/wallet-core"; -import type { Psbt } from "bitcoinjs-lib"; +import { type Psbt, Transaction } from "bitcoinjs-lib"; function getScriptType(derivationPath: DerivationPathArray) { switch (derivationPath[0]) { @@ -200,92 +201,118 @@ async function getTrezorWallet({ const address = await getAddress(); - const signTransaction = async (psbt: Psbt, inputs: UTXOType[], memo = "") => { - const TrezorConnect = (await import("@trezor/connect-web")).default; + function psbtToTrezorParams(psbt: Psbt | UtxoPsbt) { + if (!scriptType) { + throw new SwapKitError({ errorKey: "wallet_trezor_derivation_path_not_supported", info: { derivationPath } }); + } const address_n = derivationPath.map((pathElement, index) => index < 3 ? ((pathElement as number) | 0x80000000) >>> 0 : (pathElement as number), ); - const toolbox = await getUtxoToolbox(chain as Chain.BitcoinCash); + // 1. Map Inputs (logic remains the same) + const inputs = psbt.data.inputs.map((input, index) => { + const txInput = psbt.txInputs[index]; + let utxoValue: number; + + // ---- THIS IS THE CRITICAL LOGIC ---- + if (input.witnessUtxo) { + // Use this path for SegWit inputs (BTC, LTC) + utxoValue = Number(input.witnessUtxo.value); + } else if (input.nonWitnessUtxo) { + const prevTx = Transaction.fromBuffer(input.nonWitnessUtxo); + utxoValue = prevTx.outs[txInput?.index || 0]?.value || 0; + } else { + // This is an invalid PSBT for signing; it's missing the necessary UTXO info. + throw new Error(`PSBT input ${index} is missing both witnessUtxo and nonWitnessUtxo.`); + } + // ------------------------------------ - const result = await TrezorConnect.signTransaction({ - coin, - inputs: inputs.map(({ hash, index, value }) => ({ - // Hardens the first 3 elements of the derivation path - required by trezor - address_n, - // object needs amount but does not use it for signing - amount: value, - prev_hash: hash, - prev_index: index, - script_type: scriptType.input, - })), - outputs: psbt.txOutputs.map((output) => { + const bip32Derivation = input.bip32Derivation?.[0]; + if (!bip32Derivation) { + throw new Error(`PSBT input ${index} is missing derivation path required by KeepKey.`); + } + + // ... logic to determine scriptType from the UTXO's script ... + const txid = Buffer.from(psbt.txInputs[index]?.hash || "") + .reverse() + .toString("hex"); + + return { address_n, amount: utxoValue, prev_hash: txid, prev_index: index, script_type: scriptType.input }; + }); + + // 2. MAP OUTPUTS & SEPARATE THE MEMO (OP_RETURN) + const memo = psbt.txOutputs + .find((output) => output.script && output.script[0] === 0x6a) + ?.script.slice(1) + .toString("utf8"); + + const outputs = psbt.txOutputs + .map((output) => { // OP_RETURN - if (!output.address) { - return { amount: "0", op_return_data: Buffer.from(memo).toString("hex"), script_type: "PAYTOOPRETURN" }; + if (!output.address && memo) { + return { + amount: "0", + op_return_data: Buffer.from(memo).toString("hex"), + script_type: "PAYTOOPRETURN" as const, + }; } + if (!output.address) return null; + const outputAddress = - chain === Chain.BitcoinCash ? toolbox.stripPrefix(toCashAddress(output.address)) : output.address; + chain === Chain.BitcoinCash ? stripPrefix(toCashAddress(output.address)) : output.address; const isChangeAddress = outputAddress === address; return isChangeAddress ? { address_n, amount: output.value, script_type: scriptType.output } - : { address: outputAddress, amount: output.value, script_type: "PAYTOADDRESS" }; - }), - }); + : { address: outputAddress, amount: output.value, script_type: "PAYTOADDRESS" as const }; + }) + .filter((output) => output !== null); - if (result.success) { - return result.payload.serializedTx; - } + // 3. Return the final object for KeepKey + return { inputs, outputs }; + } - throw new SwapKitError({ - errorKey: "wallet_trezor_failed_to_sign_transaction", - info: { chain, error: (result.payload as { error: string; code?: string }).error }, - }); - }; + const signTransaction = async (psbt: T) => { + const TrezorConnect = (await import("@trezor/connect-web")).default; - const transfer = async ({ - recipient, - feeOptionKey, - feeRate: paramFeeRate, - memo, - ...rest - }: GenericTransferParams) => { - if (!(address && recipient)) { - throw new SwapKitError({ - errorKey: "wallet_missing_params", - info: { address, memo, recipient, wallet: WalletOption.TREZOR }, - }); - } + const { inputs, outputs } = psbtToTrezorParams(psbt); - const toolbox = await getUtxoToolbox(chain); + // @ts-expect-error + const result = await TrezorConnect.signTransaction({ coin, inputs, outputs }); - const feeRate = paramFeeRate || (await toolbox.getFeeRates())[feeOptionKey || FeeOption.Fast]; + if (result.success) { + const signatures = result.payload.signatures; - const createTxMethod = - chain === Chain.BitcoinCash - ? (toolbox as UTXOToolboxes["BCH"]).buildTx - : (toolbox as UTXOToolboxes["BTC"]).createTransaction; + psbt.data.inputs.forEach((input, index) => { + if (!input.bip32Derivation || !input.bip32Derivation[0]) { + return; + } - const { psbt, inputs } = await createTxMethod({ - ...rest, - feeRate, - fetchTxHex: true, - memo, - recipient, - sender: address, - }); + const signatureHex = signatures[index]; + if (!signatureHex) { + throw new Error(`Trezor did not return a signature for input ${index}`); + } - const txHex = await signTransaction(psbt, inputs, memo); - const tx = await toolbox.broadcastTx(txHex); + const pubkey = input.bip32Derivation[0].pubkey; - return tx; + psbt.updateInput(index, { partialSig: [{ pubkey, signature: Buffer.from(signatureHex, "hex") }] }); + }); + + return psbt; + } + + // Handle the error case + throw new SwapKitError({ + errorKey: "wallet_trezor_failed_to_sign_transaction", + info: { chain, error: (result.payload as { error: string; code?: string }).error }, + }); }; - const toolbox = await getUtxoToolbox(chain); + const signer = { getAddress: async () => address, signTransaction: signTransaction }; + const toolbox = await getUtxoToolbox>(chain, { signer }); - return { ...toolbox, address, signTransaction, transfer }; + return { ...toolbox, address }; } default: diff --git a/packages/wallets/package.json b/packages/wallets/package.json index 02bfa4799b..f9dc852733 100644 --- a/packages/wallets/package.json +++ b/packages/wallets/package.json @@ -6,7 +6,7 @@ "@cosmjs/proto-signing": "~0.33.0", "@keplr-wallet/types": "~0.12.238", "@passkeys/core": "^4.0.0", - "@passkeys/react": "^3.0.0", + "@passkeys/react": "^3.0.1", "@radixdlt/babylon-gateway-api-sdk": "~1.10.0", "@radixdlt/radix-dapp-toolkit": "~2.2.0", "@scure/base": "~1.2.0", @@ -17,7 +17,6 @@ "@swapkit/wallet-core": "workspace:*", "@swapkit/wallet-hardware": "workspace:*", "@swapkit/wallet-keystore": "workspace:*", - "@trezor/connect-web": "~9.6.0", "@walletconnect/modal": "~2.7.0", "@walletconnect/sign-client": "~2.21.0", "bitcoinjs-lib": "~6.1.0", @@ -44,7 +43,6 @@ "@solana/web3.js": "1.98.4", "@swapkit/helpers": "workspace:*", "@swapkit/toolboxes": "workspace:*", - "@trezor/connect-web": "9.6.2", "@walletconnect/logger": "2.1.2", "@walletconnect/modal": "2.7.0", "@walletconnect/sign-client": "2.21.8", diff --git a/packages/wallets/src/ctrl/index.ts b/packages/wallets/src/ctrl/index.ts index 28d8446e8c..ecbbc08ea9 100644 --- a/packages/wallets/src/ctrl/index.ts +++ b/packages/wallets/src/ctrl/index.ts @@ -6,10 +6,10 @@ import { SwapKitError, WalletOption, } from "@swapkit/helpers"; +import type { CosmosTransaction } from "@swapkit/helpers/api"; import type { NearCreateTransactionParams } from "@swapkit/toolboxes/near"; import { createWallet, getWalletSupportedChains } from "@swapkit/wallet-core"; - -import { getCtrlAddress, getCtrlProvider, walletTransfer } from "./walletHelpers"; +import { getCtrlAddress, getCtrlProvider, thorchainTransactionToCtrlParams, walletTransfer } from "./walletHelpers"; export const ctrlWallet = createWallet({ connect: ({ addChain, walletType, supportedChains }) => @@ -78,9 +78,41 @@ async function getWalletMethods(chain: (typeof CTRL_SUPPORTED_CHAINS)[number]) { const gasLimit = chain === Chain.Maya ? MAYA_GAS_VALUE : THORCHAIN_GAS_VALUE; const toolbox = await getCosmosToolbox(chain); + /** + * Convert CosmosTransaction to CTRL's proprietary format and broadcast + */ + const signAndBroadcastTransaction = async (transaction: CosmosTransaction): Promise => { + const provider = await getCtrlProvider(chain); + + if (!provider || !("request" in provider)) { + throw new SwapKitError("wallet_ctrl_not_found"); + } + + // Convert CosmosTransaction to CTRL's format + const { type, ...tx } = thorchainTransactionToCtrlParams({ chain, transaction }); + + return walletTransfer(tx, type); + }; + + /** + * Sign transaction without broadcasting + * Note: CTRL doesn't expose a signing-only API for THORChain/Maya + * This would require the transaction to be broadcast + */ + const signTransaction = (_transaction: any) => { + throw new SwapKitError("wallet_ctrl_sign_transaction_not_supported", { + chain, + info: "CTRL wallet does not support signing without broadcasting for THORChain/Maya", + method: "signTransaction", + wallet: WalletOption.CTRL, + }); + }; + return { ...toolbox, deposit: (tx: GenericTransferParams) => walletTransfer({ ...tx, recipient: "" }, "deposit"), + signAndBroadcastTransaction, + signTransaction, transfer: (tx: GenericTransferParams) => walletTransfer({ ...tx, gasLimit }, "transfer"), }; } diff --git a/packages/wallets/src/ctrl/walletHelpers.ts b/packages/wallets/src/ctrl/walletHelpers.ts index 1cbf415c83..c86a98aa7a 100644 --- a/packages/wallets/src/ctrl/walletHelpers.ts +++ b/packages/wallets/src/ctrl/walletHelpers.ts @@ -1,6 +1,6 @@ import type { Keplr } from "@keplr-wallet/types"; import { - type AssetValue, + AssetValue, Chain, ChainToChainId, type EVMChain, @@ -10,6 +10,13 @@ import { SwapKitError, WalletOption, } from "@swapkit/helpers"; +import type { + APICosmosEncodedObject, + CosmosSendMsg, + CosmosTransaction, + ThorchainDepositMsg, +} from "@swapkit/helpers/api"; +import { base64ToBech32, MAYA_GAS_VALUE, THORCHAIN_GAS_VALUE } from "@swapkit/toolboxes/cosmos"; import type { SolanaProvider } from "@swapkit/toolboxes/solana"; import type { Eip1193Provider } from "ethers"; @@ -188,3 +195,61 @@ export async function walletTransfer( return transaction({ chain: assetValue.chain, method, params }); } + +/** + * Convert a CosmosTransaction to CTRL's format for THORChain/Maya + */ +export function thorchainTransactionToCtrlParams({ + transaction, + chain, +}: { + transaction: CosmosTransaction; + chain: Chain.THORChain | Chain.Maya; +}) { + // Extract the first message (CTRL only handles single transfers) + const msg = transaction.msgs[0] as APICosmosEncodedObject; + + if (!msg) { + throw new SwapKitError("wallet_ctrl_transaction_missing_data", { key: "msgs", transaction }); + } + + if (msg.typeUrl === "/types.MsgSend") { + const typedMessage = msg as any as CosmosSendMsg; + if (!typedMessage.value.toAddress) { + throw new SwapKitError("wallet_ctrl_transaction_missing_data", { key: "toAddress", transaction }); + } + + const recipient = base64ToBech32(typedMessage.value.toAddress, chain.toLowerCase()); + const amount = typedMessage.value.amount[0]; + if (!amount) { + throw new SwapKitError("wallet_ctrl_transaction_missing_data", { key: "amount", transaction }); + } + const denom = amount.denom; + const identifier = `${chain}.${denom.toUpperCase()}`; + const assetValue = AssetValue.from({ amount: BigInt(amount.amount), asset: identifier }); + const gasLimit = chain === Chain.Maya ? MAYA_GAS_VALUE : THORCHAIN_GAS_VALUE; + + return { + assetValue, + gasLimit, + ...(transaction.memo && { memo: transaction.memo }), + recipient, + type: "transfer" as const, + }; + } + + const typedMessage = msg as any as ThorchainDepositMsg; + + const coin = typedMessage.value.coins[0]; + if (!coin) { + throw new SwapKitError("wallet_ctrl_transaction_missing_data", { key: "coins", transaction }); + } + + const asset = coin.asset; + + const identifier = `${asset.chain}.${asset.symbol.toUpperCase()}`; + + const assetValue = AssetValue.from({ amount: BigInt(coin.amount), asset: identifier }); + + return { assetValue, ...(transaction.memo && { memo: transaction.memo }), recipient: "", type: "deposit" as const }; +} diff --git a/packages/wallets/src/evm-extensions/index.ts b/packages/wallets/src/evm-extensions/index.ts index a1d59cb1e1..238edbe9d6 100644 --- a/packages/wallets/src/evm-extensions/index.ts +++ b/packages/wallets/src/evm-extensions/index.ts @@ -26,7 +26,8 @@ const getWalletForType = ( | WalletOption.OKX_MOBILE | WalletOption.METAMASK | WalletOption.TRUSTWALLET_WEB - | WalletOption.COINBASE_WEB, + | WalletOption.COINBASE_WEB + | WalletOption.EIP6963, ) => { switch (walletType) { case WalletOption.COINBASE_WEB: @@ -84,44 +85,26 @@ export const evmWallet = createWallet({ await Promise.all( filteredChains.map(async (chain) => { - if (walletType === WalletOption.EIP6963) { - if (!eip1193Provider) throw new SwapKitError("wallet_evm_extensions_no_provider"); + if (walletType === WalletOption.EIP6963 && !eip1193Provider) + throw new SwapKitError("wallet_evm_extensions_no_provider"); - await eip1193Provider.request({ method: "eth_requestAccounts" }); + const windowProvider = eip1193Provider || getWalletForType(walletType); + const browserProvider = new BrowserProvider(windowProvider, "any"); - const provider = new BrowserProvider(eip1193Provider, "any"); - await provider.send("eth_requestAccounts", []); - const signer = await provider.getSigner(); - const address = await signer.getAddress(); - - const walletMethods = await getWeb3WalletMethods({ - address, - chain, - provider, - walletProvider: eip1193Provider, - }); - - addChain({ ...walletMethods, address, chain, walletType }); - return; - } - const walletProvider = getWalletForType(walletType); - - await walletProvider.request({ method: "eth_requestAccounts" }); - - const web3provider = new BrowserProvider(walletProvider, "any"); - const signer = await web3provider.getSigner(); + await browserProvider.send("eth_requestAccounts", []); + const signer = await browserProvider.getSigner(); const address = await signer.getAddress(); const walletMethods = await getWeb3WalletMethods({ address, chain, - provider: web3provider, - walletProvider: getWalletForType(walletType), + provider: browserProvider, + walletProvider: windowProvider, }); - const disconnect = () => web3provider.send("wallet_revokePermissions", [{ eth_accounts: {} }]); - + const disconnect = () => browserProvider.send("wallet_revokePermissions", [{ eth_accounts: {} }]); addChain({ ...walletMethods, address, chain, disconnect, walletType }); + return; }), ); diff --git a/packages/wallets/src/exodus/index.ts b/packages/wallets/src/exodus/index.ts index af5fd8411d..779cf1184b 100644 --- a/packages/wallets/src/exodus/index.ts +++ b/packages/wallets/src/exodus/index.ts @@ -1,18 +1,16 @@ import type { Wallet } from "@passkeys/core"; import { - type AssetValue, Chain, EVMChains, filterSupportedChains, - type GenericTransferParams, prepareNetworkSwitch, SwapKitError, switchEVMWalletNetwork, WalletOption, } from "@swapkit/helpers"; +import type { SolanaProvider } from "@swapkit/toolboxes/solana"; import { createWallet, getWalletSupportedChains } from "@swapkit/wallet-core"; import { Psbt } from "bitcoinjs-lib"; -// BrowserProvider imported dynamically when needed import { AddressPurpose, BitcoinNetworkType, @@ -124,7 +122,9 @@ async function getWalletMethods({ wallet, chain }: { wallet: Wallet; chain: Chai case Chain.Solana: { const { getSolanaToolbox } = await import("@swapkit/toolboxes/solana"); - const provider = await wallet.getProvider("solana"); + const provider = (await wallet.getProvider("solana")) as any as SolanaProvider; + + provider?.publicKey; if (!provider) { throw new SwapKitError("wallet_exodus_not_found"); @@ -132,42 +132,14 @@ async function getWalletMethods({ wallet, chain }: { wallet: Wallet; chain: Chai const providerConnection = await provider.connect(); const address: string = providerConnection.publicKey.toString(); - const toolbox = await getSolanaToolbox(); - - const transfer = async ({ - recipient, - assetValue, - isProgramDerivedAddress, - }: GenericTransferParams & { assetValue: AssetValue; isProgramDerivedAddress?: boolean }) => { - // const { PublicKey } = await import("@solana/web3.js"); // TODO: Use for advanced transactions - const validateAddress = await toolbox.getAddressValidator(); - - if (!(isProgramDerivedAddress || validateAddress(recipient))) { - throw new SwapKitError("core_transaction_invalid_recipient_address"); - } - - // const fromPubkey = new PublicKey(address); // TODO: Use for advanced transactions - const connection = await toolbox.getConnection(); - const transaction = await toolbox.createTransaction({ - assetValue, - isProgramDerivedAddress, - recipient, - sender: address, - }); - - const signedTransaction = await provider.signTransaction(transaction); - const serialized = signedTransaction.serialize(); - const txHash = await connection.sendRawTransaction(serialized); - - return txHash; - }; + const toolbox = await getSolanaToolbox({ signer: provider }); const disconnect = async () => { await provider.disconnect(); }; - return { ...toolbox, address, disconnect, transfer }; + return { ...toolbox, address, disconnect }; } default: diff --git a/packages/wallets/src/walletconnect/index.ts b/packages/wallets/src/walletconnect/index.ts index 412c77e9d5..e701c9ba13 100644 --- a/packages/wallets/src/walletconnect/index.ts +++ b/packages/wallets/src/walletconnect/index.ts @@ -1,7 +1,6 @@ import type { StdSignDoc } from "@cosmjs/amino"; import { Chain, - ChainId, filterSupportedChains, type GenericTransferParams, getRPCUrl, @@ -9,7 +8,7 @@ import { SwapKitError, WalletOption, } from "@swapkit/helpers"; -import type { createThorchainToolbox, ThorchainDepositParams } from "@swapkit/toolboxes/cosmos"; +import type { ThorchainDepositParams } from "@swapkit/toolboxes/cosmos"; import type { NearSigner } from "@swapkit/toolboxes/near"; import type { TronSignedTransaction, TronSigner, TronTransaction } from "@swapkit/toolboxes/tron"; import { createWallet, getWalletSupportedChains } from "@swapkit/wallet-core"; @@ -140,27 +139,7 @@ async function getToolbox({ getDefaultChainFee, parseAminoMessageForDirectSigning, } = await import("@swapkit/toolboxes/cosmos"); - const toolbox = await getCosmosToolbox(Chain.THORChain); - - async function getAccount(accountAddress: string) { - const cosmosToolbox = toolbox; - const account = await (cosmosToolbox as Awaited>).getAccount( - accountAddress, - ); - - if (chain !== Chain.THORChain) { - return account; - } - - const [{ address, algo, pubkey }] = (await walletconnect?.client.request({ - chainId: THORCHAIN_MAINNET_ID, - request: { method: DEFAULT_COSMOS_METHODS.COSMOS_GET_ACCOUNTS, params: {} }, - // @ts-expect-error - topic: session.topic, - })) as [{ address: string; algo: string; pubkey: string }]; - - return { ...account, address, pubkey: { type: algo, value: pubkey } }; - } + const toolbox = await getCosmosToolbox(chain); const fee = getDefaultChainFee(chain); @@ -183,11 +162,16 @@ async function getToolbox({ const { accountNumber, sequence = 0 } = account; - const msgs = [buildAminoMsg({ assetValue, memo, sender: address, ...rest })]; + const msgs = [buildAminoMsg({ ...rest, assetValue, memo, sender: address })]; - const chainId = ChainId.THORChain; - - const signDoc = makeSignDoc(msgs, fee, chainId, memo, accountNumber?.toString(), sequence?.toString() || "0"); + const signDoc = makeSignDoc( + msgs, + fee, + assetValue.chainId, + memo, + accountNumber?.toString(), + sequence?.toString() || "0", + ); const signature: any = await signRequest(signDoc); @@ -224,7 +208,6 @@ async function getToolbox({ return { ...toolbox, deposit: (params: ThorchainDepositParams) => thorchainTransfer(params), - getAccount, transfer: (params: GenericTransferParams) => thorchainTransfer(params), }; } @@ -322,7 +305,7 @@ async function getToolbox({ async function getWalletconnect( chains: Chain[], - walletConnectProjectId?: string, + walletConnectProjectId: string, walletconnectOptions?: SignClientTypes.Options, ) { let modal: WalletConnectModal | undefined; @@ -330,9 +313,6 @@ async function getWalletconnect( let session: SessionTypes.Struct | undefined; let accounts: string[] | undefined; try { - if (!walletConnectProjectId) { - throw new SwapKitError("wallet_walletconnect_project_id_not_specified"); - } const requiredNamespaces = getRequiredNamespaces(chains.map(chainToChainId)); const { SignClient } = await import("@walletconnect/sign-client"); diff --git a/playgrounds/vite/src/App.tsx b/playgrounds/vite/src/App.tsx index a9b87713e2..befdd51117 100644 --- a/playgrounds/vite/src/App.tsx +++ b/playgrounds/vite/src/App.tsx @@ -1,7 +1,7 @@ -import { type AssetValue, Chain, type FullWallet, SKConfig } from "@swapkit/core"; -import { WalletWidget } from "@swapkit/wallets/exodus"; +import { WalletWidget } from "@passkeys/react"; +import { type AssetValue, Chain, SKConfig } from "@swapkit/core"; +import type { FullWallet } from "@swapkit/sdk"; import { useCallback, useMemo, useState } from "react"; - import Liquidity from "./Liquidity"; import Multisig from "./Multisig"; import NearNames from "./NearNames"; diff --git a/playgrounds/vite/src/Send/index.tsx b/playgrounds/vite/src/Send/index.tsx index 44caf11917..d84b50c149 100644 --- a/playgrounds/vite/src/Send/index.tsx +++ b/playgrounds/vite/src/Send/index.tsx @@ -1,4 +1,4 @@ -import type { AssetValue } from "@swapkit/core"; +import { type AssetValue, getExplorerTxUrl } from "@swapkit/core"; import { useCallback, useState } from "react"; import type { SwapKitClient } from "../swapKitClient"; @@ -22,10 +22,10 @@ export default function Send({ inputAsset, skClient }: { skClient?: SwapKitClien const handleSend = useCallback(async () => { if (!(inputAsset && inputAssetValue?.gt(0) && skClient)) return; - const from = skClient.getAddress(inputAsset.chain); - const txHash = await skClient.transfer({ assetValue: inputAssetValue, from, memo: "", recipient }); + const sender = skClient.getAddress(inputAsset.chain); + const txHash = await skClient.transfer({ assetValue: inputAssetValue, sender, memo: "", recipient }); - window.open(`${skClient.getExplorerTxUrl({ chain: inputAssetValue.chain, txHash: txHash as string })}`, "_blank"); + window.open(`${getExplorerTxUrl({ chain: inputAssetValue.chain, txHash: txHash as string })}`, "_blank"); }, [inputAsset, inputAssetValue, skClient, recipient]); return (