Skip to content

Commit

Permalink
feat: [UI] support tx v3 in wallet-ui (#271)
Browse files Browse the repository at this point in the history
* chore: revamp estimate fee and removed unused rpc calls

* fix: verify if account need upgrade or deploy does not always throw

* fix: address comments

* refactor: revamp sign delcare transaction

* feat: match starknet.js signature in estimate fee

* feat: createStructWithAdditionalProperties superstruct factory

* fix: improve error message in rpc input validation

* refactor: add universal details and invocations in superstruct util

* refactor: add universal details and invocations in superstruct util

* feat: revamped execute txn

* chore: lint + prettier

* fix: confirm dialog

* feat: save transaction in state after execute txn

* feat: txv3 in execute txn and send transaction ui

* chore: lint + prettier

* chore: lint + prettier

* chore: add superstruct test

* chore: update get-starknet interface

* fix: address comments

* chore: restrict tx version to v3 and v2

* fix: address further comments

* fix: address further comments

* refactor: rpc estimatefee test ts

* chore: lint + prettier

* feat: use define superstruct instead of union

* fix: use row components

* fix: wip

* fix: getEstimatedFees

* test: fixing

* fix: finalized txv3, and fix circular dependencies in testing

* ci: log level set to ALL in dev

* chore: refine estimate fee (#337)

* fix: address comments

* feat: conform execute input params to starknet

* refactor: unit tests

* fix: address comments

* chore: refine execute txn (#339)

* chore: refine execute txn

* fix: lint error

* fix: transaction version v2 does not apply (#340)

* fix: transaction version v2 does not apply

* chore: lint + prettier

* chore: fix default version

* fix: disable state changes in account creation

---------

Co-authored-by: stanleyyuen <[email protected]>

* fix: txn error

* fix: tx default version should be set by starknet.js

* chore: update updateAccountAsDeploy

---------

Co-authored-by: khanti42 <[email protected]>

* fix: address comments

* fix: rollback snap utils

* fix: allow create account to use txv3

* chore: lint + prettier

* fix: address comments

* fix: address comments, rollback

---------

Co-authored-by: stanleyyuen <[email protected]>
  • Loading branch information
khanti42 and stanleyyconsensys authored Sep 3, 2024
1 parent 9452f91 commit 8f50a33
Show file tree
Hide file tree
Showing 14 changed files with 119 additions and 59 deletions.
84 changes: 50 additions & 34 deletions packages/starknet-snap/src/rpcs/executeTxn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,40 +120,56 @@ describe('ExecuteTxn', () => {
expect(createAccountSpy).not.toHaveBeenCalled();
});

it('creates an account if the account is not deployed and execute the transaction', async () => {
callsExample = callsExamples[1];
const {
account,
createAccountSpy,
executeTxnUtilSpy,
getEstimatedFeesSpy,
getEstimatedFeesRepsMock,
network,
request,
} = await prepareMockExecuteTxn(
callsExample.hash,
callsExample.calls,
callsExample.details,
false,
);

await executeTxn.execute(request);

expect(getEstimatedFeesSpy).toHaveBeenCalled();
expect(createAccountSpy).toHaveBeenCalledTimes(1);
expect(executeTxnUtilSpy).toHaveBeenCalledWith(
network,
account.address,
account.privateKey,
callsExample.calls,
undefined,
{
...callsExample.details,
maxFee: getEstimatedFeesRepsMock.suggestedMaxFee,
nonce: 1,
},
);
});
it.each([constants.TRANSACTION_VERSION.V1, constants.TRANSACTION_VERSION.V3])(
'creates an account and execute the transaction with nonce 1 with transaction version %s if the account is not deployed',
async (transactionVersion) => {
callsExample = callsExamples[1];
const {
account,
createAccountSpy,
executeTxnUtilSpy,
getEstimatedFeesSpy,
getEstimatedFeesRepsMock,
network,
request,
} = await prepareMockExecuteTxn(
callsExample.hash,
callsExample.calls,
{
...callsExample.details,
version: transactionVersion,
},
false,
);

await executeTxn.execute(request);

expect(getEstimatedFeesSpy).toHaveBeenCalled();
expect(createAccountSpy).toHaveBeenCalledTimes(1);
expect(createAccountSpy).toHaveBeenCalledWith({
address: account.address,
callback: expect.any(Function),
network,
privateKey: account.privateKey,
publicKey: account.publicKey,
version: transactionVersion,
waitMode: true,
});
expect(executeTxnUtilSpy).toHaveBeenCalledWith(
network,
account.address,
account.privateKey,
callsExample.calls,
undefined,
{
...callsExample.details,
version: transactionVersion,
maxFee: getEstimatedFeesRepsMock.suggestedMaxFee,
nonce: 1,
},
);
},
);

it('throws UserRejectedRequestError if user cancels execution', async () => {
callsExample = callsExamples[1];
Expand Down
5 changes: 4 additions & 1 deletion packages/starknet-snap/src/rpcs/executeTxn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,16 @@ export class ExecuteTxnRpc extends AccountRpcController<
);

const accountDeployed = !includeDeploy;
const version =
details?.version as unknown as constants.TRANSACTION_VERSION;

if (
!(await this.getExecuteTxnConsensus(
address,
accountDeployed,
calls,
suggestedMaxFee,
details?.version as unknown as constants.TRANSACTION_VERSION,
version,
))
) {
throw new UserRejectedRequestError() as unknown as Error;
Expand All @@ -134,6 +136,7 @@ export class ExecuteTxnRpc extends AccountRpcController<
callback: async (contractAddress: string, transactionHash: string) => {
await this.updateAccountAsDeploy(contractAddress, transactionHash);
},
version,
});
}

Expand Down
12 changes: 12 additions & 0 deletions packages/starknet-snap/src/utils/starknetUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -747,13 +747,22 @@ export const getAccContractAddressAndCallDataLegacy = (publicKey) => {
* Optionally, if `waitMode` is enabled, the function will wait for the transaction to be confirmed
* before returning the account address and transaction hash.
*
* The function also supports an optional `callback` that will be invoked with the deployed account address
* and transaction hash once the deployment is complete. Can be used to update internal state after
* successful transaction
*
* Additionally, an optional `version` parameter can be specified to use a transaction version 3
* ('0x3') during the deployment process.
*
* @param network - The network on which to deploy the account.
* @param address - The account address.
* @param publicKey - The public key of the account address.
* @param privateKey - The private key used to sign the deployment transaction.
* @param cairoVersion - The version of Cairo to use for the account deployment.
* @param recordAccountDeployment - A function to record deployment information into the state.
* @param waitMode - If true, waits for the transaction to be confirmed before returning. Defaults to false.
* @param version - (Optional) Specifies the transaction version to use ('0x3').
* @param callback - (Optional) A function to be called with the account address and transaction hash once deployment is complete.
* @returns An object containing the deployed account address and the transaction hash.
*/
export async function createAccount({
Expand All @@ -764,13 +773,15 @@ export async function createAccount({
cairoVersion = CAIRO_VERSION,
waitMode = false,
callback,
version = undefined,
}: {
network: Network;
address: string;
publicKey: string;
privateKey: string;
cairoVersion?: CairoVersion;
waitMode?: boolean;
version?: constants.TRANSACTION_VERSION;
callback?: (address: string, transactionHash: string) => Promise<void>;
}) {
// Deploy account will auto estimate the fee from the network if not provided
Expand All @@ -784,6 +795,7 @@ export async function createAccount({
publicKey,
privateKey,
cairoVersion,
{ version },
);

if (contractAddress !== address) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default {
const asset = {
address: '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7',
amount: BigNumber.from('1000000000000000000'),
spendableAmount: BigNumber.from('1000000000000000000'),
chainId: constants.StarknetChainId.SN_SEPOLIA,
decimals: 18,
name: 'Ether',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export const AmountInputView = ({
const handleMaxClick = () => {
if (inputRef.current && asset.usdPrice) {
const amountStr = ethers.utils
.formatUnits(asset.amount, asset.decimals)
.formatUnits(asset.spendableAmount, asset.decimals)
.toString();
const amountFloat = parseFloat(amountStr);
inputRef.current.value = usdMode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default {
const asset: Erc20TokenBalance = {
address: '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7',
amount: BigNumber.from('1000000000000000000'),
spendableAmount: BigNumber.from('1000000000000000000'),
chainId: constants.StarknetChainId.SN_SEPOLIA,
decimals: 18,
name: 'Ether',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ export const TransactionListItemView = ({ transaction }: Props) => {
<Label>{txnName}</Label>
<Description>
{txnDate}
<br />
<Status status={transaction.executionStatus}>
{' '}
. {txnStatus}
{txnStatus}
{txnFailureReason}
</Status>
</Description>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@ export const getTxnName = (transaction: Transaction): string => {
};

export const getTxnDate = (transaction: Transaction): string => {
return new Date(transaction.timestamp * 1000)
.toDateString()
.split(' ')
.slice(1, 3)
.join(' ');
const date = new Date(transaction.timestamp * 1000);

return date.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};

export const getTxnStatus = (transaction: Transaction): string => {
Expand Down Expand Up @@ -78,7 +82,16 @@ export const getTxnToFromLabel = (transaction: Transaction): string => {
const txnName = getTxnName(transaction);
switch (txnName) {
case 'Send':
return 'To ' + shortenAddress(transaction.contractCallData[0].toString());
// TODO : This will not be needed after getTransactions revamp.
if (transaction.contractCallData.length === 3) {
return (
'To ' + shortenAddress(transaction.contractCallData[0].toString())
);
} else {
return (
'To ' + shortenAddress(transaction.contractCallData[4].toString())
);
}
case 'Receive':
return 'From ' + shortenAddress(transaction.senderAddress);
case 'Deploy':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const HeaderView = ({ address }: Props) => {
const getUSDValue = () => {
const amountFloat = parseFloat(
ethers.utils.formatUnits(
wallet.erc20TokenBalanceSelected.amount,
wallet.erc20TokenBalanceSelected.spendableAmount,
wallet.erc20TokenBalanceSelected.decimals,
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const SendModalView = ({ closeModal }: Props) => {
fieldValue,
wallet.erc20TokenBalanceSelected.decimals,
);
const userBalance = wallet.erc20TokenBalanceSelected.amount;
const userBalance = wallet.erc20TokenBalanceSelected.spendableAmount;
if (inputAmount.gt(userBalance)) {
setErrors((prevErrors) => ({
...prevErrors,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,11 @@ export const SendSummaryModalView = ({
wallet.accounts[0] as unknown as string,
gasFees.suggestedMaxFee,
chainId,
selectedFeeToken,
)
.then((result) => {
if (result) {
toastr.success('Transaction sent successfully');
// can't trigger getTransaction by calling dispatch or setErc20TokenBalance here
getTransactions(
wallet.accounts[0] as unknown as string,
wallet.erc20TokenBalanceSelected.address,
Expand Down
20 changes: 15 additions & 5 deletions packages/wallet-ui/src/services/useStarkNetSnap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { Account } from '../types';
import { Erc20TokenBalance, Erc20Token } from '../types';
import { disableLoading, enableLoadingWithMessage } from '../slices/UISlice';
import { Transaction } from 'types';
import { ethers } from 'ethers';
import { BigNumber, ethers } from 'ethers';
import { getAssetPriceUSD } from './coinGecko';
import semver from 'semver/preload';
import { setActiveNetwork } from 'slices/networkSlice';
Expand Down Expand Up @@ -382,6 +382,7 @@ export const useStarkNetSnap = () => {
address: string,
maxFee: string,
chainId: string,
feeToken: string,
) {
dispatch(enableLoadingWithMessage('Sending transaction...'));
try {
Expand All @@ -402,7 +403,13 @@ export const useStarkNetSnap = () => {
...defaultParam,
address,
calls,
details: { maxFee } as UniversalDetails,
details: {
version:
feeToken === 'STRK'
? constants.TRANSACTION_VERSION.V3
: undefined,
maxFee,
} as UniversalDetails,
chainId,
},
},
Expand Down Expand Up @@ -710,13 +717,16 @@ export const useStarkNetSnap = () => {
},
},
});
return response;
return {
balanceLatest: BigNumber.from(response.balanceLatest),
balancePending: BigNumber.from(response.balancePending),
};
} catch (err) {
//eslint-disable-next-line no-console
console.error(err);
return {
balanceLatest: '0x0',
balancePending: '0x0',
balanceLatest: BigNumber.from('0x0'),
balancePending: BigNumber.from('0x0'),
};
}
};
Expand Down
2 changes: 1 addition & 1 deletion packages/wallet-ui/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ export type Network = Pick<

export interface Erc20TokenBalance extends Types.Erc20Token {
amount: BigNumber;
spendableAmount: BigNumber;
usdPrice?: number;
spendableAmount?: BigNumber;
}
export type TransactionStatusOptions =
| 'Received'
Expand Down
16 changes: 10 additions & 6 deletions packages/wallet-ui/src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,15 @@ export const addMissingPropertiesToToken = (
balanceSpendable?: string,
usdPrice?: number,
): Erc20TokenBalance => {
// when balance is undefined, use 0
const hexBalance = balance ?? '0x0';
// when balanceSpendable is undefined, we use hexBalance
const hexSpendableBalance = balanceSpendable ?? hexBalance;

return {
...token,
amount: ethers.BigNumber.from(balance ? balance : '0x0'),
spendableAmount: ethers.BigNumber.from(
balanceSpendable ? balanceSpendable : '0x0',
),
amount: ethers.BigNumber.from(hexBalance),
spendableAmount: ethers.BigNumber.from(hexSpendableBalance),
usdPrice: usdPrice,
};
};
Expand Down Expand Up @@ -246,8 +249,9 @@ export function getTokenBalanceWithDetails(
tokenUSDPrice?: number,
): Erc20TokenBalance {
const { balancePending, balanceLatest } = tokenBalance;
const spendableBalance =
balancePending < balanceLatest ? balancePending : balanceLatest;
const spendableBalance = balancePending.lt(balanceLatest)
? balancePending
: balanceLatest;
return addMissingPropertiesToToken(
token,
balanceLatest.toString(),
Expand Down

0 comments on commit 8f50a33

Please sign in to comment.