From 541016594448748b499ec85621f1a720954bfd57 Mon Sep 17 00:00:00 2001 From: h0ngcha0 Date: Mon, 21 Oct 2024 10:35:36 +0200 Subject: [PATCH 1/4] Support chained tx --- packages/dapp/package.json | 6 +- packages/extension/package.json | 8 +- .../src/background/actionHandlers.ts | 63 +++- .../transactions/transactionExecution.ts | 34 +- .../src/inpage/alephiumWindowObject.ts | 122 ++++++- .../extension/src/shared/actionQueue/types.ts | 2 +- .../src/shared/messages/TransactionMessage.ts | 5 +- .../extension/src/shared/token/balance.ts | 12 +- .../src/shared/transactions/index.ts | 300 +++++++++++++++++- .../ui/features/accountTokens/tokens.state.ts | 14 +- .../src/ui/features/actions/ActionScreen.tsx | 22 +- .../actions/ApproveSignUnsignedTxScreen.tsx | 2 +- .../actions/ApproveTransactionScreen.tsx | 245 +++++--------- .../transaction/AccountNetworkInfo.tsx | 11 +- .../actions/transaction/DappHeader.tsx | 6 +- .../src/ui/services/backgroundTransactions.ts | 2 +- yarn.lock | 52 +-- 17 files changed, 653 insertions(+), 253 deletions(-) diff --git a/packages/dapp/package.json b/packages/dapp/package.json index 5fd815f7..1d42debf 100644 --- a/packages/dapp/package.json +++ b/packages/dapp/package.json @@ -10,15 +10,15 @@ "lint": "next lint" }, "dependencies": { - "@alephium/get-extension-wallet": "^1.7.3", - "@alephium/web3": "^1.7.3", + "@alephium/get-extension-wallet": "^1.8.0", + "@alephium/web3": "^1.8.0", "ethers": "^5.5.1", "next": "^13.0.0", "react": "^18.0.0", "react-dom": "^18.0.0" }, "devDependencies": { - "@alephium/cli": "^1.7.3", + "@alephium/cli": "^1.8.0", "@types/node": "18.11.18", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", diff --git a/packages/extension/package.json b/packages/extension/package.json index 242b1d5e..f93272eb 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -4,12 +4,12 @@ "main": "index.js", "license": "MIT", "devDependencies": { - "@alephium/get-extension-wallet": "^1.7.3", + "@alephium/get-extension-wallet": "^1.8.0", "@alephium/ledger-app": "0.6.0", "@alephium/token-list": "0.0.19", - "@alephium/web3": "^1.7.3", - "@alephium/web3-test": "^1.7.3", - "@alephium/web3-wallet": "^1.7.3", + "@alephium/web3": "^1.8.0", + "@alephium/web3-test": "^1.8.0", + "@alephium/web3-wallet": "^1.8.0", "@ledgerhq/hw-transport-webusb": "6.29.0", "@ledgerhq/hw-transport-webhid": "6.29.0", "@playwright/test": "^1.23.0", diff --git a/packages/extension/src/background/actionHandlers.ts b/packages/extension/src/background/actionHandlers.ts index 5546b11d..a3fa2de0 100644 --- a/packages/extension/src/background/actionHandlers.ts +++ b/packages/extension/src/background/actionHandlers.ts @@ -3,6 +3,7 @@ import { ActionItem, ExtQueueItem, ReviewTransactionResult, + TransactionResult, } from "../shared/actionQueue/types" import { MessageType } from "../shared/messages" import { addNetwork, getNetworks } from "../shared/network" @@ -12,7 +13,7 @@ import { assertNever } from "../ui/services/assertNever" import { analytics } from "./analytics" import { BackgroundService } from "./background" import { openUi } from "./openUi" -import { executeTransactionAction } from "./transactions/transactionExecution" +import { executeTransactionsAction, executeTransactionAction } from "./transactions/transactionExecution" import { transactionWatcher } from "./transactions/transactionWatcher" export const handleActionApproval = async ( @@ -49,21 +50,61 @@ export const handleActionApproval = async ( } case "ALPH_TRANSACTION": { - const { signature: signatureOpt, ...transaction } = - additionalData as ReviewTransactionResult & { signature?: string } + const transactions = additionalData as ReviewTransactionResult[] try { - const { signature } = await executeTransactionAction( - transaction, - signatureOpt, - background, - transaction.params.networkId, - ) + if (transactions.length === 0) { + return { + type: "ALPH_TRANSACTION_FAILED", + data: { actionHash, error: "No transactions to execute" }, + } + } + + let results: TransactionResult[] + if (transactions.length === 1) { + const { signature: signatureOpt, ...transaction } = + transactions[0] as ReviewTransactionResult & { signature?: string } + + const { signature } = await executeTransactionAction( + transaction, + signatureOpt, + background, + transaction.params.networkId, + ) + + transactionWatcher.refresh() + + results = [ + { + type: transaction.type, + result: { + ...transaction.result, + signature + } + } + ] as TransactionResult[] + } else { + const { signatures } = await executeTransactionsAction( + transactions, + background, + transactions[0].params.networkId, + ) - transactionWatcher.refresh() + transactionWatcher.refresh() + + results = transactions.map((transaction, index) => ( + { + type: transaction.type, + result: { + ...transaction.result, + signature: signatures[index], + } + } + )) as TransactionResult[] + } return { type: "ALPH_TRANSACTION_SUBMITTED", - data: { result: { ...transaction.result, signature }, actionHash }, + data: { result: results, actionHash }, } } catch (error: unknown) { return { diff --git a/packages/extension/src/background/transactions/transactionExecution.ts b/packages/extension/src/background/transactions/transactionExecution.ts index 381092d2..256cc98b 100644 --- a/packages/extension/src/background/transactions/transactionExecution.ts +++ b/packages/extension/src/background/transactions/transactionExecution.ts @@ -29,7 +29,35 @@ export const executeTransactionAction = async ( ) } - if (account !== undefined) { + + addTransaction({ + account: account, + hash: transaction.result.txId, + meta: { + request: transaction, + }, + }) + + return { signature: finalSignature } +} + +export const executeTransactionsAction = async ( + transactions: ReviewTransactionResult[], + { wallet }: BackgroundService, + networkId: string, +): Promise<{ signatures: string[] }> => { + const signatures: string[] = [] + for (const transaction of transactions) { + const account = await wallet.getAccount({ + address: transaction.params.signerAddress, + networkId: networkId, + }) + + const { signature } = await wallet.signAndSubmitUnsignedTx(account, { + signerAddress: account.address, + unsignedTx: transaction.result.unsignedTx, + }) + addTransaction({ account: account, hash: transaction.result.txId, @@ -37,7 +65,9 @@ export const executeTransactionAction = async ( request: transaction, }, }) + + signatures.push(signature) } - return { signature: finalSignature } + return { signatures } } diff --git a/packages/extension/src/inpage/alephiumWindowObject.ts b/packages/extension/src/inpage/alephiumWindowObject.ts index 20a33fa1..fc42d4cb 100644 --- a/packages/extension/src/inpage/alephiumWindowObject.ts +++ b/packages/extension/src/inpage/alephiumWindowObject.ts @@ -10,12 +10,17 @@ import { KeyType, NetworkId, NodeProvider, + SignChainedTxParams, + SignChainedTxResult, + SignDeployContractChainedTxResult, SignDeployContractTxParams, + SignExecuteScriptChainedTxResult, SignDeployContractTxResult, SignExecuteScriptTxParams, SignExecuteScriptTxResult, SignMessageParams, SignMessageResult, + SignTransferChainedTxResult, SignTransferTxParams, SignTransferTxResult, SignUnsignedTxParams, @@ -148,7 +153,7 @@ export const alephiumWindowObject: AlephiumWindowObject = signAndSubmitTransferTx = async ( params: SignTransferTxParams, ): Promise => { - const result = ( + const [txResult] = ( await this.#executeAlephiumTransaction( params, (p, host, networkId, keyType) => ({ @@ -157,14 +162,14 @@ export const alephiumWindowObject: AlephiumWindowObject = salt: Date.now().toString(), }), ) - ).result as SignTransferTxResult - return result + ).result + return txResult.result as SignTransferTxResult } signAndSubmitDeployContractTx = async ( params: SignDeployContractTxParams, ): Promise => { - const result = ( + const [txResult] = ( await this.#executeAlephiumTransaction( params, (p, host, networkId, keyType) => ({ @@ -173,14 +178,15 @@ export const alephiumWindowObject: AlephiumWindowObject = salt: Date.now().toString(), }), ) - ).result as SignDeployContractTxResult - return { ...result } + ).result + + return txResult.result as SignDeployContractTxResult } signAndSubmitExecuteScriptTx = async ( params: SignExecuteScriptTxParams, ): Promise => { - const result = ( + const [txResult] = ( await this.#executeAlephiumTransaction( params, (p, host, networkId, keyType) => ({ @@ -189,14 +195,14 @@ export const alephiumWindowObject: AlephiumWindowObject = salt: Date.now().toString(), }), ) - ).result as SignExecuteScriptTxResult - return result + ).result + return txResult.result as SignExecuteScriptTxResult } signAndSubmitUnsignedTx = async ( params: SignUnsignedTxParams, ): Promise => { - const result = ( + const [txResult] = ( await this.#executeAlephiumTransaction( params, (p, host, networkId, keyType) => ({ @@ -205,8 +211,96 @@ export const alephiumWindowObject: AlephiumWindowObject = salt: Date.now().toString(), }), ) - ).result as SignUnsignedTxResult - return result + ).result + + return txResult.result as SignUnsignedTxResult + } + + signAndSubmitChainedTx = async ( + params: SignChainedTxParams[], + ): Promise => { + if (params.length === 0) { + throw Error("Empty transaction params") + } + + const transactionParamz: TransactionParams[] = params.map(param => { + const paramsType = param.type + const salt = Date.now().toString() + switch (paramsType) { + case 'Transfer': + return { + type: 'TRANSFER', + params: { networkId: this.connectedNetworkId, ...param }, + salt + } + case 'DeployContract': + return { + type: 'DEPLOY_CONTRACT', + params: { networkId: this.connectedNetworkId, ...param }, + salt + } + case 'ExecuteScript': + return { + type: 'EXECUTE_SCRIPT', + params: { networkId: this.connectedNetworkId, ...param }, + salt + } + default: + throw new Error(`Unsupported transaction type: ${paramsType}`); + } + }) + + sendMessage({ type: "ALPH_EXECUTE_TRANSACTION", data: transactionParamz }) + + const { actionHash } = await waitForMessage( + "ALPH_EXECUTE_TRANSACTION_RES", + USER_ACTION_TIMEOUT, + ) + + sendMessage({ type: "ALPH_OPEN_UI" }) + + const result = await Promise.race([ + waitForMessage( + "ALPH_TRANSACTION_SUBMITTED", + USER_ACTION_TIMEOUT_LONGER, + (x) => x.data.actionHash === actionHash, + ), + waitForMessage( + "ALPH_TRANSACTION_FAILED", + USER_ACTION_TIMEOUT, + (x) => x.data.actionHash === actionHash, + ) + .then((res) => res) + .catch(() => { + const error = "User action time out" + sendMessage({ + type: "ALPH_TRANSACTION_FAILED", + data: { actionHash, error }, + }) + return { error } + }), + ]) + + if ("error" in result) { + throw Error(result.error) + } + + const signedChainedTxResult = result.result.map(txResult => { + const txResultType = txResult.type + switch (txResultType) { + case 'TRANSFER': + return { type: 'Transfer', ...txResult.result } as SignTransferChainedTxResult; + case 'DEPLOY_CONTRACT': + return { type: 'DeployContract', ...txResult.result } as SignDeployContractChainedTxResult; + case 'EXECUTE_SCRIPT': + return { type: 'ExecuteScript', ...txResult.result } as SignExecuteScriptChainedTxResult; + default: + throw new Error(`Unsupported transaction type: ${txResultType}`); + } + } + ) + + return signedChainedTxResult; } signUnsignedTx = async ( @@ -218,7 +312,6 @@ export const alephiumWindowObject: AlephiumWindowObject = throw new Error("Invalid unsigned tx") } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion sendMessage({ type: "ALPH_SIGN_UNSIGNED_TX", data: { @@ -368,7 +461,6 @@ export const alephiumWindowObject: AlephiumWindowObject = ) { this.#checkParams(params) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const data = dataBuilder( params, window.location.host, @@ -376,7 +468,7 @@ export const alephiumWindowObject: AlephiumWindowObject = this.connectedAccount!.keyType, ) - sendMessage({ type: "ALPH_EXECUTE_TRANSACTION", data }) + sendMessage({ type: "ALPH_EXECUTE_TRANSACTION", data: [data] }) const { actionHash } = await waitForMessage( "ALPH_EXECUTE_TRANSACTION_RES", USER_ACTION_TIMEOUT, diff --git a/packages/extension/src/shared/actionQueue/types.ts b/packages/extension/src/shared/actionQueue/types.ts index 40d3fc98..4445100e 100644 --- a/packages/extension/src/shared/actionQueue/types.ts +++ b/packages/extension/src/shared/actionQueue/types.ts @@ -106,7 +106,7 @@ export type ActionItem = } | { type: "ALPH_TRANSACTION" - payload: TransactionParams + payload: TransactionParams[] } | { type: "ALPH_SIGN_MESSAGE" diff --git a/packages/extension/src/shared/messages/TransactionMessage.ts b/packages/extension/src/shared/messages/TransactionMessage.ts index 3086d1f7..ec770610 100644 --- a/packages/extension/src/shared/messages/TransactionMessage.ts +++ b/packages/extension/src/shared/messages/TransactionMessage.ts @@ -9,6 +9,7 @@ import { TransactionParams, TransactionResult } from "../actionQueue/types" import { Transaction } from "../transactions" import { DeclareContract } from "../udc/type" import { BaseWalletAccount } from "../wallet.model" +import { SignChainedTxParams } from "@alephium/web3" export interface EstimateFeeResponse { amount: string @@ -28,13 +29,13 @@ export interface DeclareDeployEstimateFeeResponse export type TransactionMessage = | { type: "ALPH_EXECUTE_TRANSACTION" - data: TransactionParams + data: TransactionParams[] } | { type: "ALPH_EXECUTE_TRANSACTION_RES"; data: { actionHash: string } } | { type: "TRANSACTION_UPDATES"; data: Transaction[] } | { type: "ALPH_TRANSACTION_SUBMITTED" - data: { result: TransactionResult["result"]; actionHash: string } + data: { result: TransactionResult[]; actionHash: string } } | { type: "ALPH_TRANSACTION_FAILED" diff --git a/packages/extension/src/shared/token/balance.ts b/packages/extension/src/shared/token/balance.ts index 0179713d..27c2ac62 100644 --- a/packages/extension/src/shared/token/balance.ts +++ b/packages/extension/src/shared/token/balance.ts @@ -1,4 +1,4 @@ -import { explorer, ExplorerProvider, ALPH_TOKEN_ID } from "@alephium/web3" +import { explorer, ExplorerProvider, ALPH_TOKEN_ID, NodeProvider } from "@alephium/web3" import { getNetwork } from "../network" import { BaseWalletAccount } from "../wallet.model" import { BigNumber } from "ethers" @@ -32,3 +32,13 @@ export function addTokenToBalances(balances: Map, tokenId: st balances.set(tokenId, tokenBalance.add(amount)) } } + +export async function getBalances(nodeProvider: NodeProvider, address: string): Promise> { + const result = await nodeProvider.addresses.getAddressesAddressBalance(address) + const balances = new Map() + balances.set(ALPH_TOKEN_ID, BigNumber.from(result.balance)) + if (result.tokenBalances !== undefined) { + result.tokenBalances.forEach((token) => addTokenToBalances(balances, token.id, BigNumber.from(token.amount))) + } + return balances +} diff --git a/packages/extension/src/shared/transactions/index.ts b/packages/extension/src/shared/transactions/index.ts index f8f96a61..3822df1b 100644 --- a/packages/extension/src/shared/transactions/index.ts +++ b/packages/extension/src/shared/transactions/index.ts @@ -1,11 +1,15 @@ -import { ExplorerProvider } from "@alephium/web3" +import { ALPH_TOKEN_ID, DUST_AMOUNT, ExplorerProvider, NodeProvider, ONE_ALPH, SignChainedTxParams, SignChainedTxResult, SignDeployContractChainedTxParams, SignExecuteScriptChainedTxParams, SignTransferChainedTxParams, TransactionBuilder } from "@alephium/web3" import { lowerCase, upperFirst } from "lodash-es" import { Call } from "starknet" -import { ReviewTransactionResult } from "../actionQueue/types" +import { ReviewTransactionResult, TransactionParams } from "../actionQueue/types" import { WalletAccount } from "../wallet.model" import { AlephiumExplorerTransaction } from "../explorer/type" import { mapAlephiumTransactionToTransaction } from "./transformers" import { getNetwork } from "../network" +import { BaseTokenWithBalance } from "../token/type" +import { BigNumber } from "ethers" +import { addTokenToBalances, getBalances } from "../token/balance" +import i18n from "../../i18n" export type Status = 'NOT_RECEIVED' | 'RECEIVED' | 'PENDING' | 'ACCEPTED_ON_MEMPOOL' | 'ACCEPTED_ON_L2' | 'ACCEPTED_ON_CHAIN' | 'REJECTED' | 'REMOVED_FROM_MEMPOOL'; @@ -129,3 +133,295 @@ function buildGetTransactionsFn(metadataTransactions: Transaction[]) { ) } } + +export function transactionParamsToSignChainedTxParams( + transactionParams: TransactionParams +): SignChainedTxParams { + switch (transactionParams.type) { + case "TRANSFER": + return { + type: 'Transfer', + ...transactionParams.params + } as SignTransferChainedTxParams + case "DEPLOY_CONTRACT": + return { + type: 'DeployContract', + ...transactionParams.params + } as SignDeployContractChainedTxParams + case "EXECUTE_SCRIPT": + return { + type: 'ExecuteScript', + ...transactionParams.params + } as SignExecuteScriptChainedTxParams + default: + throw new Error(`Unsupported transaction type: ${transactionParams.type}`); + } +} + +export function signedChainedTxParamsToTransactionParams( + signedChainedTxParams: SignChainedTxParams, + networkId: string +): TransactionParams { + const paramsType = signedChainedTxParams.type + const salt = Date.now().toString() + switch (paramsType) { + case 'Transfer': + return { + type: 'TRANSFER', + params: { networkId, ...signedChainedTxParams }, + salt + } + case 'DeployContract': + return { + type: 'DEPLOY_CONTRACT', + params: { networkId, ...signedChainedTxParams }, + salt + } + case 'ExecuteScript': + return { + type: 'EXECUTE_SCRIPT', + params: { networkId, ...signedChainedTxParams }, + salt + } + default: + throw new Error(`Unsupported transaction type: ${paramsType}`); + } +} + +export function signedChainedTxResultToReviewTransactionResult( + signedChainedTxParams: SignChainedTxParams, + signedChainedTxResult: Omit, + networkId: string +): ReviewTransactionResult { + const txParams = signedChainedTxParamsToTransactionParams(signedChainedTxParams, networkId) + return { + type: txParams.type, + params: txParams.params, + result: signedChainedTxResult + } as ReviewTransactionResult +} + +export async function tryBuildChainedTransactions( + nodeUrl: string, + walletAccounts: WalletAccount[], + transactionParams: TransactionParams[] +): Promise { + try { + if (transactionParams.length === 0) { + throw new Error("Transaction params are empty"); + } + const networkId = transactionParams[0].params.networkId + if (transactionParams.some((params) => params.params.networkId !== networkId)) { + throw new Error(`All transaction params must have the same networkId ${networkId}`) + } + + const builder = TransactionBuilder.from(nodeUrl) + const chainedParams = transactionParams.map((params) => transactionParamsToSignChainedTxParams(params)) + const publicKeys = transactionParams.map((params) => { + const account = walletAccounts.find(acc => acc.address === params.params.signerAddress && acc.networkId === networkId) + if (!account) { + throw new Error(`No wallet account found for address ${params.params.signerAddress} in ${networkId} network`) + } + return account.signer.publicKey + }) + const chainedTxResults = await builder.buildChainedTx(chainedParams, publicKeys) + return chainedTxResults.map((signedChainedTxResult, index) => { + return signedChainedTxResultToReviewTransactionResult(chainedParams[index], signedChainedTxResult, networkId) + }) + } catch (error) { + console.log("Error building chained transaction", error) + throw error + } +} + +export async function tryBuildTransactions( + nodeUrl: string, + tokensWithBalance: BaseTokenWithBalance[], + selectedAccount: WalletAccount, + allAccounts: WalletAccount[], + transactionParams: TransactionParams, + useLedger: boolean +): Promise { + const builder = TransactionBuilder.from(nodeUrl) + try { + const transaction: ReviewTransactionResult = await buildTransaction(builder, selectedAccount, transactionParams) + return [transaction] + } catch (error) { + console.log("Error building transaction", error) + const missingBalances = await checkBalances(tokensWithBalance, transactionParams) + const isDAppTransaction = transactionParams.type === 'EXECUTE_SCRIPT' || transactionParams.type === 'DEPLOY_CONTRACT' + + if (missingBalances !== undefined) { + if (isDAppTransaction && !useLedger) { + const restOfAccounts = allAccounts.filter((account) => account.address !== selectedAccount.address) + const nodeProvider = new NodeProvider(nodeUrl) + const account = await accountWithEnoughBalance(missingBalances, restOfAccounts, nodeProvider) + + if (account !== undefined) { + const tokens = Array.from(missingBalances.entries()) + .filter(([tokenId]) => tokenId !== ALPH_TOKEN_ID) + .map(([tokenId, amount]) => ({ + id: tokenId, + amount: amount.toString() + })) + const attoAlphAmount = missingBalances.get(ALPH_TOKEN_ID)?.toString() || DUST_AMOUNT * BigInt(tokens.length) + const transferTransactionParams: SignTransferChainedTxParams = { + type: 'Transfer', + signerAddress: account.address, + signerKeyType: account.signer.keyType, + destinations: [{ + address: selectedAccount.address, + attoAlphAmount, + tokens + }] + } + + const dappTransactionParams = transactionParamsToSignChainedTxParams(transactionParams) + const chainedTxParams = [transferTransactionParams, dappTransactionParams] + const chainedTxResults = await builder.buildChainedTx( + chainedTxParams, + [account.signer.publicKey, selectedAccount.signer.publicKey] + ) + + const reviewTxResults = chainedTxParams.map((params, index) => { + return signedChainedTxResultToReviewTransactionResult(params, chainedTxResults[index], account.networkId) + }) + + return reviewTxResults + } + } + + const [firstMissingTokenId, firstMissingAmount] = missingBalances.entries().next().value; + const tokenSymbol = firstMissingTokenId === ALPH_TOKEN_ID ? 'ALPH' : firstMissingTokenId; + const expectedStr = firstMissingAmount.toString(); + const haveStr = (tokensWithBalance.find(t => t.id === firstMissingTokenId)?.balance || '0').toString(); + const errorMsg = i18n.t("Insufficient token {{ tokenSymbol }}, expected at least {{ expectedStr }}, got {{ haveStr }}", { tokenSymbol, expectedStr, haveStr }) + throw new Error(errorMsg) + } + + throw error + } +} + +async function accountWithEnoughBalance( + missingBalances: Map, + accounts: WalletAccount[], + nodeProvider: NodeProvider +): Promise { + for (const account of accounts) { + const accountBalances = await getBalances(nodeProvider, account.address) + let hasEnoughBalance = true; + for (const [tokenId, amount] of missingBalances) { + const balance = accountBalances.get(tokenId); + if (!balance || balance.lt(amount)) { + hasEnoughBalance = false; + break; + } + } + + if (hasEnoughBalance) { + return account + } + } + return undefined +} + + +async function buildTransaction( + builder: TransactionBuilder, + account: WalletAccount, + transactionParams: TransactionParams +): Promise { + switch (transactionParams.type) { + case "TRANSFER": + return { + type: transactionParams.type, + params: transactionParams.params, + result: await builder.buildTransferTx( + transactionParams.params, + account.signer.publicKey, + ), + } + case "DEPLOY_CONTRACT": + return { + type: transactionParams.type, + params: transactionParams.params, + result: await builder.buildDeployContractTx( + transactionParams.params, + account.signer.publicKey, + ), + } + case "EXECUTE_SCRIPT": + return { + type: transactionParams.type, + params: transactionParams.params, + result: await builder.buildExecuteScriptTx( + transactionParams.params, + account.signer.publicKey, + ), + } + case "UNSIGNED_TX": + return { + type: transactionParams.type, + params: transactionParams.params, + result: await builder.buildUnsignedTx(transactionParams.params), + } + } +} + +export async function checkBalances( + tokensWithBalance: BaseTokenWithBalance[], + transactionParams: TransactionParams +): Promise | undefined> { + const expectedBalances: Map = new Map() + const gasFee = BigInt(5000000) // Maximal gas fee per tx + switch (transactionParams.type) { + case 'TRANSFER': + transactionParams.params.destinations.forEach((destination) => { + addTokenToBalances(expectedBalances, ALPH_TOKEN_ID, BigNumber.from(destination.attoAlphAmount)) + if (destination.tokens !== undefined) { + destination.tokens.forEach((token) => addTokenToBalances(expectedBalances, token.id, BigNumber.from(token.amount))) + } + }) + break + case 'DEPLOY_CONTRACT': + addTokenToBalances(expectedBalances, ALPH_TOKEN_ID, + transactionParams.params.initialAttoAlphAmount !== undefined + ? BigNumber.from(transactionParams.params.initialAttoAlphAmount) + : BigNumber.from(ONE_ALPH) + ) + if (transactionParams.params.initialTokenAmounts !== undefined) { + transactionParams.params.initialTokenAmounts.forEach((token) => addTokenToBalances(expectedBalances, token.id, BigNumber.from(token.amount))) + } + break + case 'EXECUTE_SCRIPT': + if (transactionParams.params.attoAlphAmount !== undefined) { + addTokenToBalances(expectedBalances, ALPH_TOKEN_ID, BigNumber.from(transactionParams.params.attoAlphAmount)) + } + if (transactionParams.params.tokens !== undefined) { + transactionParams.params.tokens.forEach((token) => addTokenToBalances(expectedBalances, token.id, BigNumber.from(token.amount))) + } + break + case 'UNSIGNED_TX': + return + } + addTokenToBalances(expectedBalances, ALPH_TOKEN_ID, BigNumber.from(gasFee)) + + const zero = BigNumber.from(0) + const missingBalances: Map = new Map() + for (const [tokenId, amount] of expectedBalances) { + if (zero.eq(amount)) { + continue + } + const token = tokensWithBalance.find((t) => t.id === tokenId) + const tokenBalance = token?.balance + if (tokenBalance === undefined || tokenBalance.lt(amount)) { + missingBalances.set(tokenId, amount.sub(tokenBalance ?? zero)) + } + } + if (missingBalances.size > 0) { + return missingBalances + } + return undefined +} + diff --git a/packages/extension/src/ui/features/accountTokens/tokens.state.ts b/packages/extension/src/ui/features/accountTokens/tokens.state.ts index 7b34a098..e634ccad 100644 --- a/packages/extension/src/ui/features/accountTokens/tokens.state.ts +++ b/packages/extension/src/ui/features/accountTokens/tokens.state.ts @@ -1,4 +1,4 @@ -import { ALPH_TOKEN_ID, NodeProvider } from "@alephium/web3" +import { NodeProvider } from "@alephium/web3" import { BigNumber } from "ethers" import { memoize } from "lodash-es" import { useEffect, useMemo, useRef } from "react" @@ -14,7 +14,7 @@ import { getAccountIdentifier } from "../../../shared/wallet.service" import { useAccount } from "../accounts/accounts.state" import { useAccountTransactions } from "../accounts/accountTransactions.state" import { sortBy } from "lodash" -import { addTokenToBalances } from "../../../shared/token/balance" +import { getBalances } from "../../../shared/token/balance" import { Transaction, compareTransactions } from "../../../shared/transactions" import { fetchImmutable } from "../../../shared/utils/fetchImmutable" import { retryWhenRateLimited } from "../../services/swr" @@ -312,16 +312,6 @@ function hasNewPendingTx(prevTxs: Transaction[], newTxs: Transaction[]): boolean return false } -async function getBalances(nodeProvider: NodeProvider, address: string): Promise> { - const result = await nodeProvider.addresses.getAddressesAddressBalance(address) - const balances = new Map() - balances.set(ALPH_TOKEN_ID, BigNumber.from(result.balance)) - if (result.tokenBalances !== undefined) { - result.tokenBalances.forEach((token) => addTokenToBalances(balances, token.id, BigNumber.from(token.amount))) - } - return balances -} - async function fetchFungibleTokenFromFullNode(network: Network, tokenId: string): Promise { const nodeProvider = new NodeProvider(network.nodeUrl, network.nodeApiKey) try { diff --git a/packages/extension/src/ui/features/actions/ActionScreen.tsx b/packages/extension/src/ui/features/actions/ActionScreen.tsx index 89d6aa3e..11818198 100644 --- a/packages/extension/src/ui/features/actions/ActionScreen.tsx +++ b/packages/extension/src/ui/features/actions/ActionScreen.tsx @@ -32,7 +32,7 @@ export const ActionScreen: FC = () => { const [action] = actions const isLastAction = actions.length === 1 const signerAccount = useAccount(action.type === 'ALPH_TRANSACTION' && selectedAccount - ? { address: action.payload.params.signerAddress, networkId: action.payload.params.networkId ?? selectedAccount.networkId } + ? { address: action.payload[0].params.signerAddress, networkId: action.payload[0].params.networkId ?? selectedAccount.networkId } // FIXME: always use the first payload : (action.type === 'ALPH_SIGN_MESSAGE' || action.type === 'ALPH_SIGN_UNSIGNED_TX') && selectedAccount ? { address: action.payload.signerAddress, networkId: action.payload.networkId ?? selectedAccount.networkId } : undefined @@ -134,20 +134,30 @@ export const ActionScreen: FC = () => { case "ALPH_TRANSACTION": return ( { + onSubmit={async (builtTransactions) => { analytics.track("signedTransaction", { networkId: selectedAccount?.networkId || "unknown", }) - if (!builtTransaction || "error" in builtTransaction) { + + if (!builtTransactions || builtTransactions.length === 0) { useAppState.setState({ - error: `${t('Transaction building failed')}: ${builtTransaction?.error || t("unknown")}`, + error: `${t('Transaction building failed')}`, isLoading: false, }) navigate(routes.error()) } else { - await approveAction(action, builtTransaction) + const errorTransaction = builtTransactions.find(tx => 'error' in tx); + if (errorTransaction && 'error' in errorTransaction) { + useAppState.setState({ + error: `${t('Transaction building failed')}: ${errorTransaction.error}`, + isLoading: false, + }) + navigate(routes.error()) + } + + await approveAction(action, builtTransactions) useAppState.setState({ isLoading: true }) const result = await Promise.race([ waitForMessage( diff --git a/packages/extension/src/ui/features/actions/ApproveSignUnsignedTxScreen.tsx b/packages/extension/src/ui/features/actions/ApproveSignUnsignedTxScreen.tsx index 395b3d44..87bb4af8 100644 --- a/packages/extension/src/ui/features/actions/ApproveSignUnsignedTxScreen.tsx +++ b/packages/extension/src/ui/features/actions/ApproveSignUnsignedTxScreen.tsx @@ -50,7 +50,7 @@ export const ApproveSignUnsignedTxScreen: FC = } { selectedAccount && - + } { diff --git a/packages/extension/src/ui/features/actions/ApproveTransactionScreen.tsx b/packages/extension/src/ui/features/actions/ApproveTransactionScreen.tsx index 0cd83198..43c2621e 100644 --- a/packages/extension/src/ui/features/actions/ApproveTransactionScreen.tsx +++ b/packages/extension/src/ui/features/actions/ApproveTransactionScreen.tsx @@ -1,5 +1,4 @@ import { AlephiumApp as LedgerApp } from "@alephium/ledger-app" -import { ALPH_TOKEN_ID, ONE_ALPH, prettifyTokenAmount, TransactionBuilder } from "@alephium/web3" import { getHDWalletPath } from "@alephium/web3-wallet" import { L1, icons } from "@argent/ui" import { Flex, Text } from "@chakra-ui/react" @@ -10,12 +9,10 @@ import { ReviewTransactionResult, TransactionParams, } from "../../../shared/actionQueue/types" -import { BaseTokenWithBalance } from "../../../shared/token/type" import { useAppState } from "../../app.state" import { routes } from "../../routes" import { usePageTracking } from "../../services/analytics" import { rejectAction } from "../../services/backgroundActions" -import { Account } from "../accounts/Account" import { useAllTokensWithBalance } from "../accountTokens/tokens.state" import { getLedgerApp } from "../ledger/utils" import { useNetwork } from "../networks/useNetworks" @@ -27,11 +24,13 @@ import { AccountNetworkInfo } from "./transaction/AccountNetworkInfo" import { DappHeader } from "./transaction/DappHeader" import { TransactionsList } from "./transaction/TransactionsList" import { TxHashContainer } from "./TxHashContainer" -import { getToken } from "../../../shared/token/storage" -import { BigNumber } from "ethers" -import { addTokenToBalances } from "../../../shared/token/balance" import { useTranslation } from "react-i18next" -import i18n from "../../../i18n" +import { tryBuildChainedTransactions, tryBuildTransactions } from "../../../shared/transactions" +import { IconButton } from "@mui/material" +import { ChevronLeftIcon, ChevronRightIcon } from "@chakra-ui/icons" +import { getAccounts } from "../../../shared/account/store" +import { accountsOnNetwork } from "../../services/backgroundAccounts" +import { useAccount, useSelectedAccount } from "../accounts/accounts.state" const { AlertIcon } = icons @@ -58,140 +57,19 @@ const LedgerStatus = ({ ledgerState }: { ledgerState: string | undefined }): JSX ) } -const minimalGasFee = BigInt(20000) * BigInt(100000000000) - export interface ApproveTransactionScreenProps extends Omit { actionHash: string - transaction: TransactionParams + transactionParams: TransactionParams[] onSubmit: ( result: - | (ReviewTransactionResult & { signature?: string }) - | { error: string } + | (ReviewTransactionResult & { signature?: string } | { error: string })[] | undefined, ) => void } -async function tryBuildTransaction( - nodeUrl: string, - tokensWithBalance: BaseTokenWithBalance[], - account: Account, - transaction: TransactionParams -): Promise { - const builder = TransactionBuilder.from(nodeUrl) - try { - return await buildTransaction(builder, account, transaction) - } catch (error) { - const errMsg = await checkBalances(tokensWithBalance, transaction, account.networkId) - if (errMsg !== undefined) { - throw new Error(errMsg) - } - throw error - } -} - -async function buildTransaction( - builder: TransactionBuilder, - account: Account, - transaction: TransactionParams -): Promise { - switch (transaction.type) { - case "TRANSFER": - return { - type: transaction.type, - params: transaction.params, - result: await builder.buildTransferTx( - transaction.params, - account.publicKey, - ), - } - case "DEPLOY_CONTRACT": - return { - type: transaction.type, - params: transaction.params, - result: await builder.buildDeployContractTx( - transaction.params, - account.publicKey, - ), - } - case "EXECUTE_SCRIPT": - return { - type: transaction.type, - params: transaction.params, - result: await builder.buildExecuteScriptTx( - transaction.params, - account.publicKey, - ), - } - case "UNSIGNED_TX": - return { - type: transaction.type, - params: transaction.params, - result: await builder.buildUnsignedTx(transaction.params), - } - } -} - -export async function checkBalances( - tokensWithBalance: BaseTokenWithBalance[], - transaction: TransactionParams, - networkId: string -): Promise { - const expectedBalances: Map = new Map() - switch (transaction.type) { - case 'TRANSFER': - transaction.params.destinations.forEach((destination) => { - addTokenToBalances(expectedBalances, ALPH_TOKEN_ID, BigNumber.from(destination.attoAlphAmount)) - if (destination.tokens !== undefined) { - destination.tokens.forEach((token) => addTokenToBalances(expectedBalances, token.id, BigNumber.from(token.amount))) - } - }) - break - case 'DEPLOY_CONTRACT': - addTokenToBalances(expectedBalances, ALPH_TOKEN_ID, - transaction.params.initialAttoAlphAmount !== undefined - ? BigNumber.from(transaction.params.initialAttoAlphAmount) - : BigNumber.from(ONE_ALPH) - ) - if (transaction.params.initialTokenAmounts !== undefined) { - transaction.params.initialTokenAmounts.forEach((token) => addTokenToBalances(expectedBalances, token.id, BigNumber.from(token.amount))) - } - break - case 'EXECUTE_SCRIPT': - if (transaction.params.attoAlphAmount !== undefined) { - addTokenToBalances(expectedBalances, ALPH_TOKEN_ID, BigNumber.from(transaction.params.attoAlphAmount)) - } - if (transaction.params.tokens !== undefined) { - transaction.params.tokens.forEach((token) => addTokenToBalances(expectedBalances, token.id, BigNumber.from(token.amount))) - } - break - case 'UNSIGNED_TX': - return - } - addTokenToBalances(expectedBalances, ALPH_TOKEN_ID, BigNumber.from(minimalGasFee)) - - const zero = BigNumber.from(0) - for (const [tokenId, amount] of expectedBalances) { - if (zero.eq(amount)) { - continue - } - const token = tokensWithBalance.find((t) => t.id === tokenId) - const tokenBalance = token?.balance - if (tokenBalance === undefined || tokenBalance.lt(amount)) { - const tokenInfo = await getToken({ id: tokenId, networkId }) - const tokenSymbol = tokenInfo?.symbol ?? tokenId - const tokenDecimals = tokenInfo?.decimals ?? 0 - const expectedStr = prettifyTokenAmount(amount.toBigInt(), tokenDecimals) - const haveStr = prettifyTokenAmount((tokenBalance ?? zero).toBigInt(), tokenDecimals) - return i18n.t("Insufficient token {{ tokenSymbol }}, expected at least {{ expectedStr }}, got {{ haveStr }}", { tokenSymbol, expectedStr, haveStr }) - } - } - return undefined -} - export const ApproveTransactionScreen: FC = ({ - transaction, - selectedAccount, + transactionParams, actionHash, onSubmit, onReject, @@ -199,18 +77,19 @@ export const ApproveTransactionScreen: FC = ({ }) => { const { t } = useTranslation() const navigate = useNavigate() + const selectedAccount = useSelectedAccount() + usePageTracking("signTransaction", { networkId: selectedAccount?.networkId || "unknown", }) - const [buildResult, setBuildResult] = useState< - ReviewTransactionResult | undefined + const [buildResults, setBuildResults] = useState< + ReviewTransactionResult[] | undefined >() const { id: networkId, nodeUrl } = useNetwork( selectedAccount?.networkId ?? "unknown", ) - - const useLedger = - selectedAccount !== undefined && selectedAccount.signer.type === "ledger" + const [currentIndex, setCurrentIndex] = useState(0) + const useLedger = selectedAccount !== undefined && selectedAccount.signer.type === "ledger" const [ledgerState, setLedgerState] = useState< "detecting" | "notfound" | "signing" | "succeeded" | "failed" >() @@ -225,13 +104,30 @@ export const ApproveTransactionScreen: FC = ({ } try { - const buildResult = await tryBuildTransaction( - nodeUrl, - allUserTokens, - selectedAccount, - transaction - ) - setBuildResult(buildResult) + const walletAccounts = await getAccounts((account) => { + return account.networkId === selectedAccount.networkId && !account.hidden + }) + + let results: ReviewTransactionResult[] + if (transactionParams.length === 0) { + throw new Error("Transaction params are empty") + } else if (transactionParams.length === 1) { + results = await tryBuildTransactions( + nodeUrl, + allUserTokens, + selectedAccount, + walletAccounts, + transactionParams[0], + useLedger + ) + } else { + if (useLedger) { + throw new Error("Ledger does not support chained transactions") + } + results = await tryBuildChainedTransactions(nodeUrl, walletAccounts, transactionParams) + } + + setBuildResults(results) } catch (e: any) { useAppState.setState({ error: `${t("Transaction building failed")}: ${e.toString()}`, @@ -243,7 +139,7 @@ export const ApproveTransactionScreen: FC = ({ } build() - }, [nodeUrl, selectedAccount, transaction, tokenDetailsIsInitialising, actionHash, navigate, t]) + }, [nodeUrl, selectedAccount, transactionParams, tokenDetailsIsInitialising, actionHash, navigate, t]) const ledgerSign = useCallback(async () => { if (selectedAccount === undefined) { @@ -251,9 +147,14 @@ export const ApproveTransactionScreen: FC = ({ } setLedgerState(oldState => oldState === undefined ? "detecting" : oldState) - if (buildResult) { + if (buildResults) { let app: LedgerApp | undefined try { + if (buildResults.length !== 1) { + throw new Error("Ledger does not support chained transactions") + } + const buildResult = buildResults[0] + app = await getLedgerApp() setLedgerApp(app) setLedgerState("signing") @@ -264,7 +165,7 @@ export const ApproveTransactionScreen: FC = ({ const unsignedTx = Buffer.from(buildResult.result.unsignedTx, "hex") const signature = await app.signUnsignedTx(path, unsignedTx) setLedgerState("succeeded") - onSubmit({ ...buildResult, signature }) + onSubmit([{ ...buildResult, signature }]) await app.close() } catch (e) { if (app === undefined) { @@ -281,14 +182,14 @@ export const ApproveTransactionScreen: FC = ({ } } } - }, [selectedAccount, buildResult, onSubmit, onReject, navigate]) + }, [selectedAccount, buildResults, onSubmit, onReject, navigate]) if (!selectedAccount) { rejectAction(actionHash, t("No account found for network {{ networkId }}", { networkId })) return } - if (!buildResult) { + if (buildResults === undefined) { return } @@ -316,7 +217,7 @@ export const ApproveTransactionScreen: FC = ({ if (useLedger) { ledgerSign() } else { - onSubmit(buildResult) + onSubmit(buildResults) } }} onReject={() => { @@ -331,16 +232,16 @@ export const ApproveTransactionScreen: FC = ({ }} showHeader={false} footer={ - buildResult && ( + buildResults.length > 0 && ( { return }} - accountAddress={selectedAccount.address} - networkId={selectedAccount.networkId} - transaction={buildResult} + accountAddress={buildResults[currentIndex].params.signerAddress} + networkId={buildResults[currentIndex].params.networkId} + transaction={buildResults[currentIndex]} actionHash={actionHash} /> @@ -348,11 +249,39 @@ export const ApproveTransactionScreen: FC = ({ } {...props} > - - - - - + + + + + { + buildResults.length > 1 && ( + + + setCurrentIndex(prev => Math.max(0, prev - 1))} + disabled={currentIndex === 0} + size="small" + > + + + + + {`${currentIndex + 1} of ${buildResults.length}`} + + + setCurrentIndex(prev => Math.min(buildResults.length - 1, prev + 1))} + disabled={currentIndex === buildResults.length - 1} + size="small" + > + + + + + ) + } ) } diff --git a/packages/extension/src/ui/features/actions/transaction/AccountNetworkInfo.tsx b/packages/extension/src/ui/features/actions/transaction/AccountNetworkInfo.tsx index 9d32feb2..75e14913 100644 --- a/packages/extension/src/ui/features/actions/transaction/AccountNetworkInfo.tsx +++ b/packages/extension/src/ui/features/actions/transaction/AccountNetworkInfo.tsx @@ -6,10 +6,11 @@ import { PrettyAccountAddress } from "../../accounts/PrettyAccountAddress" import { useTranslation } from "react-i18next" interface AccountNetworkInfoProps { - account: Account + accountAddress: string + networkId: string } -export const AccountNetworkInfo = ({ account }: AccountNetworkInfoProps) => { +export const AccountNetworkInfo = ({ accountAddress, networkId }: AccountNetworkInfoProps) => { const { t } = useTranslation() return ( { @@ -35,7 +36,7 @@ export const AccountNetworkInfo = ({ account }: AccountNetworkInfoProps) => { {t("Network")} - {account.networkName} + {networkId} diff --git a/packages/extension/src/ui/features/actions/transaction/DappHeader.tsx b/packages/extension/src/ui/features/actions/transaction/DappHeader.tsx index 53e7f759..ec45b39d 100644 --- a/packages/extension/src/ui/features/actions/transaction/DappHeader.tsx +++ b/packages/extension/src/ui/features/actions/transaction/DappHeader.tsx @@ -1,12 +1,12 @@ import { H5, P4, icons } from "@argent/ui" import { Box, Flex } from "@chakra-ui/react" import React, { useMemo } from "react" -import { TransactionParams } from "../../../../shared/actionQueue/types" +import { ReviewTransactionResult, TransactionParams } from "../../../../shared/actionQueue/types" import { useTranslation } from "react-i18next" export interface DappHeaderProps { - transaction: TransactionParams + transaction: ReviewTransactionResult } export const DappHeader = ({ @@ -62,7 +62,7 @@ const { MulticallIcon } = icons -const TransactionIcon = ({transaction}: {transaction: TransactionParams}) => { +const TransactionIcon = ({transaction}: {transaction: ReviewTransactionResult}) => { switch (transaction.type) { case 'TRANSFER': return diff --git a/packages/extension/src/ui/services/backgroundTransactions.ts b/packages/extension/src/ui/services/backgroundTransactions.ts index 963bc5ce..464eccb7 100644 --- a/packages/extension/src/ui/services/backgroundTransactions.ts +++ b/packages/extension/src/ui/services/backgroundTransactions.ts @@ -6,7 +6,7 @@ import { DeclareContract } from "../../shared/udc/type" import { BaseWalletAccount } from "../../shared/wallet.model" export const executeTransaction = (data: TransactionParams) => { - return sendMessage({ type: "ALPH_EXECUTE_TRANSACTION", data }) + return sendMessage({ type: "ALPH_EXECUTE_TRANSACTION", data: [data] }) } export const getEstimatedFee = async (call: Call | Call[]) => { diff --git a/yarn.lock b/yarn.lock index 1bcc8000..3ee9f6c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,13 +2,13 @@ # yarn lockfile v1 -"@alephium/cli@^1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@alephium/cli/-/cli-1.7.3.tgz#85555c25dd7f3e8e3e33addf9fa74eda435a3083" - integrity sha512-mz7VcqUlfW8812AMw3lKrhHIrbcGTtFeRwiuoEXGE+zgIOWrZNu1E+648/rC89ekV/xxGIxPlesCMDET67ULPA== +"@alephium/cli@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@alephium/cli/-/cli-1.8.0.tgz#2e622a9fe9a9262d1b435a24de5e9ab88d28d330" + integrity sha512-58OvK7Gl8e8qNnVYQIc6TRlf+pYJvfxZttyAxcgV3HzGtUxdgrSFSZdkPzQkH148nleKd6pzN+/AexOa5FeNEA== dependencies: - "@alephium/web3" "^1.7.3" - "@alephium/web3-wallet" "^1.7.3" + "@alephium/web3" "^1.8.0" + "@alephium/web3-wallet" "^1.8.0" "@swc/core" "^1.4.1" commander "^9.5.0" cross-fetch "^3.1.5" @@ -18,12 +18,12 @@ jest "^28.1.3" prettier "^2.8.7" -"@alephium/get-extension-wallet@^1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@alephium/get-extension-wallet/-/get-extension-wallet-1.7.3.tgz#72974a691cf0c15c1e8eb7bea8888131ede35140" - integrity sha512-g/Pw3rg2P0aVIPRJ4Nnpyb41WTfzaZTvE+8Ge6HBPzpWP9w95gzj8CqEHfoV2zg2MkmbU6gL0x0rtoDTOYkSKQ== +"@alephium/get-extension-wallet@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@alephium/get-extension-wallet/-/get-extension-wallet-1.8.0.tgz#cee2cbb0ba499348a5961b9281656d0ae4775fdb" + integrity sha512-mB8RXQZ/zVf2tAXLesIl+mx5pGworxztz9VV4Y2aC3hLQ+vcJuIgtpWrwLk1d0aEusCX/48OqxSwllGva5626g== dependencies: - "@alephium/web3" "^1.7.3" + "@alephium/web3" "^1.8.0" bowser "^2.11.0" "@alephium/ledger-app@0.6.0": @@ -42,20 +42,20 @@ dependencies: cross-fetch "^3.1.8" -"@alephium/web3-test@^1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@alephium/web3-test/-/web3-test-1.7.3.tgz#b26c5ce51e6d57f14a0fb75d7abc9f1f63540e56" - integrity sha512-Tz32fxXgyMREWz6Q9rCGYnPDoGIdf18kNZeBlQCZYgHc255okIW7CXPmvftUUS1gTStQJ9W7rFhtbje0MzzsDg== +"@alephium/web3-test@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@alephium/web3-test/-/web3-test-1.8.0.tgz#92bac53334cdca2ee61044d5653aced307e95fea" + integrity sha512-C54S37PJmunsSh+GAty++3nFaO2YldA2Vf4ACbdaemlSNDwSRCw9qc23Ds5oxI/2bHkLYNfbUEEyAYMAV+ruBA== dependencies: - "@alephium/web3" "^1.7.3" - "@alephium/web3-wallet" "^1.7.3" + "@alephium/web3" "^1.8.0" + "@alephium/web3-wallet" "^1.8.0" -"@alephium/web3-wallet@^1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@alephium/web3-wallet/-/web3-wallet-1.7.3.tgz#7ff34b25853689b906ddfb12948104a6fe40437f" - integrity sha512-jR5hq4dZX7qkslZBMILK5wuZ/3nfQzfIWkOrT9Gln/lZ9ilLdBMIpIbVPKHbHz40fihWJDCu1UOvtw8B8p8dUg== +"@alephium/web3-wallet@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@alephium/web3-wallet/-/web3-wallet-1.8.0.tgz#03f509687df13e90252fa719bec4ed0e5b712789" + integrity sha512-3ry3c6qjI7hiP6r7s6CJ2VWp/DexSEoxqJwcLUqqWMpu8rKo3pCbboA+wHTcJhDxDV/kwbOtjrtpwIsFH0XqOw== dependencies: - "@alephium/web3" "^1.7.3" + "@alephium/web3" "^1.8.0" "@noble/secp256k1" "1.7.1" "@types/node" "^16.18.23" bip32 "3.1.0" @@ -80,10 +80,10 @@ path-browserify "^1.0.1" stream-browserify "^3.0.0" -"@alephium/web3@^1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@alephium/web3/-/web3-1.7.3.tgz#4ebaa45fca0bb7cbce1fc6c55e047c51b197d472" - integrity sha512-TvhHorYNctw9f31cuhpECpo2Ym3sMvBmCWquVqEMZ/gsF2PVeMLAFtG7QsHG625x41OSRAsfbBuQzT3lq2K/yw== +"@alephium/web3@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@alephium/web3/-/web3-1.8.0.tgz#e47877273c628a4a69778393f8cace248aa08938" + integrity sha512-eIBMPfy/cWSt0gRSM4Qdrt67wuTd4c97V/Xxqysqcf/ALoeLbv5Nufsrsmj474dIOSwdHL9cp9w4uVcYmPPpXA== dependencies: "@noble/secp256k1" "1.7.1" base-x "4.0.0" From 536b03f17e70a384c693cab9faa66d7a8a66a2fd Mon Sep 17 00:00:00 2001 From: h0ngcha0 Date: Tue, 22 Oct 2024 10:03:05 +0200 Subject: [PATCH 2/4] Upgrade SDK to 1.8.2 --- packages/dapp/package.json | 6 +-- packages/dapp/src/services/token.service.ts | 2 +- packages/extension/package.json | 8 +-- .../src/shared/transactions/index.ts | 4 +- yarn.lock | 52 +++++++++---------- 5 files changed, 36 insertions(+), 36 deletions(-) diff --git a/packages/dapp/package.json b/packages/dapp/package.json index 1d42debf..cc964ec2 100644 --- a/packages/dapp/package.json +++ b/packages/dapp/package.json @@ -10,15 +10,15 @@ "lint": "next lint" }, "dependencies": { - "@alephium/get-extension-wallet": "^1.8.0", - "@alephium/web3": "^1.8.0", + "@alephium/get-extension-wallet": "^1.8.2", + "@alephium/web3": "^1.8.2", "ethers": "^5.5.1", "next": "^13.0.0", "react": "^18.0.0", "react-dom": "^18.0.0" }, "devDependencies": { - "@alephium/cli": "^1.8.0", + "@alephium/cli": "^1.8.2", "@types/node": "18.11.18", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", diff --git a/packages/dapp/src/services/token.service.ts b/packages/dapp/src/services/token.service.ts index 9123dcae..ac07f339 100644 --- a/packages/dapp/src/services/token.service.ts +++ b/packages/dapp/src/services/token.service.ts @@ -83,7 +83,7 @@ export const mintToken = async ( decimals: 0n, totalSupply: BigInt(mintAmount) }, - initialAttoAlphAmount: BigInt(1100000000000000000), + initialAttoAlphAmount: web3.MINIMAL_CONTRACT_DEPOSIT, issueTokenAmount: BigInt(mintAmount), }) } diff --git a/packages/extension/package.json b/packages/extension/package.json index f93272eb..b87dd7c9 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -4,12 +4,12 @@ "main": "index.js", "license": "MIT", "devDependencies": { - "@alephium/get-extension-wallet": "^1.8.0", + "@alephium/get-extension-wallet": "^1.8.2", "@alephium/ledger-app": "0.6.0", "@alephium/token-list": "0.0.19", - "@alephium/web3": "^1.8.0", - "@alephium/web3-test": "^1.8.0", - "@alephium/web3-wallet": "^1.8.0", + "@alephium/web3": "^1.8.2", + "@alephium/web3-test": "^1.8.2", + "@alephium/web3-wallet": "^1.8.2", "@ledgerhq/hw-transport-webusb": "6.29.0", "@ledgerhq/hw-transport-webhid": "6.29.0", "@playwright/test": "^1.23.0", diff --git a/packages/extension/src/shared/transactions/index.ts b/packages/extension/src/shared/transactions/index.ts index 3822df1b..f4507d0a 100644 --- a/packages/extension/src/shared/transactions/index.ts +++ b/packages/extension/src/shared/transactions/index.ts @@ -1,4 +1,4 @@ -import { ALPH_TOKEN_ID, DUST_AMOUNT, ExplorerProvider, NodeProvider, ONE_ALPH, SignChainedTxParams, SignChainedTxResult, SignDeployContractChainedTxParams, SignExecuteScriptChainedTxParams, SignTransferChainedTxParams, TransactionBuilder } from "@alephium/web3" +import { ALPH_TOKEN_ID, DEFAULT_GAS_PRICE, DUST_AMOUNT, ExplorerProvider, NodeProvider, ONE_ALPH, SignChainedTxParams, SignChainedTxResult, SignDeployContractChainedTxParams, SignExecuteScriptChainedTxParams, SignTransferChainedTxParams, TransactionBuilder } from "@alephium/web3" import { lowerCase, upperFirst } from "lodash-es" import { Call } from "starknet" import { ReviewTransactionResult, TransactionParams } from "../actionQueue/types" @@ -374,7 +374,7 @@ export async function checkBalances( transactionParams: TransactionParams ): Promise | undefined> { const expectedBalances: Map = new Map() - const gasFee = BigInt(5000000) // Maximal gas fee per tx + const gasFee = BigInt(5000000) * DEFAULT_GAS_PRICE // Maximal gas fee per tx switch (transactionParams.type) { case 'TRANSFER': transactionParams.params.destinations.forEach((destination) => { diff --git a/yarn.lock b/yarn.lock index 3ee9f6c5..9d47a8aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,13 +2,13 @@ # yarn lockfile v1 -"@alephium/cli@^1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@alephium/cli/-/cli-1.8.0.tgz#2e622a9fe9a9262d1b435a24de5e9ab88d28d330" - integrity sha512-58OvK7Gl8e8qNnVYQIc6TRlf+pYJvfxZttyAxcgV3HzGtUxdgrSFSZdkPzQkH148nleKd6pzN+/AexOa5FeNEA== +"@alephium/cli@^1.8.2": + version "1.8.2" + resolved "https://registry.yarnpkg.com/@alephium/cli/-/cli-1.8.2.tgz#9cc8b0617b2fee763a283e3f0607d17f31d18bae" + integrity sha512-iBxqcgXTPR51Mmt/WtCpZVA/Kay3gn3cT74QyETSdgic0lP2LCPhbs+XUeNLIYS1fDJSVdeFzt4ZD4p+BdbAbw== dependencies: - "@alephium/web3" "^1.8.0" - "@alephium/web3-wallet" "^1.8.0" + "@alephium/web3" "^1.8.2" + "@alephium/web3-wallet" "^1.8.2" "@swc/core" "^1.4.1" commander "^9.5.0" cross-fetch "^3.1.5" @@ -18,12 +18,12 @@ jest "^28.1.3" prettier "^2.8.7" -"@alephium/get-extension-wallet@^1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@alephium/get-extension-wallet/-/get-extension-wallet-1.8.0.tgz#cee2cbb0ba499348a5961b9281656d0ae4775fdb" - integrity sha512-mB8RXQZ/zVf2tAXLesIl+mx5pGworxztz9VV4Y2aC3hLQ+vcJuIgtpWrwLk1d0aEusCX/48OqxSwllGva5626g== +"@alephium/get-extension-wallet@^1.8.2": + version "1.8.2" + resolved "https://registry.yarnpkg.com/@alephium/get-extension-wallet/-/get-extension-wallet-1.8.2.tgz#1600d7631ce57e03bf493f0e149b310a15b6c20d" + integrity sha512-4rFRq0LhMOaoz9RE6g0fzRMD+KxfY03rKVSOIOJ0/89inlhIZG2QHsr5jcDjo7Z8JkU83vt/9OAymYFAMYypNw== dependencies: - "@alephium/web3" "^1.8.0" + "@alephium/web3" "^1.8.2" bowser "^2.11.0" "@alephium/ledger-app@0.6.0": @@ -42,20 +42,20 @@ dependencies: cross-fetch "^3.1.8" -"@alephium/web3-test@^1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@alephium/web3-test/-/web3-test-1.8.0.tgz#92bac53334cdca2ee61044d5653aced307e95fea" - integrity sha512-C54S37PJmunsSh+GAty++3nFaO2YldA2Vf4ACbdaemlSNDwSRCw9qc23Ds5oxI/2bHkLYNfbUEEyAYMAV+ruBA== +"@alephium/web3-test@^1.8.2": + version "1.8.2" + resolved "https://registry.yarnpkg.com/@alephium/web3-test/-/web3-test-1.8.2.tgz#c4e6c113f4cbf48ffa053fd390707767601a8e13" + integrity sha512-FSaHf5uZKqG5eK41Xn+SlQV8bo73dRsnxjPoqNkCRfCMEdGs32VDLc+/m5H2s7h/ipfy6XkD6BwNNw395vca+Q== dependencies: - "@alephium/web3" "^1.8.0" - "@alephium/web3-wallet" "^1.8.0" + "@alephium/web3" "^1.8.2" + "@alephium/web3-wallet" "^1.8.2" -"@alephium/web3-wallet@^1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@alephium/web3-wallet/-/web3-wallet-1.8.0.tgz#03f509687df13e90252fa719bec4ed0e5b712789" - integrity sha512-3ry3c6qjI7hiP6r7s6CJ2VWp/DexSEoxqJwcLUqqWMpu8rKo3pCbboA+wHTcJhDxDV/kwbOtjrtpwIsFH0XqOw== +"@alephium/web3-wallet@^1.8.2": + version "1.8.2" + resolved "https://registry.yarnpkg.com/@alephium/web3-wallet/-/web3-wallet-1.8.2.tgz#fb39b8590c4dc1aa5115e4af0e13487012f1d55d" + integrity sha512-DjnfQBltmWflYBMsTpdl5ULMXlRvDvgVmeh8LFTTEvo0YpawdLvJ2NB4Fdfj1Y/PH3fz5+dyMFBSJ6NdLYjkTQ== dependencies: - "@alephium/web3" "^1.8.0" + "@alephium/web3" "^1.8.2" "@noble/secp256k1" "1.7.1" "@types/node" "^16.18.23" bip32 "3.1.0" @@ -80,10 +80,10 @@ path-browserify "^1.0.1" stream-browserify "^3.0.0" -"@alephium/web3@^1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@alephium/web3/-/web3-1.8.0.tgz#e47877273c628a4a69778393f8cace248aa08938" - integrity sha512-eIBMPfy/cWSt0gRSM4Qdrt67wuTd4c97V/Xxqysqcf/ALoeLbv5Nufsrsmj474dIOSwdHL9cp9w4uVcYmPPpXA== +"@alephium/web3@^1.8.2": + version "1.8.2" + resolved "https://registry.yarnpkg.com/@alephium/web3/-/web3-1.8.2.tgz#2586e8c112bba3a26db7b862800addb4142c565d" + integrity sha512-iqaPSflaXj7bz3kmkTZOFpCQpvG6CXFS5PaeiE9+bxCgf1FNCf6Q/4Ms7DQuRbZ2U8sDlTbLTH/RYBy5giPADg== dependencies: "@noble/secp256k1" "1.7.1" base-x "4.0.0" From 1e349518b67c563a063ad76409f752f9179a8daf Mon Sep 17 00:00:00 2001 From: h0ngcha0 Date: Wed, 23 Oct 2024 13:45:09 +0200 Subject: [PATCH 3/4] Address review comments --- .../src/inpage/alephiumWindowObject.ts | 56 ++---------- .../src/shared/transactions/index.ts | 80 ++--------------- .../src/shared/transactions/transformers.ts | 87 ++++++++++++++++++- .../src/ui/features/actions/ActionScreen.tsx | 63 +++++++------- 4 files changed, 131 insertions(+), 155 deletions(-) diff --git a/packages/extension/src/inpage/alephiumWindowObject.ts b/packages/extension/src/inpage/alephiumWindowObject.ts index fc42d4cb..cfca80a5 100644 --- a/packages/extension/src/inpage/alephiumWindowObject.ts +++ b/packages/extension/src/inpage/alephiumWindowObject.ts @@ -12,15 +12,12 @@ import { NodeProvider, SignChainedTxParams, SignChainedTxResult, - SignDeployContractChainedTxResult, SignDeployContractTxParams, - SignExecuteScriptChainedTxResult, SignDeployContractTxResult, SignExecuteScriptTxParams, SignExecuteScriptTxResult, SignMessageParams, SignMessageResult, - SignTransferChainedTxResult, SignTransferTxParams, SignTransferTxResult, SignUnsignedTxParams, @@ -34,6 +31,7 @@ import { TransactionParams } from "../shared/actionQueue/types" import { sendMessage, waitForMessage } from "./messageActions" import { getIsPreauthorized, removePreAuthorization } from "./messaging" import { handleAddTokenRequest } from "./requestMessageHandlers" +import { signedChainedTxParamsToTransactionParams, transactionResultToSignUnsignedTxResult } from "../shared/transactions/transformers" const VERSION = `${process.env.VERSION}` const USER_ACTION_TIMEOUT = 10 * 60 * 1000 @@ -223,32 +221,9 @@ export const alephiumWindowObject: AlephiumWindowObject = throw Error("Empty transaction params") } - const transactionParamz: TransactionParams[] = params.map(param => { - const paramsType = param.type - const salt = Date.now().toString() - switch (paramsType) { - case 'Transfer': - return { - type: 'TRANSFER', - params: { networkId: this.connectedNetworkId, ...param }, - salt - } - case 'DeployContract': - return { - type: 'DEPLOY_CONTRACT', - params: { networkId: this.connectedNetworkId, ...param }, - salt - } - case 'ExecuteScript': - return { - type: 'EXECUTE_SCRIPT', - params: { networkId: this.connectedNetworkId, ...param }, - salt - } - default: - throw new Error(`Unsupported transaction type: ${paramsType}`); - } - }) + const transactionParamz: TransactionParams[] = params.map(param => + signedChainedTxParamsToTransactionParams(param, this.#connectedNetworkId!) + ) sendMessage({ type: "ALPH_EXECUTE_TRANSACTION", data: transactionParamz }) @@ -259,7 +234,7 @@ export const alephiumWindowObject: AlephiumWindowObject = sendMessage({ type: "ALPH_OPEN_UI" }) - const result = await Promise.race([ + const txMessage = await Promise.race([ waitForMessage( "ALPH_TRANSACTION_SUBMITTED", USER_ACTION_TIMEOUT_LONGER, @@ -281,26 +256,11 @@ export const alephiumWindowObject: AlephiumWindowObject = }), ]) - if ("error" in result) { - throw Error(result.error) + if ("error" in txMessage) { + throw Error(txMessage.error) } - const signedChainedTxResult = result.result.map(txResult => { - const txResultType = txResult.type - switch (txResultType) { - case 'TRANSFER': - return { type: 'Transfer', ...txResult.result } as SignTransferChainedTxResult; - case 'DEPLOY_CONTRACT': - return { type: 'DeployContract', ...txResult.result } as SignDeployContractChainedTxResult; - case 'EXECUTE_SCRIPT': - return { type: 'ExecuteScript', ...txResult.result } as SignExecuteScriptChainedTxResult; - default: - throw new Error(`Unsupported transaction type: ${txResultType}`); - } - } - ) - - return signedChainedTxResult; + return txMessage.result.map(res => transactionResultToSignUnsignedTxResult(res)) } signUnsignedTx = async ( diff --git a/packages/extension/src/shared/transactions/index.ts b/packages/extension/src/shared/transactions/index.ts index f4507d0a..567dcb8c 100644 --- a/packages/extension/src/shared/transactions/index.ts +++ b/packages/extension/src/shared/transactions/index.ts @@ -1,10 +1,10 @@ -import { ALPH_TOKEN_ID, DEFAULT_GAS_PRICE, DUST_AMOUNT, ExplorerProvider, NodeProvider, ONE_ALPH, SignChainedTxParams, SignChainedTxResult, SignDeployContractChainedTxParams, SignExecuteScriptChainedTxParams, SignTransferChainedTxParams, TransactionBuilder } from "@alephium/web3" +import { ALPH_TOKEN_ID, DEFAULT_GAS_PRICE, DUST_AMOUNT, ExplorerProvider, MINIMAL_CONTRACT_DEPOSIT, NodeProvider, SignTransferChainedTxParams, TransactionBuilder } from "@alephium/web3" import { lowerCase, upperFirst } from "lodash-es" import { Call } from "starknet" import { ReviewTransactionResult, TransactionParams } from "../actionQueue/types" import { WalletAccount } from "../wallet.model" import { AlephiumExplorerTransaction } from "../explorer/type" -import { mapAlephiumTransactionToTransaction } from "./transformers" +import { mapAlephiumTransactionToTransaction, signedChainedTxResultToReviewTransactionResult, transactionParamsToSignChainedTxParams } from "./transformers" import { getNetwork } from "../network" import { BaseTokenWithBalance } from "../token/type" import { BigNumber } from "ethers" @@ -134,73 +134,6 @@ function buildGetTransactionsFn(metadataTransactions: Transaction[]) { } } -export function transactionParamsToSignChainedTxParams( - transactionParams: TransactionParams -): SignChainedTxParams { - switch (transactionParams.type) { - case "TRANSFER": - return { - type: 'Transfer', - ...transactionParams.params - } as SignTransferChainedTxParams - case "DEPLOY_CONTRACT": - return { - type: 'DeployContract', - ...transactionParams.params - } as SignDeployContractChainedTxParams - case "EXECUTE_SCRIPT": - return { - type: 'ExecuteScript', - ...transactionParams.params - } as SignExecuteScriptChainedTxParams - default: - throw new Error(`Unsupported transaction type: ${transactionParams.type}`); - } -} - -export function signedChainedTxParamsToTransactionParams( - signedChainedTxParams: SignChainedTxParams, - networkId: string -): TransactionParams { - const paramsType = signedChainedTxParams.type - const salt = Date.now().toString() - switch (paramsType) { - case 'Transfer': - return { - type: 'TRANSFER', - params: { networkId, ...signedChainedTxParams }, - salt - } - case 'DeployContract': - return { - type: 'DEPLOY_CONTRACT', - params: { networkId, ...signedChainedTxParams }, - salt - } - case 'ExecuteScript': - return { - type: 'EXECUTE_SCRIPT', - params: { networkId, ...signedChainedTxParams }, - salt - } - default: - throw new Error(`Unsupported transaction type: ${paramsType}`); - } -} - -export function signedChainedTxResultToReviewTransactionResult( - signedChainedTxParams: SignChainedTxParams, - signedChainedTxResult: Omit, - networkId: string -): ReviewTransactionResult { - const txParams = signedChainedTxParamsToTransactionParams(signedChainedTxParams, networkId) - return { - type: txParams.type, - params: txParams.params, - result: signedChainedTxResult - } as ReviewTransactionResult -} - export async function tryBuildChainedTransactions( nodeUrl: string, walletAccounts: WalletAccount[], @@ -374,7 +307,7 @@ export async function checkBalances( transactionParams: TransactionParams ): Promise | undefined> { const expectedBalances: Map = new Map() - const gasFee = BigInt(5000000) * DEFAULT_GAS_PRICE // Maximal gas fee per tx + switch (transactionParams.type) { case 'TRANSFER': transactionParams.params.destinations.forEach((destination) => { @@ -388,7 +321,7 @@ export async function checkBalances( addTokenToBalances(expectedBalances, ALPH_TOKEN_ID, transactionParams.params.initialAttoAlphAmount !== undefined ? BigNumber.from(transactionParams.params.initialAttoAlphAmount) - : BigNumber.from(ONE_ALPH) + : BigNumber.from(MINIMAL_CONTRACT_DEPOSIT) ) if (transactionParams.params.initialTokenAmounts !== undefined) { transactionParams.params.initialTokenAmounts.forEach((token) => addTokenToBalances(expectedBalances, token.id, BigNumber.from(token.amount))) @@ -401,10 +334,14 @@ export async function checkBalances( if (transactionParams.params.tokens !== undefined) { transactionParams.params.tokens.forEach((token) => addTokenToBalances(expectedBalances, token.id, BigNumber.from(token.amount))) } + break case 'UNSIGNED_TX': return } + + const maxGasAmountPerTx = 5000000 + const gasFee = BigInt(transactionParams.params.gasAmount ?? maxGasAmountPerTx) * BigInt(transactionParams.params.gasPrice ?? DEFAULT_GAS_PRICE) addTokenToBalances(expectedBalances, ALPH_TOKEN_ID, BigNumber.from(gasFee)) const zero = BigNumber.from(0) @@ -424,4 +361,3 @@ export async function checkBalances( } return undefined } - diff --git a/packages/extension/src/shared/transactions/transformers.ts b/packages/extension/src/shared/transactions/transformers.ts index 7c7f08b0..b1b34cf0 100644 --- a/packages/extension/src/shared/transactions/transformers.ts +++ b/packages/extension/src/shared/transactions/transformers.ts @@ -1,7 +1,7 @@ import { Transaction } from "../../shared/transactions" import { WalletAccount } from "../../shared/wallet.model" -import { explorer } from '@alephium/web3' -import { ReviewTransactionResult } from "../../shared/actionQueue/types"; +import { explorer, SignChainedTxParams, SignChainedTxResult, SignDeployContractChainedTxParams, SignDeployContractChainedTxResult, SignExecuteScriptChainedTxParams, SignExecuteScriptChainedTxResult, SignTransferChainedTxParams, SignTransferChainedTxResult, SignUnsignedTxResult } from '@alephium/web3' +import { ReviewTransactionResult, TransactionParams, TransactionResult } from "../../shared/actionQueue/types"; export const mapAlephiumTransactionToTransaction = ( transaction: explorer.Transaction, @@ -17,3 +17,86 @@ export const mapAlephiumTransactionToTransaction = ( status: "ACCEPTED_ON_CHAIN", timestamp: transaction.timestamp, }) + +export function signedChainedTxParamsToTransactionParams( + signedChainedTxParams: SignChainedTxParams, + networkId: string +): TransactionParams { + const paramsType = signedChainedTxParams.type + const salt = Date.now().toString() + switch (paramsType) { + case 'Transfer': + return { + type: 'TRANSFER', + params: { networkId, ...signedChainedTxParams }, + salt + } + case 'DeployContract': + return { + type: 'DEPLOY_CONTRACT', + params: { networkId, ...signedChainedTxParams }, + salt + } + case 'ExecuteScript': + return { + type: 'EXECUTE_SCRIPT', + params: { networkId, ...signedChainedTxParams }, + salt + } + default: + throw new Error(`Unsupported transaction type: ${paramsType}`); + } +} + +export function transactionParamsToSignChainedTxParams( + transactionParams: TransactionParams +): SignChainedTxParams { + switch (transactionParams.type) { + case "TRANSFER": + return { + type: 'Transfer', + ...transactionParams.params + } as SignTransferChainedTxParams + case "DEPLOY_CONTRACT": + return { + type: 'DeployContract', + ...transactionParams.params + } as SignDeployContractChainedTxParams + case "EXECUTE_SCRIPT": + return { + type: 'ExecuteScript', + ...transactionParams.params + } as SignExecuteScriptChainedTxParams + default: + throw new Error(`Unsupported transaction type: ${transactionParams.type}`); + } +} + +export function signedChainedTxResultToReviewTransactionResult( + signedChainedTxParams: SignChainedTxParams, + signedChainedTxResult: Omit, + networkId: string +): ReviewTransactionResult { + const txParams = signedChainedTxParamsToTransactionParams(signedChainedTxParams, networkId) + return { + type: txParams.type, + params: txParams.params, + result: signedChainedTxResult + } as ReviewTransactionResult +} + +export function transactionResultToSignUnsignedTxResult( + txResult: TransactionResult +): SignChainedTxResult { + const txResultType = txResult.type + switch (txResultType) { + case 'TRANSFER': + return { type: 'Transfer', ...txResult.result } as SignTransferChainedTxResult; + case 'DEPLOY_CONTRACT': + return { type: 'DeployContract', ...txResult.result } as SignDeployContractChainedTxResult; + case 'EXECUTE_SCRIPT': + return { type: 'ExecuteScript', ...txResult.result } as SignExecuteScriptChainedTxResult; + default: + throw new Error(`Unsupported transaction type: ${txResultType}`); + } +} diff --git a/packages/extension/src/ui/features/actions/ActionScreen.tsx b/packages/extension/src/ui/features/actions/ActionScreen.tsx index 11818198..41c4b6d8 100644 --- a/packages/extension/src/ui/features/actions/ActionScreen.tsx +++ b/packages/extension/src/ui/features/actions/ActionScreen.tsx @@ -31,11 +31,9 @@ export const ActionScreen: FC = () => { const [action] = actions const isLastAction = actions.length === 1 - const signerAccount = useAccount(action.type === 'ALPH_TRANSACTION' && selectedAccount - ? { address: action.payload[0].params.signerAddress, networkId: action.payload[0].params.networkId ?? selectedAccount.networkId } // FIXME: always use the first payload - : (action.type === 'ALPH_SIGN_MESSAGE' || action.type === 'ALPH_SIGN_UNSIGNED_TX') && selectedAccount - ? { address: action.payload.signerAddress, networkId: action.payload.networkId ?? selectedAccount.networkId } - : undefined + const signerAccount = useAccount((action.type === 'ALPH_SIGN_MESSAGE' || action.type === 'ALPH_SIGN_UNSIGNED_TX') && selectedAccount + ? { address: action.payload.signerAddress, networkId: action.payload.networkId ?? selectedAccount.networkId } + : undefined ) const closePopup = useCallback(() => { @@ -155,39 +153,38 @@ export const ActionScreen: FC = () => { isLoading: false, }) navigate(routes.error()) - } - - await approveAction(action, builtTransactions) - useAppState.setState({ isLoading: true }) - const result = await Promise.race([ - waitForMessage( - "ALPH_TRANSACTION_SUBMITTED", - ({ data }) => data.actionHash === action.meta.hash, - ), - waitForMessage( - "ALPH_TRANSACTION_FAILED", - ({ data }) => data.actionHash === action.meta.hash, - ), - ]) - // (await) blocking as the window may closes afterwards - await analytics.track("sentTransaction", { - success: !("error" in result), - networkId: selectedAccount?.networkId || t("unknown"), - }) - if ("error" in result) { - useAppState.setState({ - error: `${t('Sending transaction failed')}: ${result.error}`, - isLoading: false, - }) - navigate(routes.error()) } else { - closePopupIfLastAction() - useAppState.setState({ isLoading: false }) + await approveAction(action, builtTransactions) + useAppState.setState({ isLoading: true }) + const result = await Promise.race([ + waitForMessage( + "ALPH_TRANSACTION_SUBMITTED", + ({ data }) => data.actionHash === action.meta.hash, + ), + waitForMessage( + "ALPH_TRANSACTION_FAILED", + ({ data }) => data.actionHash === action.meta.hash, + ), + ]) + // (await) blocking as the window may closes afterwards + await analytics.track("sentTransaction", { + success: !("error" in result), + networkId: selectedAccount?.networkId || t("unknown"), + }) + if ("error" in result) { + useAppState.setState({ + error: `${t('Sending transaction failed')}: ${result.error}`, + isLoading: false, + }) + navigate(routes.error()) + } else { + closePopupIfLastAction() + useAppState.setState({ isLoading: false }) + } } } }} onReject={onReject} - selectedAccount={signerAccount} /> ) From e34c4c7947e626b95dd59b04e8942154e171bae7 Mon Sep 17 00:00:00 2001 From: h0ngcha0 Date: Wed, 23 Oct 2024 14:16:41 +0200 Subject: [PATCH 4/4] Lookup token symbol from token id when displaying out-of-balance error message --- packages/extension/src/shared/transactions/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/extension/src/shared/transactions/index.ts b/packages/extension/src/shared/transactions/index.ts index 567dcb8c..5d847c06 100644 --- a/packages/extension/src/shared/transactions/index.ts +++ b/packages/extension/src/shared/transactions/index.ts @@ -10,6 +10,7 @@ import { BaseTokenWithBalance } from "../token/type" import { BigNumber } from "ethers" import { addTokenToBalances, getBalances } from "../token/balance" import i18n from "../../i18n" +import { tokenListStore } from "../token/storage" export type Status = 'NOT_RECEIVED' | 'RECEIVED' | 'PENDING' | 'ACCEPTED_ON_MEMPOOL' | 'ACCEPTED_ON_L2' | 'ACCEPTED_ON_CHAIN' | 'REJECTED' | 'REMOVED_FROM_MEMPOOL'; @@ -225,7 +226,11 @@ export async function tryBuildTransactions( } const [firstMissingTokenId, firstMissingAmount] = missingBalances.entries().next().value; - const tokenSymbol = firstMissingTokenId === ALPH_TOKEN_ID ? 'ALPH' : firstMissingTokenId; + const tokenListTokens = await tokenListStore.get() + const tokenSymbol = firstMissingTokenId === ALPH_TOKEN_ID ? + 'ALPH' : tokenListTokens.tokens.find(token => + token.id === firstMissingTokenId && token.networkId === selectedAccount.networkId + )?.symbol ?? firstMissingTokenId const expectedStr = firstMissingAmount.toString(); const haveStr = (tokensWithBalance.find(t => t.id === firstMissingTokenId)?.balance || '0').toString(); const errorMsg = i18n.t("Insufficient token {{ tokenSymbol }}, expected at least {{ expectedStr }}, got {{ haveStr }}", { tokenSymbol, expectedStr, haveStr })