Skip to content

Commit

Permalink
feat: sip30 stx call contract, closes LEA-1954
Browse files Browse the repository at this point in the history
  • Loading branch information
fbwoolf committed Feb 5, 2025
1 parent 2562313 commit 4db5dc9
Show file tree
Hide file tree
Showing 11 changed files with 4,323 additions and 5,855 deletions.
9,864 changes: 4,018 additions & 5,846 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as yup from 'yup';
import { HIGH_FEE_WARNING_LEARN_MORE_URL_STX } from '@leather.io/constants';
import { FeeTypes } from '@leather.io/models';
import {
defaultStacksFees,
useCalculateStacksTxFees,
useNextNonce,
useStxCryptoAssetBalance,
Expand Down Expand Up @@ -40,7 +41,7 @@ import { MinimalErrorMessage } from './minimal-error-message';
import { StacksTxSubmitAction } from './submit-action';

interface StacksTransactionSignerProps {
stacksTransaction: StacksTransactionWire;
stacksTransaction?: StacksTransactionWire;
disableFeeSelection?: boolean;
disableNonceSelection?: boolean;
isMultisig: boolean;
Expand Down Expand Up @@ -123,7 +124,7 @@ export function StacksTransactionSigner({

{!isNonceAlreadySet && <NonceSetter />}
<FeeForm
fees={stxFees}
fees={stxFees ?? defaultStacksFees}
sbtcSponsorshipEligibility={{ isEligible: false }}
defaultFeeValue={Number(transactionRequest?.fee || 0)}
disableFeeSelection={disableFeeSelection}
Expand Down
23 changes: 23 additions & 0 deletions src/app/pages/rpc-stx-call-contract/rpc-stx-call-contract.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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 { useRpcStxCallContract } from './use-rpc-stx-call-contract';

export function RpcStxCallContract() {
const { onSignStacksTransaction, onCancel, stacksTransaction, txSender } =
useRpcStxCallContract();

useBreakOnNonCompliantEntity(txSender);

return (
<StacksHighFeeWarningContainer>
<StacksTransactionSigner
onSignStacksTransaction={onSignStacksTransaction}
onCancel={onCancel}
isMultisig={false}
stacksTransaction={stacksTransaction}
/>
</StacksHighFeeWarningContainer>
);
}
107 changes: 107 additions & 0 deletions src/app/pages/rpc-stx-call-contract/use-rpc-stx-call-contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { useMemo } from 'react';
import { useAsync } from 'react-async-hook';

import { RpcErrorCode } from '@leather.io/rpc';

import { logger } from '@shared/logger';
import { makeRpcErrorResponse, makeRpcSuccessResponse } from '@shared/rpc/rpc-methods';
import { closeWindow } from '@shared/utils';
import {
type TransactionPayload,
getLegacyTransactionPayloadFromToken,
} from '@shared/utils/legacy-requests';

import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-search-params';
import { initialSearchParams } from '@app/common/initial-search-params';
import {
type GenerateUnsignedTransactionOptions,
generateUnsignedTransaction,
} from '@app/common/transactions/stacks/generate-unsigned-txs';
import { getTxSenderAddress } from '@app/common/transactions/stacks/transaction.utils';
import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
import { useSignStacksTransaction } from '@app/store/transactions/transaction.hooks';

function useRpcStxCallContractParams() {
const { origin, tabId } = useDefaultRequestParams();
const requestId = initialSearchParams.get('requestId');
const request = initialSearchParams.get('request');

if (!origin || !request || !requestId) throw new Error('Invalid params');

return useMemo(
() => ({
origin,
tabId: tabId ?? 0,
request: getLegacyTransactionPayloadFromToken(request),
requestId,
}),
[origin, tabId, request, requestId]
);
}

function useUnsignedStacksTransactionFromRequest(request: TransactionPayload) {
const account = useCurrentStacksAccount();

const tx = useAsync(async () => {
if (!account) return;

const options: GenerateUnsignedTransactionOptions = {
publicKey: account.stxPublicKey,
txData: request,
fee: request.fee ?? 0,
nonce: request.nonce,
};
return generateUnsignedTransaction(options);
}, [account]);

return tx.result;
}

export function useRpcStxCallContract() {
const { origin, request, requestId, tabId } = useRpcStxCallContractParams();
const signStacksTx = useSignStacksTransaction();
const stacksTransaction = useUnsignedStacksTransactionFromRequest(request);

return {
origin,
txSender: stacksTransaction ? getTxSenderAddress(stacksTransaction) : '',
stacksTransaction,
async onSignStacksTransaction(fee: number, nonce: number) {
if (!stacksTransaction) {
return logger.error('No stacks transaction to sign');
}

stacksTransaction.setFee(fee);
stacksTransaction.setNonce(nonce);

const signedTransaction = await signStacksTx(stacksTransaction);
if (!signedTransaction) {
throw new Error('Error signing stacks transaction');
}

chrome.tabs.sendMessage(
tabId,
makeRpcSuccessResponse('stx_callContract', {
id: requestId,
result: {
txid: '', // Broadcast transaction?
transaction: signedTransaction.serialize(),
} as any, // Fix this
})
);
closeWindow();
},
onCancel() {
chrome.tabs.sendMessage(
tabId,
makeRpcErrorResponse('stx_callContract', {
id: requestId,
error: {
message: 'User denied signing stacks transaction',
code: RpcErrorCode.USER_REJECTION,
},
})
);
},
};
}
13 changes: 13 additions & 0 deletions src/app/routes/rpc-routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { RpcSignPsbt } from '@app/pages/rpc-sign-psbt/rpc-sign-psbt';
import { RpcSignPsbtSummary } from '@app/pages/rpc-sign-psbt/rpc-sign-psbt-summary';
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 { AccountGate } from '@app/routes/account-gate';

import { SuspenseLoadingSpinner } from './app-routes';
Expand Down Expand Up @@ -83,5 +84,17 @@ export const rpcRequestRoutes = (
{ledgerStacksTxSigningRoutes}
<Route path={RouteUrls.EditNonce} element={<EditNonceSheet />} />
</Route>

<Route
path={RouteUrls.RpcStxCallContract}
element={
<AccountGate>
<RpcStxCallContract />
</AccountGate>
}
>
{ledgerStacksTxSigningRoutes}
<Route path={RouteUrls.EditNonce} element={<EditNonceSheet />} />
</Route>
</>
);
38 changes: 38 additions & 0 deletions src/background/messaging/messaging-utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { To } from 'react-router-dom';

import { type PostConditionWire, serializePostConditionWire } from '@stacks/transactions';

import type { RpcParameter } from '@leather.io/rpc';
import { isDefined, isUndefined } from '@leather.io/utils';

import { InternalMethods } from '@shared/message-types';
import { sendMessage } from '@shared/messages';
import { RouteUrls } from '@shared/route-urls';
Expand Down Expand Up @@ -76,3 +81,36 @@ export async function triggerSwapWindowOpen(path: To, urlParams: URLSearchParams
if (IS_TEST_ENV) return openRequestInFullPage(path, urlParams);
return popup({ url: `/popup.html#${path}?${urlParams.toString()}` });
}

export function encodePostConditions(postConditions: PostConditionWire[]) {
return postConditions.map(pc => serializePostConditionWire(pc));
}

export function getStxDefaultMessageParamsToTransactionRequest(params: RpcParameter) {
if (isUndefined(params)) return;

const transactionRequest = {} as any;

if ('address' in params && isDefined(params.address)) {
transactionRequest.stxAddress = params.address;
}
if ('fee' in params && isDefined(params.fee)) {
transactionRequest.fee = params.fee;
}
if ('nonce' in params && isDefined(params.nonce)) {
transactionRequest.nonce = params.nonce;
}
if ('postConditions' in params && isDefined(params.postConditions)) {
transactionRequest.postConditions = encodePostConditions(
params.postConditions as PostConditionWire[]
);
}
if ('postConiditionMode' in params && isDefined(params.postConditionMode)) {
transactionRequest.postConditionMode = params.postConditionMode;
}
if ('sponsored' in params && isDefined(params.sponsored)) {
transactionRequest.sponsored = params.sponsored;
}

return transactionRequest;
}
6 changes: 6 additions & 0 deletions src/background/messaging/rpc-message-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { rpcSendTransfer } from './rpc-methods/send-transfer';
import { rpcSignMessage } from './rpc-methods/sign-message';
import { rpcSignPsbt } from './rpc-methods/sign-psbt';
import { rpcSignStacksMessage } from './rpc-methods/sign-stacks-message';
import { rpcStxCallContract } from './rpc-methods/stx-call-contract';
import { rpcStxGetAddresses } from './rpc-methods/stx-get-addresses';
import { rpcSupportedMethods } from './rpc-methods/supported-methods';

Expand Down Expand Up @@ -48,6 +49,11 @@ export async function rpcMessageHandler(message: WalletRequests, port: chrome.ru
break;
}

case 'stx_callContract': {
await rpcStxCallContract(message, port);
break;
}

case 'stx_signTransaction': {
await rpcSignStacksTransaction(message, port);
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,12 @@ import {
AuthType,
MultiSigHashMode,
PayloadType,
PostCondition,
StacksTransactionWire,
VersionedSmartContractPayloadWire,
addressToString,
cvToValue,
deserializeTransaction,
postConditionToWire,
serializeCV,
serializePostConditionWire,
} from '@stacks/transactions';
import { createUnsecuredToken } from 'jsontokens';

Expand All @@ -32,6 +29,7 @@ import { makeRpcErrorResponse } from '@shared/rpc/rpc-methods';

import {
RequestParams,
encodePostConditions,
getTabIdFromPort,
listenForPopupClose,
makeSearchParamsWithDefaults,
Expand All @@ -45,10 +43,6 @@ function cleanMemoString(memo: string): string {
return memo.replaceAll(MEMO_DESERIALIZATION_STUB, '');
}

function encodePostConditions(postConditions: PostCondition[]) {
return postConditions.map(pc => serializePostConditionWire(postConditionToWire(pc)));
}

function getStacksTransactionHexFromRequest(requestParams: StxSignTransactionRequestParams) {
if ('txHex' in requestParams) return requestParams.txHex;
return requestParams.transaction;
Expand Down
102 changes: 102 additions & 0 deletions src/background/messaging/rpc-methods/stx-call-contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { TransactionTypes } from '@stacks/connect';
import { type ClarityValue, serializeCV } from '@stacks/transactions';
import { createUnsecuredToken } from 'jsontokens';

import {
RpcErrorCode,
type StxCallContractRequest,
type StxCallContractRequestParams,
} from '@leather.io/rpc';
import { getStacksAssetStringParts } from '@leather.io/stacks';
import { isUndefined } from '@leather.io/utils';

import { RouteUrls } from '@shared/route-urls';
import {
getRpcStxCallContractParamErrors,
validateRpcStxCallContractParams,
} from '@shared/rpc/methods/stx-call-contract';
import { makeRpcErrorResponse } from '@shared/rpc/rpc-methods';

import {
RequestParams,
getStxDefaultMessageParamsToTransactionRequest,
getTabIdFromPort,
listenForPopupClose,
makeSearchParamsWithDefaults,
triggerRequestWindowOpen,
} from '../messaging-utils';
import { trackRpcRequestError, trackRpcRequestSuccess } from '../rpc-message-handler';

function getMessageParamsToTransactionRequest(params: StxCallContractRequestParams) {
const { contractAddress, contractName } = getStacksAssetStringParts(params.contract);
const defaultParams = getStxDefaultMessageParamsToTransactionRequest(params);

return {
txType: TransactionTypes.ContractCall,
contractAddress,
contractName,
functionArgs: (params.functionArgs ?? []).map(arg =>
serializeCV(arg as unknown as ClarityValue)
),
functionName: params.functionName,
...defaultParams,
};
}

export async function rpcStxCallContract(
message: StxCallContractRequest,
port: chrome.runtime.Port
) {
if (isUndefined(message.params)) {
void trackRpcRequestError({ endpoint: message.method, error: 'Undefined parameters' });
chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('stx_callContract', {
id: message.id,
error: { code: RpcErrorCode.INVALID_REQUEST, message: 'Parameters undefined' },
})
);
return;
}

if (!validateRpcStxCallContractParams(message.params)) {
void trackRpcRequestError({ endpoint: message.method, error: 'Invalid parameters' });

chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('stx_callContract', {
id: message.id,
error: {
code: RpcErrorCode.INVALID_PARAMS,
message: getRpcStxCallContractParamErrors(message.params),
},
})
);
return;
}

const request = getMessageParamsToTransactionRequest(message.params);

void trackRpcRequestSuccess({ endpoint: message.method });

const requestParams: RequestParams = [
['requestId', message.id],
['request', createUnsecuredToken(request)],
];

const { urlParams, tabId } = makeSearchParamsWithDefaults(port, requestParams);

const { id } = await triggerRequestWindowOpen(RouteUrls.RpcStxCallContract, urlParams);

listenForPopupClose({
tabId,
id,
response: makeRpcErrorResponse('stx_callContract', {
id: message.id,
error: {
code: RpcErrorCode.USER_REJECTION,
message: 'User denied signing stacks transaction',
},
}),
});
}
1 change: 1 addition & 0 deletions src/shared/route-urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,5 @@ export enum RouteUrls {

// Request routes stacks
RpcSignStacksTransaction = '/sign-stacks-transaction',
RpcStxCallContract = '/stx-call-contract',
}
Loading

0 comments on commit 4db5dc9

Please sign in to comment.