Skip to content

Commit

Permalink
fix(rpc): use SIP-30 format for stx_signTransaction
Browse files Browse the repository at this point in the history
  • Loading branch information
kyranjamie committed Jan 24, 2025
1 parent a950b32 commit fe8a131
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 49 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 10 additions & 10 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,16 @@ 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,
makeRpcSuccessResponse('stx_signTransaction', {
id: requestId,
result: {
txHex: bytesToHex(signedTransaction.serialize()),
transaction: bytesToHex(signedTransaction.serialize()),
},
})
);
Expand Down
7 changes: 6 additions & 1 deletion src/app/store/app-permissions/app-permissions.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppPermission, string>({
selectId: permission => permission.origin,
Expand All @@ -23,6 +26,7 @@ export const appPermissionsSlice = createSlice({

export function useAppPermissions() {
const dispatch = useDispatch();
const currentAccountIndex = useCurrentAccountIndex();

return useMemo(
() => ({
Expand All @@ -32,10 +36,11 @@ export function useAppPermissions() {
appPermissionsSlice.actions.updatePermission({
origin: url,
requestedAccounts: new Date().toISOString(),
accountIndex: currentAccountIndex,
})
);
},
}),
[dispatch]
[currentAccountIndex, dispatch]
);
}
50 changes: 31 additions & 19 deletions src/background/messaging/rpc-methods/sign-stacks-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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),
Expand Down Expand Up @@ -93,7 +105,7 @@ const transactionPayloadToTransactionRequest = (
}

return transactionRequest;
};
}

function validateStacksTransaction(txHex: string) {
try {
Expand Down Expand Up @@ -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(
Expand All @@ -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 ||
Expand All @@ -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);

Expand Down
14 changes: 3 additions & 11 deletions src/shared/rpc/methods/sign-stacks-transaction.ts
Original file line number Diff line number Diff line change
@@ -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));
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -33,7 +33,7 @@ test.describe('Transaction signing', () => {
};
}

function initiateTxSigning(page: Page) {
function initiateTxSigningLeatherFormat(page: Page) {
return async (txHex: string) =>
page.evaluate(
async txHex =>
Expand All @@ -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,
Expand All @@ -60,7 +71,7 @@ test.describe('Transaction signing', () => {
0
);
const [result] = await Promise.all([
initiateTxSigning(page)(multiSignatureTxHex),
initiateTxSigningLeatherFormat(page)(multiSignatureTxHex),
checkVisibleContent(context)('Confirm'),
]);

Expand Down Expand Up @@ -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'),
]);

Expand All @@ -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',
},
});
});
});
});

0 comments on commit fe8a131

Please sign in to comment.