From 16d8a557203ac41ff892f61caccf6dc5b065b323 Mon Sep 17 00:00:00 2001 From: yjin Date: Tue, 25 Nov 2025 17:03:20 +0900 Subject: [PATCH 01/19] feat: [ADN-733] SignDocument Test --- .../validation/validation-transaction.ts | 6 +- packages/adena-extension/src/inject.ts | 18 + .../src/inject/executor/executor.ts | 47 ++ .../src/inject/message/message-handler.ts | 8 + .../src/inject/message/message.ts | 14 +- .../src/inject/message/methods/transaction.ts | 50 ++- .../src/inject/types/transactions.ts | 5 +- .../wallet/approve-sign-document/index.tsx | 413 ++++++++++++++++++ .../src/router/popup/index.tsx | 2 + .../src/services/transaction/transaction.ts | 1 + packages/adena-extension/src/types/router.ts | 3 + 11 files changed, 562 insertions(+), 5 deletions(-) create mode 100644 packages/adena-extension/src/pages/popup/wallet/approve-sign-document/index.tsx diff --git a/packages/adena-extension/src/common/validation/validation-transaction.ts b/packages/adena-extension/src/common/validation/validation-transaction.ts index 382e116bf..279703103 100644 --- a/packages/adena-extension/src/common/validation/validation-transaction.ts +++ b/packages/adena-extension/src/common/validation/validation-transaction.ts @@ -37,14 +37,16 @@ export const validateInjectionDataWithAddress = ( export const validateInjectionTransactionType = (requestData: InjectionMessage): any => { const messageTypes = ['/bank.MsgSend', '/vm.m_call', '/vm.m_addpkg', '/vm.m_run']; - return requestData.data?.messages.every((message: any) => messageTypes.includes(message?.type)); + + const msgs = requestData.data?.messages || requestData.data?.msgs || []; + return msgs.every((message: any) => messageTypes.includes(message?.type)); }; export const validateInjectionTransactionMessageWithAddress = ( requestData: InjectionMessage, address: string, ): boolean => { - const messages = requestData.data?.messages; + const messages = requestData.data?.messages || requestData.data?.msgs || []; for (const message of messages) { let messageAddress = ''; switch (message?.type) { diff --git a/packages/adena-extension/src/inject.ts b/packages/adena-extension/src/inject.ts index 323bb56f8..d3c3dc71b 100644 --- a/packages/adena-extension/src/inject.ts +++ b/packages/adena-extension/src/inject.ts @@ -11,6 +11,7 @@ import { DoContractResponse, GetAccountResponse, GetNetworkResponse, + SignedDocument, SignTxResponse, SwitchNetworkResponse, TransactionParams, @@ -53,10 +54,27 @@ const init = (): void => { return response; }, async SignTx(message: TransactionParams): Promise { + console.log('시작 1! SignTx'); const executor = new AdenaExecutor(); const response = await executor.signTx(message); return response; }, + async SignDocument2(signedDocument: SignedDocument) { + console.log('시작 1! SignDocument2'); + const executor = new AdenaExecutor(); + console.log('1-1'); + const response = await executor.signDocument2(signedDocument); + console.log('1-2'); + return response; + }, + async SignDocument(message: TransactionParams) { + console.log('시작 1! SignDocument'); + const executor = new AdenaExecutor(); + console.log('1-1'); + const response = await executor.signDocument(message); + console.log('1-2'); + return response; + }, async AddNetwork(chain: AddNetworkParams): Promise { const executor = new AdenaExecutor(); const response = await executor.addNetwork(chain); diff --git a/packages/adena-extension/src/inject/executor/executor.ts b/packages/adena-extension/src/inject/executor/executor.ts index 36fbe8532..1441f389c 100644 --- a/packages/adena-extension/src/inject/executor/executor.ts +++ b/packages/adena-extension/src/inject/executor/executor.ts @@ -21,6 +21,7 @@ import { DoContractResponse, GetAccountResponse, GetNetworkResponse, + SignedDocument, SignTxResponse, SwitchNetworkResponse, TransactionParams, @@ -94,7 +95,9 @@ export class AdenaExecutor { }; public signAmino = (params: TransactionParams): Promise> => { + console.log('시작 2! signAmino 실행됨!@!@!'); const result = this.validateContractMessage(params); + console.log(result, '시작 2-2'); if (result) { return this.sendEventMessage(result); } @@ -106,6 +109,7 @@ export class AdenaExecutor { }; public signTx = (params: TransactionParams): Promise => { + console.log(params, '시작 2! signTx 실행됨!'); const result = this.validateContractMessage(params); if (result) { return this.sendEventMessage(result); @@ -117,6 +121,44 @@ export class AdenaExecutor { return this.sendEventMessage(eventMessage); }; + public signDocument = (params: TransactionParams) => { + console.log(params, '시작 2! signDocument 실행됨!@!@!'); + const result = this.validateContractMessage(params); + console.log(params, result, 'target SignDocument'); + if (result) { + return this.sendEventMessage(result); + } + const eventMessage = AdenaExecutor.createEventMessage( + 'SIGN_DOCUMENT' as WalletResponseType, + params, + ); + console.log(eventMessage, '2-2'); + return this.sendEventMessage(eventMessage); + }; + + public signDocument2 = (signedDocument: SignedDocument) => { + console.log(signedDocument, '시작 2! signDocument2 실행됨!@!@!'); + + const validationParams: TransactionParams = { + messages: signedDocument.msgs, + // memo: signedDocument.memo, + }; + + const result = this.validateContractMessage(validationParams); + console.log(validationParams, result, 'target SignDocument2'); + if (result) { + console.log('his'); + return this.sendEventMessage(result); + } + + const eventMessage = AdenaExecutor.createEventMessage( + 'SIGN_DOCUMENT' as WalletResponseType, + signedDocument, + ); + + return this.sendEventMessage(eventMessage); + }; + public addNetwork = (chain: AddNetworkParams): Promise => { const eventMessage = AdenaExecutor.createEventMessage(WalletResponseExecuteType.ADD_NETWORK, { ...chain, @@ -167,6 +209,7 @@ export class AdenaExecutor { private sendEventMessage = ( eventMessage: InjectionMessage, ): Promise> => { + console.log(eventMessage, '시작 5!'); this.listen(); this.eventMessage = { ...eventMessage, @@ -191,6 +234,7 @@ export class AdenaExecutor { }; private listen = (): void => { + console.log('listen'); if (this.isListen) { return; } @@ -208,16 +252,19 @@ export class AdenaExecutor { params?: Params, withNotification?: boolean, ): InjectionMessage => { + console.log('시작 3!', type, params); return InjectionMessageInstance.request(type, params, undefined, withNotification); }; private messageHandler = (event: MessageEvent): void => { + console.log(event, 'messageHandler event'); if (event.origin !== window.location.origin) { console.warn(`Untrusted origin: ${event.origin}`); return; } const eventData = event.data; + console.log(eventData, 'eventData'); if (eventData.status) { const { key, status, data, code, message, type } = eventData; if (key === this.eventKey) { diff --git a/packages/adena-extension/src/inject/message/message-handler.ts b/packages/adena-extension/src/inject/message/message-handler.ts index 54c5072c0..4f943dc34 100644 --- a/packages/adena-extension/src/inject/message/message-handler.ts +++ b/packages/adena-extension/src/inject/message/message-handler.ts @@ -138,6 +138,7 @@ export class MessageHandler { }); break; case 'SIGN_AMINO': + console.log('시작 6! sign amino'); HandlerMethod.checkEstablished(core, message, sendResponse).then((isEstablished) => { if (isEstablished) { HandlerMethod.signAmino(message, sendResponse); @@ -151,6 +152,13 @@ export class MessageHandler { } }); break; + case 'SIGN_DOCUMENT': + console.log('시작 6! sign document'); + HandlerMethod.checkEstablished(core, message, sendResponse).then((isEstablished) => { + if (isEstablished) { + HandlerMethod.signDocument2(message, sendResponse); + } + }); default: break; } diff --git a/packages/adena-extension/src/inject/message/message.ts b/packages/adena-extension/src/inject/message/message.ts index 55dce4218..20fe26267 100644 --- a/packages/adena-extension/src/inject/message/message.ts +++ b/packages/adena-extension/src/inject/message/message.ts @@ -1,4 +1,5 @@ import { WalletMessageInfo, WalletResponseType } from '@adena-wallet/sdk'; +import { TxSignature } from '@gnolang/tm2-js-client'; export type StatusType = 'request' | 'response' | 'common' | 'success' | 'failure'; @@ -14,6 +15,10 @@ export interface InjectionMessage { data: { [key in string]: any } | undefined; } +export interface InjectionMessageWithSignature extends InjectionMessage { + signature?: TxSignature[]; +} + export class InjectionMessageInstance { private key: string; @@ -36,9 +41,15 @@ export class InjectionMessageInstance { key?: string, withNotification?: boolean, ) { + console.log( + WalletMessageInfo[messageKey], + WalletMessageInfo, + messageKey, + 'WalletMessageInfo[messageKey]', + ); const { code, message, type } = WalletMessageInfo[messageKey]; this.key = key ?? ''; - this.code = code; + this.code = code || 0; this.type = type; this.status = status ?? 'common'; this.description = message; @@ -92,6 +103,7 @@ export class InjectionMessageInstance { key?: string, withNotification?: boolean, ): InjectionMessage => { + console.log('시작 4!', messageKey, data, key); return new InjectionMessageInstance(messageKey, 'request', data, key, withNotification).dataObj; }; diff --git a/packages/adena-extension/src/inject/message/methods/transaction.ts b/packages/adena-extension/src/inject/message/methods/transaction.ts index c286b3b45..d4f224639 100644 --- a/packages/adena-extension/src/inject/message/methods/transaction.ts +++ b/packages/adena-extension/src/inject/message/methods/transaction.ts @@ -2,12 +2,17 @@ import { WalletResponseRejectType } from '@adena-wallet/sdk'; import { validateInjectionData } from '@common/validation/validation-transaction'; import { RoutePath } from '@types'; import { HandlerMethod } from '..'; -import { InjectionMessage, InjectionMessageInstance } from '../message'; +import { + InjectionMessage, + InjectionMessageInstance, + InjectionMessageWithSignature, +} from '../message'; export const signAmino = async ( requestData: InjectionMessage, sendResponse: (message: any) => void, ): Promise => { + console.log('시작 5! signAmino 실행'); const validationMessage = validateInjectionData(requestData); if (validationMessage) { sendResponse(validationMessage); @@ -26,6 +31,7 @@ export const signTransaction = async ( requestData: InjectionMessage, sendResponse: (message: any) => void, ): Promise => { + console.log(requestData, 'signTransaction 실행!@@@'); const validationMessage = validateInjectionData(requestData); if (validationMessage) { sendResponse(validationMessage); @@ -40,6 +46,48 @@ export const signTransaction = async ( ); }; +export const signDocument = async ( + requestData: InjectionMessageWithSignature, + sendResponse: (message: any) => void, +): Promise => { + console.log('signDocument 실행@@@@@@@@@@@@@@@@@@@@@@@@@@'); + const validationMessage = validateInjectionData(requestData); + console.log(validationMessage, 'validationMessage!!!!!!!!!!'); + if (validationMessage) { + sendResponse(validationMessage); + return; + } + + console.log('팝업생성'); + HandlerMethod.createPopup( + RoutePath.ApproveSignDocument, + requestData, + InjectionMessageInstance.failure(WalletResponseRejectType.SIGN_REJECTED, {}, requestData.key), + sendResponse, + ); +}; + +export const signDocument2 = async ( + requestData: InjectionMessageWithSignature, + sendResponse: (message: any) => void, +): Promise => { + console.log(requestData, 'signDocument2 실행@@@@@@@@@@@@@@@@@@@@@@@@@@'); + const validationMessage = validateInjectionData(requestData); + console.log(validationMessage, 'validationMessage!!!!!!!!!!'); + if (validationMessage) { + sendResponse(validationMessage); + return; + } + + console.log('팝업생성'); + HandlerMethod.createPopup( + RoutePath.ApproveSignDocument, + requestData, + InjectionMessageInstance.failure(WalletResponseRejectType.SIGN_REJECTED, {}, requestData.key), + sendResponse, + ); +}; + export const doContract = async ( requestData: InjectionMessage, sendResponse: (message: any) => void, diff --git a/packages/adena-extension/src/inject/types/transactions.ts b/packages/adena-extension/src/inject/types/transactions.ts index 0cd5f18e1..a024bdf30 100644 --- a/packages/adena-extension/src/inject/types/transactions.ts +++ b/packages/adena-extension/src/inject/types/transactions.ts @@ -1,7 +1,8 @@ import { MsgAddPackage, MsgCall, MsgSend } from '@gnolang/gno-js-client'; import { MsgRun } from '@gnolang/gno-js-client/bin/proto/gno/vm'; -import { BroadcastTxCommitResult } from '@gnolang/tm2-js-client'; +import { BroadcastTxCommitResult, TxSignPayload } from '@gnolang/tm2-js-client'; import { GnoArgumentInfo } from '@inject/message/methods/gno-connect'; +import { Signature } from '@adena-wallet/sdk'; import { AdenaResponse } from '.'; @@ -38,6 +39,8 @@ export type TransactionParams = { arguments?: GnoArgumentInfo[] | null; }; +export interface SignedDocument extends TxSignPayload, Signature {} + // TODO: BroadcastTxCommitResult isn't correct in case of a VM call export type DoContractResponse = AdenaResponse; diff --git a/packages/adena-extension/src/pages/popup/wallet/approve-sign-document/index.tsx b/packages/adena-extension/src/pages/popup/wallet/approve-sign-document/index.tsx new file mode 100644 index 000000000..101150ee1 --- /dev/null +++ b/packages/adena-extension/src/pages/popup/wallet/approve-sign-document/index.tsx @@ -0,0 +1,413 @@ +import { Account, Document, isAirgapAccount, isLedgerAccount } from 'adena-module'; +import BigNumber from 'bignumber.js'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import { + WalletResponseFailureType, + WalletResponseRejectType, + WalletResponseSuccessType, +} from '@adena-wallet/sdk'; +import { GasToken } from '@common/constants/token.constant'; +import { mappedTransactionMessages } from '@common/mapper/transaction-mapper'; +import { parseTokenAmount } from '@common/utils/amount-utils'; +import { + createFaviconByHostname, + decodeParameter, + parseParameters, +} from '@common/utils/client-utils'; +import { validateInjectionDataWithAddress } from '@common/validation/validation-transaction'; +import { ApproveTransaction } from '@components/molecules'; +import useAppNavigate from '@hooks/use-app-navigate'; +import { useAdenaContext, useWalletContext } from '@hooks/use-context'; +import { useCurrentAccount } from '@hooks/use-current-account'; +import useLink from '@hooks/use-link'; +import { useNetwork } from '@hooks/use-network'; +import { useGetGnotBalance } from '@hooks/wallet/use-get-gnot-balance'; +import { useNetworkFee } from '@hooks/wallet/use-network-fee'; +import { InjectionMessage, InjectionMessageInstance } from '@inject/message'; +import { GnoArgumentInfo } from '@inject/message/methods/gno-connect'; +import { ContractMessage } from '@inject/types'; +import { RoutePath } from '@types'; + +interface TransactionData { + messages: readonly any[]; + contracts: { type: string; function: string; value: any }[]; + gasWanted: string; + gasFee: string; + memo: string; + document: Document; +} + +function mappedTransactionData(document: Document): TransactionData { + return { + messages: document.msgs, + contracts: document.msgs.map((message) => { + return { + type: message?.type || '', + function: message?.type === '/bank.MsgSend' ? 'Transfer' : message?.value?.func || '', + value: message?.value || '', + }; + }), + gasWanted: document.fee.gas, + gasFee: `${document.fee.amount[0].amount}${document.fee.amount[0].denom}`, + memo: `${document.memo || ''}`, + document, + }; +} + +const ApproveSignDocumentContainer: React.FC = () => { + const normalNavigate = useNavigate(); + const { navigate } = useAppNavigate(); + const { gnoProvider } = useWalletContext(); + const { walletService, transactionService } = useAdenaContext(); + const { currentAccount } = useCurrentAccount(); + const [transactionData, setTransactionData] = useState(); + const { currentNetwork } = useNetwork(); + const [hostname, setHostname] = useState(''); + const location = useLocation(); + const [requestData, setRequestData] = useState(); + const [favicon, setFavicon] = useState(null); + const [visibleTransactionInfo, setVisibleTransactionInfo] = useState(false); + const [document, setDocument] = useState(); + const [processType, setProcessType] = useState<'INIT' | 'PROCESSING' | 'DONE'>('INIT'); + const [response, setResponse] = useState(null); + const [memo, setMemo] = useState(''); + const { openScannerLink } = useLink(); + const [transactionMessages, setTransactionMessages] = useState([]); + + const { data: currentBalance = null } = useGetGnotBalance(); + + const useNetworkFeeReturn = useNetworkFee(document, true); + const networkFee = useNetworkFeeReturn.networkFee; + + const processing = useMemo(() => processType !== 'INIT', [processType]); + + const done = useMemo(() => processType === 'DONE', [processType]); + + const hasMemo = useMemo(() => { + if (!requestData?.data?.memo) { + return false; + } + return true; + }, [requestData?.data?.memo]); + + const displayNetworkFee = useMemo(() => { + if (!networkFee) { + return { + amount: '', + denom: '', + }; + } + + return { + amount: networkFee.amount, + denom: GasToken.symbol, + }; + }, [networkFee]); + + const consumedTokenAmount = useMemo(() => { + const accumulatedAmount = document?.msgs.reduce((acc, msg) => { + const messageValue = msg.value; + const amountStr = messageValue?.amount || messageValue?.amount || messageValue?.max_deposit; + if (!amountStr) { + return acc; + } + + try { + const amount = parseTokenAmount(amountStr); + return BigNumber(acc).plus(amount).toNumber(); + } catch { + return acc; + } + }, 0); + + const consumedBN = BigNumber(accumulatedAmount || 0).shiftedBy(GasToken.decimals * -1); + return consumedBN.toNumber(); + }, [document]); + + const isErrorNetworkFee = useMemo(() => { + if (!networkFee) { + return false; + } + + const resultConsumedAmount = BigNumber(consumedTokenAmount).plus(networkFee.amount); + + return BigNumber(currentBalance || 0) + .shiftedBy(GasToken.decimals * -1) + .isLessThan(resultConsumedAmount); + }, [networkFee?.amount, currentBalance, consumedTokenAmount]); + + const argumentInfos: GnoArgumentInfo[] = useMemo(() => { + return requestData?.data?.arguments || []; + }, [requestData?.data?.arguments]); + + useEffect(() => { + checkLockWallet(); + }, [walletService]); + + const checkLockWallet = (): void => { + walletService + .isLocked() + .then((locked) => locked && normalNavigate(RoutePath.ApproveLogin + location.search)); + }; + + useEffect(() => { + if (location.search) { + initRequestData(); + } + }, [location]); + + const initRequestData = (): void => { + const data = parseParameters(location.search); + const parsedData = decodeParameter(data['data']); + setRequestData({ ...parsedData, hostname: data['hostname'] }); + }; + + useEffect(() => { + console.log('target'); + if (currentAccount && requestData && gnoProvider) { + console.log('target1'); + if (isAirgapAccount(currentAccount)) { + console.log('target1-1'); + navigate(RoutePath.ApproveSignFailed); + return; + } + console.log(currentAccount, requestData, 'target2'); + validate(currentAccount, requestData).then((validated) => { + if (validated) { + console.log('target2-2'); + initFavicon(); + initTransactionData(); + } + }); + } + }, [currentAccount, requestData, gnoProvider]); + + const validate = async ( + currentAccount: Account, + requestData: InjectionMessage, + ): Promise => { + console.log(requestData, 'validate'); + const validationMessage = validateInjectionDataWithAddress( + requestData, + await currentAccount.getAddress('g'), + ); + if (validationMessage) { + chrome.runtime.sendMessage(validationMessage); + return false; + } + return true; + }; + + const initFavicon = async (): Promise => { + const faviconData = await createFaviconByHostname( + requestData?.hostname ? `${requestData?.protocol}//${requestData?.hostname}` : '', + ); + setFavicon(faviconData); + }; + const initTransactionData = async (): Promise => { + if (!currentAccount || !requestData || !currentNetwork) { + return false; + } + try { + const document = await transactionService.createDocument( + currentAccount, + currentNetwork.networkId, + requestData?.data?.messages || requestData?.data?.msgs, + requestData?.data?.gasWanted, + requestData?.data?.gasFee, + requestData?.data?.memo, + ); + setDocument(document); + setTransactionData(mappedTransactionData(document)); + setHostname(requestData?.hostname ?? ''); + setMemo(document.memo); + setTransactionMessages(mappedTransactionMessages(document.msgs)); + return true; + } catch (e) { + console.error(e); + const error: any = e; + if (error?.message === 'Transaction signing request was rejected by the user') { + chrome.runtime.sendMessage( + InjectionMessageInstance.failure( + WalletResponseRejectType.SIGN_REJECTED, + requestData?.data, + requestData?.key, + ), + ); + } + } + return false; + }; + + const changeMemo = (memo: string): void => { + setMemo(memo); + }; + + const updateTransactionData = (): void => { + if (!document) { + return; + } + + const currentMemo = memo; + const currentGasPrice = useNetworkFeeReturn.currentGasFeeRawAmount; + const currentGasWanted = useNetworkFeeReturn.currentGasInfo?.gasWanted || 0; + + const updatedDocument: Document = { + ...document, + memo: currentMemo, + fee: { + amount: [ + { + amount: currentGasPrice.toString(), + denom: GasToken.denom, + }, + ], + gas: currentGasWanted.toString(), + }, + }; + + setDocument(updatedDocument); + setTransactionData(mappedTransactionData(updatedDocument)); + }; + + const createSignDocument = async (): Promise => { + if (!document || !currentAccount) { + setResponse( + InjectionMessageInstance.failure( + WalletResponseFailureType.UNEXPECTED_ERROR, + {}, + requestData?.key, + ), + ); + return false; + } + + try { + const signature = await transactionService.createSignature(currentAccount, document); + setProcessType('PROCESSING'); + setResponse( + InjectionMessageInstance.success( + WalletResponseSuccessType.SIGN_SUCCESS, + { document, signature }, + requestData?.key, + ), + ); + } catch (e) { + if (e instanceof Error) { + const message = e.message; + if (message.includes('Ledger')) { + return false; + } + setResponse( + InjectionMessageInstance.failure( + WalletResponseFailureType.SIGN_FAILED, + { error: { message } }, + requestData?.key, + ), + ); + } + setResponse( + InjectionMessageInstance.failure( + WalletResponseFailureType.SIGN_FAILED, + {}, + requestData?.key, + ), + ); + } + return false; + }; + + const onToggleTransactionData = (visibleTransactionInfo: boolean): void => { + setVisibleTransactionInfo(visibleTransactionInfo); + }; + + const onClickConfirm = (): void => { + if (!currentAccount) { + return; + } + if (isLedgerAccount(currentAccount)) { + navigate(RoutePath.ApproveSignLoading, { + state: { + document, + requestData, + }, + }); + return; + } + + createSignDocument().finally(() => setProcessType('DONE')); + }; + + const onClickCancel = (): void => { + chrome.runtime.sendMessage( + InjectionMessageInstance.failure( + WalletResponseRejectType.SIGN_REJECTED, + {}, + requestData?.key, + ), + ); + }; + + const onResponseSignTransaction = useCallback(() => { + if (response) { + chrome.runtime.sendMessage(response); + } + }, [response]); + + const onTimeoutSignTransaction = useCallback(() => { + chrome.runtime.sendMessage( + InjectionMessageInstance.failure( + WalletResponseFailureType.NETWORK_TIMEOUT, + {}, + requestData?.key, + ), + ); + }, [requestData]); + + useEffect(() => { + if (transactionMessages.length === 0) { + return; + } + + updateTransactionData(); + }, [ + memo, + transactionMessages, + useNetworkFeeReturn.currentGasInfo?.gasWanted, + useNetworkFeeReturn.currentGasFeeRawAmount, + ]); + + return ( + + ); +}; + +export default ApproveSignDocumentContainer; diff --git a/packages/adena-extension/src/router/popup/index.tsx b/packages/adena-extension/src/router/popup/index.tsx index b9d56fa2d..a4f8c1803 100644 --- a/packages/adena-extension/src/router/popup/index.tsx +++ b/packages/adena-extension/src/router/popup/index.tsx @@ -27,6 +27,7 @@ import ApproveSign from '@pages/popup/wallet/approve-sign'; import ApproveSignLedgerLoading from '@pages/popup/wallet/approve-sign-ledger-loading'; import ApproveSignTransaction from '@pages/popup/wallet/approve-sign-transaction'; import ApproveSignTransactionLedgerLoading from '@pages/popup/wallet/approve-sign-transaction-ledger-loading'; +import ApproveSignDocument from '@pages/popup/wallet/approve-sign-document'; import ApproveTransactionLedgerLoading from '@pages/popup/wallet/approve-transaction-ledger-loading'; import ApproveTransactionMain from '@pages/popup/wallet/approve-transaction-main'; import BroadcastTransactionScreen from '@pages/popup/wallet/broadcast-transaction-screen'; @@ -125,6 +126,7 @@ export const PopupRouter = (): JSX.Element => { element={} /> } /> + } /> } /> } /> } /> diff --git a/packages/adena-extension/src/services/transaction/transaction.ts b/packages/adena-extension/src/services/transaction/transaction.ts index ced80872e..db9aff0dd 100644 --- a/packages/adena-extension/src/services/transaction/transaction.ts +++ b/packages/adena-extension/src/services/transaction/transaction.ts @@ -70,6 +70,7 @@ export class TransactionService { gasFee?: number, memo?: string | undefined, ): Promise => { + console.log(messages, 'messages!'); const provider = this.getGnoProvider(); const address = await account.getAddress(defaultAddressPrefix); const accountInfo = await provider.getAccountInfo(address).catch(() => null); diff --git a/packages/adena-extension/src/types/router.ts b/packages/adena-extension/src/types/router.ts index b8e887e50..42b0f7170 100644 --- a/packages/adena-extension/src/types/router.ts +++ b/packages/adena-extension/src/types/router.ts @@ -45,6 +45,9 @@ export enum RoutePath { ApproveSignLoading = '/approve/wallet/sign/loading', ApproveSignTransaction = '/approve/wallet/sign-tx', ApproveSignTransactionLoading = '/approve/wallet/sign-tx/loading', + ApproveSignDocument = 'approve/wallet/sign-document', + ApproveSignDocumentLoading = 'approve/wallet/sign-document/loading', + ApproveSignDocumentFailed = 'approve/wallet/sign-document/failed', ApproveEstablish = '/approve/wallet/establish', ApproveChangingNetwork = '/approve/wallet/network/change', ApproveAddingNetwork = '/approve/wallet/network/add', From a489544b3b9ff2b6ce551662889ebe4fab75b8fc Mon Sep 17 00:00:00 2001 From: yjin Date: Tue, 25 Nov 2025 17:35:11 +0900 Subject: [PATCH 02/19] refactor: [ADN-733] remove legacy --- packages/adena-extension/src/inject.ts | 12 ++-------- .../src/inject/executor/executor.ts | 22 +++--------------- .../src/inject/message/message-handler.ts | 2 +- .../src/inject/message/methods/transaction.ts | 23 +------------------ 4 files changed, 7 insertions(+), 52 deletions(-) diff --git a/packages/adena-extension/src/inject.ts b/packages/adena-extension/src/inject.ts index d3c3dc71b..ddbec719c 100644 --- a/packages/adena-extension/src/inject.ts +++ b/packages/adena-extension/src/inject.ts @@ -59,19 +59,11 @@ const init = (): void => { const response = await executor.signTx(message); return response; }, - async SignDocument2(signedDocument: SignedDocument) { - console.log('시작 1! SignDocument2'); - const executor = new AdenaExecutor(); - console.log('1-1'); - const response = await executor.signDocument2(signedDocument); - console.log('1-2'); - return response; - }, - async SignDocument(message: TransactionParams) { + async SignDocument(signedDocument: SignedDocument) { console.log('시작 1! SignDocument'); const executor = new AdenaExecutor(); console.log('1-1'); - const response = await executor.signDocument(message); + const response = await executor.signDocument(signedDocument); console.log('1-2'); return response; }, diff --git a/packages/adena-extension/src/inject/executor/executor.ts b/packages/adena-extension/src/inject/executor/executor.ts index 1441f389c..b10356b6b 100644 --- a/packages/adena-extension/src/inject/executor/executor.ts +++ b/packages/adena-extension/src/inject/executor/executor.ts @@ -121,31 +121,15 @@ export class AdenaExecutor { return this.sendEventMessage(eventMessage); }; - public signDocument = (params: TransactionParams) => { - console.log(params, '시작 2! signDocument 실행됨!@!@!'); - const result = this.validateContractMessage(params); - console.log(params, result, 'target SignDocument'); - if (result) { - return this.sendEventMessage(result); - } - const eventMessage = AdenaExecutor.createEventMessage( - 'SIGN_DOCUMENT' as WalletResponseType, - params, - ); - console.log(eventMessage, '2-2'); - return this.sendEventMessage(eventMessage); - }; - - public signDocument2 = (signedDocument: SignedDocument) => { - console.log(signedDocument, '시작 2! signDocument2 실행됨!@!@!'); + public signDocument = (signedDocument: SignedDocument) => { + console.log(signedDocument, '시작 2! signDocument 실행됨!@!@!'); const validationParams: TransactionParams = { messages: signedDocument.msgs, - // memo: signedDocument.memo, }; const result = this.validateContractMessage(validationParams); - console.log(validationParams, result, 'target SignDocument2'); + console.log(validationParams, result, 'target SignDocument'); if (result) { console.log('his'); return this.sendEventMessage(result); diff --git a/packages/adena-extension/src/inject/message/message-handler.ts b/packages/adena-extension/src/inject/message/message-handler.ts index 4f943dc34..2c2aa2e49 100644 --- a/packages/adena-extension/src/inject/message/message-handler.ts +++ b/packages/adena-extension/src/inject/message/message-handler.ts @@ -156,7 +156,7 @@ export class MessageHandler { console.log('시작 6! sign document'); HandlerMethod.checkEstablished(core, message, sendResponse).then((isEstablished) => { if (isEstablished) { - HandlerMethod.signDocument2(message, sendResponse); + HandlerMethod.signDocument(message, sendResponse); } }); default: diff --git a/packages/adena-extension/src/inject/message/methods/transaction.ts b/packages/adena-extension/src/inject/message/methods/transaction.ts index d4f224639..7c65c5826 100644 --- a/packages/adena-extension/src/inject/message/methods/transaction.ts +++ b/packages/adena-extension/src/inject/message/methods/transaction.ts @@ -50,28 +50,7 @@ export const signDocument = async ( requestData: InjectionMessageWithSignature, sendResponse: (message: any) => void, ): Promise => { - console.log('signDocument 실행@@@@@@@@@@@@@@@@@@@@@@@@@@'); - const validationMessage = validateInjectionData(requestData); - console.log(validationMessage, 'validationMessage!!!!!!!!!!'); - if (validationMessage) { - sendResponse(validationMessage); - return; - } - - console.log('팝업생성'); - HandlerMethod.createPopup( - RoutePath.ApproveSignDocument, - requestData, - InjectionMessageInstance.failure(WalletResponseRejectType.SIGN_REJECTED, {}, requestData.key), - sendResponse, - ); -}; - -export const signDocument2 = async ( - requestData: InjectionMessageWithSignature, - sendResponse: (message: any) => void, -): Promise => { - console.log(requestData, 'signDocument2 실행@@@@@@@@@@@@@@@@@@@@@@@@@@'); + console.log(requestData, 'signDocument 실행@@@@@@@@@@@@@@@@@@@@@@@@@@'); const validationMessage = validateInjectionData(requestData); console.log(validationMessage, 'validationMessage!!!!!!!!!!'); if (validationMessage) { From dfbb831856ee9197150a256bfed66dd1c2650994 Mon Sep 17 00:00:00 2001 From: yjin Date: Tue, 25 Nov 2025 18:26:03 +0900 Subject: [PATCH 03/19] feat: [ADN-733] validate SignedDocument type --- .../src/inject/executor/executor.ts | 77 ++++++++++++++++--- .../src/inject/types/transactions.ts | 18 ++++- 2 files changed, 81 insertions(+), 14 deletions(-) diff --git a/packages/adena-extension/src/inject/executor/executor.ts b/packages/adena-extension/src/inject/executor/executor.ts index b10356b6b..b38400c4f 100644 --- a/packages/adena-extension/src/inject/executor/executor.ts +++ b/packages/adena-extension/src/inject/executor/executor.ts @@ -124,14 +124,9 @@ export class AdenaExecutor { public signDocument = (signedDocument: SignedDocument) => { console.log(signedDocument, '시작 2! signDocument 실행됨!@!@!'); - const validationParams: TransactionParams = { - messages: signedDocument.msgs, - }; - - const result = this.validateContractMessage(validationParams); - console.log(validationParams, result, 'target SignDocument'); + const result = this.validateSignedDocument(signedDocument); + console.log(result, 'target SignDocument'); if (result) { - console.log('his'); return this.sendEventMessage(result); } @@ -158,11 +153,15 @@ export class AdenaExecutor { return this.sendEventMessage(eventMessage); }; - private validateContractMessage = (params: TransactionParams): InjectionMessage | undefined => { - if (!validateDoContractRequest(params)) { - return InjectionMessageInstance.failure(WalletResponseFailureType.INVALID_FORMAT); - } - for (const message of params.messages) { + /** + * Validates an array of transaction messages. + * Calls appropriate validation functions based on each message type to verify the format. + * + * @param messages - Array of transaction messages to validate + * @returns InjectionMessage on validation failure, undefined on success + */ + private validateMessages = (messages: any[]): InjectionMessage | undefined => { + for (const message of messages) { switch (message.type) { case '/bank.MsgSend': if (!validateTransactionMessageOfBankSend(message)) { @@ -188,6 +187,60 @@ export class AdenaExecutor { return InjectionMessageInstance.failure(WalletResponseFailureType.UNSUPPORTED_TYPE); } } + return undefined; + }; + + /** + * Validates a signed document (SignedDocument). + * Verifies the existence and format of required fields (chain_id, account_number, sequence, memo), + * fee structure (gas and amount array), signatures array, and msgs array, then validates the included messages as well. + * + * @param signedDocument - The signed document object to validate + * @returns InjectionMessage on validation failure, undefined on success + */ + private validateSignedDocument = ( + signedDocument: SignedDocument, + ): InjectionMessage | undefined => { + if (!signedDocument) { + return InjectionMessageInstance.failure(WalletResponseFailureType.INVALID_FORMAT); + } + + if ( + typeof signedDocument.chain_id !== 'string' || + typeof signedDocument.account_number !== 'string' || + typeof signedDocument.sequence !== 'string' || + typeof signedDocument.memo !== 'string' + ) { + return InjectionMessageInstance.failure(WalletResponseFailureType.INVALID_FORMAT); + } + + if (!Array.isArray(signedDocument.signatures)) { + return InjectionMessageInstance.failure(WalletResponseFailureType.INVALID_FORMAT); + } + + if ( + !signedDocument.fee || + typeof signedDocument.fee.gas !== 'string' || + !Array.isArray(signedDocument.fee.amount) || + signedDocument.fee.gas.length === 0 || + signedDocument.fee.amount.length === 0 + ) { + return InjectionMessageInstance.failure(WalletResponseFailureType.INVALID_FORMAT); + } + + if (!Array.isArray(signedDocument.msgs) || signedDocument.msgs.length === 0) { + return InjectionMessageInstance.failure(WalletResponseFailureType.INVALID_FORMAT); + } + + return this.validateMessages(signedDocument.msgs); + }; + + private validateContractMessage = (params: TransactionParams): InjectionMessage | undefined => { + if (!validateDoContractRequest(params)) { + return InjectionMessageInstance.failure(WalletResponseFailureType.INVALID_FORMAT); + } + + return this.validateMessages(params.messages); }; private sendEventMessage = ( diff --git a/packages/adena-extension/src/inject/types/transactions.ts b/packages/adena-extension/src/inject/types/transactions.ts index a024bdf30..2cc115586 100644 --- a/packages/adena-extension/src/inject/types/transactions.ts +++ b/packages/adena-extension/src/inject/types/transactions.ts @@ -1,6 +1,6 @@ import { MsgAddPackage, MsgCall, MsgSend } from '@gnolang/gno-js-client'; import { MsgRun } from '@gnolang/gno-js-client/bin/proto/gno/vm'; -import { BroadcastTxCommitResult, TxSignPayload } from '@gnolang/tm2-js-client'; +import { BroadcastTxCommitResult } from '@gnolang/tm2-js-client'; import { GnoArgumentInfo } from '@inject/message/methods/gno-connect'; import { Signature } from '@adena-wallet/sdk'; @@ -39,7 +39,21 @@ export type TransactionParams = { arguments?: GnoArgumentInfo[] | null; }; -export interface SignedDocument extends TxSignPayload, Signature {} +export interface SignedDocument { + chain_id: string; + account_number: string; + sequence: string; + fee: { + amount: { + amount: string; + denom: string; + }[]; + gas: string; + }; + msgs: any[]; + memo: string; + signatures: Signature[]; +} // TODO: BroadcastTxCommitResult isn't correct in case of a VM call export type DoContractResponse = AdenaResponse; From 63df1132e0ec1e8147d4f90d9339225e5c8337f6 Mon Sep 17 00:00:00 2001 From: yjin Date: Wed, 26 Nov 2025 16:34:57 +0900 Subject: [PATCH 04/19] feat: [ADN-733] validation signed-document --- .../src/common/validation/index.ts | 2 + .../common/validation/validation-signature.ts | 37 ++++++++++++++++ .../validation/validation-signed-document.ts | 44 +++++++++++++++++++ .../src/inject/executor/executor.ts | 28 ++++++------ .../wallet/approve-sign-document/index.tsx | 15 +++---- .../src/services/transaction/transaction.ts | 35 ++++++++++++++- 6 files changed, 137 insertions(+), 24 deletions(-) create mode 100644 packages/adena-extension/src/common/validation/validation-signature.ts create mode 100644 packages/adena-extension/src/common/validation/validation-signed-document.ts diff --git a/packages/adena-extension/src/common/validation/index.ts b/packages/adena-extension/src/common/validation/index.ts index 90223b309..f25e87c8e 100644 --- a/packages/adena-extension/src/common/validation/index.ts +++ b/packages/adena-extension/src/common/validation/index.ts @@ -3,3 +3,5 @@ export * from './validation-message'; export * from './validation-password'; export * from './validation-token'; export * from './validation-wallet'; +export * from './validation-signature'; +export * from './validation-signed-document'; diff --git a/packages/adena-extension/src/common/validation/validation-signature.ts b/packages/adena-extension/src/common/validation/validation-signature.ts new file mode 100644 index 000000000..1989b93fd --- /dev/null +++ b/packages/adena-extension/src/common/validation/validation-signature.ts @@ -0,0 +1,37 @@ +/** + * Validates a single signature object structure. + * Verifies the existence and format of pubKey (typeUrl, value) and signature fields. + * + * @param signature - The signature object to validate + * @returns true if signature structure is valid, false otherwise + */ +export const validateSignature = (signature: any): boolean => { + if (!signature || typeof signature !== 'object') { + return false; + } + + if ( + !signature.pubKey || + typeof signature.pubKey.typeUrl !== 'string' || + typeof signature.pubKey.value !== 'string' + ) { + return false; + } + + if (typeof signature.signature !== 'string') { + return false; + } + + return true; +}; + +/** + * Validates an array of signature objects. + * Checks that all signatures in the array have valid structure. + * + * @param signatures - Array of signature objects to validate + * @returns true if all signatures are valid, false if any signature is invalid + */ +export const validateSignatures = (signatures: any[]): boolean => { + return signatures.every((signature) => validateSignature(signature)); +}; diff --git a/packages/adena-extension/src/common/validation/validation-signed-document.ts b/packages/adena-extension/src/common/validation/validation-signed-document.ts new file mode 100644 index 000000000..9f5f19202 --- /dev/null +++ b/packages/adena-extension/src/common/validation/validation-signed-document.ts @@ -0,0 +1,44 @@ +/** + * Validates basic fields of a signed document. + * Verifies that chain_id, account_number, sequence, and memo are all strings. + * + * @param signedDocument - The signed document object to validate + * @returns true if all basic fields are valid strings, false otherwise + */ +export const validateSignedDocumentFields = (signedDocument: any): boolean => { + return ( + typeof signedDocument.chain_id === 'string' && + typeof signedDocument.account_number === 'string' && + typeof signedDocument.sequence === 'string' && + typeof signedDocument.memo === 'string' + ); +}; + +/** + * Validates the fee structure of a signed document. + * Verifies that fee object exists, gas is a non-empty string, and amount is a non-empty array. + * + * @param fee - The fee object to validate + * @returns true if fee structure is valid, false otherwise + */ + +export const validateSignedDocumentFee = (fee: any): boolean => { + return ( + fee && + typeof fee.gas === 'string' && + Array.isArray(fee.amount) && + fee.gas.length > 0 && + fee.amount.length > 0 + ); +}; + +/** + * Validates the messages array of a signed document. + * Verifies that msgs is an array and contains at least one message. + * + * @param msgs - The messages array to validate + * @returns true if messages array is valid and not empty, false otherwise + */ +export const validateSignedDocumentMessages = (msgs: any): boolean => { + return Array.isArray(msgs) && msgs.length > 0; +}; diff --git a/packages/adena-extension/src/inject/executor/executor.ts b/packages/adena-extension/src/inject/executor/executor.ts index b38400c4f..e448c8cb5 100644 --- a/packages/adena-extension/src/inject/executor/executor.ts +++ b/packages/adena-extension/src/inject/executor/executor.ts @@ -27,6 +27,12 @@ import { TransactionParams, } from '@inject/types'; import { InjectionMessage, InjectionMessageInstance } from '../message'; +import { + validateSignatures, + validateSignedDocumentFee, + validateSignedDocumentFields, + validateSignedDocumentMessages, +} from '@common/validation'; type Params = { [key in string]: any }; @@ -201,16 +207,12 @@ export class AdenaExecutor { private validateSignedDocument = ( signedDocument: SignedDocument, ): InjectionMessage | undefined => { + console.log('new validation code'); if (!signedDocument) { return InjectionMessageInstance.failure(WalletResponseFailureType.INVALID_FORMAT); } - if ( - typeof signedDocument.chain_id !== 'string' || - typeof signedDocument.account_number !== 'string' || - typeof signedDocument.sequence !== 'string' || - typeof signedDocument.memo !== 'string' - ) { + if (!validateSignedDocumentFields(signedDocument)) { return InjectionMessageInstance.failure(WalletResponseFailureType.INVALID_FORMAT); } @@ -218,17 +220,15 @@ export class AdenaExecutor { return InjectionMessageInstance.failure(WalletResponseFailureType.INVALID_FORMAT); } - if ( - !signedDocument.fee || - typeof signedDocument.fee.gas !== 'string' || - !Array.isArray(signedDocument.fee.amount) || - signedDocument.fee.gas.length === 0 || - signedDocument.fee.amount.length === 0 - ) { + if (!validateSignatures(signedDocument.signatures)) { + return InjectionMessageInstance.failure(WalletResponseFailureType.INVALID_FORMAT); + } + + if (!validateSignedDocumentFee(signedDocument.fee)) { return InjectionMessageInstance.failure(WalletResponseFailureType.INVALID_FORMAT); } - if (!Array.isArray(signedDocument.msgs) || signedDocument.msgs.length === 0) { + if (!validateSignedDocumentMessages(signedDocument.msgs)) { return InjectionMessageInstance.failure(WalletResponseFailureType.INVALID_FORMAT); } diff --git a/packages/adena-extension/src/pages/popup/wallet/approve-sign-document/index.tsx b/packages/adena-extension/src/pages/popup/wallet/approve-sign-document/index.tsx index 101150ee1..98603d5fd 100644 --- a/packages/adena-extension/src/pages/popup/wallet/approve-sign-document/index.tsx +++ b/packages/adena-extension/src/pages/popup/wallet/approve-sign-document/index.tsx @@ -27,7 +27,7 @@ import { useGetGnotBalance } from '@hooks/wallet/use-get-gnot-balance'; import { useNetworkFee } from '@hooks/wallet/use-network-fee'; import { InjectionMessage, InjectionMessageInstance } from '@inject/message'; import { GnoArgumentInfo } from '@inject/message/methods/gno-connect'; -import { ContractMessage } from '@inject/types'; +import { ContractMessage, SignedDocument } from '@inject/types'; import { RoutePath } from '@types'; interface TransactionData { @@ -207,17 +207,14 @@ const ApproveSignDocumentContainer: React.FC = () => { setFavicon(faviconData); }; const initTransactionData = async (): Promise => { - if (!currentAccount || !requestData || !currentNetwork) { + if (!currentAccount || !requestData || !currentNetwork || !requestData?.data) { return false; } + try { - const document = await transactionService.createDocument( - currentAccount, - currentNetwork.networkId, - requestData?.data?.messages || requestData?.data?.msgs, - requestData?.data?.gasWanted, - requestData?.data?.gasFee, - requestData?.data?.memo, + const document = await transactionService.createSignedDocument( + currentNetwork.chainId, + requestData.data as SignedDocument, ); setDocument(document); setTransactionData(mappedTransactionData(document)); diff --git a/packages/adena-extension/src/services/transaction/transaction.ts b/packages/adena-extension/src/services/transaction/transaction.ts index db9aff0dd..834fae677 100644 --- a/packages/adena-extension/src/services/transaction/transaction.ts +++ b/packages/adena-extension/src/services/transaction/transaction.ts @@ -20,6 +20,7 @@ import { DEFAULT_GAS_FEE, DEFAULT_GAS_WANTED } from '@common/constants/tx.consta import { mappedDocumentMessagesWithCaller } from '@common/mapper/transaction-mapper'; import { GnoProvider } from '@common/provider/gno/gno-provider'; import { WalletService } from '..'; +import { SignedDocument } from '@inject/types'; interface EncodeTxSignature { pubKey: { @@ -70,7 +71,6 @@ export class TransactionService { gasFee?: number, memo?: string | undefined, ): Promise => { - console.log(messages, 'messages!'); const provider = this.getGnoProvider(); const address = await account.getAddress(defaultAddressPrefix); const accountInfo = await provider.getAccountInfo(address).catch(() => null); @@ -94,6 +94,39 @@ export class TransactionService { }; }; + /** + * Create a signed document + * + * @param chainId + * @param signedDocument + * @returns + */ + public createSignedDocument = async ( + chainId: string, + signedDocument: SignedDocument, + ): Promise => { + return { + ...signedDocument, + chain_id: signedDocument.chain_id || chainId, + fee: { + gas: signedDocument.fee.gas || DEFAULT_GAS_WANTED.toString(), + amount: + signedDocument.fee.amount.length > 0 + ? signedDocument.fee.amount.map((fee) => ({ + ...fee, + amount: fee.amount || DEFAULT_GAS_FEE.toString(), + denom: fee.denom || GasToken.denom, + })) + : [ + { + amount: DEFAULT_GAS_FEE.toString(), + denom: GasToken.denom, + }, + ], + }, + }; + }; + /** Create a signature * * @param account From 1883c07ee1a9c598bee9a3dd23fad6c77e25fcfb Mon Sep 17 00:00:00 2001 From: yjin Date: Thu, 27 Nov 2025 11:47:09 +0900 Subject: [PATCH 05/19] feat: [ADN-733] accumulate signatures in document.signatures array --- .../wallet/approve-sign-document/index.tsx | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/adena-extension/src/pages/popup/wallet/approve-sign-document/index.tsx b/packages/adena-extension/src/pages/popup/wallet/approve-sign-document/index.tsx index 98603d5fd..4e240e5c1 100644 --- a/packages/adena-extension/src/pages/popup/wallet/approve-sign-document/index.tsx +++ b/packages/adena-extension/src/pages/popup/wallet/approve-sign-document/index.tsx @@ -69,7 +69,7 @@ const ApproveSignDocumentContainer: React.FC = () => { const [requestData, setRequestData] = useState(); const [favicon, setFavicon] = useState(null); const [visibleTransactionInfo, setVisibleTransactionInfo] = useState(false); - const [document, setDocument] = useState(); + const [document, setDocument] = useState(); const [processType, setProcessType] = useState<'INIT' | 'PROCESSING' | 'DONE'>('INIT'); const [response, setResponse] = useState(null); const [memo, setMemo] = useState(''); @@ -251,7 +251,7 @@ const ApproveSignDocumentContainer: React.FC = () => { const currentGasPrice = useNetworkFeeReturn.currentGasFeeRawAmount; const currentGasWanted = useNetworkFeeReturn.currentGasInfo?.gasWanted || 0; - const updatedDocument: Document = { + const updatedSignedDocument: SignedDocument = { ...document, memo: currentMemo, fee: { @@ -265,8 +265,8 @@ const ApproveSignDocumentContainer: React.FC = () => { }, }; - setDocument(updatedDocument); - setTransactionData(mappedTransactionData(updatedDocument)); + setDocument(updatedSignedDocument); + setTransactionData(mappedTransactionData(updatedSignedDocument)); }; const createSignDocument = async (): Promise => { @@ -283,11 +283,19 @@ const ApproveSignDocumentContainer: React.FC = () => { try { const signature = await transactionService.createSignature(currentAccount, document); + + const updateSignedDocument = { + ...document, + signatures: [...document.signatures, signature], + }; setProcessType('PROCESSING'); setResponse( InjectionMessageInstance.success( - WalletResponseSuccessType.SIGN_SUCCESS, - { document, signature }, + WalletResponseSuccessType.SIGN_DOCUMENT_SUCCESS, + { + document: updateSignedDocument, + signature, + }, requestData?.key, ), ); @@ -299,7 +307,7 @@ const ApproveSignDocumentContainer: React.FC = () => { } setResponse( InjectionMessageInstance.failure( - WalletResponseFailureType.SIGN_FAILED, + WalletResponseFailureType.SIGN_DOCUMENT_FAILED, { error: { message } }, requestData?.key, ), @@ -307,7 +315,7 @@ const ApproveSignDocumentContainer: React.FC = () => { } setResponse( InjectionMessageInstance.failure( - WalletResponseFailureType.SIGN_FAILED, + WalletResponseFailureType.SIGN_DOCUMENT_FAILED, {}, requestData?.key, ), @@ -333,7 +341,6 @@ const ApproveSignDocumentContainer: React.FC = () => { }); return; } - createSignDocument().finally(() => setProcessType('DONE')); }; From 2166a26b399eea4a002e5a3f42c58e22f3017c21 Mon Sep 17 00:00:00 2001 From: yjin Date: Thu, 27 Nov 2025 12:54:07 +0900 Subject: [PATCH 06/19] feat: [ADN-733] NetworkFee --- .../wallet/approve-sign-document/index.tsx | 49 +++++++++++++++---- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/packages/adena-extension/src/pages/popup/wallet/approve-sign-document/index.tsx b/packages/adena-extension/src/pages/popup/wallet/approve-sign-document/index.tsx index 4e240e5c1..eb4e2e077 100644 --- a/packages/adena-extension/src/pages/popup/wallet/approve-sign-document/index.tsx +++ b/packages/adena-extension/src/pages/popup/wallet/approve-sign-document/index.tsx @@ -28,7 +28,7 @@ import { useNetworkFee } from '@hooks/wallet/use-network-fee'; import { InjectionMessage, InjectionMessageInstance } from '@inject/message'; import { GnoArgumentInfo } from '@inject/message/methods/gno-connect'; import { ContractMessage, SignedDocument } from '@inject/types'; -import { RoutePath } from '@types'; +import { NetworkFee, RoutePath } from '@types'; interface TransactionData { messages: readonly any[]; @@ -79,20 +79,38 @@ const ApproveSignDocumentContainer: React.FC = () => { const { data: currentBalance = null } = useGetGnotBalance(); const useNetworkFeeReturn = useNetworkFee(document, true); - const networkFee = useNetworkFeeReturn.networkFee; - const processing = useMemo(() => processType !== 'INIT', [processType]); + const rawNetworkFee: NetworkFee | null = useMemo(() => { + if (!document?.fee?.amount?.[0]) { + return null; + } - const done = useMemo(() => processType === 'DONE', [processType]); + const feeAmount = document.fee.amount[0]; - const hasMemo = useMemo(() => { - if (!requestData?.data?.memo) { - return false; + return { + amount: feeAmount.amount, + denom: feeAmount.denom, + }; + }, [document?.fee]); + + const networkFee: NetworkFee | null = useMemo(() => { + if (!rawNetworkFee) { + return null; } - return true; - }, [requestData?.data?.memo]); - const displayNetworkFee = useMemo(() => { + const networkFeeAmount = BigNumber(rawNetworkFee.amount) + .shiftedBy(-GasToken.decimals) + .toFixed(GasToken.decimals) + .replace(/(\.\d*?)0+$/, '$1') + .replace(/\.$/, ''); + + return { + amount: networkFeeAmount, + denom: GasToken.symbol, + }; + }, [rawNetworkFee]); + + const displayNetworkFee: NetworkFee = useMemo(() => { if (!networkFee) { return { amount: '', @@ -106,6 +124,17 @@ const ApproveSignDocumentContainer: React.FC = () => { }; }, [networkFee]); + const processing = useMemo(() => processType !== 'INIT', [processType]); + + const done = useMemo(() => processType === 'DONE', [processType]); + + const hasMemo = useMemo(() => { + if (!requestData?.data?.memo) { + return false; + } + return true; + }, [requestData?.data?.memo]); + const consumedTokenAmount = useMemo(() => { const accumulatedAmount = document?.msgs.reduce((acc, msg) => { const messageValue = msg.value; From 5acbf7c04f9e7052a7d0c2eb05068a0335d2a2b5 Mon Sep 17 00:00:00 2001 From: yjin Date: Thu, 27 Nov 2025 14:31:09 +0900 Subject: [PATCH 07/19] feat: [ADN-733] Approve-signed-document page --- .../src/common/utils/gas-utils.ts | 22 ++ .../approve-signed-document.stories.tsx | 33 ++ .../approve-signed-document.styles.ts | 192 +++++++++++ .../approve-signed-document/index.tsx | 302 ++++++++++++++++++ .../src/components/molecules/index.ts | 1 + .../src/hooks/wallet/use-network-fee.ts | 7 +- .../wallet/approve-sign-document/index.tsx | 11 +- 7 files changed, 556 insertions(+), 12 deletions(-) create mode 100644 packages/adena-extension/src/common/utils/gas-utils.ts create mode 100644 packages/adena-extension/src/components/molecules/approve-signed-document/approve-signed-document.stories.tsx create mode 100644 packages/adena-extension/src/components/molecules/approve-signed-document/approve-signed-document.styles.ts create mode 100644 packages/adena-extension/src/components/molecules/approve-signed-document/index.tsx diff --git a/packages/adena-extension/src/common/utils/gas-utils.ts b/packages/adena-extension/src/common/utils/gas-utils.ts new file mode 100644 index 000000000..c976c3403 --- /dev/null +++ b/packages/adena-extension/src/common/utils/gas-utils.ts @@ -0,0 +1,22 @@ +import BigNumber from 'bignumber.js'; +import { GasToken } from '@common/constants/token.constant'; + +export const convertRawGasAmountToDisplayAmount = (rawAmount: string | number): string => { + try { + if (!rawAmount) { + return '0'; + } + + return BigNumber(rawAmount) + .shiftedBy(-GasToken.decimals) + .toFixed(GasToken.decimals) + .replace(/(\.\d*?)0+$/, '$1') + .replace(/\.$/, ''); + } catch (e) { + console.warn('[convertRawGasAmountToDisplayAmount] Failed to convert:', { + rawAmount, + error: e, + }); + return '0'; + } +}; diff --git a/packages/adena-extension/src/components/molecules/approve-signed-document/approve-signed-document.stories.tsx b/packages/adena-extension/src/components/molecules/approve-signed-document/approve-signed-document.stories.tsx new file mode 100644 index 000000000..2d255a902 --- /dev/null +++ b/packages/adena-extension/src/components/molecules/approve-signed-document/approve-signed-document.stories.tsx @@ -0,0 +1,33 @@ +import { ApproveSignedDocument, type ApproveSignedDocumentProps } from '.'; +import { Meta, StoryObj } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; + +export default { + title: 'components/approve/ApproveTransaction', + component: ApproveSignedDocument, +} as Meta; + +export const Default: StoryObj = { + args: { + domain: '', + loading: true, + logo: '', + title: 'Sign Transaction', + contracts: [ + { + type: '/vm.m_call', + function: 'GetBoardIDFromName', + value: '', + }, + ], + networkFee: { + amount: '0.0048', + denom: 'GNOT', + }, + transactionData: '', + opened: false, + onToggleTransactionData: action('openTransactionData'), + onClickConfirm: action('confirm'), + onClickCancel: action('cancel'), + }, +}; diff --git a/packages/adena-extension/src/components/molecules/approve-signed-document/approve-signed-document.styles.ts b/packages/adena-extension/src/components/molecules/approve-signed-document/approve-signed-document.styles.ts new file mode 100644 index 000000000..37dd205c6 --- /dev/null +++ b/packages/adena-extension/src/components/molecules/approve-signed-document/approve-signed-document.styles.ts @@ -0,0 +1,192 @@ +import styled from 'styled-components'; + +import mixins from '@styles/mixins'; +import { fonts, getTheme } from '@styles/theme'; + +export const ApproveSignedDocumentNetworkFeeWrapper = styled.div` + ${mixins.flex({ justify: 'flex-start' })}; + padding: 24px 20px; +`; + +export const ApproveSignedDocumentWrapper = styled.div<{ isErrorNetworkFee: boolean }>` + ${mixins.flex({ justify: 'flex-start' })}; + width: 100%; + padding: 0 20px; + padding-bottom: 96px; + align-self: center; + + .row { + ${mixins.flex({ direction: 'row' })}; + position: relative; + padding: 10px 18px; + justify-content: space-between; + border-bottom: 2px solid ${getTheme('neutral', '_8')}; + ${fonts.body1Reg}; + + &:last-child { + border-bottom: none; + } + + .key { + display: inline-flex; + width: fit-content; + flex-shrink: 0; + color: ${getTheme('neutral', 'a')}; + } + + .value { + display: block; + max-width: 204px; + text-align: right; + text-overflow: ellipsis; + overflow: hidden; + } + } + + .main-title { + text-overflow: ellipsis; + margin-top: 24px; + overflow: hidden; + white-space: nowrap; + width: 100%; + text-align: center; + } + + .logo-wrapper { + margin: 24px auto; + width: 100%; + height: auto; + text-align: center; + + img { + width: 80px; + height: 80px; + } + } + + .domain-wrapper { + ${mixins.flex({ direction: 'row', align: 'center', justify: 'center' })}; + width: 100%; + min-height: 41px; + border-radius: 24px; + padding: 10px 18px; + margin: 24px auto 12px auto; + gap: 7px; + background-color: ${getTheme('neutral', '_9')}; + ${fonts.body2Reg}; + + .logo { + width: 20px; + height: 20px; + border-radius: 50%; + } + } + + .info-table { + width: 100%; + height: auto; + border-radius: 18px; + margin-bottom: 8px; + background-color: ${getTheme('neutral', '_9')}; + } + + .memo-wrapper { + width: 100%; + min-height: 48px; + border-radius: 30px; + padding: 10px 18px; + margin-bottom: 8px; + background-color: ${getTheme('neutral', '_9')}; + border: 1px solid ${getTheme('neutral', '_8')}; + gap: 10px; + ${fonts.body2Reg}; + + span.value { + display: block; + width: 100%; + max-width: 100%; + height: auto; + word-break: break-all; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + input.value { + display: block; + width: 100%; + max-width: 100%; + height: auto; + + &::placeholder { + color: ${getTheme('neutral', 'a')}; + } + } + + &.editable { + border: 1px solid ${getTheme('neutral', '_7')}; + } + } + + .fee-amount-wrapper { + ${mixins.flex({ justify: 'flex-start' })}; + width: 100%; + gap: 8px; + margin-bottom: 8px; + } + + .error-message { + position: relative; + width: 100%; + padding: 0 16px; + ${fonts.captionReg}; + color: ${getTheme('red', '_5')}; + white-space: pre-line; + } + + .transaction-data-wrapper { + width: 100%; + ${fonts.body2Reg}; + ${mixins.flex()}; + + .visible-button { + color: ${getTheme('neutral', 'a')}; + height: fit-content; + margin-bottom: 5px; + + img { + margin-left: 3px; + } + } + .textarea-wrapper { + width: 100%; + height: 200px; + border-radius: 24px; + background-color: ${getTheme('neutral', '_9')}; + border: 1px solid ${getTheme('neutral', '_7')}; + padding: 12px 16px; + } + .raw-info-textarea { + width: 100%; + height: 100%; + overflow: auto; + ${fonts.body2Reg}; + resize: none; + } + .raw-info-textarea::-webkit-scrollbar { + width: 2px; + padding: 1px 1px 1px 0px; + margin-right: 10px; + } + + .raw-info-textarea::-webkit-scrollbar-thumb { + background-color: darkgrey; + } + + .raw-info-textarea::-webkit-resizer { + display: none !important; + } + + margin-bottom: 20px; + } +`; diff --git a/packages/adena-extension/src/components/molecules/approve-signed-document/index.tsx b/packages/adena-extension/src/components/molecules/approve-signed-document/index.tsx new file mode 100644 index 000000000..a60ae6da8 --- /dev/null +++ b/packages/adena-extension/src/components/molecules/approve-signed-document/index.tsx @@ -0,0 +1,302 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; + +import { Button, Text } from '@components/atoms'; +import { BottomFixedLoadingButtonGroup } from '@components/molecules'; + +import IconArraowDown from '@assets/arrowS-down-gray.svg'; +import IconArraowUp from '@assets/arrowS-up-gray.svg'; +import UnknownLogo from '@assets/common-unknown-logo.svg'; +import NetworkFeeSetting from '@components/pages/network-fee-setting/network-fee-setting/network-fee-setting'; +import { UseNetworkFeeReturn } from '@hooks/wallet/use-network-fee'; +import { GnoArgumentInfo } from '@inject/message/methods/gno-connect'; +import { ContractMessage } from '@inject/types'; +import { NetworkFee as NetworkFeeType } from '@types'; +import { ApproveTransactionLoading } from '../approve-transaction-loading'; +import ApproveTransactionMessageBox from '../approve-transaction-message-box/approve-transaction-message-box'; +import NetworkFee from '../network-fee/network-fee'; +import StorageDeposit from '../storage-deposit/storage-deposit'; +import { + ApproveSignedDocumentNetworkFeeWrapper, + ApproveSignedDocumentWrapper, +} from './approve-signed-document.styles'; + +export interface ApproveSignedDocumentProps { + loading: boolean; + title: string; + logo: string; + domain: string; + contracts: { + type: string; + function: string; + value: string; + }[]; + memo: string; + hasMemo: boolean; + currentBalance?: number; + isErrorNetworkFee?: boolean; + networkFee: NetworkFeeType | null; + transactionData: string; + opened: boolean; + argumentInfos?: GnoArgumentInfo[]; + processing: boolean; + done: boolean; + transactionMessages: ContractMessage[]; + maxDepositAmount?: number; + changeTransactionMessages: (messages: ContractMessage[]) => void; + changeMemo: (memo: string) => void; + openScannerLink: (path: string, parameters?: { [key in string]: string }) => void; + onToggleTransactionData: (opened: boolean) => void; + onResponse: () => void; + onTimeout: () => void; + onClickConfirm: () => void; + onClickCancel: () => void; + useNetworkFeeReturn: UseNetworkFeeReturn; +} + +export const ApproveSignedDocument: React.FC = ({ + loading, + title, + logo, + domain, + transactionMessages, + memo, + currentBalance, + hasMemo, + networkFee, + isErrorNetworkFee, + transactionData, + opened, + processing, + done, + useNetworkFeeReturn, + argumentInfos, + maxDepositAmount, + changeTransactionMessages, + changeMemo, + onToggleTransactionData, + onResponse, + onClickConfirm, + onClickCancel, + openScannerLink, +}) => { + const [openedNetworkFeeSetting, setOpenedNetworkFeeSetting] = useState(false); + + const disabledApprove = useMemo(() => { + if (useNetworkFeeReturn.isLoading) { + return true; + } + + if (isErrorNetworkFee || useNetworkFeeReturn.isSimulateError) { + return true; + } + + return Number(networkFee?.amount || 0) <= 0; + }, [ + isErrorNetworkFee, + useNetworkFeeReturn.isLoading, + useNetworkFeeReturn.isSimulateError, + networkFee, + ]); + + const isMaxDepositError = useMemo(() => { + if (!maxDepositAmount || currentBalance === undefined) { + return false; + } + + return currentBalance < maxDepositAmount; + }, [currentBalance, maxDepositAmount]); + + const maxDepositErrorMessage = useMemo(() => { + if (useNetworkFeeReturn.isLoading) { + return ''; + } + + if (isMaxDepositError) { + return 'Insufficient balance'; + } + + return ''; + }, [useNetworkFeeReturn.isLoading, isMaxDepositError]); + + const networkFeeErrorMessage = useMemo(() => { + if (useNetworkFeeReturn.isSimulateError) { + if (currentBalance !== 0) { + return 'This transaction cannot be simulated. Try again.'; + } + } + + if (isErrorNetworkFee) { + return 'Insufficient network fee'; + } + + return ''; + }, [useNetworkFeeReturn.isSimulateError, isErrorNetworkFee, currentBalance]); + + const simulateErrorMessage = useMemo(() => { + if (useNetworkFeeReturn.isSimulateError) { + return useNetworkFeeReturn.currentGasInfo?.simulateErrorMessage || null; + } + + return null; + }, [useNetworkFeeReturn.isSimulateError, useNetworkFeeReturn.currentGasInfo]); + + const onChangeMemo = useCallback( + (e: React.ChangeEvent) => { + if (hasMemo) { + return; + } + + const value = e.target.value; + changeMemo(value); + }, + [hasMemo, changeMemo], + ); + + const onClickNetworkFeeSetting = useCallback(() => { + setOpenedNetworkFeeSetting(true); + }, []); + + const onClickNetworkFeeClose = useCallback(() => { + setOpenedNetworkFeeSetting(false); + }, []); + + const onClickNetworkFeeSave = useCallback(() => { + useNetworkFeeReturn.save(); + setOpenedNetworkFeeSetting(false); + }, [useNetworkFeeReturn.save]); + + const onClickConfirmButton = useCallback(() => { + if (disabledApprove) { + return; + } + + onClickConfirm(); + }, [onClickConfirm, disabledApprove]); + + useEffect(() => { + if (done) { + onResponse(); + } + }, [done, onResponse]); + + if (loading) { + return ; + } + + if (openedNetworkFeeSetting) { + return ( + + + + ); + } + + return ( + + + {title} + + +
+ logo img + {domain} +
+ + + +
+ Memo: + {hasMemo ? ( + {memo} + ) : ( + + )} +
+ +
+ + + +
+ +
+ + + {opened && ( +
+