diff --git a/package.json b/package.json index 558bb5d2224..480dccee487 100644 --- a/package.json +++ b/package.json @@ -270,7 +270,7 @@ "@leather.io/eslint-config": "0.7.0", "@leather.io/panda-preset": "0.8.0", "@leather.io/prettier-config": "0.6.0", - "@leather.io/rpc": "2.4.0", + "@leather.io/rpc": "2.5.3", "@ls-lint/ls-lint": "2.2.3", "@mdx-js/loader": "3.0.0", "@pandacss/dev": "0.46.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55af9d02f4e..3461526fe03 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -415,8 +415,8 @@ importers: specifier: 0.6.0 version: 0.6.0(@vue/compiler-sfc@3.5.13) '@leather.io/rpc': - specifier: 2.4.0 - version: 2.4.0(encoding@0.1.13) + specifier: 2.5.3 + version: 2.5.3(encoding@0.1.13) '@ls-lint/ls-lint': specifier: 2.2.3 version: 2.2.3 @@ -7943,8 +7943,8 @@ packages: caniuse-lite@1.0.30001680: resolution: {integrity: sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==} - caniuse-lite@1.0.30001692: - resolution: {integrity: sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==} + caniuse-lite@1.0.30001695: + resolution: {integrity: sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==} case-sensitive-paths-webpack-plugin@2.4.0: resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==} @@ -9105,8 +9105,8 @@ packages: electron-to-chromium@1.5.63: resolution: {integrity: sha512-ddeXKuY9BHo/mw145axlyWjlJ1UBt4WK3AlvkT7W2AbqfRQoacVoRUCF6wL3uIx/8wT9oLKXzI+rFqHHscByaA==} - electron-to-chromium@1.5.82: - resolution: {integrity: sha512-Zq16uk1hfQhyGx5GpwPAYDwddJuSGhtRhgOA2mCxANYaDT79nAeGnaXogMGng4KqLaJUVnOnuL0+TDop9nLOiA==} + electron-to-chromium@1.5.86: + resolution: {integrity: sha512-/D7GAAaCRBQFBBcop6SfAAGH37djtpWkOuYhyAajw0l5vsfeSsUQYxaFPwr1c/mC/flARCDdKFo5gpFqNI+18w==} electron@27.3.11: resolution: {integrity: sha512-E1SiyEoI8iW5LW/MigCr7tJuQe7+0105UjqY7FkmCD12e2O6vtUbQ0j05HaBh2YgvkcEVgvQ2A8suIq5b5m6Gw==} @@ -25919,8 +25919,8 @@ snapshots: browserslist@4.24.4: dependencies: - caniuse-lite: 1.0.30001692 - electron-to-chromium: 1.5.82 + caniuse-lite: 1.0.30001695 + electron-to-chromium: 1.5.86 node-releases: 2.0.19 update-browserslist-db: 1.1.2(browserslist@4.24.4) @@ -26125,7 +26125,7 @@ snapshots: caniuse-lite@1.0.30001680: {} - caniuse-lite@1.0.30001692: {} + caniuse-lite@1.0.30001695: {} case-sensitive-paths-webpack-plugin@2.4.0: {} @@ -27349,7 +27349,7 @@ snapshots: electron-to-chromium@1.5.63: {} - electron-to-chromium@1.5.82: {} + electron-to-chromium@1.5.86: {} electron@27.3.11: dependencies: diff --git a/src/app/pages/rpc-sign-stacks-transaction/use-rpc-sign-stacks-transaction.ts b/src/app/pages/rpc-sign-stacks-transaction/use-rpc-sign-stacks-transaction.ts index b22abb02f29..ba303bb3af0 100644 --- a/src/app/pages/rpc-sign-stacks-transaction/use-rpc-sign-stacks-transaction.ts +++ b/src/app/pages/rpc-sign-stacks-transaction/use-rpc-sign-stacks-transaction.ts @@ -54,9 +54,8 @@ export function useRpcSignStacksTransaction() { stacksTransaction.setNonce(nonce); const signedTransaction = await signStacksTx(stacksTransaction); - if (!signedTransaction) { - throw new Error('Error signing stacks transaction'); - } + + if (!signedTransaction) throw new Error('Error signing stacks transaction'); chrome.tabs.sendMessage( tabId, @@ -64,6 +63,7 @@ export function useRpcSignStacksTransaction() { id: requestId, result: { txHex: bytesToHex(signedTransaction.serialize()), + transaction: bytesToHex(signedTransaction.serialize()), }, }) ); diff --git a/src/app/store/app-permissions/app-permissions.slice.ts b/src/app/store/app-permissions/app-permissions.slice.ts index 22076d6d3b1..bf3ea06f5fa 100644 --- a/src/app/store/app-permissions/app-permissions.slice.ts +++ b/src/app/store/app-permissions/app-permissions.slice.ts @@ -3,11 +3,14 @@ import { useDispatch } from 'react-redux'; import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; +import { useCurrentAccountIndex } from '../accounts/account'; + interface AppPermission { origin: string; // Very simple permission system. If property exists with date, user // has given permission requestedAccounts?: string; + accountIndex: number; } const appPermissionsAdapter = createEntityAdapter({ selectId: permission => permission.origin, @@ -23,6 +26,7 @@ export const appPermissionsSlice = createSlice({ export function useAppPermissions() { const dispatch = useDispatch(); + const currentAccountIndex = useCurrentAccountIndex(); return useMemo( () => ({ @@ -32,10 +36,11 @@ export function useAppPermissions() { appPermissionsSlice.actions.updatePermission({ origin: url, requestedAccounts: new Date().toISOString(), + accountIndex: currentAccountIndex, }) ); }, }), - [dispatch] + [currentAccountIndex, dispatch] ); } diff --git a/src/background/messaging/rpc-methods/sign-stacks-transaction.ts b/src/background/messaging/rpc-methods/sign-stacks-transaction.ts index b3794afdfda..b6763cda010 100644 --- a/src/background/messaging/rpc-methods/sign-stacks-transaction.ts +++ b/src/background/messaging/rpc-methods/sign-stacks-transaction.ts @@ -16,7 +16,11 @@ import { } from '@stacks/transactions'; import { createUnsecuredToken } from 'jsontokens'; -import { RpcErrorCode, type StxSignTransactionRequest } from '@leather.io/rpc'; +import { + RpcErrorCode, + type StxSignTransactionRequest, + type StxSignTransactionRequestParams, +} from '@leather.io/rpc'; import { isDefined, isUndefined } from '@leather.io/utils'; import { RouteUrls } from '@shared/route-urls'; @@ -37,21 +41,29 @@ import { trackRpcRequestError, trackRpcRequestSuccess } from '../rpc-message-han const MEMO_DESERIALIZATION_STUB = '\u0000'; -const cleanMemoString = (memo: string): string => { +function cleanMemoString(memo: string): string { return memo.replaceAll(MEMO_DESERIALIZATION_STUB, ''); -}; +} function encodePostConditions(postConditions: PostCondition[]) { return postConditions.map(pc => bytesToHex(serializePostCondition(pc))); } -const transactionPayloadToTransactionRequest = ( +function getStacksTransactionHexFromRequest(requestParams: StxSignTransactionRequestParams) { + if ('txHex' in requestParams) return requestParams.txHex; + return requestParams.transaction; +} + +function getAccountAddressFromRequest(requestParams: StxSignTransactionRequestParams) { + if ('txHex' in requestParams) return requestParams.stxAddress; + return; +} + +function transactionPayloadToTransactionRequest( stacksTransaction: StacksTransaction, - stxAddress?: string, - attachment?: string -) => { + stxAddress?: string +) { const transactionRequest = { - attachment, stxAddress, sponsored: stacksTransaction.auth.authType === AuthType.Sponsored, nonce: Number(stacksTransaction.auth.spendingCondition.nonce), @@ -93,7 +105,7 @@ const transactionPayloadToTransactionRequest = ( } return transactionRequest; -}; +} function validateStacksTransaction(txHex: string) { try { @@ -136,7 +148,7 @@ export async function rpcSignStacksTransaction( return; } - if (!validateStacksTransaction(message.params.txHex)) { + if (!validateStacksTransaction(getStacksTransactionHexFromRequest(message.params))) { void trackRpcRequestError({ endpoint: message.method, error: 'Invalid Stacks transaction' }); chrome.tabs.sendMessage( @@ -149,14 +161,16 @@ export async function rpcSignStacksTransaction( return; } - const stacksTransaction = deserializeTransaction(message.params.txHex); + const stacksTransaction = deserializeTransaction( + getStacksTransactionHexFromRequest(message.params) + ); const request = transactionPayloadToTransactionRequest( stacksTransaction, - message.params.stxAddress, - message.params.attachment + getAccountAddressFromRequest(message.params) ); const hashMode = stacksTransaction.auth.spendingCondition.hashMode as MultiSigHashMode; + const isMultisig = hashMode === AddressHashMode.SerializeP2SH || hashMode === AddressHashMode.SerializeP2WSH || @@ -166,15 +180,13 @@ export async function rpcSignStacksTransaction( void trackRpcRequestSuccess({ endpoint: message.method }); const requestParams = [ - ['txHex', message.params.txHex], + ['txHex', getStacksTransactionHexFromRequest(message.params)], ['requestId', message.id], ['request', createUnsecuredToken(request)], - ['isMultisig', isMultisig], - ] as RequestParams; + ['isMultisig', String(isMultisig)], + ] satisfies RequestParams; - if (isDefined(message.params.network)) { - requestParams.push(['network', message.params.network]); - } + if (isDefined(message.params.network)) requestParams.push(['network', message.params.network]); const { urlParams, tabId } = makeSearchParamsWithDefaults(port, requestParams); diff --git a/src/shared/rpc/methods/sign-stacks-transaction.ts b/src/shared/rpc/methods/sign-stacks-transaction.ts index 6ef8c764cd2..3cb73d7226b 100644 --- a/src/shared/rpc/methods/sign-stacks-transaction.ts +++ b/src/shared/rpc/methods/sign-stacks-transaction.ts @@ -1,19 +1,11 @@ -import { StacksNetworks } from '@stacks/network'; -import { z } from 'zod'; +import { stxSignTransactionRequestParamsSchema } from '@leather.io/rpc'; import { formatValidationErrors, getRpcParamErrors, validateRpcParams } from './validation.utils'; -const rpcSignStacksTransactionParamsSchema = z.object({ - stxAddress: z.string().optional(), - txHex: z.string(), - attachment: z.string().optional(), - network: z.enum(StacksNetworks).optional(), -}); - export function validateRpcSignStacksTransactionParams(obj: unknown) { - return validateRpcParams(obj, rpcSignStacksTransactionParamsSchema); + return validateRpcParams(obj, stxSignTransactionRequestParamsSchema); } export function getRpcSignStacksTransactionParamErrors(obj: unknown) { - return formatValidationErrors(getRpcParamErrors(obj, rpcSignStacksTransactionParamsSchema)); + return formatValidationErrors(getRpcParamErrors(obj, stxSignTransactionRequestParamsSchema)); } diff --git a/tests/specs/rpc-stacks-transaction/transaction-signing.spec.ts b/tests/specs/rpc-stacks-transaction/rpc-stx-sign-transaction.spec.ts similarity index 77% rename from tests/specs/rpc-stacks-transaction/transaction-signing.spec.ts rename to tests/specs/rpc-stacks-transaction/rpc-stx-sign-transaction.spec.ts index b5eefa57cf0..8c24162ea68 100644 --- a/tests/specs/rpc-stacks-transaction/transaction-signing.spec.ts +++ b/tests/specs/rpc-stacks-transaction/rpc-stx-sign-transaction.spec.ts @@ -10,7 +10,7 @@ import { generateMultisigUnsignedStxTransfer, generateUnsignedStxTransfer } from import { test } from '../../fixtures/fixtures'; -test.describe('Transaction signing', () => { +test.describe('RPC: stx_signTransaction', () => { test.beforeEach(async ({ extensionId, globalPage, onboardingPage, page }) => { await globalPage.setupAndUseApiCalls(extensionId); await onboardingPage.signInWithTestAccount(extensionId); @@ -33,7 +33,7 @@ test.describe('Transaction signing', () => { }; } - function initiateTxSigning(page: Page) { + function initiateTxSigningLeatherFormat(page: Page) { return async (txHex: string) => page.evaluate( async txHex => @@ -45,6 +45,17 @@ test.describe('Transaction signing', () => { ); } + function initiateTxSigningSip30Format(page: Page) { + return async (hex: string) => + page.evaluate( + async transaction => + (window as any).LeatherProvider.request('stx_signTransaction', { transaction }).catch( + (e: unknown) => e + ), + hex + ); + } + test('that transaction details are the same after signing multi-signature STX transfer', async ({ page, context, @@ -60,7 +71,7 @@ test.describe('Transaction signing', () => { 0 ); const [result] = await Promise.all([ - initiateTxSigning(page)(multiSignatureTxHex), + initiateTxSigningLeatherFormat(page)(multiSignatureTxHex), checkVisibleContent(context)('Confirm'), ]); @@ -113,7 +124,7 @@ test.describe('Transaction signing', () => { TEST_ACCOUNT_3_PUBKEY ); const [result] = await Promise.all([ - initiateTxSigning(page)(singleSignatureTxHex), + initiateTxSigningLeatherFormat(page)(singleSignatureTxHex), checkVisibleContent(context)('Cancel'), ]); @@ -128,4 +139,29 @@ test.describe('Transaction signing', () => { }, }); }); + + test.describe('SIP-30 compatibility', () => { + test('it works with SIP-30 formatted transactions', async ({ page, context }) => { + const singleSignatureTxHex = await generateUnsignedStxTransfer( + TEST_ACCOUNT_2_STX_ADDRESS, + 500, + 'mainnet', + TEST_ACCOUNT_3_PUBKEY + ); + const [result] = await Promise.all([ + initiateTxSigningSip30Format(page)(singleSignatureTxHex), + checkVisibleContent(context)('Cancel'), + ]); + + delete result.id; + + test.expect(result).toEqual({ + jsonrpc: '2.0', + error: { + code: 4001, + message: 'User rejected the Stacks transaction signing request', + }, + }); + }); + }); });