diff --git a/packages/appkit/src/actions/nft/get-nft.ts b/packages/appkit/src/actions/nft/get-nft.ts index 62b588a2c..b96269ba9 100644 --- a/packages/appkit/src/actions/nft/get-nft.ts +++ b/packages/appkit/src/actions/nft/get-nft.ts @@ -19,7 +19,7 @@ export interface GetNftOptions { network?: Network; } -export type GetNftReturnType = NFT | null; +export type GetNftReturnType = NFT | undefined; export const getNft = async (appKit: AppKit, options: GetNftOptions): Promise => { const { address, network } = options; diff --git a/packages/appkit/src/queries/nft/get-nft.ts b/packages/appkit/src/queries/nft/get-nft.ts index 4d6175b90..0831caff6 100644 --- a/packages/appkit/src/queries/nft/get-nft.ts +++ b/packages/appkit/src/queries/nft/get-nft.ts @@ -41,7 +41,7 @@ export const getNftQueryOptions = ( }; }; -export type GetNftQueryFnData = Compute; +export type GetNftQueryFnData = Compute; export type GetNftData = GetNftQueryFnData; diff --git a/packages/mcp/src/services/McpWalletService.ts b/packages/mcp/src/services/McpWalletService.ts index 76897c415..2b5adb4c7 100644 --- a/packages/mcp/src/services/McpWalletService.ts +++ b/packages/mcp/src/services/McpWalletService.ts @@ -1104,7 +1104,8 @@ export class McpWalletService { */ async resolveDns(domain: string): Promise { const client = this.wallet.getClient(); - return client.resolveDnsWallet(domain); + const address = await client.resolveDnsWallet(domain); + return address ?? null; } /** @@ -1112,7 +1113,8 @@ export class McpWalletService { */ async backResolveDns(address: string): Promise { const client = this.wallet.getClient(); - return client.backResolveDnsWallet(address); + const domain = await client.backResolveDnsWallet(address); + return domain ?? null; } /** diff --git a/packages/walletkit-android-bridge/src/adapters/AndroidAPIClientAdapter.ts b/packages/walletkit-android-bridge/src/adapters/AndroidAPIClientAdapter.ts index 88041c723..a0ff1232f 100644 --- a/packages/walletkit-android-bridge/src/adapters/AndroidAPIClientAdapter.ts +++ b/packages/walletkit-android-bridge/src/adapters/AndroidAPIClientAdapter.ts @@ -19,7 +19,7 @@ import type { TransactionsResponse, JettonsResponse, FullAccountState, - ToncenterEmulationResult, + EmulationResult, ToncenterResponseJettonMasters, ToncenterTracesResponse, TransactionsByAddressRequest, @@ -139,7 +139,7 @@ export class AndroidAPIClientAdapter implements ApiClient { throw new Error('nftItemsByOwner is not implemented yet'); } - async fetchEmulation(_messageBoc: Base64String, _ignoreSignature?: boolean): Promise { + async fetchEmulation(_messageBoc: Base64String, _ignoreSignature?: boolean): Promise { throw new Error('fetchEmulation is not implemented yet'); } diff --git a/packages/walletkit-ios-bridge/src/SwiftAPIClientAdapter.ts b/packages/walletkit-ios-bridge/src/SwiftAPIClientAdapter.ts index b16bd1aab..8acdd1d96 100644 --- a/packages/walletkit-ios-bridge/src/SwiftAPIClientAdapter.ts +++ b/packages/walletkit-ios-bridge/src/SwiftAPIClientAdapter.ts @@ -19,7 +19,7 @@ import type { TransactionsResponse, JettonsResponse, FullAccountState, - ToncenterEmulationResult, + EmulationResult, ToncenterResponseJettonMasters, ToncenterTracesResponse, TransactionsByAddressRequest, @@ -62,7 +62,7 @@ export class SwiftAPIClientAdapter implements ApiClient { throw new Error('nftItemsByOwner is not implemented yet'); } - async fetchEmulation(_messageBoc: Base64String, _ignoreSignature?: boolean): Promise { + async fetchEmulation(_messageBoc: Base64String, _ignoreSignature?: boolean): Promise { throw new Error('fetchEmulation is not implemented yet'); } diff --git a/packages/walletkit/package.json b/packages/walletkit/package.json index 0fcb2e3dc..fbbc8190e 100644 --- a/packages/walletkit/package.json +++ b/packages/walletkit/package.json @@ -85,6 +85,7 @@ "build:esm": "tsc -p tsconfig.esm.json", "dev": "tsc -p tsconfig.esm.json --watch", "test": "vitest run", + "test:coverage": "vitest run --coverage", "test:mutation": "stryker run stryker.config.js", "quality": "pnpm test:coverage", diff --git a/packages/walletkit/src/types/toncenter/ApiClient.ts b/packages/walletkit/src/api/interfaces/ApiClient.ts similarity index 90% rename from packages/walletkit/src/types/toncenter/ApiClient.ts rename to packages/walletkit/src/api/interfaces/ApiClient.ts index e0ad6d43a..3f210616a 100644 --- a/packages/walletkit/src/types/toncenter/ApiClient.ts +++ b/packages/walletkit/src/api/interfaces/ApiClient.ts @@ -8,9 +8,9 @@ import type { Address } from '@ton/core'; -import type { ToncenterResponseJettonMasters, ToncenterTracesResponse } from './emulation'; -import type { FullAccountState } from './api'; -import type { Event } from './AccountEvent'; +import type { ToncenterResponseJettonMasters, ToncenterTracesResponse } from '../../types/toncenter/emulation'; +import type { FullAccountState } from '../../types/toncenter/api'; +import type { Event } from '../../types/toncenter/AccountEvent'; import type { Base64String, UserNFTsRequest, @@ -23,8 +23,8 @@ import type { RawStackItem, GetMethodResult, MasterchainInfo, -} from '../../api/models'; -import type { ToncenterEmulationResult } from '../../utils/toncenterEmulation'; +} from '../models'; +import type { EmulationResult } from '../models'; export interface LimitRequest { limit?: number; @@ -97,7 +97,7 @@ export interface GetEventsResponse { export interface ApiClient { nftItemsByAddress(request: NFTsRequest): Promise; nftItemsByOwner(request: UserNFTsRequest): Promise; - fetchEmulation(messageBoc: Base64String, ignoreSignature?: boolean): Promise; + fetchEmulation(messageBoc: Base64String, ignoreSignature?: boolean): Promise; sendBoc(boc: Base64String): Promise; runGetMethod( address: UserFriendlyAddress, @@ -116,8 +116,8 @@ export interface ApiClient { getTrace(request: GetTraceRequest): Promise; getPendingTrace(request: GetPendingTraceRequest): Promise; - resolveDnsWallet(domain: string): Promise; - backResolveDnsWallet(address: UserFriendlyAddress): Promise; + resolveDnsWallet(domain: string): Promise; + backResolveDnsWallet(address: UserFriendlyAddress): Promise; jettonsByAddress(request: GetJettonsByAddressRequest): Promise; jettonsByOwnerAddress(request: GetJettonsByOwnerRequest): Promise; diff --git a/packages/walletkit/src/api/interfaces/Wallet.ts b/packages/walletkit/src/api/interfaces/Wallet.ts index 370323995..174586fee 100644 --- a/packages/walletkit/src/api/interfaces/Wallet.ts +++ b/packages/walletkit/src/api/interfaces/Wallet.ts @@ -6,7 +6,7 @@ * */ -import type { ApiClient } from '../../types/toncenter/ApiClient'; +import type { ApiClient } from './ApiClient'; import type { TokenAmount, TONTransferRequest, @@ -52,5 +52,5 @@ export interface WalletNftInterface { createTransferNftTransaction(params: NFTTransferRequest): Promise; createTransferNftRawTransaction(params: NFTRawTransferRequest): Promise; getNfts(params: NFTsRequest): Promise; - getNft(address: UserFriendlyAddress): Promise; + getNft(address: UserFriendlyAddress): Promise; } diff --git a/packages/walletkit/src/api/interfaces/index.ts b/packages/walletkit/src/api/interfaces/index.ts index fbe184f64..32f025c3b 100644 --- a/packages/walletkit/src/api/interfaces/index.ts +++ b/packages/walletkit/src/api/interfaces/index.ts @@ -22,3 +22,19 @@ export type { TONConnectSessionManager } from './TONConnectSessionManager'; // Streaming interfaces export type { StreamingProvider, StreamingProviderFactory } from './StreamingProvider'; export type { StreamingAPI } from './StreamingAPI'; + +export type { + LimitRequest, + NftItemsRequest, + NftItemsByOwnerRequest, + TransactionsByAddressRequest, + GetTransactionByHashRequest, + GetPendingTransactionsRequest, + GetTraceRequest, + GetPendingTraceRequest, + GetJettonsByOwnerRequest, + GetJettonsByAddressRequest, + GetEventsRequest, + GetEventsResponse, + ApiClient, +} from './ApiClient'; diff --git a/packages/walletkit/src/api/models/emulation/EmulationAction.ts b/packages/walletkit/src/api/models/emulation/EmulationAction.ts new file mode 100644 index 000000000..43e3ac95c --- /dev/null +++ b/packages/walletkit/src/api/models/emulation/EmulationAction.ts @@ -0,0 +1,93 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { UserFriendlyAddress, LogicalTime, Hex } from '../core/Primitives'; + +/** + * High-level action extracted from an emulated transaction trace. + */ +export interface EmulationAction { + /** + * Trace identifier this action belongs to + */ + traceId?: string; + + /** + * Hex-encoded unique identifier of the action + */ + actionId: Hex; + + /** + * Logical time when the action started + */ + startLt: LogicalTime; + + /** + * Logical time when the action ended + */ + endLt: LogicalTime; + + /** + * Unix timestamp when the action started + * @format timestamp + */ + startUtime: number; + + /** + * Unix timestamp when the action ended + * @format timestamp + */ + endUtime: number; + + /** + * Logical time when the trace ended + */ + traceEndLt: LogicalTime; + + /** + * Unix timestamp when the trace ended + * @format timestamp + */ + traceEndUtime: number; + + /** + * Masterchain block sequence number when the trace ended + * @format int + */ + traceMcSeqnoEnd: number; + + /** + * Hex-encoded hashes of transactions involved in this action + */ + transactions: Hex[]; + + /** + * Whether the action completed successfully + */ + isSuccess: boolean; + + /** + * Action type identifier (e.g. "jetton_transfer", "ton_transfer", "jetton_swap") + */ + type: string; + + /** + * Hex-encoded external message hash of the root trace + */ + traceExternalHash: Hex; + + /** + * Addresses of accounts involved in this action + */ + accounts: UserFriendlyAddress[]; + + /** + * Action-specific detail fields keyed by name + */ + details: { [key: string]: unknown }; +} diff --git a/packages/walletkit/src/api/models/emulation/EmulationAddressBookEntry.ts b/packages/walletkit/src/api/models/emulation/EmulationAddressBookEntry.ts new file mode 100644 index 000000000..4712c1c7d --- /dev/null +++ b/packages/walletkit/src/api/models/emulation/EmulationAddressBookEntry.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { UserFriendlyAddress } from '../core/Primitives'; + +/** + * Address book entry providing human-readable metadata for an on-chain address. + */ +export interface EmulationAddressBookEntry { + /** + * DNS domain name associated with the address, if any + */ + domain?: string; + + /** + * User-friendly representation of the address + */ + userFriendly: UserFriendlyAddress; + + /** + * List of known interfaces implemented by the contract + */ + interfaces: string[]; +} diff --git a/packages/walletkit/src/api/models/emulation/EmulationMessage.ts b/packages/walletkit/src/api/models/emulation/EmulationMessage.ts new file mode 100644 index 000000000..5d1628b67 --- /dev/null +++ b/packages/walletkit/src/api/models/emulation/EmulationMessage.ts @@ -0,0 +1,122 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { UserFriendlyAddress, LogicalTime, Hex, Base64String } from '../core/Primitives'; +import type { TokenAmount } from '../core/TokenAmount'; +import type { ExtraCurrencies } from '../core/ExtraCurrencies'; + +/** + * Message sent or received within an emulated transaction trace. + */ +export interface EmulationMessage { + /** + * Hex-encoded hash of the message + */ + hash: Hex; + + /** + * Hex-encoded normalized hash used for deduplication across message variants + */ + normalizedHash?: Hex; + + /** + * Source address of the message, or undefined for external inbound messages + */ + source?: UserFriendlyAddress; + + /** + * Destination address of the message + */ + destination: UserFriendlyAddress; + + /** + * Amount of nanotons transferred, or undefined for external inbound messages + */ + value?: TokenAmount; + + /** + * Extra currencies transferred with the message + */ + valueExtraCurrencies: ExtraCurrencies; + + /** + * Forwarding fee in nanotons, or undefined for external inbound messages + */ + fwdFee?: TokenAmount; + + /** + * IHR (Instant Hypercube Routing) fee in nanotons, or undefined for external inbound messages + */ + ihrFee?: TokenAmount; + + /** + * Logical time when the message was created, or undefined for external inbound messages + */ + createdLt?: LogicalTime; + + /** + * Unix timestamp when the message was created, or undefined for external inbound messages + * @format timestamp + */ + createdAt?: number; + + /** + * Hex-encoded opcode from the message body, if present + */ + opcode?: Hex; + + /** + * Whether IHR delivery is disabled, or undefined for external inbound messages + */ + ihrDisabled?: boolean; + + /** + * Whether the message requested a bounce on failure, or undefined for external inbound messages + */ + isBounce?: boolean; + + /** + * Whether the message was bounced back, or undefined for external inbound messages + */ + isBounced?: boolean; + + /** + * Import fee paid for delivering an external inbound message, undefined for all other message types + */ + importFee?: TokenAmount; + + /** + * Decoded content of the message body + */ + messageContent: EmulationMessageContent; + + /** + * Initial state (StateInit) attached to the message, if any + */ + initState?: unknown; +} + +/** + * Decoded content of an emulation message body. + */ +export interface EmulationMessageContent { + /** + * Hex-encoded hash of the message content, if available + */ + hash?: Hex; + + /** + * Message body in BOC base64 format, if available + */ + body?: Base64String; + + /** + * Structured decoded representation of the message body, if available + */ + decoded?: unknown; +} diff --git a/packages/walletkit/src/api/models/emulation/EmulationResponse.ts b/packages/walletkit/src/api/models/emulation/EmulationResponse.ts new file mode 100644 index 000000000..73c654607 --- /dev/null +++ b/packages/walletkit/src/api/models/emulation/EmulationResponse.ts @@ -0,0 +1,64 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { Hex, Base64String } from '../core/Primitives'; +import type { EmulationAction } from './EmulationAction'; +import type { EmulationAddressBookEntry } from './EmulationAddressBookEntry'; +import type { EmulationTraceNode } from './EmulationTraceNode'; +import type { EmulationTransaction } from './EmulationTransaction'; + +/** + * Unified emulation response model, normalised from either Toncenter or TonAPI sources. + */ +export interface EmulationResponse { + /** + * Masterchain block sequence number used during emulation + * @format int + */ + mcBlockSeqno: number; + + /** + * Root node of the transaction execution tree + */ + trace: EmulationTraceNode; + + /** + * Map of transaction hashes to transaction details + */ + transactions: { [key: string]: EmulationTransaction }; + + /** + * High-level actions extracted from the trace + */ + actions: EmulationAction[]; + + /** + * Random seed used during emulation, hex-encoded + */ + randSeed: Hex; + + /** + * Whether the trace is incomplete due to limits or errors + */ + isIncomplete: boolean; + + /** + * Map of code cell hashes to their BOC base64 representations + */ + codeCells: { [key: string]: Base64String }; + + /** + * Map of data cell hashes to their BOC base64 representations + */ + dataCells: { [key: string]: Base64String }; + + /** + * Address book mapping raw addresses to human-readable metadata + */ + addressBook: { [key: string]: EmulationAddressBookEntry }; +} diff --git a/packages/walletkit/src/api/models/emulation/EmulationResult.ts b/packages/walletkit/src/api/models/emulation/EmulationResult.ts new file mode 100644 index 000000000..e4450f13d --- /dev/null +++ b/packages/walletkit/src/api/models/emulation/EmulationResult.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { EmulationResponse } from './EmulationResponse'; + +/** + * Successful outcome of a transaction emulation attempt. + * Contains the full emulation response including trace and actions. + */ +export type EmulationResultSuccess = { + /** Discriminant tag indicating a successful emulation */ + result: 'success'; + /** The emulation response data including trace, actions, and messages */ + emulationResult: EmulationResponse; +}; + +/** + * Failed outcome of a transaction emulation attempt. + * Contains the error code and message describing why emulation could not complete. + */ +export type EmulationResultError = { + /** Discriminant tag indicating a failed emulation */ + result: 'error'; + /** Details of the error that caused emulation to fail */ + emulationError: EmulationError; +}; + +/** + * Result of a transaction emulation attempt. + * @discriminator result + */ +export type EmulationResult = EmulationResultSuccess | EmulationResultError; + +/** + * Error returned when transaction emulation fails. + */ +export interface EmulationError { + /** + * Numeric error code + * @format int + */ + code: number; + + /** + * Human-readable error message + */ + message: string; +} diff --git a/packages/walletkit/src/api/models/emulation/EmulationTraceNode.ts b/packages/walletkit/src/api/models/emulation/EmulationTraceNode.ts new file mode 100644 index 000000000..072341ae7 --- /dev/null +++ b/packages/walletkit/src/api/models/emulation/EmulationTraceNode.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { Hex } from '../core/Primitives'; + +/** + * Node in the emulation execution tree. + */ +export interface EmulationTraceNode { + /** + * Hex-encoded hash of the transaction at this node + */ + txHash: Hex; + + /** + * Hex-encoded hash of the incoming message that triggered this transaction + */ + inMsgHash?: Hex; + + /** + * Child nodes representing spawned messages + */ + children: EmulationTraceNode[]; +} diff --git a/packages/walletkit/src/api/models/emulation/EmulationTransaction.ts b/packages/walletkit/src/api/models/emulation/EmulationTransaction.ts new file mode 100644 index 000000000..f16f272d6 --- /dev/null +++ b/packages/walletkit/src/api/models/emulation/EmulationTransaction.ts @@ -0,0 +1,431 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { UserFriendlyAddress, LogicalTime, Hex } from '../core/Primitives'; +import type { TokenAmount } from '../core/TokenAmount'; +import type { ExtraCurrencies } from '../core/ExtraCurrencies'; +import type { EmulationMessage } from './EmulationMessage'; + +/** + * Account status on the TON blockchain. + */ +export type EmulationAccountStatus = 'active' | 'frozen' | 'uninit' | 'nonexist'; + +/** + * State of an account at a specific point in an emulated transaction. + */ +export interface EmulationAccountState { + /** + * Hex-encoded hash of the account state, if available + */ + hash?: Hex; + + /** + * Account balance in nanotons + */ + balance: TokenAmount; + + /** + * Extra currencies held by the account, if any + */ + extraCurrencies?: ExtraCurrencies; + + /** + * Account status + */ + accountStatus: EmulationAccountStatus; + + /** + * Hex-encoded hash of the frozen account state, if frozen + */ + frozenHash?: Hex; + + /** + * Hex-encoded hash of the contract data cell + */ + dataHash?: Hex; + + /** + * Hex-encoded hash of the contract code cell + */ + codeHash?: Hex; +} + +/** + * Reference to a block in the TON blockchain. + */ +export interface EmulationBlockRef { + /** + * Workchain identifier + * @format int + */ + workchain: number; + + /** + * Shard identifier + */ + shard: string; + + /** + * Block sequence number + * @format int + */ + seqno: number; +} + +/** + * Storage phase of transaction execution. + */ +export interface EmulationStoragePhase { + /** + * Storage fees collected during this phase in nanotons + */ + storageFeesCollected: TokenAmount; + + /** + * Account status change applied during the storage phase + */ + statusChange: string; +} + +/** + * Credit phase of transaction execution. + */ +export interface EmulationCreditPhase { + /** + * Amount credited to the account in nanotons + */ + credit: TokenAmount; +} + +/** + * Compute phase of transaction execution (TVM execution). + */ +export interface EmulationComputePhase { + /** + * Whether the compute phase was skipped + */ + isSkipped: boolean; + + /** + * Whether the TVM execution succeeded + */ + isSuccess: boolean; + + /** + * Whether the message state was used during compute + */ + isMsgStateUsed: boolean; + + /** + * Whether the account was activated during compute + */ + isAccountActivated: boolean; + + /** + * Gas fees charged in nanotons + */ + gasFees: TokenAmount; + + /** + * Total gas consumed + */ + gasUsed: string; + + /** + * Gas limit for this execution + */ + gasLimit: string; + + /** + * Gas credit, if any + */ + gasCredit?: string; + + /** + * Compute execution mode + * @format int + */ + mode: number; + + /** + * TVM exit code + * @format int + */ + exitCode: number; + + /** + * Number of TVM steps executed + * @format int + */ + vmSteps: number; + + /** + * Hex-encoded hash of the initial VM state + */ + vmInitStateHash?: Hex; + + /** + * Hex-encoded hash of the final VM state + */ + vmFinalStateHash?: Hex; +} + +/** + * Total size of messages created in the action phase. + */ +export interface EmulationActionMessageSize { + /** + * Number of cells used + * @format int + */ + cells: number; + + /** + * Number of bits used + * @format int + */ + bits: number; +} + +/** + * Action phase of transaction execution (outgoing message sending). + */ +export interface EmulationActionPhase { + /** + * Whether the action phase succeeded + */ + isSuccess: boolean; + + /** + * Whether the action list was valid + */ + isValid: boolean; + + /** + * Whether the transaction failed due to insufficient funds + */ + hasNoFunds: boolean; + + /** + * Account status change applied during the action phase + */ + statusChange: string; + + /** + * Total forwarding fees charged in nanotons + */ + totalFwdFees?: TokenAmount; + + /** + * Total action fees charged in nanotons + */ + totalActionFees?: TokenAmount; + + /** + * Result code of the action phase + * @format int + */ + resultCode: number; + + /** + * Total number of actions processed + * @format int + */ + totalActions: number; + + /** + * Number of special actions executed + * @format int + */ + specActions: number; + + /** + * Number of actions skipped + * @format int + */ + skippedActions: number; + + /** + * Number of messages created + * @format int + */ + msgsCreated: number; + + /** + * Hex-encoded hash of the action list + */ + actionListHash?: Hex; + + /** + * Total size of all messages created + */ + totalMsgSize: EmulationActionMessageSize; +} + +/** + * Detailed description of all execution phases in an emulated transaction. + */ +export interface EmulationTransactionDescription { + /** + * Transaction type (e.g. "ord", "ticktock", "storage") + */ + type: string; + + /** + * Whether the transaction was aborted + */ + isAborted: boolean; + + /** + * Whether the account was destroyed by this transaction + */ + isDestroyed: boolean; + + /** + * Whether the credit phase was executed before the storage phase + */ + isCreditFirst: boolean; + + /** + * Whether this was a tock transaction + */ + isTock: boolean; + + /** + * Whether a contract was installed in this transaction + */ + isInstalled: boolean; + + /** + * Storage phase data + */ + storagePhase: EmulationStoragePhase; + + /** + * Credit phase data, present only if credit was processed + */ + creditPhase?: EmulationCreditPhase; + + /** + * Compute phase data (TVM execution) + */ + computePhase: EmulationComputePhase; + + /** + * Action phase data, present only if actions were executed + */ + actionPhase?: EmulationActionPhase; +} + +/** + * Transaction within an emulated trace. + */ +export interface EmulationTransaction { + /** + * Address of the account that executed this transaction + */ + account: UserFriendlyAddress; + + /** + * Hex-encoded transaction hash + */ + hash: Hex; + + /** + * Logical time of the transaction + */ + lt: LogicalTime; + + /** + * Unix timestamp of the transaction + * @format timestamp + */ + now: number; + + /** + * Masterchain block sequence number + * @format int + */ + mcBlockSeqno: number; + + /** + * Hex-encoded external message hash of the root trace + */ + traceExternalHash: Hex; + + /** + * Hex-encoded hash of the previous transaction on this account + */ + prevTransHash?: Hex; + + /** + * Logical time of the previous transaction on this account + */ + prevTransLt?: LogicalTime; + + /** + * Account status before this transaction was applied + */ + origStatus: EmulationAccountStatus; + + /** + * Account status after this transaction was applied + */ + endStatus: EmulationAccountStatus; + + /** + * Total fees paid in nanotons + */ + totalFees: TokenAmount; + + /** + * Extra currencies paid as fees + */ + totalFeesExtraCurrencies: ExtraCurrencies; + + /** + * Detailed breakdown of transaction execution phases + */ + description: EmulationTransactionDescription; + + /** + * Block reference where this transaction was included + */ + blockRef: EmulationBlockRef; + + /** + * Incoming message that triggered this transaction, or undefined for tick-tock transactions + */ + inMsg?: EmulationMessage; + + /** + * Outgoing messages produced by this transaction + */ + outMsgs: EmulationMessage[]; + + /** + * Account state before the transaction was applied + */ + accountStateBefore: EmulationAccountState; + + /** + * Account state after the transaction was applied + */ + accountStateAfter: EmulationAccountState; + + /** + * Whether this transaction was produced by emulation rather than executed on-chain + */ + isEmulated: boolean; + + /** + * Trace identifier, if available + */ + traceId?: string; +} diff --git a/packages/walletkit/src/api/models/index.ts b/packages/walletkit/src/api/models/index.ts index 86668aecd..3864dccab 100644 --- a/packages/walletkit/src/api/models/index.ts +++ b/packages/walletkit/src/api/models/index.ts @@ -54,6 +54,31 @@ export type { export type { RequestErrorEvent } from './bridge/RequestErrorEvent'; export type { TONConnectSession } from './sessions/TONConnectSession'; +// Emulation models +export type { EmulationAction } from './emulation/EmulationAction'; +export type { EmulationAddressBookEntry } from './emulation/EmulationAddressBookEntry'; +export type { EmulationMessage, EmulationMessageContent } from './emulation/EmulationMessage'; +export type { EmulationResponse } from './emulation/EmulationResponse'; +export type { + EmulationResult, + EmulationResultSuccess, + EmulationResultError, + EmulationError, +} from './emulation/EmulationResult'; +export type { EmulationTraceNode } from './emulation/EmulationTraceNode'; +export type { + EmulationTransaction, + EmulationAccountState, + EmulationAccountStatus, + EmulationBlockRef, + EmulationTransactionDescription, + EmulationStoragePhase, + EmulationCreditPhase, + EmulationComputePhase, + EmulationActionPhase, + EmulationActionMessageSize, +} from './emulation/EmulationTransaction'; + // Jetton models export type { Jetton } from './jettons/Jetton'; export type { JettonsRequest } from './jettons/JettonsRequest'; diff --git a/packages/walletkit/src/api/models/transactions/Transaction.ts b/packages/walletkit/src/api/models/transactions/Transaction.ts index 9634bc1e9..e914a5564 100644 --- a/packages/walletkit/src/api/models/transactions/Transaction.ts +++ b/packages/walletkit/src/api/models/transactions/Transaction.ts @@ -123,6 +123,7 @@ export type AccountStatus = | { type: 'active' } | { type: 'frozen' } | { type: 'uninit' } + | { type: 'nonexist' } | { type: 'unknown'; value: string }; /** @@ -527,11 +528,13 @@ export interface TransactionAction { export interface TransactionActionMessageSize { /** * The total number of cells used + * @format int */ - cells?: string; + cells?: number; /** * The total number of bits used + * @format int */ - bits?: string; + bits?: number; } diff --git a/packages/walletkit/src/clients/tonapi/ApiClientTonApi.ts b/packages/walletkit/src/clients/tonapi/ApiClientTonApi.ts index d930a212c..d537bf3a5 100644 --- a/packages/walletkit/src/clients/tonapi/ApiClientTonApi.ts +++ b/packages/walletkit/src/clients/tonapi/ApiClientTonApi.ts @@ -19,7 +19,7 @@ import type { GetTraceRequest, GetTransactionByHashRequest, TransactionsByAddressRequest, -} from '../../types/toncenter/ApiClient'; +} from '../../api/interfaces'; import { Network } from '../../api/models'; import type { Base64String, @@ -34,9 +34,10 @@ import type { UserFriendlyAddress, UserNFTsRequest, } from '../../api/models'; -import type { ToncenterEmulationResult } from '../../utils/toncenterEmulation'; +import type { EmulationResult } from '../../api/models'; import type { FullAccountState } from '../../types/toncenter/api'; -import type { ToncenterResponseJettonMasters, ToncenterTracesResponse } from '../../types/toncenter/emulation'; +import type { ToncenterTracesResponse } from '../../types/toncenter/emulation'; +import type { ToncenterResponseJettonMasters } from '../toncenter/types/jettons'; import { BaseApiClient } from '../BaseApiClient'; import type { BaseApiClientConfig } from '../BaseApiClient'; import { TonClientError } from '../TonClientError'; @@ -52,9 +53,11 @@ import type { TonApiDnsResolveResponse, TonApiDnsBackresolveResponse } from './t import type { TonApiMethodExecutionResult } from './types/methods'; import type { TonApiMasterchainHeadResponse } from './types/masterchain'; import { mapTonApiGetMethodArgs, mapTonApiTvmStackRecord } from './mappers/map-methods'; +import { mapTonApiEmulationResponse } from './mappers/map-emulation'; import { Base64Normalize, Base64ToBigInt, Base64ToHex, getNormalizedExtMessageHash, isHex } from '../../utils'; import type { TonApiTransactionsResponse, TonApiTransaction } from './types/transactions'; import type { TonApiTrace } from './types/traces'; +import type { TonApiMessageConsequences } from './types/emulation'; import type { TonApiAccountEventsResponse } from './types/events'; import { mapTonApiTransaction } from './mappers/map-transactions'; import { mapTonApiTrace, mapTonApiTraceTransaction } from './mappers/map-traces'; @@ -166,8 +169,15 @@ export class ApiClientTonApi extends BaseApiClient implements ApiClient { return Base64ToBigInt(hash).toString(16); } - async fetchEmulation(_messageBoc: Base64String, _ignoreSignature?: boolean): Promise { - throw new Error('Method not implemented.'); + async fetchEmulation(messageBoc: Base64String, ignoreSignature?: boolean): Promise { + const result = await this.postJson('/v2/wallet/emulate', { + boc: messageBoc, + ignore_signature_check: ignoreSignature === true, + }); + return { + result: 'success', + emulationResult: mapTonApiEmulationResponse(result), + }; } async runGetMethod( @@ -300,23 +310,23 @@ export class ApiClientTonApi extends BaseApiClient implements ApiClient { throw new Error('Failed to fetch pending trace'); } - async resolveDnsWallet(domain: string): Promise { + async resolveDnsWallet(domain: string): Promise { try { const raw = await this.getJson(`/v2/dns/${domain}/resolve`); const address = raw?.wallet?.address; - return address ? asAddressFriendly(address) : null; + return address ? asAddressFriendly(address) : undefined; } catch (_e) { - return null; + return undefined; } } - async backResolveDnsWallet(address: UserFriendlyAddress): Promise { + async backResolveDnsWallet(address: UserFriendlyAddress): Promise { try { const raw = await this.getJson(`/v2/accounts/${address}/dns/backresolve`); - return raw.domains && raw.domains.length > 0 ? raw.domains[0] : null; + return raw.domains && raw.domains.length > 0 ? raw.domains[0] : undefined; } catch (_e) { - return null; + return undefined; } } diff --git a/packages/walletkit/src/clients/tonapi/TONAPI_INCOMPATIBILITIES.md b/packages/walletkit/src/clients/tonapi/TONAPI_INCOMPATIBILITIES.md index 3a2696d90..5efbe86e7 100644 --- a/packages/walletkit/src/clients/tonapi/TONAPI_INCOMPATIBILITIES.md +++ b/packages/walletkit/src/clients/tonapi/TONAPI_INCOMPATIBILITIES.md @@ -17,6 +17,3 @@ ## `nftItemsByOwner` - ❌ No `codeHash`, `dataHash` for items and collections - -## Not implemented -- `fetchEmulation` diff --git a/packages/walletkit/src/clients/tonapi/mappers/map-emulation.spec.ts b/packages/walletkit/src/clients/tonapi/mappers/map-emulation.spec.ts new file mode 100644 index 000000000..7a9eccc28 --- /dev/null +++ b/packages/walletkit/src/clients/tonapi/mappers/map-emulation.spec.ts @@ -0,0 +1,164 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { describe, expect, it } from 'vitest'; + +import type { TonApiMessageConsequences } from '../types/emulation'; +import type { TonApiTrace } from '../types/traces'; +import { mapTonApiEmulationResponse } from './map-emulation'; + +const ROOT_HASH = 'rootHashABCDEFGH'; +const CHILD_HASH = 'childHashABCDEFG'; +const EXT_MSG_HASH = 'extMsgHashABCDEF'; +const INT_MSG_HASH = 'intMsgHashABCDEF'; + +const SENDER = '0:1111111111111111111111111111111111111111111111111111111111111111'; +const RECIPIENT = '0:2222222222222222222222222222222222222222222222222222222222222222'; + +function makeConsequences(trace: TonApiTrace): TonApiMessageConsequences { + return { + trace, + risk: { transfer_all_remaining_balance: false, ton: 100000000, jettons: [], nfts: [] }, + event: { + event_id: ROOT_HASH, + timestamp: 1700000000, + actions: [], + account: { address: SENDER }, + lt: '1000000', + }, + }; +} + +describe('mapTonApiEmulationResponse', () => { + it('derives outMsgs from children when out_msgs is empty', () => { + const trace: TonApiTrace = { + transaction: { + hash: ROOT_HASH, + lt: '1000000', + account: { address: SENDER }, + in_msg: { hash: EXT_MSG_HASH, source: undefined, destination: { address: SENDER } }, + out_msgs: [], + }, + children: [ + { + transaction: { + hash: CHILD_HASH, + lt: '1000001', + account: { address: RECIPIENT }, + in_msg: { + hash: INT_MSG_HASH, + source: { address: SENDER }, + destination: { address: RECIPIENT }, + value: '100000000', + }, + out_msgs: [], + }, + }, + ], + }; + + const result = mapTonApiEmulationResponse(makeConsequences(trace)); + // root tx has no in_msg source (external message) + const rootTx = Object.values(result.transactions).find((tx) => !tx.inMsg?.source); + + expect(rootTx).toBeDefined(); + expect(rootTx!.outMsgs).toHaveLength(1); + expect(rootTx!.outMsgs[0].hash).toBeTruthy(); + }); + + it('keeps existing out_msgs when already populated', () => { + const trace: TonApiTrace = { + transaction: { + hash: ROOT_HASH, + lt: '1000000', + account: { address: SENDER }, + in_msg: { hash: EXT_MSG_HASH, source: undefined, destination: { address: SENDER } }, + out_msgs: [ + { + hash: INT_MSG_HASH, + source: { address: SENDER }, + destination: { address: RECIPIENT }, + value: '100000000', + }, + ], + }, + }; + + const result = mapTonApiEmulationResponse(makeConsequences(trace)); + const rootHashHex = Object.keys(result.transactions)[0]; + const rootTx = result.transactions[rootHashHex]; + + expect(rootTx!.outMsgs).toHaveLength(1); + }); + + it('sets isCreditFirst=true for external in_msg (bounce=false)', () => { + const trace: TonApiTrace = { + transaction: { + hash: ROOT_HASH, + lt: '1000000', + account: { address: SENDER }, + in_msg: { hash: EXT_MSG_HASH, source: undefined, destination: { address: SENDER }, bounce: false }, + out_msgs: [], + }, + }; + + const result = mapTonApiEmulationResponse(makeConsequences(trace)); + const rootHashHex = Object.keys(result.transactions)[0]; + const rootTx = result.transactions[rootHashHex]; + + expect(rootTx!.description.isCreditFirst).toBe(true); + }); + + it('sets isCreditFirst=true for internal in_msg with bounce=false', () => { + const trace: TonApiTrace = { + transaction: { + hash: CHILD_HASH, + lt: '1000001', + account: { address: RECIPIENT }, + in_msg: { + hash: INT_MSG_HASH, + source: { address: SENDER }, + destination: { address: RECIPIENT }, + value: '100000000', + bounce: false, + }, + out_msgs: [], + }, + }; + + const result = mapTonApiEmulationResponse(makeConsequences(trace)); + const txHashHex = Object.keys(result.transactions)[0]; + const tx = result.transactions[txHashHex]; + + expect(tx!.description.isCreditFirst).toBe(true); + }); + + it('sets isCreditFirst=false for internal in_msg with bounce=true', () => { + const trace: TonApiTrace = { + transaction: { + hash: CHILD_HASH, + lt: '1000001', + account: { address: RECIPIENT }, + in_msg: { + hash: INT_MSG_HASH, + source: { address: SENDER }, + destination: { address: RECIPIENT }, + value: '100000000', + bounce: true, + }, + out_msgs: [], + }, + }; + + const result = mapTonApiEmulationResponse(makeConsequences(trace)); + const txHashHex = Object.keys(result.transactions)[0]; + const tx = result.transactions[txHashHex]; + + expect(tx!.description.isCreditFirst).toBe(false); + }); +}); diff --git a/packages/walletkit/src/clients/tonapi/mappers/map-emulation.ts b/packages/walletkit/src/clients/tonapi/mappers/map-emulation.ts new file mode 100644 index 000000000..08aaf5bc1 --- /dev/null +++ b/packages/walletkit/src/clients/tonapi/mappers/map-emulation.ts @@ -0,0 +1,358 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { TonApiMessage, TonApiAccountRef } from '../types/transactions'; +import type { TonApiTrace } from '../types/traces'; +import type { TonApiMessageConsequences } from '../types/emulation'; +import type { TonApiAction, TonApiAccountEvent } from '../types/events'; +import type { + EmulationResponse, + EmulationTraceNode, + EmulationTransaction, + EmulationMessage, + EmulationAction, + EmulationAddressBookEntry, + EmulationAccountStatus, + Hex, +} from '../../../api/models'; +import { parseBlockRef, toHex } from './map-transactions'; +import { asHex } from '../../../utils/hex'; +import { asAddressFriendly, asMaybeAddressFriendly } from '../../../utils/address'; + +function mapTraceNode(trace: TonApiTrace): EmulationTraceNode { + return { + txHash: toHex(trace.transaction.hash), + inMsgHash: trace.transaction.in_msg?.hash ? toHex(trace.transaction.in_msg.hash) : undefined, + children: (trace.children ?? []).map(mapTraceNode), + }; +} + +function mapMessage(raw: TonApiMessage, kind: 'in' | 'out'): EmulationMessage { + const extraCurrencies: Record = {}; + for (const c of raw.value_extra ?? []) { + extraCurrencies[String(c.id)] = String(c.amount ?? 0); + } + // External in_msgs have no source; TonAPI returns 0 for value/fees on them, but correct value is null + const isExternal = !raw.source; + return { + hash: toHex(raw.hash), + source: raw.source?.address ? asAddressFriendly(raw.source.address) : undefined, + destination: raw.destination?.address ? (asAddressFriendly(raw.destination.address) ?? '') : '', + value: isExternal ? undefined : raw.value != null ? String(raw.value) : undefined, + valueExtraCurrencies: extraCurrencies, + fwdFee: isExternal ? undefined : raw.fwd_fee != null ? String(raw.fwd_fee) : undefined, + ihrFee: isExternal ? undefined : raw.ihr_fee != null ? String(raw.ihr_fee) : undefined, + // ext_in messages don't have created_lt/created_at/ihr_disabled/bounce/bounced in TON protocol + createdLt: isExternal ? undefined : raw.created_lt != null ? String(raw.created_lt) : undefined, + createdAt: isExternal ? undefined : raw.created_at != null ? Number(raw.created_at) : undefined, + opcode: raw.op_code ? asHex(raw.op_code) : undefined, + ihrDisabled: isExternal ? undefined : (raw.ihr_disabled ?? undefined), + isBounce: isExternal ? undefined : (raw.bounce ?? undefined), + isBounced: isExternal ? undefined : (raw.bounced ?? undefined), + // importFee only applies to external in_msgs; internal in_msgs and out_msgs have undefined + importFee: + kind === 'out' || !isExternal ? undefined : raw.import_fee != null ? String(raw.import_fee) : undefined, + messageContent: { + hash: undefined, + body: undefined, + decoded: raw.decoded_body ?? undefined, + }, + initState: undefined, + }; +} + +function mapAccountStatus(status: string | undefined): EmulationAccountStatus { + if (status === 'active') return 'active'; + if (status === 'frozen') return 'frozen'; + if (status === 'nonexist') return 'nonexist'; + return 'uninit'; +} + +function normalizeTransactionType(type: string | undefined): string { + switch (type) { + case 'TransOrd': + return 'ord'; + case 'TransTickTock': + return 'ticktock'; + case 'TransStorage': + return 'storage'; + case 'TransCreditFirst': + return 'credit_first'; + default: + return type ?? 'ord'; + } +} + +function normalizeStatusChange(status: string | undefined): string { + if (!status) return 'unchanged'; + return status.startsWith('acst_') ? status.slice(5) : status; +} + +function mapTransaction(traceNode: TonApiTrace, rootHash: Hex): EmulationTransaction { + const raw = traceNode.transaction; + // TonAPI omits out_msgs for child transactions — derive them from children's in_msg + const childInMsgs = (traceNode.children ?? []) + .map((c) => c.transaction.in_msg) + .filter((m): m is TonApiMessage => m != null); + const outMsgs = raw.out_msgs && raw.out_msgs.length > 0 ? raw.out_msgs : childInMsgs; + + const blockRef = parseBlockRef(raw.block); + return { + account: asAddressFriendly(raw.account.address), + hash: toHex(raw.hash), + lt: String(raw.lt ?? 0), + now: Number(raw.utime ?? 0), + mcBlockSeqno: blockRef.seqno, + traceExternalHash: rootHash, + prevTransHash: raw.prev_trans_hash ? toHex(raw.prev_trans_hash) : undefined, + prevTransLt: raw.prev_trans_lt != null ? String(raw.prev_trans_lt) : undefined, + origStatus: mapAccountStatus(raw.orig_status), + endStatus: mapAccountStatus(raw.end_status), + totalFees: String(raw.total_fees ?? 0), + totalFeesExtraCurrencies: {}, + description: { + type: normalizeTransactionType(raw.transaction_type), + isAborted: raw.aborted ?? !(raw.success ?? true), + isDestroyed: raw.destroyed ?? false, + // TonAPI doesn't expose credit_first; derive from bounce flag: + // external (bounce=false/null) and internal bounce=false → credit_first=true + // internal bounce=true → credit_first=false + isCreditFirst: !raw.in_msg?.bounce, + isTock: false, + isInstalled: false, + storagePhase: { + storageFeesCollected: String(raw.storage_phase?.storage_fees_collected ?? 0), + statusChange: normalizeStatusChange(raw.storage_phase?.status_change), + }, + creditPhase: raw.credit_phase?.credit != null ? { credit: String(raw.credit_phase.credit) } : undefined, + computePhase: { + isSkipped: raw.compute_phase?.skipped ?? false, + isSuccess: raw.compute_phase?.success ?? raw.success ?? true, + isMsgStateUsed: false, + isAccountActivated: false, + gasFees: String(raw.compute_phase?.gas_fees ?? 0), + gasUsed: String(raw.compute_phase?.gas_used ?? 0), + gasLimit: String(raw.compute_phase?.gas_used ?? 0), + mode: 0, + exitCode: raw.compute_phase?.exit_code ?? (raw.success ? 0 : 1), + vmSteps: raw.compute_phase?.vm_steps ?? 0, + }, + actionPhase: raw.action_phase + ? { + isSuccess: raw.action_phase.success ?? raw.success ?? true, + isValid: true, + hasNoFunds: false, + statusChange: 'unchanged', + totalFwdFees: String(raw.action_phase.fwd_fees ?? 0), + totalActionFees: String(raw.action_phase.total_fees ?? 0), + resultCode: raw.action_phase.result_code ?? 0, + totalActions: raw.action_phase.total_actions ?? 0, + specActions: 0, + skippedActions: raw.action_phase.skipped_actions ?? 0, + msgsCreated: outMsgs.length, + totalMsgSize: { cells: 0, bits: 0 }, + } + : undefined, + }, + blockRef, + inMsg: raw.in_msg ? mapMessage(raw.in_msg, 'in') : undefined, + outMsgs: outMsgs.map((m) => mapMessage(m, 'out')), + accountStateBefore: { + balance: String(raw.end_balance ?? 0), + accountStatus: mapAccountStatus(raw.orig_status), + }, + accountStateAfter: { + balance: String(raw.end_balance ?? 0), + accountStatus: mapAccountStatus(raw.end_status), + }, + isEmulated: true, + traceId: undefined, + }; +} + +function flattenTrace(trace: TonApiTrace): TonApiTrace[] { + return [trace, ...(trace.children ?? []).flatMap(flattenTrace)]; +} + +function buildAddressBook(traces: TonApiTrace[]): Record { + const book: Record = {}; + + function addRef(ref: TonApiAccountRef | undefined) { + if (!ref?.address) return; + const key = ref.address.toUpperCase(); + if (!book[key]) { + book[key] = { + userFriendly: asAddressFriendly(ref.address), + domain: ref.name ?? undefined, + interfaces: [], + }; + } + } + + for (const node of traces) { + const tx = node.transaction; + addRef(tx.account); + if (tx.in_msg?.source) addRef(tx.in_msg.source); + if (tx.in_msg?.destination) addRef(tx.in_msg.destination); + for (const msg of tx.out_msgs ?? []) { + if (msg.source) addRef(msg.source); + if (msg.destination) addRef(msg.destination); + } + } + + return book; +} + +function normalizeJettonTransferDetails(payload: Record): Record { + const sender = payload.sender as { address?: string } | undefined; + const recipient = payload.recipient as { address?: string } | undefined; + const jetton = payload.jetton as { address?: string } | undefined; + return { + asset: jetton?.address ?? null, + sender: sender?.address ?? null, + receiver: recipient?.address ?? null, + sender_jetton_wallet: (payload.senders_wallet as string | undefined) ?? null, + receiver_jetton_wallet: (payload.recipients_wallet as string | undefined) ?? null, + amount: payload.amount ?? '0', + comment: (payload.comment as string | undefined) ?? null, + is_encrypted_comment: (payload.is_encrypted_comment as boolean | undefined) ?? false, + query_id: payload.query_id ?? '0', + response_destination: payload.response_destination ?? null, + custom_payload: payload.custom_payload ?? null, + forward_payload: payload.forward_payload ?? null, + forward_amount: payload.forward_amount ?? '0', + }; +} + +function mapAction(action: TonApiAction, event: TonApiAccountEvent, rootHash: Hex): EmulationAction { + const lt = String(event.lt ?? 0); + const utime = Number(event.timestamp ?? 0); + const actionId = toHex(String(action.base_transactions?.[0] ?? event.event_id)); + const transactions = (action.base_transactions ?? []).map((h) => toHex(String(h))); + + let type = action.type ?? 'unknown'; + let details: Record = {}; + + const payload = type ? (action[type] as Record | undefined) : undefined; + + if (type === 'TonTransfer' && payload) { + type = 'ton_transfer'; + details = { + source: (payload.sender as { address?: string } | undefined)?.address ?? '', + destination: (payload.recipient as { address?: string } | undefined)?.address ?? '', + value: String(payload.amount ?? 0), + value_extra_currencies: null, + comment: (payload.comment as string | undefined) ?? null, + encrypted: false, + }; + } else if (type === 'JettonTransfer' && payload) { + type = 'jetton_transfer'; + details = normalizeJettonTransferDetails(payload); + } else if (type === 'NftItemTransfer' && payload) { + type = 'nft_transfer'; + const sender = payload.sender as { address?: string } | undefined; + const recipient = payload.recipient as { address?: string } | undefined; + details = { + nft_collection: null, + nft_item: (payload.nft as string | undefined) ?? null, + nft_item_index: null, + old_owner: sender?.address ?? null, + new_owner: recipient?.address ?? null, + is_purchase: false, + query_id: '0', + response_destination: null, + custom_payload: null, + forward_payload: null, + forward_amount: '0', + comment: null, + is_encrypted_comment: false, + marketplace: null, + }; + } else if (type === 'JettonSwap' && payload) { + type = 'jetton_swap'; + const inAsset = payload.in as { jetton?: { address?: string }; amount?: string } | undefined; + const outAsset = payload.out as { jetton?: { address?: string }; amount?: string } | undefined; + const userWallet = payload.user_wallet as { address?: string } | undefined; + const router = payload.router as { address?: string } | undefined; + details = { + dex: payload.dex ?? '', + sender: userWallet?.address ?? '', + dex_incoming_transfer: { + asset: inAsset?.jetton?.address ?? 'TON', + source: userWallet?.address ?? '', + destination: router?.address ?? '', + source_jetton_wallet: null, + destination_jetton_wallet: null, + amount: String(payload.amount_in ?? payload.ton_in ?? inAsset?.amount ?? 0), + }, + dex_outgoing_transfer: { + asset: outAsset?.jetton?.address ?? 'TON', + source: router?.address ?? '', + destination: userWallet?.address ?? '', + source_jetton_wallet: null, + destination_jetton_wallet: null, + amount: String(payload.amount_out ?? payload.ton_out ?? outAsset?.amount ?? 0), + }, + peer_swaps: [], + }; + } else if (type === 'ContractDeploy' && payload) { + type = 'contract_deploy'; + const addr = payload.address as string | undefined; + details = { + opcode: null, + destination: addr ?? null, + }; + } else if (payload) { + details = payload; + } + + return { + actionId, + startLt: lt, + endLt: lt, + startUtime: utime, + endUtime: utime, + traceEndLt: '0', + traceEndUtime: 0, + traceMcSeqnoEnd: 0, + transactions, + isSuccess: action.status === 'ok', + type, + traceExternalHash: rootHash, + accounts: (action.simple_preview?.accounts ?? []).map((a) => { + const addr = typeof a === 'string' ? a : (a as { address: string }).address; + return asMaybeAddressFriendly(addr) ?? addr; + }), + details, + }; +} + +export function mapTonApiEmulationResponse(result: TonApiMessageConsequences): EmulationResponse { + const rootTxHash = toHex(result.trace.transaction.hash); + // Use the external in_msg hash as the trace identifier — matches Toncenter's traceExternalHash convention. + const externalHash = result.trace.transaction.in_msg?.hash + ? toHex(result.trace.transaction.in_msg.hash) + : rootTxHash; + const allTraces = flattenTrace(result.trace); + const transactions = Object.fromEntries( + allTraces.map((traceNode) => [toHex(traceNode.transaction.hash), mapTransaction(traceNode, externalHash)]), + ); + const actions = (result.event.actions ?? []).map((a) => mapAction(a, result.event, externalHash)); + + return { + mcBlockSeqno: transactions[rootTxHash]?.mcBlockSeqno ?? 0, + trace: mapTraceNode(result.trace), + transactions, + actions, + randSeed: asHex('0x' + '0'.repeat(64)), + isIncomplete: false, + codeCells: {}, + dataCells: {}, + addressBook: buildAddressBook(allTraces), + }; +} diff --git a/packages/walletkit/src/clients/tonapi/mappers/map-events.ts b/packages/walletkit/src/clients/tonapi/mappers/map-events.ts index c82281100..8393c7519 100644 --- a/packages/walletkit/src/clients/tonapi/mappers/map-events.ts +++ b/packages/walletkit/src/clients/tonapi/mappers/map-events.ts @@ -7,10 +7,10 @@ */ import { toAccount } from '../../../types/toncenter/AccountEvent'; -import type { TonApiAccountEvent, TonApiAccountRef } from '../types/events'; +import type { TonApiAccountEvent, TonApiSimplePreviewAccount } from '../types/events'; import { toHex } from './map-transactions'; -export function normalizeTonApiAccountAddress(account: TonApiAccountRef): string { +export function normalizeTonApiAccountAddress(account: TonApiSimplePreviewAccount): string { if (typeof account === 'string') { return account; } @@ -20,7 +20,7 @@ export function normalizeTonApiAccountAddress(account: TonApiAccountRef): string export function mapTonApiEvent(raw: TonApiAccountEvent) { return { eventId: toHex(raw.event_id), - account: toAccount(raw.account, {}), + account: toAccount(raw.account.address, {}), timestamp: Number(raw.timestamp ?? 0), actions: (raw.actions ?? []).map((action) => { const status: 'success' | 'failure' = action.status === 'failed' ? 'failure' : 'success'; diff --git a/packages/walletkit/src/clients/tonapi/mappers/map-jetton-masters.ts b/packages/walletkit/src/clients/tonapi/mappers/map-jetton-masters.ts index e6ef85386..e6f221305 100644 --- a/packages/walletkit/src/clients/tonapi/mappers/map-jetton-masters.ts +++ b/packages/walletkit/src/clients/tonapi/mappers/map-jetton-masters.ts @@ -8,7 +8,7 @@ import { Address } from '@ton/core'; -import type { ToncenterResponseJettonMasters } from '../../../types/toncenter/emulation'; +import type { ToncenterResponseJettonMasters } from '../../toncenter/types/jettons'; import type { TonApiJettonInfo } from '../types/jettons'; import { asAddressFriendly } from '../../../utils/address'; import type { AddressBookRowV3 } from '../../../types/toncenter/v3/AddressBookRowV3'; diff --git a/packages/walletkit/src/clients/tonapi/mappers/map-traces.ts b/packages/walletkit/src/clients/tonapi/mappers/map-traces.ts index e627b1399..7c5a4a56b 100644 --- a/packages/walletkit/src/clients/tonapi/mappers/map-traces.ts +++ b/packages/walletkit/src/clients/tonapi/mappers/map-traces.ts @@ -25,14 +25,23 @@ export function mapTraceStatus(status: string | undefined): 'active' | 'frozen' return status; } -export function flattenTrace(trace: TonApiTrace): TonApiTransaction[] { - const out: TonApiTransaction[] = [trace.transaction]; +export function flattenTrace(trace: TonApiTrace): TonApiTrace[] { + const out: TonApiTrace[] = [trace]; for (const child of trace.children ?? []) { out.push(...flattenTrace(child)); } return out; } +function resolveOutMsgs(node: TonApiTrace): TonApiTransaction { + const raw = node.transaction; + if (raw.out_msgs && raw.out_msgs.length > 0) return raw; + const childInMsgs = (node.children ?? []) + .map((c) => c.transaction.in_msg) + .filter((m): m is NonNullable => m != null); + return { ...raw, out_msgs: childInMsgs }; +} + export function mapTonApiTraceNode(trace: TonApiTrace): EmulationTraceNode { return { tx_hash: trace.transaction.hash, @@ -94,7 +103,7 @@ export function mapTonApiTraceTransaction(raw: TonApiTransaction): ToncenterTran type: raw.transaction_type ?? 'ord', aborted: raw.aborted ?? !(raw.success ?? true), destroyed: raw.destroyed ?? false, - credit_first: false, + credit_first: !raw.in_msg?.bounce, is_tock: false, installed: false, storage_ph: { @@ -172,14 +181,19 @@ export function mapTonApiTrace( trace: TonApiTrace, mapTraceTransaction: (tx: TonApiTransaction) => ToncenterTransaction, ): ToncenterTracesResponse { - const traceTransactions = flattenTrace(trace); - const transactions = Object.fromEntries(traceTransactions.map((tx) => [tx.hash, mapTraceTransaction(tx)])); - const transactionsOrder = [...traceTransactions] - .sort((a, b) => (BigInt(a.lt ?? 0) < BigInt(b.lt ?? 0) ? -1 : 1)) - .map((tx) => tx.hash); + const traceNodes = flattenTrace(trace); + const transactions = Object.fromEntries( + traceNodes.map((node) => { + const tx = resolveOutMsgs(node); + return [tx.hash, mapTraceTransaction(tx)]; + }), + ); + const transactionsOrder = [...traceNodes] + .sort((a, b) => (BigInt(a.transaction.lt ?? 0) < BigInt(b.transaction.lt ?? 0) ? -1 : 1)) + .map((node) => node.transaction.hash); - const lts = traceTransactions.map((tx) => BigInt(tx.lt ?? 0)); - const times = traceTransactions.map((tx) => Number(tx.utime ?? 0)); + const lts = traceNodes.map((node) => BigInt(node.transaction.lt ?? 0)); + const times = traceNodes.map((node) => Number(node.transaction.utime ?? 0)); const startLt = lts.length > 0 ? lts.reduce((min, value) => (value < min ? value : min), lts[0]) : 0n; const endLt = lts.length > 0 ? lts.reduce((max, value) => (value > max ? value : max), lts[0]) : 0n; @@ -187,9 +201,10 @@ export function mapTonApiTrace( const endUtime = times.length > 0 ? Math.max(...times) : 0; const traceId = trace.transaction.hash; - const rootTx = mapTraceTransaction(trace.transaction); - const messagesCount = traceTransactions.reduce( - (acc, tx) => acc + (tx.in_msg ? 1 : 0) + (tx.out_msgs?.length ?? 0), + const rootTx = mapTraceTransaction(resolveOutMsgs(trace)); + const messagesCount = traceNodes.reduce( + (acc, node) => + acc + (node.transaction.in_msg ? 1 : 0) + (node.transaction.out_msgs?.length ?? node.children?.length ?? 0), 0, ); @@ -214,7 +229,7 @@ export function mapTonApiTrace( messages: messagesCount, pending_messages: 0, trace_state: 'complete', - transactions: traceTransactions.length, + transactions: traceNodes.length, }, transactions, transactions_order: transactionsOrder, diff --git a/packages/walletkit/src/clients/tonapi/mappers/map-transactions.ts b/packages/walletkit/src/clients/tonapi/mappers/map-transactions.ts index f5c02a809..1a57d6447 100644 --- a/packages/walletkit/src/clients/tonapi/mappers/map-transactions.ts +++ b/packages/walletkit/src/clients/tonapi/mappers/map-transactions.ts @@ -137,8 +137,8 @@ export function mapTonApiDescription(raw: TonApiTransaction): TransactionDescrip skippedActionsNumber: raw.action_phase?.skipped_actions ?? 0, messagesCreatedNumber: raw.out_msgs?.length ?? 0, totalMessagesSize: { - cells: '0', - bits: '0', + cells: 0, + bits: 0, }, }, }; diff --git a/packages/walletkit/src/clients/tonapi/types/emulation.ts b/packages/walletkit/src/clients/tonapi/types/emulation.ts new file mode 100644 index 000000000..5a639a43b --- /dev/null +++ b/packages/walletkit/src/clients/tonapi/types/emulation.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { TonApiAccountEvent } from './events'; +import type { TonApiTrace } from './traces'; + +export interface TonApiJettonQuantity { + quantity: string; + jetton: { + address: string; + name?: string; + symbol?: string; + decimals?: number; + }; +} + +export interface TonApiRisk { + transfer_all_remaining_balance: boolean; + ton: number; + jettons: TonApiJettonQuantity[]; + nfts: unknown[]; +} + +export interface TonApiMessageConsequences { + trace: TonApiTrace; + risk: TonApiRisk; + event: TonApiAccountEvent; +} diff --git a/packages/walletkit/src/clients/tonapi/types/events.ts b/packages/walletkit/src/clients/tonapi/types/events.ts index ef0066307..ffb0ea7c4 100644 --- a/packages/walletkit/src/clients/tonapi/types/events.ts +++ b/packages/walletkit/src/clients/tonapi/types/events.ts @@ -11,7 +11,7 @@ export interface TonApiActionSimplePreview { description?: string; value?: string; value_image?: string; - accounts?: TonApiAccountRef[]; + accounts?: TonApiSimplePreviewAccount[]; } export interface TonApiAction { @@ -26,7 +26,7 @@ export interface TonApiAccountEvent { event_id: string; timestamp: number; actions: TonApiAction[]; - account: string; + account: { address: string }; is_scam?: boolean; lt?: string | number; in_progress?: boolean; @@ -37,4 +37,4 @@ export interface TonApiAccountEventsResponse { next_from?: number; } -export type TonApiAccountRef = string | { address: string }; +export type TonApiSimplePreviewAccount = string | { address: string }; diff --git a/packages/walletkit/src/clients/tonapi/types/transactions.ts b/packages/walletkit/src/clients/tonapi/types/transactions.ts index 1e345053d..6de4b617e 100644 --- a/packages/walletkit/src/clients/tonapi/types/transactions.ts +++ b/packages/walletkit/src/clients/tonapi/types/transactions.ts @@ -15,10 +15,17 @@ export interface TonApiExtraCurrency { amount: string | number; } +export interface TonApiAccountRef { + address: string; + name?: string; + is_scam?: boolean; + is_wallet?: boolean; +} + export interface TonApiMessage { hash: string; - source?: { address: string }; - destination?: { address: string }; + source?: TonApiAccountRef; + destination?: TonApiAccountRef; value?: string | number | null; value_extra?: TonApiExtraCurrency[]; fwd_fee?: string | number | null; @@ -48,6 +55,7 @@ export interface TonApiPhaseCompute { gas_fees?: string | number; gas_used?: string | number; exit_code?: number; + exit_code_description?: string; vm_steps?: number; } @@ -63,7 +71,7 @@ export interface TonApiPhaseAction { export interface TonApiTransaction { hash: string; lt: string | number; - account: { address: string }; + account: TonApiAccountRef; end_balance?: string | number; success?: boolean; utime?: number; diff --git a/packages/walletkit/src/clients/toncenter/ApiClientToncenter.ts b/packages/walletkit/src/clients/toncenter/ApiClientToncenter.ts index cf6dd38b9..514b44fd1 100644 --- a/packages/walletkit/src/clients/toncenter/ApiClientToncenter.ts +++ b/packages/walletkit/src/clients/toncenter/ApiClientToncenter.ts @@ -11,7 +11,8 @@ import { Address } from '@ton/core'; import { Base64ToBigInt, Base64Normalize, Base64ToHex } from '../../utils/base64'; import type { FullAccountState } from '../../types/toncenter/api'; -import type { JettonInfo, ToncenterEmulationResponse } from '../../types'; +import type { JettonInfo } from '../../types'; +import type { ToncenterEmulationResponse } from './types/raw-emulation'; import type { ApiClient, GetJettonsByOwnerRequest, @@ -23,21 +24,17 @@ import type { TransactionsByAddressRequest, GetEventsResponse, GetEventsRequest, -} from '../../types/toncenter/ApiClient'; -import type { NftItemsResponseV3 } from '../../types/toncenter/v3/NftItemsResponseV3'; -import { toNftItemsResponse } from '../../types/toncenter/v3/NftItemsResponseV3'; -import type { - ToncenterResponseJettonMasters, - ToncenterResponseJettonWallets, - ToncenterTracesResponse, - ToncenterTransactionsResponse, - EmulationTokenInfoMasters, -} from '../../types/toncenter/emulation'; -import { toTransactionsResponse } from '../../types/toncenter/emulation'; +} from '../../api/interfaces'; +import type { NftItemsResponseV3 } from './types/v3/NftItemsResponseV3'; +import { toNftItemsResponse } from './types/v3/NftItemsResponseV3'; +import type { ToncenterTracesResponse, ToncenterTransactionsResponse } from '../../types/toncenter/emulation'; +import type { ToncenterResponseJettonMasters, ToncenterResponseJettonWallets } from './types/jettons'; +import type { EmulationTokenInfoMasters } from './types/metadata'; +import { toTransactionsResponse } from './mappers/map-transactions'; import { CallForSuccess } from '../../utils/retry'; import { globalLogger } from '../../core/Logger'; -import type { DNSRecordsResponseV3 } from '../../types/toncenter/v3/DNSRecordsResponseV3'; -import { toDnsRecords } from '../../types/toncenter/v3/DNSRecordsResponseV3'; +import type { DNSRecordsResponseV3 } from './types/v3/DNSRecordsResponseV3'; +import { toDnsRecords } from './types/v3/DNSRecordsResponseV3'; import { toAddressBook, toEvent } from '../../types/toncenter/AccountEvent'; import { Network } from '../../api/models'; import type { @@ -55,10 +52,11 @@ import type { MasterchainInfo, } from '../../api/models'; import { asAddressFriendly } from '../../utils/address'; -import type { ToncenterEmulationResult } from '../../utils/toncenterEmulation'; +import type { EmulationResult } from '../../api/models'; +import { mapToncenterEmulationResponse } from './mappers/map-emulation'; import { BaseApiClient } from '../BaseApiClient'; import type { BaseApiClientConfig } from '../BaseApiClient'; -import type { V2AddressInformation, V2SendMessageResult, V3RunGetMethodRequest, TonBlockIdExt } from './types'; +import type { V2AddressInformation, V2SendMessageResult, V3RunGetMethodRequest, TonBlockIdExt } from './types/internal'; import { padBase64, parseInternalTransactionId, prepareAddress } from './utils'; import { TonClientError } from '../TonClientError'; import { isHex } from '../../utils'; @@ -102,7 +100,7 @@ export class ApiClientToncenter extends BaseApiClient implements ApiClient { return formattedResponse; } - async fetchEmulation(messageBoc: Base64String, ignoreSignature?: boolean): Promise { + async fetchEmulation(messageBoc: Base64String, ignoreSignature?: boolean): Promise { const props: Record = { boc: messageBoc, ignore_chksig: ignoreSignature === true, @@ -114,7 +112,7 @@ export class ApiClientToncenter extends BaseApiClient implements ApiClient { const response = await this.postJson('/api/emulate/v1/emulateTrace', props); return { result: 'success', - emulationResult: response, + emulationResult: mapToncenterEmulationResponse(response), }; } @@ -289,7 +287,7 @@ export class ApiClientToncenter extends BaseApiClient implements ApiClient { throw new Error('Failed to fetch pending trace'); } - async resolveDnsWallet(domain: string): Promise { + async resolveDnsWallet(domain: string): Promise { const response = toDnsRecords( await this.getJson('/api/v3/dns/records', { domain, @@ -302,10 +300,10 @@ export class ApiClientToncenter extends BaseApiClient implements ApiClient { return response.records[0].dnsWallet; } - return null; + return undefined; } - async backResolveDnsWallet(wallet: Address | string): Promise { + async backResolveDnsWallet(wallet: Address | string): Promise { if (wallet instanceof Address) { wallet = wallet.toString(); } @@ -322,7 +320,7 @@ export class ApiClientToncenter extends BaseApiClient implements ApiClient { return response.records[0].domain; } - return null; + return undefined; } async jettonsByAddress(request: GetJettonsByAddressRequest): Promise { diff --git a/packages/walletkit/src/clients/toncenter/mappers/map-emulation-trace.ts b/packages/walletkit/src/clients/toncenter/mappers/map-emulation-trace.ts new file mode 100644 index 000000000..da22e225b --- /dev/null +++ b/packages/walletkit/src/clients/toncenter/mappers/map-emulation-trace.ts @@ -0,0 +1,303 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { + AccountState, + AccountStatus, + Transaction, + TransactionMessage, + TransactionDescription, + TransactionTraceNode, + TransactionTraceAction, + TransactionTraceActionDetails, + TransactionTraceActionJettonSwapDetails, + TransactionTraceActionCallContractDetails, + TransactionTraceActionTONTransferDetails, + TransactionEmulatedTrace, + UserFriendlyAddress, + AddressBook, + EmulationResponse, + EmulationTraceNode as DomainEmulationTraceNode, + EmulationTransaction as DomainEmulationTransaction, + EmulationTransactionDescription as DomainEmulationTransactionDescription, + EmulationAccountState as DomainEmulationAccountState, + EmulationMessage as DomainEmulationMessage, + EmulationAction as DomainEmulationAction, + EmulationAddressBookEntry as DomainEmulationAddressBookEntry, + EmulationAccountStatus, +} from '../../../api/models'; +import { asMaybeAddressFriendly } from '../../../utils/address'; +import { parseMsgSizeCount } from '../utils'; +import type { + EmulationJettonSwapDetails, + EmulationCallContractDetails, + EmulationTonTransferDetails, +} from '../types/raw-emulation'; + +export function toTransactionEmulatedTrace(response: EmulationResponse): TransactionEmulatedTrace { + return { + mcBlockSeqno: response.mcBlockSeqno, + trace: domainTraceNodeToTransactionTraceNode(response.trace), + transactions: Object.fromEntries( + Object.entries(response.transactions ?? {}).map(([hash, tx]) => [hash, emulationTxToTransaction(tx)]), + ), + actions: response.actions.map(emulationActionToTransactionTraceAction), + randSeed: response.randSeed, + isIncomplete: response.isIncomplete, + codeCells: Object.fromEntries(Object.entries(response.codeCells ?? {}).map(([hash, cell]) => [hash, cell])), + dataCells: Object.fromEntries(Object.entries(response.dataCells ?? {}).map(([hash, cell]) => [hash, cell])), + metadata: {}, + addressBook: emulationAddressBookToAddressBook(response.addressBook), + }; +} + +function domainTraceNodeToTransactionTraceNode(node: DomainEmulationTraceNode): TransactionTraceNode { + return { + txHash: node.txHash, + inMsgHash: node.inMsgHash, + children: node.children?.map(domainTraceNodeToTransactionTraceNode) ?? [], + }; +} + +function emulationAccountStateToAccountState(state: DomainEmulationAccountState): AccountState { + return { + hash: state.hash ?? undefined, + balance: state.balance, + extraCurrencies: state.extraCurrencies ?? undefined, + accountStatus: toAccountStatus(state.accountStatus), + frozenHash: state.frozenHash ?? undefined, + dataHash: state.dataHash ?? undefined, + codeHash: state.codeHash ?? undefined, + }; +} + +function toAccountStatus(status: EmulationAccountStatus): AccountStatus { + switch (status) { + case 'active': + return { type: 'active' }; + case 'frozen': + return { type: 'frozen' }; + case 'uninit': + return { type: 'uninit' }; + case 'nonexist': + return { type: 'nonexist' }; + default: + return { type: 'unknown', value: status }; + } +} + +function emulationMsgToTransactionMessage(msg: DomainEmulationMessage): TransactionMessage { + return { + hash: msg.hash, + normalizedHash: msg.normalizedHash, + source: msg.source ?? undefined, + destination: msg.destination ?? undefined, + value: msg.value ?? undefined, + valueExtraCurrencies: msg.valueExtraCurrencies ?? undefined, + fwdFee: msg.fwdFee ?? undefined, + ihrFee: msg.ihrFee ?? undefined, + creationLogicalTime: msg.createdLt ?? undefined, + createdAt: msg.createdAt ?? undefined, + ihrDisabled: msg.ihrDisabled ?? undefined, + isBounce: msg.isBounce ?? undefined, + isBounced: msg.isBounced ?? undefined, + importFee: msg.importFee ?? undefined, + opcode: msg.opcode ?? undefined, + messageContent: { + hash: msg.messageContent.hash ?? undefined, + body: msg.messageContent.body ?? undefined, + decoded: msg.messageContent.decoded ?? undefined, + }, + }; +} + +function emulationDescToTransactionDescription(desc: DomainEmulationTransactionDescription): TransactionDescription { + return { + type: desc.type, + isAborted: desc.isAborted, + isDestroyed: desc.isDestroyed, + isCreditFirst: desc.isCreditFirst, + isTock: desc.isTock, + isInstalled: desc.isInstalled, + storagePhase: { + storageFeesCollected: desc.storagePhase.storageFeesCollected, + statusChange: desc.storagePhase.statusChange, + }, + creditPhase: desc.creditPhase ? { credit: desc.creditPhase.credit } : undefined, + computePhase: { + isSkipped: desc.computePhase.isSkipped, + isSuccess: desc.computePhase.isSuccess, + isMessageStateUsed: desc.computePhase.isMsgStateUsed, + isAccountActivated: desc.computePhase.isAccountActivated, + gasFees: desc.computePhase.gasFees, + gasUsed: desc.computePhase.gasUsed, + gasLimit: desc.computePhase.gasLimit, + gasCredit: desc.computePhase.gasCredit, + mode: desc.computePhase.mode, + exitCode: desc.computePhase.exitCode, + vmStepsNumber: desc.computePhase.vmSteps, + vmInitStateHash: desc.computePhase.vmInitStateHash, + vmFinalStateHash: desc.computePhase.vmFinalStateHash, + }, + action: desc.actionPhase + ? { + isSuccess: desc.actionPhase.isSuccess, + isValid: desc.actionPhase.isValid, + hasNoFunds: desc.actionPhase.hasNoFunds, + statusChange: desc.actionPhase.statusChange, + totalForwardingFees: desc.actionPhase.totalFwdFees, + totalActionFees: desc.actionPhase.totalActionFees, + resultCode: desc.actionPhase.resultCode, + totalActionsNumber: desc.actionPhase.totalActions, + specActionsNumber: desc.actionPhase.specActions, + skippedActionsNumber: desc.actionPhase.skippedActions, + messagesCreatedNumber: desc.actionPhase.msgsCreated, + actionListHash: desc.actionPhase.actionListHash, + totalMessagesSize: { + cells: parseMsgSizeCount(desc.actionPhase.totalMsgSize.cells), + bits: parseMsgSizeCount(desc.actionPhase.totalMsgSize.bits), + }, + } + : undefined, + }; +} + +function emulationTxToTransaction(tx: DomainEmulationTransaction): Transaction { + return { + account: tx.account, + hash: tx.hash, + logicalTime: tx.lt, + now: tx.now, + mcBlockSeqno: tx.mcBlockSeqno, + traceExternalHash: tx.traceExternalHash, + traceId: tx.traceId, + previousTransactionHash: tx.prevTransHash ?? undefined, + previousTransactionLogicalTime: tx.prevTransLt ?? undefined, + origStatus: toAccountStatus(tx.origStatus), + endStatus: toAccountStatus(tx.endStatus), + totalFees: tx.totalFees, + totalFeesExtraCurrencies: tx.totalFeesExtraCurrencies, + blockRef: tx.blockRef, + inMessage: tx.inMsg ? emulationMsgToTransactionMessage(tx.inMsg) : undefined, + outMessages: tx.outMsgs.map(emulationMsgToTransactionMessage), + accountStateBefore: emulationAccountStateToAccountState(tx.accountStateBefore), + accountStateAfter: emulationAccountStateToAccountState(tx.accountStateAfter), + isEmulated: tx.isEmulated, + description: emulationDescToTransactionDescription(tx.description), + }; +} + +function emulationActionToTransactionTraceAction(action: DomainEmulationAction): TransactionTraceAction { + return { + traceId: action.traceId ?? undefined, + actionId: action.actionId, + startLt: action.startLt, + endLt: action.endLt, + startUtime: action.startUtime, + endUtime: action.endUtime, + traceEndLt: action.traceEndLt, + traceEndUtime: action.traceEndUtime, + traceMcSeqnoEnd: action.traceMcSeqnoEnd, + transactions: action.transactions, + isSuccess: action.isSuccess, + traceExternalHash: action.traceExternalHash, + accounts: action.accounts as UserFriendlyAddress[], + details: domainActionDetailsToTransactionTraceActionDetails(action.type, action.details), + }; +} + +function domainActionDetailsToTransactionTraceActionDetails( + type: string, + details: Record, +): TransactionTraceActionDetails { + if (type === 'jetton_swap') { + return { + type: 'jetton_swap', + value: toTransactionTraceActionJettonSwapDetails(details as unknown as EmulationJettonSwapDetails), + }; + } else if (type === 'call_contract') { + return { + type: 'call_contract', + value: toTransactionTraceActionCallContractDetails(details as unknown as EmulationCallContractDetails), + }; + } else if (type === 'ton_transfer') { + return { + type: 'ton_transfer', + value: toTransactionTraceActionTONTransferDetails(details as unknown as EmulationTonTransferDetails), + }; + } else { + return { type: 'unknown', value: details }; + } +} + +function emulationAddressBookToAddressBook(book: Record): AddressBook { + const result: AddressBook = {}; + for (const [, entry] of Object.entries(book)) { + result[entry.userFriendly] = { + address: entry.userFriendly, + domain: entry.domain, + interfaces: entry.interfaces, + }; + } + return result; +} + +function toTransactionTraceActionJettonSwapDetails( + details: EmulationJettonSwapDetails, +): TransactionTraceActionJettonSwapDetails { + return { + dex: details.dex, + sender: asMaybeAddressFriendly(details.sender) ?? undefined, + dexIncomingTransfer: { + asset: asMaybeAddressFriendly(details.dex_incoming_transfer?.asset) ?? undefined, + source: asMaybeAddressFriendly(details.dex_incoming_transfer?.source) ?? undefined, + destination: asMaybeAddressFriendly(details.dex_incoming_transfer?.destination) ?? undefined, + sourceJettonWallet: + asMaybeAddressFriendly(details.dex_incoming_transfer?.source_jetton_wallet) ?? undefined, + destinationJettonWallet: + asMaybeAddressFriendly(details.dex_incoming_transfer?.destination_jetton_wallet) ?? undefined, + amount: details.dex_incoming_transfer?.amount, + }, + dexOutgoingTransfer: { + asset: asMaybeAddressFriendly(details.dex_outgoing_transfer?.asset) ?? undefined, + source: asMaybeAddressFriendly(details.dex_outgoing_transfer?.source) ?? undefined, + destination: asMaybeAddressFriendly(details.dex_outgoing_transfer?.destination) ?? undefined, + sourceJettonWallet: + asMaybeAddressFriendly(details.dex_outgoing_transfer?.source_jetton_wallet) ?? undefined, + destinationJettonWallet: + asMaybeAddressFriendly(details.dex_outgoing_transfer?.destination_jetton_wallet) ?? undefined, + amount: details.dex_outgoing_transfer?.amount, + }, + peerSwaps: details.peer_swaps, + }; +} + +function toTransactionTraceActionCallContractDetails( + details: EmulationCallContractDetails, +): TransactionTraceActionCallContractDetails { + return { + opcode: details.opcode, + source: asMaybeAddressFriendly(details.source) ?? undefined, + destination: asMaybeAddressFriendly(details.destination) ?? undefined, + value: details.value, + valueExtraCurrencies: details.extra_currencies ?? undefined, + }; +} + +function toTransactionTraceActionTONTransferDetails( + details: EmulationTonTransferDetails, +): TransactionTraceActionTONTransferDetails { + return { + source: asMaybeAddressFriendly(details.source) ?? undefined, + destination: asMaybeAddressFriendly(details.destination) ?? undefined, + value: details.value, + valueExtraCurrencies: details.value_extra_currencies, + comment: details.comment ?? undefined, + isEncrypted: details.encrypted, + }; +} diff --git a/packages/walletkit/src/clients/toncenter/mappers/map-emulation.spec.ts b/packages/walletkit/src/clients/toncenter/mappers/map-emulation.spec.ts new file mode 100644 index 000000000..426f18e0f --- /dev/null +++ b/packages/walletkit/src/clients/toncenter/mappers/map-emulation.spec.ts @@ -0,0 +1,223 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { describe, expect, it } from 'vitest'; + +import type { ToncenterEmulationResponse } from '../types/raw-emulation'; +import { mapToncenterEmulationResponse } from './map-emulation'; +import { computeMoneyFlow } from '../../../utils/computeMoneyFlow'; + +// 32 bytes of 0x00 in base64 +const TX_HASH_B64 = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='; +// 32 bytes of 0x01 in base64 +const MSG_HASH_B64 = 'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE='; +// 32 bytes of 0x02 in base64 +const RAND_SEED_B64 = 'AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI='; + +const SENDER_RAW = '0:10C1073837B93FDAAD594284CE8B8EFF7B9CF25427440EB2FC682762E1471365'; +const RECIPIENT_RAW = '0:E93E7D444180608B8520C00DC664383A387356FB6E16FDDF99DBE5E1415A574B'; + +const RAW_RESPONSE = { + mc_block_seqno: 42, + rand_seed: RAND_SEED_B64, + is_incomplete: false, + trace: { tx_hash: TX_HASH_B64, in_msg_hash: null, children: [] }, + transactions: { + [TX_HASH_B64]: { + account: SENDER_RAW, + hash: TX_HASH_B64, + lt: '100000001', + now: 1700000000, + mc_block_seqno: 42, + trace_external_hash: TX_HASH_B64, + prev_trans_hash: null, + prev_trans_lt: null, + orig_status: 'active', + end_status: 'active', + total_fees: '1000000', + total_fees_extra_currencies: {}, + description: { + type: 'ord', + aborted: false, + destroyed: false, + credit_first: false, + is_tock: false, + installed: false, + storage_ph: { storage_fees_collected: '0', status_change: 'unchanged' }, + compute_ph: { + skipped: false, + success: true, + msg_state_used: false, + account_activated: false, + gas_fees: '0', + gas_used: '21000', + gas_limit: '1000000', + mode: 0, + exit_code: 0, + vm_steps: 100, + vm_init_state_hash: '', + vm_final_state_hash: '', + }, + action: { + success: true, + valid: true, + no_funds: false, + status_change: 'unchanged', + result_code: 0, + tot_actions: 1, + spec_actions: 0, + skipped_actions: 0, + msgs_created: 1, + action_list_hash: '', + tot_msg_size: { cells: '1', bits: '267' }, + }, + }, + block_ref: { workchain: -1, shard: '8000000000000000', seqno: 42 }, + in_msg: null, + out_msgs: [ + { + hash: MSG_HASH_B64, + source: SENDER_RAW, + destination: RECIPIENT_RAW, + value: '1000000000', + value_extra_currencies: {}, + fwd_fee: '1000000', + ihr_fee: null, + created_lt: '100000002', + created_at: '1700000000', + opcode: null, + ihr_disabled: true, + bounce: true, + bounced: false, + import_fee: null, + message_content: { + hash: MSG_HASH_B64, + body: 'te6cckEBAQEAAgAAAEysuc0=', + decoded: null, + }, + init_state: null, + }, + ], + account_state_before: { + hash: TX_HASH_B64, + balance: '5000000000', + extra_currencies: null, + account_status: 'active', + frozen_hash: null, + data_hash: null, + code_hash: null, + }, + account_state_after: { + hash: TX_HASH_B64, + balance: '4000000000', + extra_currencies: null, + account_status: 'active', + frozen_hash: null, + data_hash: null, + code_hash: null, + }, + emulated: true, + }, + }, + actions: [], + code_cells: {}, + data_cells: {}, + address_book: {}, + metadata: {}, +} as unknown as ToncenterEmulationResponse; + +describe('mapToncenterEmulationResponse', () => { + it('maps all transaction fields to camelCase', () => { + const rawTx = Object.values(RAW_RESPONSE.transactions)[0]; + const mapped = mapToncenterEmulationResponse(RAW_RESPONSE); + const mappedTx = Object.values(mapped.transactions)[0]; + + const rawFields = Object.keys(rawTx); + const mappedFields = Object.keys(mappedTx); + + // eslint-disable-next-line no-console + console.log('raw tx fields :', rawFields); + // eslint-disable-next-line no-console + console.log('mapped tx fields:', mappedFields); + // eslint-disable-next-line no-console + console.log(`field count: ${rawFields.length} → ${mappedFields.length}`); + + expect(mappedFields.length).toBe(rawFields.length); + expect(mappedFields).toEqual([ + 'account', + 'hash', + 'lt', + 'now', + 'mcBlockSeqno', + 'traceExternalHash', + 'prevTransHash', + 'prevTransLt', + 'origStatus', + 'endStatus', + 'totalFees', + 'totalFeesExtraCurrencies', + 'description', + 'blockRef', + 'inMsg', + 'outMsgs', + 'accountStateBefore', + 'accountStateAfter', + 'isEmulated', + ]); + }); + + it('converts hashes from base64 to hex', () => { + const mapped = mapToncenterEmulationResponse(RAW_RESPONSE); + + expect(mapped.trace.txHash).toMatch(/^0x[0-9a-f]{64}$/); + expect(mapped.trace.txHash).toBe('0x0000000000000000000000000000000000000000000000000000000000000000'); + + const tx = Object.values(mapped.transactions)[0]; + expect(tx.hash).toMatch(/^0x[0-9a-f]{64}$/); + expect(tx.outMsgs[0].hash).toMatch(/^0x[0-9a-f]{64}$/); + }); + + it('converts addresses to user-friendly format', () => { + const mapped = mapToncenterEmulationResponse(RAW_RESPONSE); + const tx = Object.values(mapped.transactions)[0]; + + expect(tx.account).not.toBe(SENDER_RAW); + expect(tx.account).toMatch(/^EQ|UQ/); + + expect(tx.outMsgs[0].source).toMatch(/^EQ|UQ/); + expect(tx.outMsgs[0].destination).toMatch(/^EQ|UQ/); + }); + + it('maps top-level fields correctly', () => { + const mapped = mapToncenterEmulationResponse(RAW_RESPONSE); + + expect(mapped.mcBlockSeqno).toBe(42); + expect(mapped.isIncomplete).toBe(false); + expect(mapped.randSeed).toMatch(/^0x[0-9a-f]+$/); + expect(mapped.actions).toEqual([]); + expect(mapped.codeCells).toEqual({}); + expect(mapped.dataCells).toEqual({}); + + const moneyFlow = computeMoneyFlow(mapped); + expect(moneyFlow).toBeDefined(); + expect(moneyFlow.outputs).toBe('1000000000'); + expect(moneyFlow.inputs).toBe('0'); + expect(moneyFlow.ourTransfers).toHaveLength(1); + expect(moneyFlow.ourTransfers[0].assetType).toBe('ton'); + }); + + it('maps out_msg value through', () => { + const mapped = mapToncenterEmulationResponse(RAW_RESPONSE); + const tx = Object.values(mapped.transactions)[0]; + + expect(tx.inMsg).toBeUndefined(); + expect(tx.outMsgs).toHaveLength(1); + expect(tx.outMsgs[0].value).toBe('1000000000'); + expect(tx.outMsgs[0].messageContent.body).toBe('te6cckEBAQEAAgAAAEysuc0='); + }); +}); diff --git a/packages/walletkit/src/clients/toncenter/mappers/map-emulation.ts b/packages/walletkit/src/clients/toncenter/mappers/map-emulation.ts new file mode 100644 index 000000000..6a81234fc --- /dev/null +++ b/packages/walletkit/src/clients/toncenter/mappers/map-emulation.ts @@ -0,0 +1,222 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { ToncenterEmulationResponse, EmulationAction as RawAction } from '../types/raw-emulation'; +import type { + EmulationMessage as RawMessage, + EmulationTraceNode as RawTraceNode, + EmulationAccountState as RawAccountState, + EmulationTransactionDescription as RawDescription, + ToncenterTransaction as RawTransaction, +} from '../../../types/toncenter/emulation'; +import type { Base64String } from '../../../api/models'; +import type { + EmulationResponse, + EmulationTraceNode, + EmulationTransaction, + EmulationTransactionDescription, + EmulationAccountState, + EmulationMessage, + EmulationAction, + EmulationAddressBookEntry, + EmulationAccountStatus, +} from '../../../api/models'; +import { Base64ToHex, asBase64 } from '../../../utils/base64'; +import { asHex } from '../../../utils/hex'; +import { asAddressFriendly, asMaybeAddressFriendly } from '../../../utils/address'; +import { parseMsgSizeCount } from '../utils'; + +function normalizeAccountStatus(status: string): EmulationAccountStatus { + if (status === 'active') return 'active'; + if (status === 'frozen') return 'frozen'; + if (status === 'nonexist') return 'nonexist'; + return 'uninit'; +} + +function mapTraceNode(node: RawTraceNode): EmulationTraceNode { + return { + txHash: Base64ToHex(node.tx_hash), + inMsgHash: node.in_msg_hash ? Base64ToHex(node.in_msg_hash) : undefined, + children: node.children?.map(mapTraceNode) ?? [], + }; +} + +function mapAccountState(state: RawAccountState): EmulationAccountState { + return { + hash: Base64ToHex(state.hash), + balance: state.balance, + extraCurrencies: state.extra_currencies ?? undefined, + accountStatus: normalizeAccountStatus(state.account_status), + frozenHash: state.frozen_hash ? Base64ToHex(state.frozen_hash) : undefined, + dataHash: state.data_hash ? Base64ToHex(state.data_hash) : undefined, + codeHash: state.code_hash ? Base64ToHex(state.code_hash) : undefined, + }; +} + +function mapDescription(desc: RawDescription): EmulationTransactionDescription { + return { + type: desc.type, + isAborted: desc.aborted, + isDestroyed: desc.destroyed, + isCreditFirst: desc.credit_first, + isTock: desc.is_tock, + isInstalled: desc.installed, + storagePhase: { + storageFeesCollected: desc.storage_ph.storage_fees_collected, + statusChange: desc.storage_ph.status_change, + }, + creditPhase: desc.credit_ph ? { credit: desc.credit_ph.credit } : undefined, + computePhase: { + isSkipped: desc.compute_ph.skipped, + isSuccess: desc.compute_ph.success, + isMsgStateUsed: desc.compute_ph.msg_state_used, + isAccountActivated: desc.compute_ph.account_activated, + gasFees: desc.compute_ph.gas_fees, + gasUsed: desc.compute_ph.gas_used, + gasLimit: desc.compute_ph.gas_limit, + gasCredit: desc.compute_ph.gas_credit, + mode: desc.compute_ph.mode, + exitCode: desc.compute_ph.exit_code, + vmSteps: desc.compute_ph.vm_steps, + vmInitStateHash: desc.compute_ph.vm_init_state_hash + ? Base64ToHex(desc.compute_ph.vm_init_state_hash) + : undefined, + vmFinalStateHash: desc.compute_ph.vm_final_state_hash + ? Base64ToHex(desc.compute_ph.vm_final_state_hash) + : undefined, + }, + actionPhase: desc.action + ? { + isSuccess: desc.action.success, + isValid: desc.action.valid, + hasNoFunds: desc.action.no_funds, + statusChange: desc.action.status_change, + totalFwdFees: desc.action.total_fwd_fees, + totalActionFees: desc.action.total_action_fees, + resultCode: desc.action.result_code, + totalActions: desc.action.tot_actions, + specActions: desc.action.spec_actions, + skippedActions: desc.action.skipped_actions, + msgsCreated: desc.action.msgs_created, + actionListHash: desc.action.action_list_hash ? Base64ToHex(desc.action.action_list_hash) : undefined, + totalMsgSize: { + cells: parseMsgSizeCount(desc.action.tot_msg_size.cells) ?? 0, + bits: parseMsgSizeCount(desc.action.tot_msg_size.bits) ?? 0, + }, + } + : undefined, + }; +} + +function mapMessage(msg: RawMessage): EmulationMessage { + return { + hash: Base64ToHex(msg.hash), + normalizedHash: msg.hash_norm ? Base64ToHex(msg.hash_norm) : undefined, + source: asMaybeAddressFriendly(msg.source) ?? undefined, + destination: asMaybeAddressFriendly(msg.destination) ?? msg.destination, + value: msg.value ?? undefined, + valueExtraCurrencies: msg.value_extra_currencies, + fwdFee: msg.fwd_fee ?? undefined, + ihrFee: msg.ihr_fee ?? undefined, + createdLt: msg.created_lt ?? undefined, + createdAt: msg.created_at !== null ? Number(msg.created_at) : undefined, + opcode: msg.opcode ? asHex(msg.opcode) : undefined, + ihrDisabled: msg.ihr_disabled ?? undefined, + isBounce: msg.bounce ?? undefined, + isBounced: msg.bounced ?? undefined, + importFee: msg.import_fee ?? undefined, + messageContent: { + hash: msg.message_content?.hash ? Base64ToHex(msg.message_content.hash) : undefined, + body: msg.message_content?.body ? asBase64(msg.message_content.body) : undefined, + decoded: msg.message_content?.decoded ?? undefined, + }, + initState: msg.init_state ?? undefined, + }; +} + +function mapTransaction(tx: RawTransaction): EmulationTransaction { + return { + account: asAddressFriendly(tx.account), + hash: Base64ToHex(tx.hash), + lt: tx.lt, + now: tx.now, + mcBlockSeqno: tx.mc_block_seqno, + traceExternalHash: Base64ToHex(tx.trace_external_hash), + prevTransHash: tx.prev_trans_hash ? Base64ToHex(tx.prev_trans_hash) : undefined, + prevTransLt: tx.prev_trans_lt ?? undefined, + origStatus: normalizeAccountStatus(tx.orig_status), + endStatus: normalizeAccountStatus(tx.end_status), + totalFees: tx.total_fees, + totalFeesExtraCurrencies: tx.total_fees_extra_currencies, + description: mapDescription(tx.description), + blockRef: { workchain: tx.block_ref.workchain, shard: tx.block_ref.shard, seqno: tx.block_ref.seqno }, + inMsg: tx.in_msg ? mapMessage(tx.in_msg) : undefined, + outMsgs: tx.out_msgs?.map(mapMessage) ?? [], + accountStateBefore: mapAccountState(tx.account_state_before), + accountStateAfter: mapAccountState(tx.account_state_after), + isEmulated: tx.emulated, + ...(tx.trace_id !== undefined ? { traceId: tx.trace_id } : {}), + }; +} + +function mapAction(action: RawAction): EmulationAction { + return { + traceId: action.trace_id ?? undefined, + actionId: Base64ToHex(action.action_id), + startLt: action.start_lt, + endLt: action.end_lt, + startUtime: action.start_utime, + endUtime: action.end_utime, + traceEndLt: action.trace_end_lt, + traceEndUtime: action.trace_end_utime, + traceMcSeqnoEnd: action.trace_mc_seqno_end, + transactions: action.transactions.map(Base64ToHex), + isSuccess: action.success, + type: action.type, + traceExternalHash: Base64ToHex(action.trace_external_hash), + accounts: action.accounts.map(asMaybeAddressFriendly).filter((a): a is string => a !== null), + details: action.details as Record, + }; +} + +export function mapToncenterEmulationResponse(raw: ToncenterEmulationResponse): EmulationResponse { + const transactions: Record = Object.fromEntries( + Object.entries(raw.transactions ?? {}).map(([hash, tx]) => [Base64ToHex(hash), mapTransaction(tx)]), + ); + + const addressBook: Record = Object.fromEntries( + Object.entries(raw.address_book ?? {}).map(([addr, row]) => [ + addr, + { + domain: row.domain ?? undefined, + userFriendly: asAddressFriendly(row.user_friendly), + interfaces: row.interfaces ?? [], + }, + ]), + ); + + const codeCells: Record = Object.fromEntries( + Object.entries(raw.code_cells ?? {}).map(([hash, cell]) => [Base64ToHex(hash), asBase64(cell)]), + ); + + const dataCells: Record = Object.fromEntries( + Object.entries(raw.data_cells ?? {}).map(([hash, cell]) => [Base64ToHex(hash), asBase64(cell)]), + ); + + return { + mcBlockSeqno: raw.mc_block_seqno, + trace: mapTraceNode(raw.trace), + transactions, + actions: raw.actions?.map(mapAction) ?? [], + randSeed: Base64ToHex(raw.rand_seed), + isIncomplete: raw.is_incomplete, + codeCells, + dataCells, + addressBook, + }; +} diff --git a/packages/walletkit/src/clients/toncenter/mappers/map-transactions.ts b/packages/walletkit/src/clients/toncenter/mappers/map-transactions.ts new file mode 100644 index 000000000..cf8841a9d --- /dev/null +++ b/packages/walletkit/src/clients/toncenter/mappers/map-transactions.ts @@ -0,0 +1,177 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { Base64ToHex } from '../../../utils/base64'; +import type { + AccountState, + AccountStatus, + Transaction, + TransactionMessage, + TransactionDescription, + TransactionBlockRef, + TransactionsResponse, + Base64String, +} from '../../../api/models'; +import { asAddressFriendly, asMaybeAddressFriendly } from '../../../utils/address'; +import { parseMsgSizeCount } from '../utils'; +import { toAddressBook } from '../../../types/toncenter/v3/AddressBookRowV3'; +import type { + ToncenterTransaction, + ToncenterTransactionsResponse, + EmulationAccountStatus, + EmulationBlockRef, + EmulationTransactionDescription, + EmulationMessage, + EmulationAccountState, +} from '../../../types/toncenter/emulation'; + +export function toTransactionsResponse(response: ToncenterTransactionsResponse): TransactionsResponse { + return { + transactions: response.transactions?.map(toTransaction) ?? [], + addressBook: toAddressBook(response.address_book), + }; +} + +function toTransaction(tx: ToncenterTransaction): Transaction { + return { + account: asAddressFriendly(tx.account), + accountStateBefore: toAccountState(tx.account_state_before), + accountStateAfter: toAccountState(tx.account_state_after), + description: toTransactionDescription(tx.description), + hash: Base64ToHex(tx.hash), + logicalTime: tx.lt, + now: tx.now, + mcBlockSeqno: tx.mc_block_seqno, + traceExternalHash: Base64ToHex(tx.trace_external_hash), + traceId: tx.trace_id ?? undefined, + previousTransactionHash: tx.prev_trans_hash ? Base64ToHex(tx.prev_trans_hash) : undefined, + previousTransactionLogicalTime: tx.prev_trans_lt ?? undefined, + origStatus: toAccountStatus(tx.orig_status), + endStatus: toAccountStatus(tx.end_status), + totalFees: tx.total_fees, + totalFeesExtraCurrencies: tx.total_fees_extra_currencies, + blockRef: toTransactionBlockRef(tx.block_ref), + inMessage: tx.in_msg ? toTransactionMessage(tx.in_msg) : undefined, + outMessages: tx.out_msgs?.map(toTransactionMessage) ?? [], + isEmulated: tx.emulated, + }; +} + +function toAccountStatus(status: EmulationAccountStatus | string): AccountStatus { + if (status === 'active') { + return { type: 'active' }; + } else if (status === 'frozen') { + return { type: 'frozen' }; + } else if (status === 'uninit') { + return { type: 'uninit' }; + } else { + return { type: 'unknown', value: status }; + } +} + +function toTransactionBlockRef(ref: EmulationBlockRef): TransactionBlockRef { + return { + workchain: ref.workchain, + shard: ref.shard, + seqno: ref.seqno, + }; +} + +function toTransactionDescription(desc: EmulationTransactionDescription): TransactionDescription { + return { + type: desc.type, + isAborted: desc.aborted, + isDestroyed: desc.destroyed, + isCreditFirst: desc.credit_first, + isTock: desc.is_tock, + isInstalled: desc.installed, + storagePhase: { + storageFeesCollected: desc.storage_ph?.storage_fees_collected, + statusChange: desc.storage_ph?.status_change, + }, + creditPhase: desc.credit_ph + ? { + credit: desc.credit_ph?.credit, + } + : undefined, + computePhase: { + isSkipped: desc.compute_ph?.skipped, + isSuccess: desc.compute_ph?.success, + isMessageStateUsed: desc.compute_ph?.msg_state_used, + isAccountActivated: desc.compute_ph?.account_activated, + gasFees: desc.compute_ph?.gas_fees, + gasUsed: desc.compute_ph?.gas_used, + gasLimit: desc.compute_ph?.gas_limit, + gasCredit: desc.compute_ph?.gas_credit, + mode: desc.compute_ph?.mode, + exitCode: desc.compute_ph?.exit_code, + vmStepsNumber: desc.compute_ph?.vm_steps, + vmInitStateHash: desc.compute_ph?.vm_init_state_hash + ? Base64ToHex(desc.compute_ph.vm_init_state_hash) + : undefined, + vmFinalStateHash: desc.compute_ph?.vm_final_state_hash + ? Base64ToHex(desc.compute_ph.vm_final_state_hash) + : undefined, + }, + action: { + isSuccess: desc.action?.success, + isValid: desc.action?.valid, + hasNoFunds: desc.action?.no_funds, + statusChange: desc.action?.status_change, + totalForwardingFees: desc.action?.total_fwd_fees, + totalActionFees: desc.action?.total_action_fees, + resultCode: desc.action?.result_code, + totalActionsNumber: desc.action?.tot_actions, + specActionsNumber: desc.action?.spec_actions, + skippedActionsNumber: desc.action?.skipped_actions, + messagesCreatedNumber: desc.action?.msgs_created, + actionListHash: desc.action?.action_list_hash ? Base64ToHex(desc.action.action_list_hash) : undefined, + totalMessagesSize: { + cells: parseMsgSizeCount(desc.action?.tot_msg_size.cells), + bits: parseMsgSizeCount(desc.action?.tot_msg_size.bits), + }, + }, + }; +} + +function toTransactionMessage(msg: EmulationMessage): TransactionMessage { + return { + hash: Base64ToHex(msg.hash), + normalizedHash: msg.hash_norm ? Base64ToHex(msg.hash_norm) : undefined, + source: asMaybeAddressFriendly(msg.source) ?? undefined, + destination: asMaybeAddressFriendly(msg.destination) ?? undefined, + value: msg.value ?? undefined, + valueExtraCurrencies: msg.value_extra_currencies, + fwdFee: msg.fwd_fee ?? undefined, + ihrFee: msg.ihr_fee ?? undefined, + creationLogicalTime: msg.created_lt ?? undefined, + createdAt: msg.created_at ? Number(msg.created_at) : undefined, + ihrDisabled: msg.ihr_disabled ?? undefined, + isBounce: msg.bounce ?? undefined, + isBounced: msg.bounced ?? undefined, + importFee: msg.import_fee ?? undefined, + opcode: msg.opcode ?? undefined, + messageContent: { + hash: msg.message_content?.hash ? Base64ToHex(msg.message_content.hash) : undefined, + body: msg.message_content?.body ? (msg.message_content.body as Base64String) : undefined, + decoded: msg.message_content?.decoded ?? undefined, + }, + }; +} + +function toAccountState(state: EmulationAccountState): AccountState { + return { + hash: Base64ToHex(state.hash), + balance: state.balance, + extraCurrencies: state.extra_currencies ?? undefined, + accountStatus: state.account_status ? toAccountStatus(state.account_status) : undefined, + frozenHash: state.frozen_hash ? Base64ToHex(state.frozen_hash) : undefined, + dataHash: state.data_hash ? Base64ToHex(state.data_hash) : undefined, + codeHash: state.code_hash ? Base64ToHex(state.code_hash) : undefined, + }; +} diff --git a/packages/walletkit/src/types/toncenter/DnsRecord.ts b/packages/walletkit/src/clients/toncenter/types/dns.ts similarity index 66% rename from packages/walletkit/src/types/toncenter/DnsRecord.ts rename to packages/walletkit/src/clients/toncenter/types/dns.ts index 97add9da5..71123b18a 100644 --- a/packages/walletkit/src/types/toncenter/DnsRecord.ts +++ b/packages/walletkit/src/clients/toncenter/types/dns.ts @@ -6,7 +6,8 @@ * */ -import type { UserFriendlyAddress } from '../../api/models'; +import type { UserFriendlyAddress } from '../../../api/models'; +import type { AddressBookRow } from './nfts'; export interface DnsRecord { dnsNextResolver: string | null; @@ -17,3 +18,8 @@ export interface DnsRecord { nftItemAddress: UserFriendlyAddress; nftItemOwner: UserFriendlyAddress; } + +export interface DnsRecords { + addressBook: { [key: string]: AddressBookRow }; + records: DnsRecord[]; +} diff --git a/packages/walletkit/src/clients/toncenter/types.ts b/packages/walletkit/src/clients/toncenter/types/internal.ts similarity index 94% rename from packages/walletkit/src/clients/toncenter/types.ts rename to packages/walletkit/src/clients/toncenter/types/internal.ts index c7c945590..3f35bd413 100644 --- a/packages/walletkit/src/clients/toncenter/types.ts +++ b/packages/walletkit/src/clients/toncenter/types/internal.ts @@ -8,7 +8,7 @@ import type { AccountStatus } from '@ton/core'; -import type { RawStackItem } from '../../utils'; +import type { RawStackItem } from '../../../utils'; export interface InternalTransactionId { lt: string; diff --git a/packages/walletkit/src/clients/toncenter/types/jettons.ts b/packages/walletkit/src/clients/toncenter/types/jettons.ts new file mode 100644 index 000000000..932656254 --- /dev/null +++ b/packages/walletkit/src/clients/toncenter/types/jettons.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { AddressBookRowV3 } from '../../../types/toncenter/v3/AddressBookRowV3'; +import type { EmulationAddressMetadata } from './metadata'; + +export interface ToncenterResponseJettonMasters { + jetton_masters: ToncenterJettonWallet[]; + address_book: Record; + metadata: Record; +} + +export interface ToncenterResponseJettonWallets { + jetton_wallets: ToncenterJettonWallet[]; + address_book: Record; + metadata: Record; +} + +export interface ToncenterJettonWallet { + address: string; + balance: string; + owner: string; + jetton: string; + last_transaction_lt: string; + code_hash: string; + data_hash: string; +} diff --git a/packages/walletkit/src/clients/toncenter/types/metadata.ts b/packages/walletkit/src/clients/toncenter/types/metadata.ts new file mode 100644 index 000000000..0c5b93df1 --- /dev/null +++ b/packages/walletkit/src/clients/toncenter/types/metadata.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export interface EmulationAddressMetadata { + is_indexed: boolean; + token_info?: EmulationTokenInfo[]; +} + +export type EmulationTokenInfo = + | EmulationTokenInfoWallets + | EmulationTokenInfoMasters + | (EmulationTokenInfoBase & Record); + +export interface EmulationTokenInfoBase { + valid: boolean; + type: string; +} + +export interface EmulationTokenInfoWallets extends EmulationTokenInfoBase { + type: 'jetton_wallets'; + extra: { + balance: string; + jetton: string; + owner: string; + }; +} + +export interface EmulationTokenInfoMasters extends EmulationTokenInfoBase { + type: 'jetton_masters'; + name: string; + symbol: string; + description: string; + image?: string; + extra: { + _image_big?: string; + _image_medium?: string; + _image_small?: string; + decimals: string; + image_data?: string; + social?: string[]; + uri?: string; + websites?: string[]; + [key: string]: unknown; + }; +} diff --git a/packages/walletkit/src/clients/toncenter/types/nfts.ts b/packages/walletkit/src/clients/toncenter/types/nfts.ts new file mode 100644 index 000000000..f8d373ee6 --- /dev/null +++ b/packages/walletkit/src/clients/toncenter/types/nfts.ts @@ -0,0 +1,146 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { Hex, UserFriendlyAddress, TokenInfo as APITokenInfo } from '../../../api/models'; + +export interface Pagination { + offset: number; + limit: number; + pages?: number; +} + +export interface AddressBookRow { + domain: string | null; +} + +export interface NftItemAttribute { + trait_type: string; + value: string; +} + +export interface TokenInfo { + description?: string; + extra?: { + attributes?: NftItemAttribute[]; + lottie?: string; + uri?: string; + _image_big?: string; + _image_medium?: string; + _image_small?: string; + animation_url?: string; + content_url?: string; + [key: string]: unknown; + }; + image?: string; + lottie?: string; + name?: string; + symbol?: string; + type?: string; + valid?: boolean; + animation?: string; +} + +export function toApiTokenInfo(data: TokenInfo): APITokenInfo { + let lottie: string | undefined; + let animationUrl: string | undefined; + + if (data?.extra?.animation_url) { + animationUrl = data.extra.animation_url; + } else if (data?.extra?.content_url && data.extra.content_url.includes('mp4')) { + animationUrl = data.extra.content_url; + } + + if (data.lottie) { + lottie = data.lottie; + } else if (data.extra && typeof data.extra === 'object' && 'lottie' in data.extra) { + const lottieValue = (data.extra as Record).lottie; + if (typeof lottieValue === 'string') { + lottie = lottieValue; + } + } + + return { + name: data.name, + description: data.description, + image: { + url: data.image ?? data.extra?._image_medium, + smallUrl: data.extra?._image_small, + mediumUrl: data.extra?._image_medium, + largeUrl: data.extra?._image_big, + }, + animation: { + url: animationUrl, + lottie: lottie, + }, + symbol: data.symbol, + }; +} + +export interface NftCollection { + address: UserFriendlyAddress; + collectionContent?: { + uri?: string; + [key: string]: unknown; + }; + lastTransactionLt?: string; + name?: string; + nextItemIndex?: string; + ownerAddress?: UserFriendlyAddress | null; + codeHash?: Hex | null; + dataHash?: Hex | null; + description?: string; + image?: string; + extra?: { + cover_image?: string; + uri?: string; + _image_big?: string; + _image_medium?: string; + _image_small?: string; + [key: string]: unknown; + }; +} + +export interface AddressMetadata { + isIndexed: boolean; + tokenInfo: TokenInfo[]; +} + +export interface NftItem { + address: UserFriendlyAddress; + auctionContractAddress: UserFriendlyAddress | null; + codeHash: Hex | null; + dataHash: Hex | null; + collection: NftCollection | null; + collectionAddress: UserFriendlyAddress | null; + content?: { + uri?: string; + [key: string]: unknown; + }; + metadata?: TokenInfo; + index: string; + init: boolean; + isSbt?: boolean; + lastTransactionLt?: string; + onSale: boolean; + ownerAddress: UserFriendlyAddress | null; + realOwner: UserFriendlyAddress | null; + saleContractAddress: UserFriendlyAddress | null; + attributes?: NftItemAttribute[]; +} + +export type NftMetadata = { [key: UserFriendlyAddress]: AddressMetadata }; + +export interface NftItems { + items: NftItem[]; + pagination: Pagination; +} + +export interface NftItemsResponse extends NftItems { + addressBook: { [key: UserFriendlyAddress]: AddressBookRow }; + metadata: NftMetadata; +} diff --git a/packages/walletkit/src/clients/toncenter/types/raw-emulation.ts b/packages/walletkit/src/clients/toncenter/types/raw-emulation.ts new file mode 100644 index 000000000..9c2ddde3b --- /dev/null +++ b/packages/walletkit/src/clients/toncenter/types/raw-emulation.ts @@ -0,0 +1,91 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { ToncenterTransaction, EmulationTraceNode } from '../../../types/toncenter/emulation'; +import type { MetadataV3 } from '../../../types/toncenter/v3/AddressBookRowV3'; + +export interface ToncenterEmulationResponse extends MetadataV3 { + mc_block_seqno: number; + trace: EmulationTraceNode; + transactions: Record; + actions: EmulationAction[]; + code_cells: Record; + data_cells: Record; + rand_seed: string; + is_incomplete: boolean; +} + +type EmulationActionType = 'jetton_swap' | 'call_contract' | string; + +interface EmulationActionBase { + trace_id: string | null; + action_id: string; + start_lt: string; + end_lt: string; + start_utime: number; + end_utime: number; + trace_end_lt: string; + trace_end_utime: number; + trace_mc_seqno_end: number; + transactions: string[]; + success: boolean; + type: EmulationActionType; + trace_external_hash: string; + accounts: string[]; +} + +export interface EmulationJettonSwapDetails { + dex: string; + sender: string; + asset_in: string; + asset_out: string; + dex_incoming_transfer: { + asset: string; + source: string; + destination: string; + source_jetton_wallet: string | null; + destination_jetton_wallet: string | null; + amount: string; + }; + dex_outgoing_transfer: { + asset: string; + source: string; + destination: string; + source_jetton_wallet: string | null; + destination_jetton_wallet: string | null; + amount: string; + }; + peer_swaps: unknown[]; +} + +export interface EmulationCallContractDetails { + opcode: string; + source: string; + destination: string; + value: string; + extra_currencies: Record | null; +} + +export interface EmulationTonTransferDetails { + source: string; + destination: string; + value: string; + value_extra_currencies: Record; + comment: string | null; + encrypted: boolean; +} + +type EmulationActionDetails = + | EmulationTonTransferDetails + | EmulationJettonSwapDetails + | EmulationCallContractDetails + | Record; + +export interface EmulationAction extends EmulationActionBase { + details: EmulationActionDetails; +} diff --git a/packages/walletkit/src/types/toncenter/v3/AddressMetadataV3.ts b/packages/walletkit/src/clients/toncenter/types/v3/AddressMetadataV3.ts similarity index 100% rename from packages/walletkit/src/types/toncenter/v3/AddressMetadataV3.ts rename to packages/walletkit/src/clients/toncenter/types/v3/AddressMetadataV3.ts diff --git a/packages/walletkit/src/types/toncenter/v3/DNSRecordV3.ts b/packages/walletkit/src/clients/toncenter/types/v3/DNSRecordV3.ts similarity index 93% rename from packages/walletkit/src/types/toncenter/v3/DNSRecordV3.ts rename to packages/walletkit/src/clients/toncenter/types/v3/DNSRecordV3.ts index 7f75ab969..9cfece571 100644 --- a/packages/walletkit/src/types/toncenter/v3/DNSRecordV3.ts +++ b/packages/walletkit/src/clients/toncenter/types/v3/DNSRecordV3.ts @@ -6,8 +6,8 @@ * */ -import type { DnsRecord } from '../DnsRecord'; -import { asAddressFriendly, asMaybeAddressFriendly } from '../../../utils/address'; +import type { DnsRecord } from '../dns'; +import { asAddressFriendly, asMaybeAddressFriendly } from '../../../../utils/address'; export interface DNSRecordV3 { dns_next_resolver: string | null; diff --git a/packages/walletkit/src/types/toncenter/v3/DNSRecordsResponseV3.ts b/packages/walletkit/src/clients/toncenter/types/v3/DNSRecordsResponseV3.ts similarity index 76% rename from packages/walletkit/src/types/toncenter/v3/DNSRecordsResponseV3.ts rename to packages/walletkit/src/clients/toncenter/types/v3/DNSRecordsResponseV3.ts index c1de68c46..0d534c386 100644 --- a/packages/walletkit/src/types/toncenter/v3/DNSRecordsResponseV3.ts +++ b/packages/walletkit/src/clients/toncenter/types/v3/DNSRecordsResponseV3.ts @@ -8,10 +8,10 @@ import type { DNSRecordV3 } from './DNSRecordV3'; import { toDnsRecord } from './DNSRecordV3'; -import type { AddressBookRowV3 } from './AddressBookRowV3'; -import type { DnsRecords } from '../DnsRecords'; -import type { AddressBookRow } from '../AddressBookRow'; -import { asAddressFriendly } from '../../../utils/address'; +import type { AddressBookRowV3 } from '../../../../types/toncenter/v3/AddressBookRowV3'; +import type { DnsRecords } from '../dns'; +import type { AddressBookRow } from '../nfts'; +import { asAddressFriendly } from '../../../../utils/address'; export interface DNSRecordsResponseV3 { address_book: { [key: string]: AddressBookRowV3 }; diff --git a/packages/walletkit/src/types/toncenter/v3/NFTCollectionV3.ts b/packages/walletkit/src/clients/toncenter/types/v3/NFTCollectionV3.ts similarity index 96% rename from packages/walletkit/src/types/toncenter/v3/NFTCollectionV3.ts rename to packages/walletkit/src/clients/toncenter/types/v3/NFTCollectionV3.ts index 7e455745d..755cfccca 100644 --- a/packages/walletkit/src/types/toncenter/v3/NFTCollectionV3.ts +++ b/packages/walletkit/src/clients/toncenter/types/v3/NFTCollectionV3.ts @@ -6,8 +6,8 @@ * */ -import { asAddressFriendly, asMaybeAddressFriendly, Base64ToHex } from '../../../utils'; -import type { NFTCollection } from '../../../api/models'; +import { asAddressFriendly, asMaybeAddressFriendly, Base64ToHex } from '../../../../utils'; +import type { NFTCollection } from '../../../../api/models'; export interface NFTCollectionV3 { address: string; diff --git a/packages/walletkit/src/types/toncenter/v3/NftItemV3.ts b/packages/walletkit/src/clients/toncenter/types/v3/NftItemV3.ts similarity index 95% rename from packages/walletkit/src/types/toncenter/v3/NftItemV3.ts rename to packages/walletkit/src/clients/toncenter/types/v3/NftItemV3.ts index f1eded0af..31d4e7550 100644 --- a/packages/walletkit/src/types/toncenter/v3/NftItemV3.ts +++ b/packages/walletkit/src/clients/toncenter/types/v3/NftItemV3.ts @@ -7,9 +7,9 @@ */ import type { NFTCollectionV3 } from './NFTCollectionV3'; -import { asAddressFriendly, asMaybeAddressFriendly, Base64ToHex } from '../../../utils'; +import { asAddressFriendly, asMaybeAddressFriendly, Base64ToHex } from '../../../../utils'; import { toNftCollection } from './NFTCollectionV3'; -import type { NFT } from '../../../api/models'; +import type { NFT } from '../../../../api/models'; export interface NftItemV3 { address: string; diff --git a/packages/walletkit/src/types/toncenter/v3/NftItemsResponseV3.ts b/packages/walletkit/src/clients/toncenter/types/v3/NftItemsResponseV3.ts similarity index 92% rename from packages/walletkit/src/types/toncenter/v3/NftItemsResponseV3.ts rename to packages/walletkit/src/clients/toncenter/types/v3/NftItemsResponseV3.ts index 0d575de24..19bbe4670 100644 --- a/packages/walletkit/src/types/toncenter/v3/NftItemsResponseV3.ts +++ b/packages/walletkit/src/clients/toncenter/types/v3/NftItemsResponseV3.ts @@ -7,15 +7,15 @@ */ import type { NftItemV3 } from './NftItemV3'; -import type { AddressBookRowV3 } from './AddressBookRowV3'; +import type { AddressBookRowV3 } from '../../../../types/toncenter/v3/AddressBookRowV3'; import type { AddressMetadataV3 } from './AddressMetadataV3'; import { toNftItem } from './NftItemV3'; -import { asAddressFriendly } from '../../../utils'; +import { asAddressFriendly } from '../../../../utils'; import { toTokenInfo } from './NftTokenInfoV3'; -import type { NftMetadata } from '../NftMetadata'; +import type { NftMetadata } from '../nfts'; import { tokenMetaToNftCollection } from './NFTCollectionV3'; -import type { UserFriendlyAddress, NFTsResponse, NFTCollection } from '../../../api/models'; -import { toApiTokenInfo } from '../TokenInfo'; +import type { UserFriendlyAddress, NFTsResponse, NFTCollection } from '../../../../api/models'; +import { toApiTokenInfo } from '../nfts'; export interface NftItemsResponseV3 { address_book?: { [key: string]: AddressBookRowV3 }; diff --git a/packages/walletkit/src/types/toncenter/v3/NftTokenInfoV3.ts b/packages/walletkit/src/clients/toncenter/types/v3/NftTokenInfoV3.ts similarity index 90% rename from packages/walletkit/src/types/toncenter/v3/NftTokenInfoV3.ts rename to packages/walletkit/src/clients/toncenter/types/v3/NftTokenInfoV3.ts index 4288424ee..99c00ec37 100644 --- a/packages/walletkit/src/types/toncenter/v3/NftTokenInfoV3.ts +++ b/packages/walletkit/src/clients/toncenter/types/v3/NftTokenInfoV3.ts @@ -6,8 +6,7 @@ * */ -import type { NftItemAttribute } from '../NftItem'; -import type { TokenInfo } from '../TokenInfo'; +import type { NftItemAttribute, TokenInfo } from '../nfts'; export interface NftTokenInfoV3 { description?: string; @@ -41,7 +40,6 @@ export function toTokenInfo(data: NftTokenInfoV3): TokenInfo { extra: data.extra, animation: data?.extra?.animation_url, }; - // Extract lottie from extra if it exists, or use direct lottie field if (data.lottie) { result.lottie = data.lottie; } else if (data.extra && typeof data.extra === 'object' && 'lottie' in data.extra) { diff --git a/packages/walletkit/src/clients/toncenter/utils.ts b/packages/walletkit/src/clients/toncenter/utils.ts index cf66f6c0d..4b70690ef 100644 --- a/packages/walletkit/src/clients/toncenter/utils.ts +++ b/packages/walletkit/src/clients/toncenter/utils.ts @@ -9,13 +9,20 @@ import { Address } from '@ton/core'; import { Base64ToHex } from '../../utils'; -import type { InternalTransactionId } from './types'; +import type { InternalTransactionId } from './types/internal'; import type { TransactionId } from '../../types/toncenter/api'; export const padBase64 = (data: string): string => { return data.padEnd(data.length + (4 - (data.length % 4)), '='); }; +export const parseMsgSizeCount = (value: string | undefined): number | undefined => { + if (value === undefined) return undefined; + const num = Number(value); + if (!Number.isFinite(num)) return undefined; + return Math.trunc(num); +}; + export const prepareAddress = (address: Address | string): string => { if (address instanceof Address) { address = address.toString(); diff --git a/packages/walletkit/src/contracts/Wallet.ts b/packages/walletkit/src/contracts/Wallet.ts index 874706be6..6df502750 100644 --- a/packages/walletkit/src/contracts/Wallet.ts +++ b/packages/walletkit/src/contracts/Wallet.ts @@ -8,7 +8,7 @@ import type { Cell } from '@ton/core'; -import type { ApiClient } from '../types/toncenter/ApiClient'; +import type { ApiClient } from '../api/interfaces'; export type WalletOptions = { code: Cell; diff --git a/packages/walletkit/src/contracts/v4r2/WalletV4R2.ts b/packages/walletkit/src/contracts/v4r2/WalletV4R2.ts index 8ffbfa630..22577d7cc 100644 --- a/packages/walletkit/src/contracts/v4r2/WalletV4R2.ts +++ b/packages/walletkit/src/contracts/v4r2/WalletV4R2.ts @@ -12,7 +12,7 @@ import type { Address, Cell, Contract, ContractProvider, Sender, MessageRelaxed import { beginCell, contractAddress, SendMode, storeMessageRelaxed } from '@ton/core'; import type { Maybe } from '@ton/core/dist/utils/maybe'; -import type { ApiClient } from '../../types/toncenter/ApiClient'; +import type { ApiClient } from '../../api/interfaces'; import { ParseStack } from '../../utils/tvmStack'; import { asAddressFriendly } from '../../utils'; diff --git a/packages/walletkit/src/contracts/v4r2/WalletV4R2Adapter.ts b/packages/walletkit/src/contracts/v4r2/WalletV4R2Adapter.ts index a3e6f79b7..af433a481 100644 --- a/packages/walletkit/src/contracts/v4r2/WalletV4R2Adapter.ts +++ b/packages/walletkit/src/contracts/v4r2/WalletV4R2Adapter.ts @@ -27,7 +27,7 @@ import type { WalletV4R2Config } from './WalletV4R2'; import { WalletV4R2 } from './WalletV4R2'; import { WalletV4R2CodeCell } from './WalletV4R2.source'; import { defaultWalletIdV4R2 } from './constants'; -import type { ApiClient } from '../../types/toncenter/ApiClient'; +import type { ApiClient } from '../../api/interfaces'; import { HexToBigInt, HexToUint8Array } from '../../utils/base64'; import { asAddressFriendly, formatWalletAddress } from '../../utils/address'; import { CallForSuccess } from '../../utils/retry'; diff --git a/packages/walletkit/src/contracts/v4r2/types.ts b/packages/walletkit/src/contracts/v4r2/types.ts index 57bc56dbc..6c67707dd 100644 --- a/packages/walletkit/src/contracts/v4r2/types.ts +++ b/packages/walletkit/src/contracts/v4r2/types.ts @@ -6,7 +6,7 @@ * */ -import type { ApiClient } from '../../types/toncenter/ApiClient'; +import type { ApiClient } from '../../api/interfaces'; import type { Hex, Network, SignatureDomain } from '../../api/models'; import type { WalletSigner } from '../../api/interfaces'; diff --git a/packages/walletkit/src/contracts/w5/WalletV5R1.fixture.ts b/packages/walletkit/src/contracts/w5/WalletV5R1.fixture.ts index 1c3fcc7b5..9c8aeeada 100644 --- a/packages/walletkit/src/contracts/w5/WalletV5R1.fixture.ts +++ b/packages/walletkit/src/contracts/w5/WalletV5R1.fixture.ts @@ -9,11 +9,12 @@ import { Dictionary } from '@ton/core'; import { mockFn } from '../../../mock.config'; -import type { ApiClient, GetEventsResponse } from '../../types/toncenter/ApiClient'; +import type { ApiClient, GetEventsResponse } from '../../api/interfaces'; import type { FullAccountState } from '../../types/toncenter/api'; -import type { ToncenterEmulationResponse, ToncenterTracesResponse } from '../../types'; +import type { ToncenterTracesResponse } from '../../types'; +import type { EmulationResponse, MasterchainInfo } from '../../api/models'; import type { ResponseUserJettons } from '../../types/export/responses/jettons'; -import type { NftItemsResponse } from '../../types/toncenter/NftItemsResponse'; +import type { NftItemsResponse } from '../../clients/toncenter/types/nfts'; import type { WalletV5R1Id } from './WalletV5R1'; import { walletV5ConfigToCell } from './WalletV5R1'; import { WalletV5R1Adapter } from './WalletV5R1Adapter'; @@ -76,9 +77,10 @@ const walletDataBase64 = walletDataCell.toBoc().toString('base64'); export function createMockApiClient(): ApiClient { return { + getMasterchainInfo: mockFn().mockResolvedValue({} as MasterchainInfo), nftItemsByAddress: mockFn().mockResolvedValue({} as NftItemsResponse), nftItemsByOwner: mockFn().mockResolvedValue({} as NftItemsResponse), - fetchEmulation: mockFn().mockResolvedValue({} as ToncenterEmulationResponse), + fetchEmulation: mockFn().mockResolvedValue({} as EmulationResponse), sendBoc: mockFn().mockResolvedValue('mock-tx-hash'), runGetMethod: mockFn().mockResolvedValue({}), getAccountState: mockFn().mockResolvedValue({ @@ -95,8 +97,8 @@ export function createMockApiClient(): ApiClient { getPendingTransactions: mockFn().mockResolvedValue({} as ToncenterTransactionsResponse), getTrace: mockFn().mockResolvedValue({} as ToncenterTracesResponse), getTransactionsByHash: mockFn().mockResolvedValue({} as ToncenterTransactionsResponse), - resolveDnsWallet: mockFn().mockResolvedValue({} as string | null), - backResolveDnsWallet: mockFn().mockResolvedValue({} as string | null), + resolveDnsWallet: mockFn().mockResolvedValue({} as string | undefined), + backResolveDnsWallet: mockFn().mockResolvedValue({} as string | undefined), jettonsByAddress: mockFn().mockResolvedValue({} as ToncenterResponseJettonMasters), jettonsByOwnerAddress: mockFn().mockResolvedValue({ jettons: [], diff --git a/packages/walletkit/src/contracts/w5/WalletV5R1.ts b/packages/walletkit/src/contracts/w5/WalletV5R1.ts index cc82c926a..ccd2b52a8 100644 --- a/packages/walletkit/src/contracts/w5/WalletV5R1.ts +++ b/packages/walletkit/src/contracts/w5/WalletV5R1.ts @@ -9,7 +9,7 @@ import type { Address, Contract, Sender, ContractProvider, AccountStatus } from '@ton/core'; import { beginCell, Cell, contractAddress, Dictionary, SendMode } from '@ton/core'; -import type { ApiClient } from '../../types/toncenter/ApiClient'; +import type { ApiClient } from '../../api/interfaces'; import type { WalletOptions } from '../Wallet'; import { defaultWalletIdV5R1 } from './WalletV5R1Adapter'; import { ParseStack } from '../../utils/tvmStack'; diff --git a/packages/walletkit/src/contracts/w5/WalletV5R1Adapter.spec.ts b/packages/walletkit/src/contracts/w5/WalletV5R1Adapter.spec.ts index f2136c404..b3f65b866 100644 --- a/packages/walletkit/src/contracts/w5/WalletV5R1Adapter.spec.ts +++ b/packages/walletkit/src/contracts/w5/WalletV5R1Adapter.spec.ts @@ -12,7 +12,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { clearAllMocks, mocked } from '../../../mock.config'; import { WalletV5R1Adapter } from './WalletV5R1Adapter'; -import type { ApiClient } from '../../types/toncenter/ApiClient'; +import type { ApiClient } from '../../api/interfaces'; import type { FullAccountState } from '../../types/toncenter/api'; import { HexToBase64, Uint8ArrayToHex } from '../../utils/base64'; import { Signer } from '../../utils/Signer'; diff --git a/packages/walletkit/src/contracts/w5/WalletV5R1Adapter.ts b/packages/walletkit/src/contracts/w5/WalletV5R1Adapter.ts index 10144f0c1..13dd67514 100644 --- a/packages/walletkit/src/contracts/w5/WalletV5R1Adapter.ts +++ b/packages/walletkit/src/contracts/w5/WalletV5R1Adapter.ts @@ -30,7 +30,7 @@ import { FakeSignature } from '../../utils/sign'; import { asAddressFriendly, formatWalletAddress } from '../../utils/address'; import { CallForSuccess } from '../../utils/retry'; import { ActionSendMsg, packActionsList } from './actions'; -import type { ApiClient } from '../../types/toncenter/ApiClient'; +import type { ApiClient } from '../../api/interfaces'; import { HexToBigInt, HexToUint8Array } from '../../utils/base64'; import { CreateTonProofMessageBytes } from '../../utils/tonProof'; import type { WalletId } from '../../utils/walletId'; diff --git a/packages/walletkit/src/core/JettonsManager.ts b/packages/walletkit/src/core/JettonsManager.ts index 5d2aa3e50..c2ea96840 100644 --- a/packages/walletkit/src/core/JettonsManager.ts +++ b/packages/walletkit/src/core/JettonsManager.ts @@ -54,19 +54,6 @@ export class JettonsManager implements JettonsAPI { } log.info('JettonsManager initialized', { cacheSize }); - - // Set up event listener for emulation results for jetton caching - // TODO Fix network in emulation result - // this.eventEmitter.on('emulationResult', ({ payload: emulationResult }) => { - // if (emulationResult && emulationResult.metadata) { - // const network = (emulationResult as { network: ChainId }).network; - // this.addJettonsFromEmulationMetadata( - // Network.custom(network), - // (emulationResult as { metadata: Record }) - // .metadata, - // ); - // } - // }); } /** diff --git a/packages/walletkit/src/core/NetworkManager.ts b/packages/walletkit/src/core/NetworkManager.ts index 021739425..9d7a1cc2a 100644 --- a/packages/walletkit/src/core/NetworkManager.ts +++ b/packages/walletkit/src/core/NetworkManager.ts @@ -8,7 +8,7 @@ // Network management for multi-network support -import type { ApiClient } from '../types/toncenter/ApiClient'; +import type { ApiClient } from '../api/interfaces'; import type { ApiClientConfig, TonWalletKitOptions } from '../types/config'; import { ApiClientToncenter } from '../clients/toncenter'; import { globalLogger } from './Logger'; diff --git a/packages/walletkit/src/core/TonWalletKit.ts b/packages/walletkit/src/core/TonWalletKit.ts index dad6a2fa4..1e8a8cf55 100644 --- a/packages/walletkit/src/core/TonWalletKit.ts +++ b/packages/walletkit/src/core/TonWalletKit.ts @@ -42,7 +42,7 @@ import { EventEmitter } from './EventEmitter'; import type { StorageEventProcessor } from './EventProcessor'; import type { BridgeManager } from './BridgeManager'; import type { BridgeEventMessageInfo, InjectedToExtensionBridgeRequestPayload } from '../types/jsBridge'; -import type { ApiClient } from '../types/toncenter/ApiClient'; +import type { ApiClient } from '../api/interfaces'; import { StreamingManager } from '../streaming/StreamingManager'; import type { WalletKitEvents, WalletKitEventEmitter } from '../types/emitter'; import { AnalyticsManager } from '../analytics'; diff --git a/packages/walletkit/src/core/wallet/extensions/nft.ts b/packages/walletkit/src/core/wallet/extensions/nft.ts index 0fdaf3c1a..2f5d249f9 100644 --- a/packages/walletkit/src/core/wallet/extensions/nft.ts +++ b/packages/walletkit/src/core/wallet/extensions/nft.ts @@ -35,7 +35,7 @@ export class WalletNftClass implements WalletNftInterface { return getNftsFromClient(this.getClient(), this.getAddress(), params); } - async getNft(this: Wallet, address: UserFriendlyAddress): Promise { + async getNft(this: Wallet, address: UserFriendlyAddress): Promise { return getNftFromClient(this.getClient(), address); } diff --git a/packages/walletkit/src/core/wallet/extensions/ton.ts b/packages/walletkit/src/core/wallet/extensions/ton.ts index 7532aea77..b8761a503 100644 --- a/packages/walletkit/src/core/wallet/extensions/ton.ts +++ b/packages/walletkit/src/core/wallet/extensions/ton.ts @@ -9,7 +9,7 @@ import { isValidAddress } from '../../../utils/address'; import { isValidNanotonAmount, validateTransactionMessage } from '../../../validation'; import { CallForSuccess } from '../../../utils/retry'; -import { createTransactionPreview as createTransactionPreviewHelper } from '../../../utils/toncenterEmulation'; +import { createTransactionPreview as createTransactionPreviewHelper } from '../../../utils/transactionPreview'; import { createCommentPayloadBase64 } from '../../../utils/messageBuilders'; import { getNormalizedExtMessageHash } from '../../../utils/getNormalizedExtMessageHash'; import { ERROR_CODES, WalletKitError } from '../../../errors'; diff --git a/packages/walletkit/src/defi/staking/tonstakers/PoolContract.ts b/packages/walletkit/src/defi/staking/tonstakers/PoolContract.ts index 8979efb43..241442679 100644 --- a/packages/walletkit/src/defi/staking/tonstakers/PoolContract.ts +++ b/packages/walletkit/src/defi/staking/tonstakers/PoolContract.ts @@ -9,7 +9,7 @@ import { Address, beginCell } from '@ton/core'; import type { Base64String, TokenAmount, TransactionRequestMessage, UserFriendlyAddress } from '../../../api/models'; -import type { ApiClient } from '../../../types/toncenter/ApiClient'; +import type { ApiClient } from '../../../api/interfaces'; import { CONTRACT } from './constants'; import { asAddressFriendly, ReaderStack, SerializeStack } from '../../../utils'; import { formatUnits } from '../../../utils/units'; diff --git a/packages/walletkit/src/defi/staking/tonstakers/TonStakersStakingProvider.spec.ts b/packages/walletkit/src/defi/staking/tonstakers/TonStakersStakingProvider.spec.ts index b5442fe50..a775e8d59 100644 --- a/packages/walletkit/src/defi/staking/tonstakers/TonStakersStakingProvider.spec.ts +++ b/packages/walletkit/src/defi/staking/tonstakers/TonStakersStakingProvider.spec.ts @@ -16,7 +16,7 @@ import { PoolContract } from './PoolContract'; import { CONTRACT } from './constants'; import { Network, UnstakeMode } from '../../../api/models'; import type { Base64String, UserFriendlyAddress } from '../../../api/models'; -import type { ApiClient } from '../../../types/toncenter/ApiClient'; +import type { ApiClient } from '../../../api/interfaces'; const mockApiClient = { runGetMethod: vi.fn(), diff --git a/packages/walletkit/src/defi/staking/tonstakers/TonStakersStakingProvider.ts b/packages/walletkit/src/defi/staking/tonstakers/TonStakersStakingProvider.ts index d6d5c165b..ead87e34e 100644 --- a/packages/walletkit/src/defi/staking/tonstakers/TonStakersStakingProvider.ts +++ b/packages/walletkit/src/defi/staking/tonstakers/TonStakersStakingProvider.ts @@ -27,7 +27,7 @@ import { PoolContract } from './PoolContract'; import { StakingCache } from './StakingCache'; import { ApiClientTonApi } from '../../../clients/tonapi/ApiClientTonApi'; import { formatUnits, parseUnits } from '../../../utils/units'; -import type { ApiClient } from '../../../types/toncenter/ApiClient'; +import type { ApiClient } from '../../../api/interfaces'; import type { UnstakeModes } from '../../../api/models/staking/UnstakeMode'; const log = globalLogger.createChild('TonStakersStakingProvider'); diff --git a/packages/walletkit/src/handlers/TransactionHandler.ts b/packages/walletkit/src/handlers/TransactionHandler.ts index fcba5bf05..11c43ed70 100644 --- a/packages/walletkit/src/handlers/TransactionHandler.ts +++ b/packages/walletkit/src/handlers/TransactionHandler.ts @@ -21,7 +21,7 @@ import type { import { validateTransactionMessages as validateTonConnectTransactionMessages } from '../validation/transaction'; import { globalLogger } from '../core/Logger'; import { isValidAddress } from '../utils/address'; -import { createTransactionPreview as createTransactionPreviewHelper } from '../utils/toncenterEmulation'; +import { createTransactionPreview as createTransactionPreviewHelper } from '../utils/transactionPreview'; import { BasicHandler } from './BasicHandler'; import { CallForSuccess } from '../utils/retry'; import type { WalletKitEventEmitter } from '../types/emitter'; diff --git a/packages/walletkit/src/index.ts b/packages/walletkit/src/index.ts index a3bc2841f..3c50e1721 100644 --- a/packages/walletkit/src/index.ts +++ b/packages/walletkit/src/index.ts @@ -49,7 +49,6 @@ export { LocalStorageAdapter } from './storage/adapters/local'; export { MemoryStorageAdapter } from './storage/adapters/memory'; export { ExtensionStorageAdapter } from './storage/adapters/extension'; export { Storage } from './storage/Storage'; -export type { ApiClient } from './types/toncenter/ApiClient'; export { formatWalletAddress } from './utils/address'; export { CallForSuccess } from './utils/retry'; export { createWalletId } from './utils/walletId'; @@ -110,20 +109,7 @@ export { RESTORE_CONNECTION_TIMEOUT, DEFAULT_REQUEST_TIMEOUT } from './bridge/ut export { CreateTonProofMessageBytes } from './utils/tonProof'; export type { AnalyticsAppInfo, AnalyticsManagerOptions } from './analytics'; -// API Client types (ApiClient is exported above) -export type { - TransactionsByAddressRequest, - GetTransactionByHashRequest, - GetPendingTransactionsRequest, - GetTraceRequest, - GetPendingTraceRequest, - GetJettonsByOwnerRequest, - GetJettonsByAddressRequest, - GetEventsRequest, - GetEventsResponse, -} from './types/toncenter/ApiClient'; export type { FullAccountState } from './types/toncenter/api'; -export type { ToncenterEmulationResult } from './utils/toncenterEmulation'; export type { ToncenterResponseJettonMasters } from './types/toncenter/emulation'; export { asHex } from './utils/hex'; export { diff --git a/packages/walletkit/src/streaming/toncenter/mappers/map-transaction.ts b/packages/walletkit/src/streaming/toncenter/mappers/map-transaction.ts index 0a322596c..d103863c1 100644 --- a/packages/walletkit/src/streaming/toncenter/mappers/map-transaction.ts +++ b/packages/walletkit/src/streaming/toncenter/mappers/map-transaction.ts @@ -17,6 +17,7 @@ import type { } from '../../../api/models'; import { Base64ToHex } from '../../../utils/base64'; import { asAddressFriendly, asMaybeAddressFriendly } from '../../../utils/address'; +import { parseMsgSizeCount } from '../../../clients/toncenter/utils'; import type { StreamingV2AccountState, StreamingV2TransactionRaw, StreamingV2TransactionDescription } from '../types'; import type { EmulationBlockRef, EmulationMessage } from '../../../types/toncenter/emulation'; @@ -131,7 +132,12 @@ const toTransactionDescription = (desc: StreamingV2TransactionDescription): Tran skippedActionsNumber: action.skipped_actions, messagesCreatedNumber: action.msgs_created, actionListHash: action.action_list_hash ? Base64ToHex(action.action_list_hash) : undefined, - totalMessagesSize: action.tot_msg_size, + totalMessagesSize: action.tot_msg_size + ? { + cells: parseMsgSizeCount(action.tot_msg_size.cells), + bits: parseMsgSizeCount(action.tot_msg_size.bits), + } + : undefined, } : undefined, }; diff --git a/packages/walletkit/src/types/config.ts b/packages/walletkit/src/types/config.ts index 3acad1afe..dd3a52864 100644 --- a/packages/walletkit/src/types/config.ts +++ b/packages/walletkit/src/types/config.ts @@ -12,7 +12,7 @@ import type { StorageAdapter, StorageConfig } from '../storage'; import type { EventProcessorConfig } from '../core/EventProcessor'; import type { DeviceInfo, WalletInfo } from './jsBridge'; import type { BridgeConfig } from './internal'; -import type { ApiClient } from './toncenter/ApiClient'; +import type { ApiClient } from '../api/interfaces'; import type { AnalyticsManagerOptions } from '../analytics'; import type { TONConnectSessionManager } from '../api/interfaces'; diff --git a/packages/walletkit/src/types/export/responses/jettons.ts b/packages/walletkit/src/types/export/responses/jettons.ts index c3c846aaa..e0fd6221c 100644 --- a/packages/walletkit/src/types/export/responses/jettons.ts +++ b/packages/walletkit/src/types/export/responses/jettons.ts @@ -7,7 +7,7 @@ */ import type { AddressJetton } from '../../jettons'; -import type { Pagination } from '../../toncenter/Pagination'; +import type { Pagination } from '../../../clients/toncenter/types/nfts'; import type { AddressBookRowV3 } from '../../toncenter/v3/AddressBookRowV3'; // Toncenter Jetton Wallets API Response Types diff --git a/packages/walletkit/src/types/index.ts b/packages/walletkit/src/types/index.ts index af483d97a..44687f07a 100644 --- a/packages/walletkit/src/types/index.ts +++ b/packages/walletkit/src/types/index.ts @@ -49,8 +49,8 @@ export type { export { JettonError, JettonErrorCode } from './jettons'; // Toncenter types +export type { ToncenterEmulationResponse } from '../clients/toncenter/types/raw-emulation'; export type { - ToncenterEmulationResponse, ToncenterResponseJettonWallets, ToncenterResponseJettonMasters, ToncenterJettonWallet, @@ -65,23 +65,7 @@ export type { export type { FullAccountState } from './toncenter/api'; -export type { MasterchainInfo } from '../api/models'; - -export type { - TransactionsByAddressRequest, - GetTransactionByHashRequest, - GetPendingTransactionsRequest, - GetTraceRequest, - GetPendingTraceRequest, - GetJettonsByOwnerRequest, - GetJettonsByAddressRequest, - GetEventsRequest, - GetEventsResponse, -} from './toncenter/ApiClient'; - -export type { NftItem } from './toncenter/NftItem'; - -export type { NftItems } from './toncenter/NftItems'; +export type { NftItem, NftItems } from '../clients/toncenter/types/nfts'; export { emulationEvent, toEvent, toAddressBook } from './toncenter/AccountEvent'; // Account Event types diff --git a/packages/walletkit/src/types/kit.ts b/packages/walletkit/src/types/kit.ts index 878fe718f..7bc6e6907 100644 --- a/packages/walletkit/src/types/kit.ts +++ b/packages/walletkit/src/types/kit.ts @@ -12,7 +12,7 @@ import type { CONNECT_EVENT_ERROR_CODES, SendTransactionRpcResponseError } from import type { JettonsAPI } from './jettons'; import type { StreamingAPI } from '../api/interfaces'; -import type { ApiClient } from './toncenter/ApiClient'; +import type { ApiClient } from '../api/interfaces'; import type { Wallet, WalletAdapter } from '../api/interfaces'; import type { Network } from '../api/models/core/Network'; import type { WalletId } from '../utils/walletId'; diff --git a/packages/walletkit/src/types/toncenter/AccountEvent.ts b/packages/walletkit/src/types/toncenter/AccountEvent.ts index a01079c98..d212779f8 100644 --- a/packages/walletkit/src/types/toncenter/AccountEvent.ts +++ b/packages/walletkit/src/types/toncenter/AccountEvent.ts @@ -8,12 +8,12 @@ import type { EmulationTraceNode, - ToncenterEmulationResponse, ToncenterTraceItem, ToncenterTransaction, EmulationTokenInfoMasters, EmulationTokenInfoWallets, } from './emulation'; +import type { ToncenterEmulationResponse } from '../../clients/toncenter/types/raw-emulation'; import { asAddressFriendly, asMaybeAddressFriendly } from '../../utils/address'; import { Base64NormalizeUrl, Base64ToHex } from '../../utils/base64'; import { computeStatus, parseIncomingTonTransfers, parseOutgoingTonTransfers } from './parsers/TonTransfer'; diff --git a/packages/walletkit/src/types/toncenter/AddressBookRow.ts b/packages/walletkit/src/types/toncenter/AddressBookRow.ts deleted file mode 100644 index 9eca2f1fb..000000000 --- a/packages/walletkit/src/types/toncenter/AddressBookRow.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -export interface AddressBookRow { - domain: string | null; -} diff --git a/packages/walletkit/src/types/toncenter/AddressMetadata.ts b/packages/walletkit/src/types/toncenter/AddressMetadata.ts deleted file mode 100644 index 1db270677..000000000 --- a/packages/walletkit/src/types/toncenter/AddressMetadata.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import type { TokenInfo } from './TokenInfo'; - -export interface AddressMetadata { - isIndexed: boolean; - tokenInfo: TokenInfo[]; -} diff --git a/packages/walletkit/src/types/toncenter/DnsRecords.ts b/packages/walletkit/src/types/toncenter/DnsRecords.ts deleted file mode 100644 index 567fa2ed5..000000000 --- a/packages/walletkit/src/types/toncenter/DnsRecords.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import type { AddressBookRow } from './AddressBookRow'; -import type { DnsRecord } from './DnsRecord'; - -export interface DnsRecords { - addressBook: { [key: string]: AddressBookRow }; - records: DnsRecord[]; -} diff --git a/packages/walletkit/src/types/toncenter/NftCollection.ts b/packages/walletkit/src/types/toncenter/NftCollection.ts deleted file mode 100644 index 35fbd3d43..000000000 --- a/packages/walletkit/src/types/toncenter/NftCollection.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import type { Hex, UserFriendlyAddress } from '../../api/models'; - -export interface NftCollection { - address: UserFriendlyAddress; - - collectionContent?: { - uri?: string; - [key: string]: unknown; - }; - - lastTransactionLt?: string; - name?: string; - nextItemIndex?: string; - ownerAddress?: UserFriendlyAddress | null; - - codeHash?: Hex | null; - dataHash?: Hex | null; - - description?: string; - image?: string; - extra?: { - cover_image?: string; - uri?: string; - _image_big?: string; - _image_medium?: string; - _image_small?: string; - [key: string]: unknown; - }; -} diff --git a/packages/walletkit/src/types/toncenter/NftCollections.ts b/packages/walletkit/src/types/toncenter/NftCollections.ts deleted file mode 100644 index 83ad38389..000000000 --- a/packages/walletkit/src/types/toncenter/NftCollections.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import type { NftCollection } from './NftCollection'; -import type { AddressMetadata } from './AddressMetadata'; -import type { AddressBookRow } from './AddressBookRow'; - -export interface NftCollections { - addressBook?: { [key: string]: AddressBookRow }; - metadata?: { [key: string]: AddressMetadata }; - nftCollections?: NftCollection[]; -} diff --git a/packages/walletkit/src/types/toncenter/NftItem.ts b/packages/walletkit/src/types/toncenter/NftItem.ts deleted file mode 100644 index 8336e4f82..000000000 --- a/packages/walletkit/src/types/toncenter/NftItem.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import type { UserFriendlyAddress, Hex } from '../../api/models'; -import type { NftCollection } from './NftCollection'; -import type { TokenInfo } from './TokenInfo'; - -export interface NftItemAttribute { - trait_type: string; - value: string; -} - -export interface NftItem { - address: UserFriendlyAddress; - auctionContractAddress: UserFriendlyAddress | null; - codeHash: Hex | null; - dataHash: Hex | null; - collection: NftCollection | null; - collectionAddress: UserFriendlyAddress | null; - content?: { - uri?: string; - [key: string]: unknown; - }; - metadata?: TokenInfo; - index: string; - init: boolean; - isSbt?: boolean; - lastTransactionLt?: string; - onSale: boolean; - ownerAddress: UserFriendlyAddress | null; - realOwner: UserFriendlyAddress | null; - saleContractAddress: UserFriendlyAddress | null; - attributes?: NftItemAttribute[]; -} diff --git a/packages/walletkit/src/types/toncenter/NftItems.ts b/packages/walletkit/src/types/toncenter/NftItems.ts deleted file mode 100644 index d87d2fc11..000000000 --- a/packages/walletkit/src/types/toncenter/NftItems.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import type { NftItem } from './NftItem'; -import type { Pagination } from './Pagination'; - -export interface NftItems { - items: NftItem[]; - pagination: Pagination; -} diff --git a/packages/walletkit/src/types/toncenter/NftItemsResponse.ts b/packages/walletkit/src/types/toncenter/NftItemsResponse.ts deleted file mode 100644 index a730a5d7e..000000000 --- a/packages/walletkit/src/types/toncenter/NftItemsResponse.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import type { UserFriendlyAddress } from '../../api/models'; -import type { AddressBookRow } from './AddressBookRow'; -import type { NftItems } from './NftItems'; -import type { NftMetadata } from './NftMetadata'; - -export interface NftItemsResponse extends NftItems { - addressBook: { [key: UserFriendlyAddress]: AddressBookRow }; - metadata: NftMetadata; -} diff --git a/packages/walletkit/src/types/toncenter/NftMetadata.ts b/packages/walletkit/src/types/toncenter/NftMetadata.ts deleted file mode 100644 index e7d192c62..000000000 --- a/packages/walletkit/src/types/toncenter/NftMetadata.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import type { UserFriendlyAddress } from '../../api/models'; -import type { AddressMetadata } from './AddressMetadata'; - -export type NftMetadata = { [key: UserFriendlyAddress]: AddressMetadata }; diff --git a/packages/walletkit/src/types/toncenter/Pagination.ts b/packages/walletkit/src/types/toncenter/Pagination.ts deleted file mode 100644 index bd8f3bde6..000000000 --- a/packages/walletkit/src/types/toncenter/Pagination.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -export interface Pagination { - offset: number; - limit: number; - pages?: number; -} diff --git a/packages/walletkit/src/types/toncenter/TokenInfo.ts b/packages/walletkit/src/types/toncenter/TokenInfo.ts deleted file mode 100644 index fb9ae5eab..000000000 --- a/packages/walletkit/src/types/toncenter/TokenInfo.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import type { NftItemAttribute } from './NftItem'; -import type { TokenInfo as APITokenInfo } from '../../api/models'; - -export interface TokenInfo { - description?: string; - - extra?: { - attributes?: NftItemAttribute[]; - lottie?: string; - uri?: string; - _image_big?: string; - _image_medium?: string; - _image_small?: string; - animation_url?: string; - content_url?: string; - [key: string]: unknown; - }; - image?: string; - lottie?: string; - name?: string; - symbol?: string; - type?: string; - valid?: boolean; - animation?: string; -} - -export function toApiTokenInfo(data: TokenInfo): APITokenInfo { - var lottie: string | undefined; - var animationUrl: string | undefined; - - if (data?.extra?.animation_url) { - animationUrl = data.extra.animation_url; - } else if (data?.extra?.content_url && data.extra.content_url.includes('mp4')) { - animationUrl = data.extra.content_url; - } - - if (data.lottie) { - lottie = data.lottie; - } else if (data.extra && typeof data.extra === 'object' && 'lottie' in data.extra) { - const lottieValue = (data.extra as Record).lottie; - if (typeof lottieValue === 'string') { - lottie = lottieValue; - } - } - - const result: APITokenInfo = { - name: data.name, - description: data.description, - image: { - url: data.image ?? data.extra?._image_medium, - smallUrl: data.extra?._image_small, - mediumUrl: data.extra?._image_medium, - largeUrl: data.extra?._image_big, - }, - animation: { - url: animationUrl, - lottie: lottie, - }, - symbol: data.symbol, - }; - return result; -} diff --git a/packages/walletkit/src/types/toncenter/emulation.ts b/packages/walletkit/src/types/toncenter/emulation.ts index 3c63bc15b..c874d828a 100644 --- a/packages/walletkit/src/types/toncenter/emulation.ts +++ b/packages/walletkit/src/types/toncenter/emulation.ts @@ -6,70 +6,21 @@ * */ -import { Base64ToHex } from '../../utils/base64'; -import type { - AccountState, - AccountStatus, - Transaction, - TransactionMessage, - TransactionDescription, - TransactionBlockRef, - TransactionTraceNode, - TransactionTraceAction, - TransactionTraceActionDetails, - TransactionTraceActionJettonSwapDetails, - TransactionTraceActionCallContractDetails, - TransactionTraceActionTONTransferDetails, - TransactionEmulatedTrace, - TransactionsResponse, - UserFriendlyAddress, - Base64String, -} from '../../api/models'; -import { asAddressFriendly, asMaybeAddressFriendly } from '../../utils/address'; import type { AddressBookRowV3, MetadataV3 } from './v3/AddressBookRowV3'; -import { toAddressBook } from './v3/AddressBookRowV3'; -// Types for Toncenter emulation endpoint response - -// Root response -export interface ToncenterEmulationResponse extends MetadataV3 { - mc_block_seqno: number; - trace: EmulationTraceNode; - transactions: Record; - actions: EmulationAction[]; - code_cells: Record; // base64-encoded cells by code hash - data_cells: Record; // base64-encoded cells by data hash - rand_seed: string; - is_incomplete: boolean; -} - -export function toTransactionEmulatedTrace(response: ToncenterEmulationResponse): TransactionEmulatedTrace { - return { - mcBlockSeqno: response.mc_block_seqno, - trace: toTransactionTraceNode(response.trace), - transactions: Object.fromEntries( - Object.entries(response.transactions ?? {}).map(([hash, tx]) => [Base64ToHex(hash), toTransaction(tx)]), - ), - actions: response.actions.map(toTransactionTraceAction), - randSeed: Base64ToHex(response.rand_seed), - isIncomplete: response.is_incomplete, - codeCells: Object.fromEntries( - Object.entries(response.code_cells ?? {}).map(([hash, cell]) => [Base64ToHex(hash), cell as Base64String]), - ), - dataCells: Object.fromEntries( - Object.entries(response.data_cells ?? {}).map(([hash, cell]) => [Base64ToHex(hash), cell as Base64String]), - ), - metadata: {}, // to be filled later - addressBook: toAddressBook(response.address_book), - }; -} - -function toTransactionTraceNode(node: EmulationTraceNode): TransactionTraceNode { - return { - txHash: node.tx_hash ? Base64ToHex(node.tx_hash) : undefined, - inMsgHash: node.in_msg_hash ? Base64ToHex(node.in_msg_hash) : undefined, - children: node.children?.map(toTransactionTraceNode) ?? [], - }; -} +import type { EmulationAction } from '../../clients/toncenter/types/raw-emulation'; + +export type { + EmulationAddressMetadata, + EmulationTokenInfo, + EmulationTokenInfoBase, + EmulationTokenInfoWallets, + EmulationTokenInfoMasters, +} from '../../clients/toncenter/types/metadata'; +export type { + ToncenterResponseJettonMasters, + ToncenterResponseJettonWallets, + ToncenterJettonWallet, +} from '../../clients/toncenter/types/jettons'; export interface ToncenterTracesResponse extends MetadataV3 { traces: ToncenterTraceItem[]; @@ -80,13 +31,6 @@ export interface ToncenterTransactionsResponse { address_book: Record; } -export function toTransactionsResponse(response: ToncenterTransactionsResponse): TransactionsResponse { - return { - transactions: response.transactions?.map(toTransaction) ?? [], - addressBook: toAddressBook(response.address_book), - }; -} - export interface ToncenterTraceItem { actions?: EmulationAction[]; end_lt: string; @@ -144,58 +88,14 @@ export interface ToncenterTransaction { trace_id?: string; } -function toTransaction(tx: ToncenterTransaction): Transaction { - return { - account: asAddressFriendly(tx.account), - accountStateBefore: toAccountState(tx.account_state_before), - accountStateAfter: toAccountState(tx.account_state_after), - description: toTransactionDescription(tx.description), - hash: Base64ToHex(tx.hash), - logicalTime: tx.lt, - now: tx.now, - mcBlockSeqno: tx.mc_block_seqno, - traceExternalHash: Base64ToHex(tx.trace_external_hash), - traceId: tx.trace_id ?? undefined, - previousTransactionHash: tx.prev_trans_hash ? Base64ToHex(tx.prev_trans_hash) : undefined, - previousTransactionLogicalTime: tx.prev_trans_lt ?? undefined, - origStatus: toAccountStatus(tx.orig_status), - endStatus: toAccountStatus(tx.end_status), - totalFees: tx.total_fees, - totalFeesExtraCurrencies: tx.total_fees_extra_currencies, - blockRef: toTransactionBlockRef(tx.block_ref), - inMessage: tx.in_msg ? toTransactionMessage(tx.in_msg) : undefined, - outMessages: tx.out_msgs?.map(toTransactionMessage) ?? [], - isEmulated: tx.emulated, - }; -} - export type EmulationAccountStatus = 'active' | 'frozen' | 'uninit'; -function toAccountStatus(status: EmulationAccountStatus | string): AccountStatus { - if (status === 'active') { - return { type: 'active' }; - } else if (status === 'frozen') { - return { type: 'frozen' }; - } else if (status === 'uninit') { - return { type: 'uninit' }; - } else { - return { type: 'unknown', value: status }; - } -} export interface EmulationBlockRef { workchain: number; shard: string; seqno: number; } -function toTransactionBlockRef(ref: EmulationBlockRef): TransactionBlockRef { - return { - workchain: ref.workchain, - shard: ref.shard, - seqno: ref.seqno, - }; -} - export interface EmulationTransactionDescription { type: string; // e.g. "ord" aborted: boolean; @@ -245,63 +145,6 @@ export interface EmulationTransactionDescription { }; } -function toTransactionDescription(desc: EmulationTransactionDescription): TransactionDescription { - return { - type: desc.type, - isAborted: desc.aborted, - isDestroyed: desc.destroyed, - isCreditFirst: desc.credit_first, - isTock: desc.is_tock, - isInstalled: desc.installed, - storagePhase: { - storageFeesCollected: desc.storage_ph?.storage_fees_collected, - statusChange: desc.storage_ph?.status_change, - }, - creditPhase: desc.credit_ph - ? { - credit: desc.credit_ph?.credit, - } - : undefined, - computePhase: { - isSkipped: desc.compute_ph?.skipped, - isSuccess: desc.compute_ph?.success, - isMessageStateUsed: desc.compute_ph?.msg_state_used, - isAccountActivated: desc.compute_ph?.account_activated, - gasFees: desc.compute_ph?.gas_fees, - gasUsed: desc.compute_ph?.gas_used, - gasLimit: desc.compute_ph?.gas_limit, - gasCredit: desc.compute_ph?.gas_credit, - mode: desc.compute_ph?.mode, - exitCode: desc.compute_ph?.exit_code, - vmStepsNumber: desc.compute_ph?.vm_steps, - vmInitStateHash: desc.compute_ph?.vm_init_state_hash - ? Base64ToHex(desc.compute_ph.vm_init_state_hash) - : undefined, - vmFinalStateHash: desc.compute_ph?.vm_final_state_hash - ? Base64ToHex(desc.compute_ph.vm_final_state_hash) - : undefined, - }, - action: { - isSuccess: desc.action?.success, - isValid: desc.action?.valid, - hasNoFunds: desc.action?.no_funds, - statusChange: desc.action?.status_change, - totalForwardingFees: desc.action?.total_fwd_fees, - totalActionFees: desc.action?.total_action_fees, - resultCode: desc.action?.result_code, - totalActionsNumber: desc.action?.tot_actions, - specActionsNumber: desc.action?.spec_actions, - skippedActionsNumber: desc.action?.skipped_actions, - messagesCreatedNumber: desc.action?.msgs_created, - actionListHash: desc.action?.action_list_hash ? Base64ToHex(desc.action.action_list_hash) : undefined, - totalMessagesSize: { - cells: desc.action?.tot_msg_size.cells, - bits: desc.action?.tot_msg_size.bits, - }, - }, - }; -} - export interface EmulationMessage { hash: string; source: string | null; @@ -326,31 +169,6 @@ export interface EmulationMessage { hash_norm?: string; // present on external message in some responses } -function toTransactionMessage(msg: EmulationMessage): TransactionMessage { - return { - hash: Base64ToHex(msg.hash), - normalizedHash: msg.hash_norm ? Base64ToHex(msg.hash_norm) : undefined, - source: asMaybeAddressFriendly(msg.source) ?? undefined, - destination: asMaybeAddressFriendly(msg.destination) ?? undefined, - value: msg.value ?? undefined, - valueExtraCurrencies: msg.value_extra_currencies, - fwdFee: msg.fwd_fee ?? undefined, - ihrFee: msg.ihr_fee ?? undefined, - creationLogicalTime: msg.created_lt ?? undefined, - createdAt: msg.created_at ? Number(msg.created_at) : undefined, - ihrDisabled: msg.ihr_disabled ?? undefined, - isBounce: msg.bounce ?? undefined, - isBounced: msg.bounced ?? undefined, - importFee: msg.import_fee ?? undefined, - opcode: msg.opcode ?? undefined, - messageContent: { - hash: msg.message_content?.hash ? Base64ToHex(msg.message_content.hash) : undefined, - body: msg.message_content?.body ? (msg.message_content.body as Base64String) : undefined, - decoded: msg.message_content?.decoded ?? undefined, - }, - }; -} - export interface EmulationAccountState { hash: string; balance: string; @@ -360,253 +178,3 @@ export interface EmulationAccountState { data_hash: string | null; code_hash: string | null; } - -function toAccountState(state: EmulationAccountState): AccountState { - return { - hash: Base64ToHex(state.hash), - balance: state.balance, - extraCurrencies: state.extra_currencies ?? undefined, - accountStatus: state.account_status ? toAccountStatus(state.account_status) : undefined, - frozenHash: state.frozen_hash ? Base64ToHex(state.frozen_hash) : undefined, - dataHash: state.data_hash ? Base64ToHex(state.data_hash) : undefined, - codeHash: state.code_hash ? Base64ToHex(state.code_hash) : undefined, - }; -} - -// Actions -export type EmulationActionType = 'jetton_swap' | 'call_contract' | string; - -export interface EmulationActionBase { - trace_id: string | null; - action_id: string; - start_lt: string; - end_lt: string; - start_utime: number; - end_utime: number; - trace_end_lt: string; - trace_end_utime: number; - trace_mc_seqno_end: number; - transactions: string[]; // list of tx hashes in this action - success: boolean; - type: EmulationActionType; - trace_external_hash: string; - accounts: string[]; -} - -export interface EmulationJettonSwapDetails { - dex: string; // e.g. "stonfi" - sender: string; // address - asset_in: string; // jetton master - asset_out: string; // jetton master - dex_incoming_transfer: { - asset: string; - source: string; - destination: string; - source_jetton_wallet: string | null; - destination_jetton_wallet: string | null; - amount: string; - }; - dex_outgoing_transfer: { - asset: string; - source: string; - destination: string; - source_jetton_wallet: string | null; - destination_jetton_wallet: string | null; - amount: string; - }; - peer_swaps: unknown[]; -} - -export interface EmulationCallContractDetails { - opcode: string; - source: string; - destination: string; - value: string; - extra_currencies: Record | null; -} - -export interface EmulationTonTransferDetails { - source: string; - destination: string; - value: string; - value_extra_currencies: Record; - comment: string | null; - encrypted: boolean; -} - -export type EmulationActionDetails = - | EmulationTonTransferDetails - | EmulationJettonSwapDetails - | EmulationCallContractDetails - | Record; // fallback for unknown action types - -export interface EmulationAction extends EmulationActionBase { - details: EmulationActionDetails; -} - -function toTransactionTraceAction(action: EmulationAction): TransactionTraceAction { - return { - traceId: action.trace_id ?? undefined, - actionId: action.action_id, - startLt: action.start_lt, - endLt: action.end_lt, - startUtime: action.start_utime, - endUtime: action.end_utime, - traceEndLt: action.trace_end_lt, - traceEndUtime: action.trace_end_utime, - traceMcSeqnoEnd: action.trace_mc_seqno_end, - transactions: action.transactions.map(Base64ToHex), - isSuccess: action.success, - traceExternalHash: Base64ToHex(action.trace_external_hash), - // Filter out invalid addresses - accounts: (action.accounts ?? []) - .map(asMaybeAddressFriendly) - .filter((addr): addr is UserFriendlyAddress => addr !== null), - details: toTransactionTraceActionDetails(action), - }; -} - -function toTransactionTraceActionDetails(action: EmulationAction): TransactionTraceActionDetails { - if (action.type === 'jetton_swap') { - return { - type: 'jetton_swap', - value: toTransactionTraceActionJettonSwapDetails(action.details as EmulationJettonSwapDetails), - }; - } else if (action.type === 'call_contract') { - return { - type: 'call_contract', - value: toTransactionTraceActionCallContractDetails(action.details as EmulationCallContractDetails), - }; - } else if (action.type === 'ton_transfer') { - return { - type: 'ton_transfer', - value: toTransactionTraceActionTONTransferDetails(action.details as EmulationTonTransferDetails), - }; - } else { - return { - type: 'unknown', - value: action.details as Record, - }; - } -} - -function toTransactionTraceActionJettonSwapDetails( - details: EmulationJettonSwapDetails, -): TransactionTraceActionJettonSwapDetails { - return { - dex: details.dex, - sender: asMaybeAddressFriendly(details.sender) ?? undefined, - dexIncomingTransfer: { - asset: asMaybeAddressFriendly(details.dex_incoming_transfer?.asset) ?? undefined, - source: asMaybeAddressFriendly(details.dex_incoming_transfer?.source) ?? undefined, - destination: asMaybeAddressFriendly(details.dex_incoming_transfer?.destination) ?? undefined, - sourceJettonWallet: - asMaybeAddressFriendly(details.dex_incoming_transfer?.source_jetton_wallet) ?? undefined, - destinationJettonWallet: - asMaybeAddressFriendly(details.dex_incoming_transfer?.destination_jetton_wallet) ?? undefined, - amount: details.dex_incoming_transfer?.amount, - }, - dexOutgoingTransfer: { - asset: asMaybeAddressFriendly(details.dex_outgoing_transfer?.asset) ?? undefined, - source: asMaybeAddressFriendly(details.dex_outgoing_transfer?.source) ?? undefined, - destination: asMaybeAddressFriendly(details.dex_outgoing_transfer?.destination) ?? undefined, - sourceJettonWallet: - asMaybeAddressFriendly(details.dex_outgoing_transfer?.source_jetton_wallet) ?? undefined, - destinationJettonWallet: - asMaybeAddressFriendly(details.dex_outgoing_transfer?.destination_jetton_wallet) ?? undefined, - amount: details.dex_outgoing_transfer?.amount, - }, - peerSwaps: details.peer_swaps, - }; -} - -function toTransactionTraceActionCallContractDetails( - details: EmulationCallContractDetails, -): TransactionTraceActionCallContractDetails { - return { - opcode: details.opcode, - source: asMaybeAddressFriendly(details.source) ?? undefined, - destination: asMaybeAddressFriendly(details.destination) ?? undefined, - value: details.value, - valueExtraCurrencies: details.extra_currencies ?? undefined, - }; -} - -function toTransactionTraceActionTONTransferDetails( - details: EmulationTonTransferDetails, -): TransactionTraceActionTONTransferDetails { - return { - source: asMaybeAddressFriendly(details.source) ?? undefined, - destination: asMaybeAddressFriendly(details.destination) ?? undefined, - value: details.value, - valueExtraCurrencies: details.value_extra_currencies, - comment: details.comment ?? undefined, - isEncrypted: details.encrypted, - }; -} - -// Metadata by address -export interface EmulationAddressMetadata { - is_indexed: boolean; - token_info?: EmulationTokenInfo[]; -} - -export type EmulationTokenInfo = - | EmulationTokenInfoWallets - | EmulationTokenInfoMasters - | (EmulationTokenInfoBase & Record); - -export interface EmulationTokenInfoBase { - valid: boolean; - type: string; -} - -export interface EmulationTokenInfoWallets extends EmulationTokenInfoBase { - type: 'jetton_wallets'; - extra: { - balance: string; - jetton: string; // jetton master address - owner: string; // owner address - }; -} - -export interface EmulationTokenInfoMasters extends EmulationTokenInfoBase { - type: 'jetton_masters'; - name: string; - symbol: string; - description: string; - image?: string; - extra: { - _image_big?: string; - _image_medium?: string; - _image_small?: string; - decimals: string; - image_data?: string; // base64 encoded image data - social?: string[]; - uri?: string; - websites?: string[]; - [key: string]: unknown; - }; -} - -export interface ToncenterResponseJettonMasters { - jetton_masters: ToncenterJettonWallet[]; - address_book: Record; - metadata: Record; -} - -export interface ToncenterResponseJettonWallets { - jetton_wallets: ToncenterJettonWallet[]; - address_book: Record; - metadata: Record; -} - -export interface ToncenterJettonWallet { - address: string; - balance: string; - owner: string; - jetton: string; - last_transaction_lt: string; - code_hash: string; - data_hash: string; -} diff --git a/packages/walletkit/src/types/toncenter/parsers/body.ts b/packages/walletkit/src/types/toncenter/parsers/body.ts index e97dea2fc..e8d7bddc1 100644 --- a/packages/walletkit/src/types/toncenter/parsers/body.ts +++ b/packages/walletkit/src/types/toncenter/parsers/body.ts @@ -6,41 +6,50 @@ * */ -/** - * Common helpers for extracting decoded body and operation types from messages - * Refactored to use centralized opcode registry and message decoder - */ - import type { EmulationMessage } from '../emulation'; -import { getDecodedBody, getDecodedType } from './messageDecoder'; import { resolveOpCode, MessageType, matchesDecodedType } from './opcodes'; type Json = Record; -/** - * Get decoded body from message - * @deprecated Use getDecodedBody from messageDecoder instead - */ +function isRecord(v: unknown): v is Record { + return v !== null && typeof v === 'object'; +} + +function getDecodedBody(msg?: EmulationMessage | null): Json | null { + if (!msg) return null; + const mc = msg.message_content as unknown; + if (isRecord(mc)) { + const decoded = (mc as Json).decoded as unknown; + return isRecord(decoded) ? (decoded as Json) : null; + } + return null; +} + +function getDecodedType(msg?: EmulationMessage | null): string | null { + const decoded = getDecodedBody(msg); + if (decoded) { + const type = decoded['@type']; + if (typeof type === 'string') return type; + + const value = decoded['value']; + if (isRecord(value) && typeof value['@type'] === 'string') { + return value['@type'] as string; + } + } + return null; +} + export function getDecoded(msg?: EmulationMessage | null): Json | null { return getDecodedBody(msg); } -/** - * Extract operation type from message body - * @deprecated Use getDecodedType from messageDecoder instead - */ export function extractOpFromBody(msg?: EmulationMessage | null): string | null { return getDecodedType(msg); } -/** - * Match operation code with type mapping - * Now uses centralized opcode registry - */ export function matchOpWithMap(op: string, types: string[], mapping: Record): string | '' { if (!op) return ''; - // First try the new resolver const messageType = resolveOpCode(op); if (messageType !== MessageType.Unknown) { const typeString = messageType as string; @@ -49,14 +58,10 @@ export function matchOpWithMap(op: string, types: string[], mapping: Record; - const amount = this.toBigInt(this.readAmountValue(this.getProperty(payload, 'amount'))); - const dest = this.toAddr(this.getProperty(payload, 'destination')); - const forwardPayload = this.getProperty(payload, 'forward_payload'); - const forwardValue = this.isRecord(forwardPayload) ? forwardPayload['value'] : undefined; - const comment = this.extractComment(forwardValue) ?? undefined; - - const senderWallet = asAddressFriendly(tx.account); - const recipientWallet = this.findRecipientJettonWalletFromOut(tx); - - const status = this.computeStatus(tx); - const id = (this.findFirstOwnerTxId(context) ?? Base64ToHex(tx.hash)) as Hex; - const base = this.collectBaseTransactionsSent(context); - const jetton = this.buildJettonInfo(context, senderWallet); - - const action: JettonTransferAction = { - type: 'JettonTransfer', - id, - status, - JettonTransfer: { - sender: toAccount(context.ownerAddress, context.addressBook), - recipient: toAccount(dest, context.addressBook), - sendersWallet: senderWallet, - recipientsWallet: recipientWallet ?? '', - amount, - comment, - jetton, - }, - simplePreview: this.jettonPreview(amount, jetton.symbol, jetton.decimals, jetton.image, [ - toAccount(dest, context.addressBook), - toAccount(context.ownerAddress, context.addressBook), - this.toContractAccount( - jetton.address || this.inferMinterFromAddressBook(context.addressBook)?.address || '', - context.addressBook, - ), - ]), - baseTransactions: base as Hex[], - }; - - return [action]; - } - - private findMessageInTransaction( - message: DecodedMessage, - context: MessageHandlerContext, - ): { source?: string } | null { - for (const tx of Object.values(context.transactions)) { - if (tx.in_msg && this.messageMatches(tx.in_msg, message.rawMessage)) { - // Convert null to undefined for type compatibility - return { - ...tx.in_msg, - source: tx.in_msg.source ?? undefined, - }; - } - } - return null; - } - - private findTransactionForMessage( - message: DecodedMessage, - context: MessageHandlerContext, - ): ToncenterTransaction | null { - for (const tx of Object.values(context.transactions)) { - if (tx.in_msg && this.messageMatches(tx.in_msg, message.rawMessage)) { - return tx; - } - } - return null; - } - - private messageMatches(msg1: unknown, msg2: unknown): boolean { - // Simple comparison - can be enhanced - return msg1 === msg2; - } - - private readAmountValue(obj: unknown): string | number | undefined { - if (!this.isRecord(obj)) return undefined; - const v = obj['value']; - if (typeof v === 'string' || typeof v === 'number') return v; - return undefined; - } - - private toAddr(raw?: unknown): string { - if (!raw) return ''; - if (typeof raw === 'string') { - if (/^[A-Fa-f0-9]{64}$/.test(raw)) { - return asAddressFriendly(`0:${raw}`); - } - return asAddressFriendly(raw); - } - if (this.isRecord(raw)) { - const wc = raw['workchain_id']; - const addr = raw['address']; - if ((typeof wc === 'string' || typeof wc === 'number') && typeof addr === 'string') { - return asAddressFriendly(`${wc}:${addr}`); - } - } - return ''; - } - - private findRecipientJettonWalletFromOut(tx: ToncenterTransaction): string | null { - for (const m of tx.out_msgs || []) { - if (m.opcode === '0x178d4519') { - return asAddressFriendly(m.destination); - } - } - return null; - } - - private computeStatus(tx: ToncenterTransaction): StatusAction { - const aborted = Boolean(tx.description?.aborted); - const computePh = (tx.description as unknown as Record)?.['compute_ph'] as - | Record - | undefined; - const action = (tx.description as unknown as Record)?.['action'] as - | Record - | undefined; - const computeSuccess = Boolean(computePh && Boolean(computePh['success'])); - const actionSuccess = Boolean(action && Boolean(action['success'])); - return !aborted && computeSuccess && actionSuccess ? 'success' : 'failure'; - } - - private findFirstOwnerTxId(context: MessageHandlerContext): Hex | null { - const order = context.traceItem.transactions_order || []; - for (const h of order) { - const tx = context.transactions[h]; - if (tx && asAddressFriendly(tx.account) === context.ownerAddress) { - return Base64ToHex(h); - } - } - return null; - } - - private collectBaseTransactionsSent(context: MessageHandlerContext): Hex[] { - const order = context.traceItem.transactions_order || []; - const pairs: { type: string; hex: Hex }[] = []; - for (const h of order) { - const tx = context.transactions[h]; - if (!tx) continue; - if (asAddressFriendly(tx.account) === context.ownerAddress) continue; - const t = this.getTxType(tx); - if (t) pairs.push({ type: t, hex: Base64ToHex(h) }); - } - const priority: Record = { - jetton_transfer: 1, - jetton_notify: 2, - jetton_internal_transfer: 3, - excess: 4, - }; - pairs.sort((a, b) => (priority[a.type] ?? 99) - (priority[b.type] ?? 99)); - return pairs.map((p) => p.hex); - } - - private getTxType(tx: ToncenterTransaction): string | '' { - const opcode = tx.in_msg?.opcode || ''; - const mapping: Record = { - '0x0f8a7ea5': 'jetton_transfer', - '0x178d4519': 'jetton_internal_transfer', - '0x7362d09c': 'jetton_notify', - '0xd53276db': 'excess', - }; - return mapping[opcode] ?? ''; - } - - private buildJettonInfo( - context: MessageHandlerContext, - walletFriendly: UserFriendlyAddress, - ): { - address: string; - name: string; - symbol: string; - decimals: number; - image: string; - verification: string; - score: number; - } { - const walletInfo = context.addressBook[walletFriendly]; - if (walletInfo?.jettonWallet?.jettonMaster) { - const masterAddress = asMaybeAddressFriendly(walletInfo.jettonWallet.jettonMaster); - const masterInfo = masterAddress ? context.addressBook[masterAddress] : undefined; - if (masterInfo?.jetton) { - return masterInfo.jetton; - } - } - - const metadata = (context.traceItem as unknown as { metadata?: Record }).metadata; - let master: string | undefined; - if (metadata) { - for (const [raw, infoAny] of Object.entries(metadata)) { - const info = infoAny as Record; - const tokenInfo = info['token_info']; - if (!Array.isArray(tokenInfo)) continue; - for (const tAny of tokenInfo) { - const t = tAny as Record; - if (t['type'] === 'jetton_wallets') { - const extra = t['extra'] as Record | undefined; - const owner = extra?.['owner']; - if ( - typeof owner === 'string' && - asAddressFriendly(owner) && - asAddressFriendly(raw) === walletFriendly - ) { - const j = extra?.['jetton']; - if (typeof j === 'string') master = j; - } - } - } - } - } - - let name = ''; - let symbol = ''; - let decimals = 0; - let image: string | undefined; - - if (master && metadata && metadata[master]) { - const m = metadata[master] as Record; - name = (m['name'] as string) || ''; - symbol = (m['symbol'] as string) || ''; - const extra = m['extra'] as Record | undefined; - const dec = extra?.['decimals']; - decimals = typeof dec === 'string' ? parseInt(dec, 10) : 0; - image = - (m['image'] as string) || - (extra?.['_image_small'] as string) || - (extra?.['_image_medium'] as string) || - (extra?.['_image_big'] as string); - } - - let outAddress = master ? asAddressFriendly(master) : ''; - if (!outAddress) { - const inferred = this.inferMinterFromAddressBook(context.addressBook); - if (inferred) { - outAddress = inferred.address; - if (!name) name = inferred.name; - if (!symbol) symbol = inferred.symbol; - if (!decimals) decimals = inferred.decimals; - if (!image) image = inferred.image; - } - } - - return { - address: outAddress, - name, - symbol, - decimals, - image: image ?? '', - verification: 'whitelist', - score: 100, - }; - } - - private inferMinterFromAddressBook(addressBook: AddressBook): { - address: string; - name: string; - symbol: string; - decimals: number; - image: string; - } | null { - const knownMinterByDomain = 'usdt-minter.ton'; - for (const key of Object.keys(addressBook)) { - const entry = (addressBook as Record)[key]; - const domain = entry && entry.domain; - if (domain === knownMinterByDomain || (typeof domain === 'string' && domain.includes('minter'))) { - return { - address: key, - name: 'Tether USD', - symbol: 'USD₮', - decimals: 6, - image: 'https://cache.tonapi.io/imgproxy/T3PB4s7oprNVaJkwqbGg54nexKE0zzKhcrPv8jcWYzU/rs:fill:200:200:1/g:no/aHR0cHM6Ly90ZXRoZXIudG8vaW1hZ2VzL2xvZ29DaXJjbGUucG5n.webp', - }; - } - } - return null; - } - - private jettonPreview( - amount: bigint, - symbol: string, - decimals: number, - image: string | undefined, - accounts: Account[], - ): SimplePreview { - let denom = BigInt(1); - for (let i = 0; i < (decimals || 0); i++) denom = denom * BigInt(10); - const value = Number(amount) / Number(denom); - const human = symbol ? `${this.trimAmount(value)} ${symbol}` : `${this.trimAmount(value)}`; - const preview: SimplePreview = { - name: 'Jetton Transfer', - description: `Transferring ${human}`, - value: human, - accounts, - }; - if (image) preview.valueImage = image; - return preview; - } - - private trimAmount(v: number): string { - if (v >= 1) return `${Number(v.toFixed(3)).toString().replace(/\.0+$/, '')}`; - const s = v.toFixed(9); - return s.replace(/0+$/, '').replace(/\.$/, ''); - } - - private toContractAccount(address: string, addressBook: AddressBook): Account { - const acct = toAccount(address, addressBook); - return { ...acct, isWallet: false }; - } -} - -/** - * Jetton Internal Transfer Handler (receiving jettons) - */ -export class JettonInternalTransferHandler extends BaseMessageHandler { - messageType = MessageType.JettonInternalTransfer; - priority = 10; - - canHandle(message: DecodedMessage, context: MessageHandlerContext): boolean { - // Check if this is a jetton receive to owner's wallet - const tx = this.findTransactionForMessage(message, context); - if (!tx) return false; - - // Skip if this is the owner's main wallet transaction - return asAddressFriendly(tx.account) !== context.ownerAddress; - } - - handle(message: DecodedMessage, context: MessageHandlerContext): Action[] { - const tx = this.findTransactionForMessage(message, context); - if (!tx || !tx.in_msg) return []; - - const payload = message.payload as Record; - const amount = this.toBigInt(this.readAmountValue(this.getProperty(payload, 'amount'))); - const senderMain = this.toAddr(this.getProperty(payload, 'from')); - const recipientWallet = asAddressFriendly(tx.account); - const senderWallet = asAddressFriendly(tx.in_msg.source!); - - const status = this.computeStatus(tx); - const id = (this.getTraceRootId(context.traceItem) ?? Base64ToHex(tx.hash)) as Hex; - const base = this.collectBaseTransactionsReceived(context); - const jetton = this.buildJettonInfo(context, recipientWallet); - - const action: JettonTransferAction = { - type: 'JettonTransfer', - id, - status, - JettonTransfer: { - sender: toAccount(senderMain, context.addressBook), - recipient: toAccount(context.ownerAddress, context.addressBook), - sendersWallet: senderWallet, - recipientsWallet: recipientWallet, - amount, - jetton, - }, - simplePreview: this.jettonPreview(amount, jetton.symbol, jetton.decimals, jetton.image, [ - toAccount(context.ownerAddress, context.addressBook), - toAccount(senderMain, context.addressBook), - this.toContractAccount(jetton.address || '', context.addressBook), - ]), - baseTransactions: base as Hex[], - }; - - return [action]; - } - - private findTransactionForMessage( - message: DecodedMessage, - context: MessageHandlerContext, - ): ToncenterTransaction | null { - for (const tx of Object.values(context.transactions)) { - if (tx.in_msg && this.messageMatches(tx.in_msg, message.rawMessage)) { - return tx; - } - } - return null; - } - - private messageMatches(msg1: unknown, msg2: unknown): boolean { - return msg1 === msg2; - } - - private readAmountValue(obj: unknown): string | number | undefined { - if (!this.isRecord(obj)) return undefined; - const v = obj['value']; - if (typeof v === 'string' || typeof v === 'number') return v; - return undefined; - } - - private toAddr(raw?: unknown): string { - if (!raw) return ''; - if (typeof raw === 'string') { - if (/^[A-Fa-f0-9]{64}$/.test(raw)) { - return asAddressFriendly(`0:${raw}`); - } - return asAddressFriendly(raw); - } - if (this.isRecord(raw)) { - const wc = raw['workchain_id']; - const addr = raw['address']; - if ((typeof wc === 'string' || typeof wc === 'number') && typeof addr === 'string') { - return asAddressFriendly(`${wc}:${addr}`); - } - } - return ''; - } - - private computeStatus(tx: ToncenterTransaction): StatusAction { - const aborted = Boolean(tx.description?.aborted); - const computePh = (tx.description as unknown as Record)?.['compute_ph'] as - | Record - | undefined; - const action = (tx.description as unknown as Record)?.['action'] as - | Record - | undefined; - const computeSuccess = Boolean(computePh && Boolean(computePh['success'])); - const actionSuccess = Boolean(action && Boolean(action['success'])); - return !aborted && computeSuccess && actionSuccess ? 'success' : 'failure'; - } - - private getTraceRootId(item: ToncenterTraceItem): Hex | null { - const first = (item.transactions_order || [])[0]; - return first ? Base64ToHex(first) : null; - } - - private collectBaseTransactionsReceived(context: MessageHandlerContext): Hex[] { - const order = context.traceItem.transactions_order || []; - const findTx = (predicate: (tx: ToncenterTransaction) => boolean): Hex | null => { - for (const h of order) { - const tx = context.transactions[h]; - if (!tx) continue; - if (predicate(tx)) return Base64ToHex(h); - } - return null; - }; - - const root = this.getTraceRootId(context.traceItem); - const isType = (tx: ToncenterTransaction, type: string) => this.getTxType(tx) === type; - const jt = findTx((tx) => isType(tx, 'jetton_transfer')); - const internal = findTx( - (tx) => isType(tx, 'jetton_internal_transfer') && asAddressFriendly(tx.account) !== context.ownerAddress, - ); - const excess = findTx((tx) => isType(tx, 'excess')); - - const out: Hex[] = []; - if (root) out.push(root); - if (jt) out.push(jt); - if (internal) out.push(internal); - if (excess) out.push(excess); - return out; - } - - private getTxType(tx: ToncenterTransaction): string | '' { - const opcode = tx.in_msg?.opcode || ''; - const mapping: Record = { - '0x0f8a7ea5': 'jetton_transfer', - '0x178d4519': 'jetton_internal_transfer', - '0x7362d09c': 'jetton_notify', - '0xd53276db': 'excess', - }; - return mapping[opcode] ?? ''; - } - - private buildJettonInfo( - context: MessageHandlerContext, - walletFriendly: UserFriendlyAddress, - ): { - address: string; - name: string; - symbol: string; - decimals: number; - image: string; - verification: string; - score: number; - } { - const walletInfo = context.addressBook[walletFriendly]; - if (walletInfo?.jettonWallet?.jettonMaster) { - const masterAddress = asMaybeAddressFriendly(walletInfo.jettonWallet.jettonMaster); - const masterInfo = masterAddress ? context.addressBook[masterAddress] : undefined; - if (masterInfo?.jetton) { - return masterInfo.jetton; - } - } - - const metadata = (context.traceItem as unknown as { metadata?: Record }).metadata; - let master: string | undefined; - if (metadata) { - for (const [raw, infoAny] of Object.entries(metadata)) { - const info = infoAny as Record; - const tokenInfo = info['token_info']; - if (!Array.isArray(tokenInfo)) continue; - for (const tAny of tokenInfo) { - const t = tAny as Record; - if (t['type'] === 'jetton_wallets') { - const extra = t['extra'] as Record | undefined; - const owner = extra?.['owner']; - if ( - typeof owner === 'string' && - asAddressFriendly(owner) && - asAddressFriendly(raw) === walletFriendly - ) { - const j = extra?.['jetton']; - if (typeof j === 'string') master = j; - } - } - } - } - } - - let name = ''; - let symbol = ''; - let decimals = 0; - let image: string | undefined; - - if (master && metadata && metadata[master]) { - const m = metadata[master] as Record; - name = (m['name'] as string) || ''; - symbol = (m['symbol'] as string) || ''; - const extra = m['extra'] as Record | undefined; - const dec = extra?.['decimals']; - decimals = typeof dec === 'string' ? parseInt(dec, 10) : 0; - image = - (m['image'] as string) || - (extra?.['_image_small'] as string) || - (extra?.['_image_medium'] as string) || - (extra?.['_image_big'] as string); - } - - return { - address: master ? asAddressFriendly(master) : '', - name, - symbol, - decimals, - image: image ?? '', - verification: 'whitelist', - score: 100, - }; - } - - private jettonPreview( - amount: bigint, - symbol: string, - decimals: number, - image: string | undefined, - accounts: Account[], - ): SimplePreview { - let denom = BigInt(1); - for (let i = 0; i < (decimals || 0); i++) denom = denom * BigInt(10); - const value = Number(amount) / Number(denom); - const human = symbol ? `${this.trimAmount(value)} ${symbol}` : `${this.trimAmount(value)}`; - const preview: SimplePreview = { - name: 'Jetton Transfer', - description: `Transferring ${human}`, - value: human, - accounts, - }; - if (image) preview.valueImage = image; - return preview; - } - - private trimAmount(v: number): string { - if (v >= 1) return `${Number(v.toFixed(3)).toString().replace(/\.0+$/, '')}`; - const s = v.toFixed(9); - return s.replace(/0+$/, '').replace(/\.$/, ''); - } - - private toContractAccount(address: string, addressBook: AddressBook): Account { - const acct = toAccount(address, addressBook); - return { ...acct, isWallet: false }; - } -} diff --git a/packages/walletkit/src/types/toncenter/parsers/index.ts b/packages/walletkit/src/types/toncenter/parsers/index.ts index cc45f6c85..a0746ecf2 100644 --- a/packages/walletkit/src/types/toncenter/parsers/index.ts +++ b/packages/walletkit/src/types/toncenter/parsers/index.ts @@ -6,17 +6,5 @@ * */ -/** - * Public API for message parsing with extensible architecture - */ - -// Core modules export * from './opcodes'; -export * from './messageDecoder'; -export * from './messageHandler'; - -// Handlers -export { JettonTransferHandler, JettonInternalTransferHandler } from './handlers/JettonHandler'; - -// Legacy exports for backwards compatibility export { getDecoded, extractOpFromBody, matchOpWithMap } from './body'; diff --git a/packages/walletkit/src/types/toncenter/parsers/messageDecoder.ts b/packages/walletkit/src/types/toncenter/parsers/messageDecoder.ts deleted file mode 100644 index 522e839b5..000000000 --- a/packages/walletkit/src/types/toncenter/parsers/messageDecoder.ts +++ /dev/null @@ -1,299 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -/** - * Pattern matching based message decoder - * Provides extensible message decoding with type-safe pattern matching - */ - -import type { EmulationMessage } from '../emulation'; -import { MessageType, resolveOpCode, matchesDecodedType } from './opcodes'; - -/** - * Decoded message payload with type information - */ -export interface DecodedMessage { - messageType: MessageType; - opcode?: string; - decodedType?: string; - payload: T; - rawMessage: EmulationMessage; -} - -/** - * Pattern matcher for message types - */ -export interface MessagePattern { - messageType: MessageType; - match: (msg: EmulationMessage) => boolean; - decode: (msg: EmulationMessage) => T | null; -} - -/** - * Generic decoded payload type - */ -export type DecodedPayload = Record; - -/** - * Registry of message patterns - */ -class MessagePatternRegistry { - private patterns: Map = new Map(); - - /** - * Register a pattern for a message type - */ - register(pattern: MessagePattern): void { - const existing = this.patterns.get(pattern.messageType) || []; - existing.push(pattern as MessagePattern); - this.patterns.set(pattern.messageType, existing); - } - - /** - * Find matching pattern for a message - */ - match(msg: EmulationMessage): MessagePattern | null { - for (const patterns of this.patterns.values()) { - for (const pattern of patterns) { - if (pattern.match(msg)) { - return pattern; - } - } - } - return null; - } - - /** - * Get all patterns for a message type - */ - getPatterns(messageType: MessageType): MessagePattern[] { - return this.patterns.get(messageType) || []; - } -} - -/** - * Global pattern registry - */ -export const messagePatternRegistry = new MessagePatternRegistry(); - -/** - * Extracts decoded body from message - */ -export function getDecodedBody(msg?: EmulationMessage | null): DecodedPayload | null { - if (!msg) return null; - const mc = msg.message_content as unknown; - if (isRecord(mc)) { - const decoded = (mc as DecodedPayload).decoded as unknown; - return isRecord(decoded) ? (decoded as DecodedPayload) : null; - } - return null; -} - -/** - * Extracts @type from decoded body - */ -export function getDecodedType(msg?: EmulationMessage | null): string | null { - const decoded = getDecodedBody(msg); - if (decoded) { - const type = decoded['@type']; - if (typeof type === 'string') return type; - - const value = decoded['value']; - if (isRecord(value) && typeof value['@type'] === 'string') { - return value['@type'] as string; - } - } - return null; -} - -/** - * Decodes a message using pattern matching - */ -export function decodeMessage(msg: EmulationMessage): DecodedMessage | null { - // Try pattern matching first - const pattern = messagePatternRegistry.match(msg); - if (pattern) { - const payload = pattern.decode(msg); - if (payload !== null) { - return { - messageType: pattern.messageType, - opcode: msg.opcode ?? undefined, - decodedType: getDecodedType(msg) || undefined, - payload, - rawMessage: msg, - }; - } - } - - // Fallback: try opcode resolution - if (msg.opcode) { - const messageType = resolveOpCode(msg.opcode); - if (messageType !== MessageType.Unknown) { - return { - messageType, - opcode: msg.opcode ?? undefined, - decodedType: getDecodedType(msg) || undefined, - payload: getDecodedBody(msg) || {}, - rawMessage: msg, - }; - } - } - - // Fallback: try decoded type - const decodedType = getDecodedType(msg); - if (decodedType) { - const matched = matchesDecodedType(decodedType, Object.values(MessageType)); - if (matched) { - return { - messageType: matched, - opcode: msg.opcode ?? undefined, - decodedType, - payload: getDecodedBody(msg) || {}, - rawMessage: msg, - }; - } - } - - return null; -} - -/** - * Decodes multiple messages - */ -export function decodeMessages(messages: EmulationMessage[]): DecodedMessage[] { - return messages.map(decodeMessage).filter((m): m is DecodedMessage => m !== null); -} - -/** - * Helper: check if value is a record - */ -function isRecord(v: unknown): v is Record { - return typeof v === 'object' && v !== null; -} - -/** - * Helper: extract property from decoded payload - */ -export function getPayloadProperty(payload: DecodedPayload, key: string): T | undefined { - return payload[key] as T | undefined; -} - -/** - * Helper: extract nested property from decoded payload - */ -export function getNestedProperty(payload: DecodedPayload, path: string[]): T | undefined { - let current: unknown = payload; - for (const key of path) { - if (!isRecord(current)) return undefined; - current = current[key]; - } - return current as T | undefined; -} - -// Register built-in patterns - -// Jetton Transfer pattern -messagePatternRegistry.register({ - messageType: MessageType.JettonTransfer, - match: (msg) => { - const decoded = getDecodedBody(msg); - const type = getDecodedType(msg); - return ( - msg.opcode === '0x0f8a7ea5' || - type === 'jetton_transfer' || - (decoded !== null && decoded['@type'] === 'jetton_transfer') - ); - }, - decode: (msg) => getDecodedBody(msg), -}); - -// Jetton Internal Transfer pattern -messagePatternRegistry.register({ - messageType: MessageType.JettonInternalTransfer, - match: (msg) => { - const decoded = getDecodedBody(msg); - const type = getDecodedType(msg); - return ( - msg.opcode === '0x178d4519' || - type === 'jetton_internal_transfer' || - (decoded !== null && decoded['@type'] === 'jetton_internal_transfer') - ); - }, - decode: (msg) => getDecodedBody(msg), -}); - -// Jetton Notify pattern -messagePatternRegistry.register({ - messageType: MessageType.JettonNotify, - match: (msg) => { - const decoded = getDecodedBody(msg); - const type = getDecodedType(msg); - return ( - msg.opcode === '0x7362d09c' || - type === 'jetton_notify' || - (decoded !== null && decoded['@type'] === 'jetton_notify') - ); - }, - decode: (msg) => getDecodedBody(msg), -}); - -// NFT Transfer pattern -messagePatternRegistry.register({ - messageType: MessageType.NftTransfer, - match: (msg) => { - const decoded = getDecodedBody(msg); - const type = getDecodedType(msg); - return ( - msg.opcode === '0x5fcc3d14' || - type === 'nft_transfer' || - (decoded !== null && decoded['@type'] === 'nft_transfer') - ); - }, - decode: (msg) => getDecodedBody(msg), -}); - -// NFT Ownership Assigned pattern -messagePatternRegistry.register({ - messageType: MessageType.NftOwnershipAssigned, - match: (msg) => { - const decoded = getDecodedBody(msg); - const type = getDecodedType(msg); - return ( - msg.opcode === '0x05138d91' || - type === 'nft_ownership_assigned' || - (decoded !== null && decoded['@type'] === 'nft_ownership_assigned') - ); - }, - decode: (msg) => getDecodedBody(msg), -}); - -// NFT Owner Changed pattern -messagePatternRegistry.register({ - messageType: MessageType.NftOwnerChanged, - match: (msg) => { - const decoded = getDecodedBody(msg); - const type = getDecodedType(msg); - return ( - msg.opcode === '0x7bdd97de' || - type === 'nft_owner_changed' || - (decoded !== null && decoded['@type'] === 'nft_owner_changed') - ); - }, - decode: (msg) => getDecodedBody(msg), -}); - -// Excess pattern -messagePatternRegistry.register({ - messageType: MessageType.Excess, - match: (msg) => { - const decoded = getDecodedBody(msg); - const type = getDecodedType(msg); - return msg.opcode === '0xd53276db' || type === 'excess' || (decoded !== null && decoded['@type'] === 'excess'); - }, - decode: (msg) => getDecodedBody(msg), -}); diff --git a/packages/walletkit/src/types/toncenter/parsers/messageHandler.ts b/packages/walletkit/src/types/toncenter/parsers/messageHandler.ts deleted file mode 100644 index 21f2ff254..000000000 --- a/packages/walletkit/src/types/toncenter/parsers/messageHandler.ts +++ /dev/null @@ -1,266 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -/** - * Extensible message handler architecture - * Allows registration of custom handlers for different message types - */ - -import type { MessageType } from './opcodes'; -import type { DecodedMessage } from './messageDecoder'; -import type { Action, AddressBook } from '../AccountEvent'; -import type { ToncenterTraceItem, ToncenterTransaction } from '../emulation'; - -/** - * Context for message handling - */ -export interface MessageHandlerContext { - ownerAddress: string; - traceItem: ToncenterTraceItem; - transactions: Record; - addressBook: AddressBook; -} - -/** - * Message handler interface - */ -export interface MessageHandler { - /** - * Message type this handler processes - */ - messageType: MessageType; - - /** - * Priority (lower number = higher priority) - */ - priority?: number; - - /** - * Check if this handler can process the message - */ - canHandle(message: DecodedMessage, context: MessageHandlerContext): boolean; - - /** - * Process the message and return actions - */ - handle(message: DecodedMessage, context: MessageHandlerContext): Action[]; -} - -/** - * Message handler registry - */ -class MessageHandlerRegistry { - private handlers: Map = new Map(); - - /** - * Register a message handler - */ - register(handler: MessageHandler): void { - const existing = this.handlers.get(handler.messageType) || []; - existing.push(handler); - // Sort by priority (lower number = higher priority) - existing.sort((a, b) => (a.priority || 100) - (b.priority || 100)); - this.handlers.set(handler.messageType, existing); - } - - /** - * Unregister a handler - */ - unregister(handler: MessageHandler): void { - const existing = this.handlers.get(handler.messageType); - if (existing) { - const filtered = existing.filter((h) => h !== handler); - this.handlers.set(handler.messageType, filtered); - } - } - - /** - * Get all handlers for a message type - */ - getHandlers(messageType: MessageType): MessageHandler[] { - return this.handlers.get(messageType) || []; - } - - /** - * Find the first handler that can handle the message - */ - findHandler(message: DecodedMessage, context: MessageHandlerContext): MessageHandler | null { - const handlers = this.getHandlers(message.messageType); - for (const handler of handlers) { - if (handler.canHandle(message, context)) { - return handler; - } - } - return null; - } - - /** - * Process a message with the appropriate handler - */ - handle(message: DecodedMessage, context: MessageHandlerContext): Action[] { - const handler = this.findHandler(message, context); - if (handler) { - return handler.handle(message, context); - } - return []; - } - - /** - * Get all registered message types - */ - getRegisteredTypes(): MessageType[] { - return Array.from(this.handlers.keys()); - } - - /** - * Clear all handlers - */ - clear(): void { - this.handlers.clear(); - } -} - -/** - * Global message handler registry - */ -export const messageHandlerRegistry = new MessageHandlerRegistry(); - -/** - * Base message handler with common utilities - */ -export abstract class BaseMessageHandler implements MessageHandler { - abstract messageType: MessageType; - priority?: number; - - abstract canHandle(message: DecodedMessage, context: MessageHandlerContext): boolean; - abstract handle(message: DecodedMessage, context: MessageHandlerContext): Action[]; - - /** - * Helper: check if value is a record - */ - protected isRecord(v: unknown): v is Record { - return typeof v === 'object' && v !== null; - } - - /** - * Helper: get property from payload - */ - protected getProperty(payload: unknown, key: string): T | undefined { - if (this.isRecord(payload)) { - return payload[key] as T | undefined; - } - return undefined; - } - - /** - * Helper: safely convert to bigint - */ - protected toBigInt(value?: string | number): bigint { - if (value === undefined || value === null) return BigInt(0); - const n = typeof value === 'string' ? Number(value) : value; - return BigInt(Number.isFinite(n) ? n : 0); - } - - /** - * Helper: extract comment from decoded payload - */ - protected extractComment(payload: unknown): string | null { - if (!this.isRecord(payload)) return null; - const type = payload['@type']; - if (type === 'text_comment') { - const text = payload['text']; - if (typeof text === 'string' && text.length > 0) return text; - } - const comment = payload['comment']; - if (typeof comment === 'string' && comment.length > 0) return comment; - return null; - } -} - -/** - * Decorator for registering handlers automatically - */ -export function RegisterMessageHandler(messageType: MessageType, priority?: number) { - return function (constructor: T): T { - // Create and register instance - const instance = new constructor(); - instance.messageType = messageType; - if (priority !== undefined) { - instance.priority = priority; - } - messageHandlerRegistry.register(instance); - return constructor; - }; -} - -/** - * Fluent builder for creating handlers - */ -export class MessageHandlerBuilder { - private _messageType?: MessageType; - private _priority?: number; - private _canHandle?: (message: DecodedMessage, context: MessageHandlerContext) => boolean; - private _handle?: (message: DecodedMessage, context: MessageHandlerContext) => Action[]; - - messageType(type: MessageType): this { - this._messageType = type; - return this; - } - - priority(priority: number): this { - this._priority = priority; - return this; - } - - canHandle(fn: (message: DecodedMessage, context: MessageHandlerContext) => boolean): this { - this._canHandle = fn; - return this; - } - - handle(fn: (message: DecodedMessage, context: MessageHandlerContext) => Action[]): this { - this._handle = fn; - return this; - } - - build(): MessageHandler { - if (!this._messageType) { - throw new Error('MessageType is required'); - } - if (!this._canHandle) { - throw new Error('canHandle function is required'); - } - if (!this._handle) { - throw new Error('handle function is required'); - } - - const messageType = this._messageType; - const priority = this._priority; - const canHandle = this._canHandle; - const handle = this._handle; - - return { - messageType, - priority, - canHandle, - handle, - }; - } - - register(): this { - const handler = this.build(); - messageHandlerRegistry.register(handler); - return this; - } -} - -/** - * Create a new message handler builder - */ -export function createMessageHandler(): MessageHandlerBuilder { - return new MessageHandlerBuilder(); -} diff --git a/packages/walletkit/src/types/toncenter/v3/AddressBookRowV3.ts b/packages/walletkit/src/types/toncenter/v3/AddressBookRowV3.ts index 55cfcfda7..6d054e458 100644 --- a/packages/walletkit/src/types/toncenter/v3/AddressBookRowV3.ts +++ b/packages/walletkit/src/types/toncenter/v3/AddressBookRowV3.ts @@ -8,7 +8,7 @@ import type { AddressBook, AddressBookEntry } from '../../../api/models'; import { asAddressFriendly } from '../../../utils/address'; -import type { EmulationAddressMetadata } from '../emulation'; +import type { EmulationAddressMetadata } from '../../../clients/toncenter/types/metadata'; export interface MetadataV3 { address_book: Record; diff --git a/packages/walletkit/src/utils/assetHelpers.ts b/packages/walletkit/src/utils/assetHelpers.ts index c639680b6..c029b13f7 100644 --- a/packages/walletkit/src/utils/assetHelpers.ts +++ b/packages/walletkit/src/utils/assetHelpers.ts @@ -10,7 +10,7 @@ import { Address, beginCell } from '@ton/core'; import { isValidAddress, asAddressFriendly } from './address'; import { ParseStack, SerializeStack } from './tvmStack'; -import type { ApiClient } from '../types/toncenter/ApiClient'; +import type { ApiClient } from '../api/interfaces'; import type { JettonsRequest, JettonsResponse, @@ -122,7 +122,7 @@ export async function getNftsFromClient( /** * Gets a single NFT by address */ -export async function getNftFromClient(client: ApiClient, address: UserFriendlyAddress): Promise { +export async function getNftFromClient(client: ApiClient, address: UserFriendlyAddress): Promise { const result = await client.nftItemsByAddress({ address }); - return result.nfts.length > 0 ? result.nfts[0] : null; + return result.nfts.length > 0 ? result.nfts[0] : undefined; } diff --git a/packages/walletkit/src/utils/computeMoneyFlow.ts b/packages/walletkit/src/utils/computeMoneyFlow.ts new file mode 100644 index 000000000..700135ff6 --- /dev/null +++ b/packages/walletkit/src/utils/computeMoneyFlow.ts @@ -0,0 +1,94 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { EmulationResponse } from '../api/models'; +import type { + TransactionTraceMoneyFlow, + TransactionTraceMoneyFlowItem, +} from '../api/models/transactions/TransactionTraceMoneyFlow'; +import { AssetType } from '../api/models/core/AssetType'; +import { asMaybeAddressFriendly } from './address'; +import type { Hex } from '../api/models'; + +const JETTON_TRANSFER_OPCODE = '0x0f8a7ea5' as Hex; + +// pTON proxy contracts (STON.fi v1 and v2) — wrapped TON used in DEX swaps. +// TON flow is already captured in outputs/inputs, so exclude these from ourTransfers. +const TON_PROXY_ADDRESSES = new Set([ + 'EQCM3B12QK1e4yZSf8GtBRT0aLMNyEsBc_DhVfRRtOEffLez', + 'EQBnGWMCf3-FZZq1W4IWcWiGAc3PHuZ0_H-7sad2oY00o83S', +]); + +export function computeMoneyFlow(response: EmulationResponse): TransactionTraceMoneyFlow { + const empty: TransactionTraceMoneyFlow = { + outputs: '0', + inputs: '0', + allJettonTransfers: [], + ourTransfers: [], + ourAddress: undefined, + }; + + const rootTxHash = response.trace?.txHash; + if (!rootTxHash) return empty; + + const rootTx = response.transactions[rootTxHash]; + if (!rootTx) return empty; + + const ourAddress = rootTx.account; + const ourTxs = Object.values(response.transactions).filter((tx) => tx.account === ourAddress); + + const outputs = ourTxs.reduce((acc, tx) => tx.outMsgs.reduce((a, m) => a + BigInt(m.value ?? 0), acc), 0n); + + const inputs = ourTxs.reduce((acc, tx) => (tx.inMsg?.value ? acc + BigInt(tx.inMsg.value) : acc), 0n); + + const jettonTxHashes = new Set( + Object.values(response.transactions) + .filter((tx) => tx.inMsg?.opcode === JETTON_TRANSFER_OPCODE) + .map((tx) => tx.hash), + ); + + const allJettonTransfers: TransactionTraceMoneyFlowItem[] = response.actions + .filter((a) => a.isSuccess && a.transactions.some((h) => jettonTxHashes.has(h))) + .map((a) => { + const asset = a.details?.asset; + const sender = a.details?.sender; + const receiver = a.details?.receiver; + return { + assetType: AssetType.jetton, + tokenAddress: typeof asset === 'string' ? (asMaybeAddressFriendly(asset) ?? undefined) : undefined, + fromAddress: typeof sender === 'string' ? (asMaybeAddressFriendly(sender) ?? undefined) : undefined, + toAddress: typeof receiver === 'string' ? (asMaybeAddressFriendly(receiver) ?? undefined) : undefined, + amount: String(a.details?.amount ?? 0), + }; + }); + + const ourJettonByAddress: Record = {}; + for (const jt of allJettonTransfers) { + if (!jt.tokenAddress) continue; + if (TON_PROXY_ADDRESSES.has(jt.tokenAddress)) continue; + ourJettonByAddress[jt.tokenAddress] ??= 0n; + if (jt.toAddress === ourAddress) ourJettonByAddress[jt.tokenAddress] += BigInt(jt.amount); + if (jt.fromAddress === ourAddress) ourJettonByAddress[jt.tokenAddress] -= BigInt(jt.amount); + } + + const ourJettonTransfers: TransactionTraceMoneyFlowItem[] = Object.entries(ourJettonByAddress).map( + ([addr, amount]) => ({ + assetType: AssetType.jetton, + tokenAddress: addr, + amount: amount.toString(), + }), + ); + + return { + outputs: outputs.toString(), + inputs: inputs.toString(), + allJettonTransfers, + ourTransfers: [{ assetType: AssetType.ton, amount: (inputs - outputs).toString() }, ...ourJettonTransfers], + ourAddress, + }; +} diff --git a/packages/walletkit/src/utils/index.ts b/packages/walletkit/src/utils/index.ts index 54feb0113..edbdd6268 100644 --- a/packages/walletkit/src/utils/index.ts +++ b/packages/walletkit/src/utils/index.ts @@ -21,7 +21,7 @@ export * from './signData/hash'; export * from './signData/sign'; export * from './time'; export * from './tonProof'; -export * from './toncenterEmulation'; +export * from './transactionPreview'; export * from './tvmStack'; export * from './url'; export * from './uuid.mjs'; diff --git a/packages/walletkit/src/utils/toncenter/__tests__/testFixtures.ts b/packages/walletkit/src/utils/toncenter/__tests__/testFixtures.ts index b562c06af..4fc891925 100644 --- a/packages/walletkit/src/utils/toncenter/__tests__/testFixtures.ts +++ b/packages/walletkit/src/utils/toncenter/__tests__/testFixtures.ts @@ -10,7 +10,7 @@ import { vi } from 'vitest'; import type { Mock } from 'vitest'; import type { ToncenterTransaction, ToncenterTracesResponse } from '../../../types/toncenter/emulation'; -import type { ApiClient } from '../../../types/toncenter/ApiClient'; +import type { ApiClient } from '../../../api/interfaces'; export const JETTON_TRANSFER = '0x0f8a7ea5'; export const JETTON_NOTIFY = '0x7362d09c'; diff --git a/packages/walletkit/src/utils/toncenter/getTransactionStatus.ts b/packages/walletkit/src/utils/toncenter/getTransactionStatus.ts index 0d6d68aa7..51c55a4a5 100644 --- a/packages/walletkit/src/utils/toncenter/getTransactionStatus.ts +++ b/packages/walletkit/src/utils/toncenter/getTransactionStatus.ts @@ -6,7 +6,7 @@ * */ -import type { ApiClient } from '../../types/toncenter/ApiClient'; +import type { ApiClient } from '../../api/interfaces'; import type { TransactionStatusResponse } from '../../api/models'; import { getNormalizedExtMessageHash } from '../getNormalizedExtMessageHash'; import { parseTraceResponse } from './parseTraceResponse'; diff --git a/packages/walletkit/src/utils/toncenterEmulation.ts b/packages/walletkit/src/utils/toncenterEmulation.ts deleted file mode 100644 index 9df9eecf3..000000000 --- a/packages/walletkit/src/utils/toncenterEmulation.ts +++ /dev/null @@ -1,329 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import type { Slice } from '@ton/core'; -import { Address, Cell } from '@ton/core'; - -import type { EmulationTokenInfoWallets, ToncenterEmulationResponse } from '../types/toncenter/emulation'; -import { toTransactionEmulatedTrace } from '../types/toncenter/emulation'; -import type { ErrorInfo } from '../errors/WalletKitError'; -import { ERROR_CODES } from '../errors/codes'; -import { CallForSuccess } from './retry'; -import type { - TransactionEmulatedPreview, - TransactionTraceMoneyFlow, - TransactionTraceMoneyFlowItem, - TransactionRequest, -} from '../api/models'; -import { Result, AssetType } from '../api/models'; -import { SendModeToValue } from './sendMode'; -import type { Wallet } from '../api/interfaces'; -import { asAddressFriendly, asMaybeAddressFriendly } from './address'; -import type { ApiClient } from '../types/toncenter/ApiClient'; - -// import { ConnectMessageTransactionMessage } from '@/types/connect'; - -export interface ToncenterMessage { - method: string; - headers: { - 'Content-Type': string; - }; - body: string; -} - -export type ToncenterEmulationResult = - | { - result: 'success'; - emulationResult: ToncenterEmulationResponse; - } - | { - result: 'error'; - emulationError: ErrorInfo; - }; - -const TON_PROXY_ADDRESSES = [ - 'EQCM3B12QK1e4yZSf8GtBRT0aLMNyEsBc_DhVfRRtOEffLez', - 'EQBnGWMCf3-FZZq1W4IWcWiGAc3PHuZ0_H-7sad2oY00o83S', -]; - -/** - * Creates a toncenter message payload for emulation - */ -export function createToncenterMessage( - walletAddress: string | undefined, - messages: TransactionRequest['messages'], -): ToncenterMessage { - return { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - from: walletAddress, - valid_until: Math.floor(Date.now() / 1000) + 60, - include_code_data: true, - include_address_book: true, - include_metadata: true, - with_actions: true, - messages: messages.map((m) => ({ - address: m.address, - amount: m.amount, - payload: m.payload, - stateInit: m.stateInit, - extraCurrency: m.extraCurrency, - mode: m.mode ? SendModeToValue(m.mode) : undefined, - })), - }), - }; -} - -export class FetchToncenterEmulationError extends Error { - response: Response; - constructor(message: string, response: Response) { - super(message); - this.name = 'fetchToncenterEmulationError'; - this.response = response; - } -} - -const JETTON_TRANSFER_OPCODE = 0x0f8a7ea5; - -interface JettonTransfer { - kind: 'JettonTransfer'; - query_id: bigint; - amount: bigint; - destination: Address | null; - response_destination: Address | null; - custom_payload: Cell | null; - forward_ton_amount: bigint; -} - -function parseJettonTransfer(slice: Slice): JettonTransfer { - if (slice.remainingBits < 32 || slice.preloadUint(32) !== JETTON_TRANSFER_OPCODE) { - throw new Error('Expected JettonTransfer opcode 0x0f8a7ea5'); - } - - slice.loadUint(32); // skip opcode - - const queryId = slice.loadUintBig(64); - const amount = slice.loadCoins(); - const destination = slice.loadMaybeAddress(); - const responseDestination = slice.loadMaybeAddress(); - - // Load Maybe for custom_payload - const customPayloadFlag = slice.loadUint(1); - const customPayload = customPayloadFlag === 0 ? null : slice.loadRef(); - - const forwardTonAmount = slice.loadCoins(); - - return { - kind: 'JettonTransfer', - query_id: queryId, - amount, - destination, - response_destination: responseDestination, - custom_payload: customPayload, - forward_ton_amount: forwardTonAmount, - }; -} - -/** - * Processes toncenter emulation result to extract money flow - */ -export function processToncenterMoneyFlow(emulation: ToncenterEmulationResponse): TransactionTraceMoneyFlow { - if (!emulation || !emulation.transactions) { - return { - outputs: '0', - inputs: '0', - allJettonTransfers: [], - ourTransfers: [], - ourAddress: undefined, - }; - } - - const firstTx = emulation.transactions[emulation.trace.tx_hash]; - - // Get all transactions for our account - const ourTxes = Object.values(emulation.transactions).filter((t) => t.account === firstTx.account); - - const messagesFrom = ourTxes.flatMap((t) => t.out_msgs); - const messagesTo = ourTxes.flatMap((t) => t.in_msg).filter((m) => m !== null); - - // Calculate TON outputs - const outputs = messagesFrom - .reduce((acc, m) => { - if (m.value) { - return acc + BigInt(m.value); - } - return acc + 0n; - }, 0n) - .toString(); - - // Calculate TON inputs - const inputs = messagesTo - .reduce((acc, m) => { - if (m.value) { - return acc + BigInt(m.value); - } - return acc + 0n; - }, 0n) - .toString(); - - // Process jetton transfers - const jettonTransfers: TransactionTraceMoneyFlowItem[] = []; - - for (const t of Object.values(emulation.transactions)) { - if (!t.in_msg?.source) { - continue; - } - - let parsed: JettonTransfer | null = null; - try { - parsed = parseJettonTransfer(Cell.fromBase64(t.in_msg.message_content.body).beginParse()); - } catch (_) { - continue; - } - if (!parsed) { - continue; - } - const from = asMaybeAddressFriendly(t.in_msg.source); - const to = parsed.destination instanceof Address ? parsed.destination : null; - if (!to) { - continue; - } - const jettonAmount = parsed.amount; - - const metadata = emulation.metadata[t.account]; - if (!metadata || !metadata?.token_info) { - continue; - } - - const tokenInfo = metadata.token_info.find((t) => t.valid && t.type === 'jetton_wallets') as - | EmulationTokenInfoWallets - | undefined; - - if (!tokenInfo) { - continue; - } - - const jettonAddress = asMaybeAddressFriendly(tokenInfo.extra.jetton); - - jettonTransfers.push({ - fromAddress: from ?? undefined, - toAddress: asMaybeAddressFriendly(to.toString()) ?? undefined, - tokenAddress: jettonAddress ?? undefined, - amount: jettonAmount.toString(), - assetType: AssetType.jetton, - }); - } - - const ourAddress = Address.parse(firstTx.account); - - const selfTransfers: TransactionTraceMoneyFlowItem[] = []; - const ourJettonTransfersByAddress = jettonTransfers.reduce>((acc, transfer) => { - if (transfer.assetType !== AssetType.jetton) { - return acc; - } - const jettonKey = transfer.tokenAddress?.toString() || 'unknown'; - - // TON Proxy - if (TON_PROXY_ADDRESSES.includes(jettonKey)) { - return acc; - } - - const rawKey = Address.parse(jettonKey).toRawString().toUpperCase(); - if (!acc[rawKey]) { - acc[rawKey] = 0n; - } - - // Add to balance if receiving tokens (to our address) - // Subtract from balance if sending tokens (from our address) - if (ourAddress && transfer.toAddress === ourAddress.toString()) { - acc[rawKey] += BigInt(transfer.amount); - } - if (ourAddress && transfer.fromAddress === ourAddress.toString()) { - acc[rawKey] -= BigInt(transfer.amount); - } - - return acc; - }, {}); - - const ourJettonTransfers: TransactionTraceMoneyFlowItem[] = Object.entries(ourJettonTransfersByAddress).map( - ([jettonKey, amount]) => ({ - assetType: AssetType.jetton, - tokenAddress: asMaybeAddressFriendly(jettonKey) ?? undefined, - amount: amount.toString(), - }), - ); - selfTransfers.push({ - assetType: AssetType.ton, - amount: (BigInt(inputs) - BigInt(outputs)).toString(), - }); - selfTransfers.push(...ourJettonTransfers); - - return { - outputs, - inputs, - allJettonTransfers: jettonTransfers, - ourTransfers: selfTransfers, - ourAddress: asAddressFriendly(ourAddress), - }; -} - -/** - * Creates a transaction preview by emulating the transaction - */ -export async function createTransactionPreview( - client: ApiClient, - request: TransactionRequest, - wallet?: Wallet, -): Promise { - const txData = await wallet?.getSignedSendTransaction(request, { fakeSignature: true }); - - if (!txData) { - return { - result: Result.failure, - error: { - code: ERROR_CODES.UNKNOWN_EMULATION_ERROR, - message: 'Unknown emulation error', - }, - }; - } - - let emulationResult: ToncenterEmulationResponse; - try { - const emulatedResult = await CallForSuccess(() => client.fetchEmulation(txData, true)); - if (emulatedResult.result === 'success') { - emulationResult = emulatedResult.emulationResult; - } else { - return { - result: Result.failure, - error: { - code: emulatedResult.emulationError.code, - message: emulatedResult.emulationError.message, - }, - }; - } - } catch (_error) { - return { - result: Result.failure, - error: { - code: ERROR_CODES.UNKNOWN_EMULATION_ERROR, - message: 'Unknown emulation error', - }, - }; - } - - const moneyFlow = processToncenterMoneyFlow(emulationResult); - - return { - result: Result.success, - trace: toTransactionEmulatedTrace(emulationResult), - moneyFlow, - }; -} diff --git a/packages/walletkit/src/utils/transactionPreview.ts b/packages/walletkit/src/utils/transactionPreview.ts new file mode 100644 index 000000000..248717c29 --- /dev/null +++ b/packages/walletkit/src/utils/transactionPreview.ts @@ -0,0 +1,65 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { toTransactionEmulatedTrace } from '../clients/toncenter/mappers/map-emulation-trace'; +import { computeMoneyFlow } from './computeMoneyFlow'; +import type { EmulationResponse } from '../api/models'; +import { ERROR_CODES } from '../errors/codes'; +import { CallForSuccess } from './retry'; +import type { TransactionEmulatedPreview, TransactionRequest } from '../api/models'; +import { Result } from '../api/models'; +import type { Wallet } from '../api/interfaces'; +import type { ApiClient } from '../api/interfaces'; + +export async function createTransactionPreview( + client: ApiClient, + request: TransactionRequest, + wallet?: Wallet, +): Promise { + const txData = await wallet?.getSignedSendTransaction(request, { fakeSignature: true }); + + if (!txData) { + return { + result: Result.failure, + error: { + code: ERROR_CODES.UNKNOWN_EMULATION_ERROR, + message: 'Unknown emulation error', + }, + }; + } + + let emulationResult: EmulationResponse; + try { + const emulatedResult = await CallForSuccess(() => client.fetchEmulation(txData, true)); + if (emulatedResult.result === 'success') { + emulationResult = emulatedResult.emulationResult; + } else { + return { + result: Result.failure, + error: { + code: emulatedResult.emulationError.code, + message: emulatedResult.emulationError.message, + }, + }; + } + } catch (_error) { + return { + result: Result.failure, + error: { + code: ERROR_CODES.UNKNOWN_EMULATION_ERROR, + message: 'Unknown emulation error', + }, + }; + } + + return { + result: Result.success, + trace: toTransactionEmulatedTrace(emulationResult), + moneyFlow: computeMoneyFlow(emulationResult), + }; +}