diff --git a/src/app/pages/rpc-stx-transfer-stx/rpc-stx-transfer-stx.tsx b/src/app/pages/rpc-stx-transfer-stx/rpc-stx-transfer-stx.tsx new file mode 100644 index 0000000000..0eea4b7d95 --- /dev/null +++ b/src/app/pages/rpc-stx-transfer-stx/rpc-stx-transfer-stx.tsx @@ -0,0 +1,29 @@ +import { isDefined } from '@leather.io/utils'; + +import { StacksHighFeeWarningContainer } from '@app/features/stacks-high-fee-warning/stacks-high-fee-warning-container'; +import { StacksTransactionSigner } from '@app/features/stacks-transaction-request/stacks-transaction-signer'; +import { useBreakOnNonCompliantEntity } from '@app/query/common/compliance-checker/compliance-checker.query'; +import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; + +import { useRpcStxTransferStx } from './use-rpc-stx-transfer-stx'; + +export function RpcStxTransferStx() { + const { params, onCancel } = useRpcStxTransferStx(); + + // Grab account details from store + const currentStacksAccount = useCurrentStacksAccount(); + useBreakOnNonCompliantEntity([currentStacksAccount?.address, params.recipient].filter(isDefined)); + + return ( + + + + ); +} diff --git a/src/app/pages/rpc-stx-transfer-stx/use-rpc-stx-transfer-stx.ts b/src/app/pages/rpc-stx-transfer-stx/use-rpc-stx-transfer-stx.ts new file mode 100644 index 0000000000..779dbc6da9 --- /dev/null +++ b/src/app/pages/rpc-stx-transfer-stx/use-rpc-stx-transfer-stx.ts @@ -0,0 +1,86 @@ +import { useMemo } from 'react'; + +import { makeUnsignedSTXTokenTransfer } from '@stacks/transactions'; + +import { RpcErrorCode, type StxTransferStxRequestParams } from '@leather.io/rpc'; + +import { logger } from '@shared/logger'; +import { makeRpcErrorResponse, makeRpcSuccessResponse } from '@shared/rpc/rpc-methods'; +import { closeWindow } from '@shared/utils'; + +import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-search-params'; +import { initialSearchParams } from '@app/common/initial-search-params'; +import { useStacksBroadcastTransaction } from '@app/features/stacks-transaction-request/hooks/use-stacks-broadcast-transaction'; + +export function useRpcStxTransferStxParams() { + const { origin, tabId } = useDefaultRequestParams(); + const requestId = initialSearchParams.get('requestId'); + const params = initialSearchParams.get('params'); + + if (!origin || !params || !requestId) throw new Error('Invalid params'); + + return useMemo( + () => ({ + origin, + tabId: tabId ?? 0, + requestId, + params: JSON.parse(decodeURIComponent(params)), + // params: stxTransferStxRequestParamsSchema.parse(JSON.parse(decodeURIComponent(params))), + }), + [origin, tabId, requestId, params] + ); +} + +export function useRpcStxTransferStx() { + const { origin, params, requestId, tabId } = useRpcStxTransferStxParams(); + const { stacksBroadcastTransaction } = useStacksBroadcastTransaction({ token: '' }); + + return useMemo( + () => ({ + origin, + // TODO: replace with schema infererence + params: params as StxTransferStxRequestParams, + async onSignStacksTransaction(fee: number, nonce: number) { + const tx = await makeUnsignedSTXTokenTransfer({ + recipient: params.recipient, + amount: params.amount, + memo: params.memo, + publicKey: params.publicKey, + }); + + if (!tx) return logger.error('No stacks transaction to sign'); + + tx.setFee(fee); + tx.setNonce(nonce); + + const result = await stacksBroadcastTransaction(tx); + if (!result) throw new Error('Error broadcasting stacks transaction'); + + chrome.tabs.sendMessage( + tabId, + makeRpcSuccessResponse('stx_transferStx', { + id: requestId, + result: { + txid: result.txid, + transaction: result.transaction.serialize(), + } as any, + }) + ); + closeWindow(); + }, + onCancel() { + chrome.tabs.sendMessage( + tabId, + makeRpcErrorResponse('stx_transferStx', { + id: requestId, + error: { + message: 'User denied signing stacks transaction', + code: RpcErrorCode.USER_REJECTION, + }, + }) + ); + }, + }), + [origin, params, requestId, stacksBroadcastTransaction, tabId] + ); +} diff --git a/src/app/routes/rpc-routes.tsx b/src/app/routes/rpc-routes.tsx index d5dde60ddb..5b0cfb653a 100644 --- a/src/app/routes/rpc-routes.tsx +++ b/src/app/routes/rpc-routes.tsx @@ -14,6 +14,7 @@ import { RpcSignPsbtSummary } from '@app/pages/rpc-sign-psbt/rpc-sign-psbt-summa import { RpcStacksMessageSigning } from '@app/pages/rpc-sign-stacks-message/rpc-sign-stacks-message'; import { RpcSignStacksTransaction } from '@app/pages/rpc-sign-stacks-transaction/rpc-sign-stacks-transaction'; import { RpcStxCallContract } from '@app/pages/rpc-stx-call-contract/rpc-stx-call-contract'; +import { RpcStxTransferStx } from '@app/pages/rpc-stx-transfer-stx/rpc-stx-transfer-stx'; import { AccountGate } from '@app/routes/account-gate'; import { SuspenseLoadingSpinner } from './app-routes'; @@ -96,5 +97,17 @@ export const rpcRequestRoutes = ( {ledgerStacksTxSigningRoutes} } /> + + + + + } + > + {ledgerStacksTxSigningRoutes} + } /> + ); diff --git a/src/background/messaging/rpc-message-handler.ts b/src/background/messaging/rpc-message-handler.ts index 99e8361028..928859019e 100644 --- a/src/background/messaging/rpc-message-handler.ts +++ b/src/background/messaging/rpc-message-handler.ts @@ -1,4 +1,4 @@ -import { RpcErrorCode } from '@leather.io/rpc'; +import { RpcErrorCode, stxTransferStxMethodName } from '@leather.io/rpc'; import { WalletRequests, makeRpcErrorResponse } from '@shared/rpc/rpc-methods'; @@ -18,11 +18,14 @@ import { } from './rpc-methods/sign-stacks-message'; import { rpcStxCallContract } from './rpc-methods/stx-call-contract'; import { rpcStxGetAddresses } from './rpc-methods/stx-get-addresses'; +import { rpcStxTransferStx } from './rpc-methods/stx-transfer-stx'; import { rpcSupportedMethods } from './rpc-methods/supported-methods'; export async function rpcMessageHandler(message: WalletRequests, port: chrome.runtime.Port) { listenForOriginTabClose({ tabId: port.sender?.tab?.id }); + console.log(message); + switch (message.method) { case 'open': { await rpcOpen(message, port); @@ -82,10 +85,15 @@ export async function rpcMessageHandler(message: WalletRequests, port: chrome.ru break; } + case stxTransferStxMethodName: { + await rpcStxTransferStx(message, port); + break; + } + default: chrome.tabs.sendMessage( getTabIdFromPort(port), - makeRpcErrorResponse('' as any, { + makeRpcErrorResponse(message.method, { id: message.id, error: { code: RpcErrorCode.METHOD_NOT_FOUND, diff --git a/src/background/messaging/rpc-methods/stx-transfer-stx.ts b/src/background/messaging/rpc-methods/stx-transfer-stx.ts new file mode 100644 index 0000000000..00efcbb4ba --- /dev/null +++ b/src/background/messaging/rpc-methods/stx-transfer-stx.ts @@ -0,0 +1,34 @@ +import { RpcErrorCode, type StxTransferStxRequest } from '@leather.io/rpc'; + +import { RouteUrls } from '@shared/route-urls'; +import { makeRpcErrorResponse } from '@shared/rpc/rpc-methods'; + +import { + type RequestParams, + listenForPopupClose, + makeSearchParamsWithDefaults, + triggerRequestWindowOpen, +} from '../messaging-utils'; + +export async function rpcStxTransferStx(message: StxTransferStxRequest, port: chrome.runtime.Port) { + const requestParams: RequestParams = [ + ['params', encodeURIComponent(JSON.stringify(message.params))], + ['requestId', message.id], + ]; + + const { urlParams, tabId } = makeSearchParamsWithDefaults(port, requestParams); + + const { id } = await triggerRequestWindowOpen(RouteUrls.RpcStxTransferStx, urlParams); + + listenForPopupClose({ + tabId, + id, + response: makeRpcErrorResponse('stx_transferStx', { + id: message.id, + error: { + code: RpcErrorCode.USER_REJECTION, + message: 'User rejected the Stacks transaction signing request', + }, + }), + }); +} diff --git a/src/background/monitors/address-monitors/bitcoin-transaction-monitor.ts b/src/background/monitors/address-monitors/bitcoin-transaction-monitor.ts index 0f6323c8cc..c802be1678 100644 --- a/src/background/monitors/address-monitors/bitcoin-transaction-monitor.ts +++ b/src/background/monitors/address-monitors/bitcoin-transaction-monitor.ts @@ -131,8 +131,6 @@ export function createBitcoinTransactionMonitor(addresses: MonitoredAddress[]): await handleTransactionMessage(message); } else if (message['conversions']) { _btcPriceUsd = readMempooWsBtcPriceUsd(message['conversions']); - } else { - logger.debug('Unrecognized Message Type: ', event.data); } } diff --git a/src/shared/route-urls.ts b/src/shared/route-urls.ts index 61453389bc..69515ff6e5 100644 --- a/src/shared/route-urls.ts +++ b/src/shared/route-urls.ts @@ -107,4 +107,5 @@ export enum RouteUrls { // Request routes stacks RpcSignStacksTransaction = '/sign-stacks-transaction', RpcStxCallContract = '/stx-call-contract', + RpcStxTransferStx = '/stx-transfer-stx', } diff --git a/src/shared/rpc/rpc-methods.ts b/src/shared/rpc/rpc-methods.ts index f37f55ad0d..7040775c4c 100644 --- a/src/shared/rpc/rpc-methods.ts +++ b/src/shared/rpc/rpc-methods.ts @@ -24,3 +24,8 @@ export function makeRpcErrorResponse( ) { return { jsonrpc: '2.0', ...response } as LeatherRpcMethodMap[T]['response']; } + +// makeRpcSuccessResponse('stx_transferStx', { +// id: '123', +// result: {}, +// });