diff --git a/demo/v4ledger-adapter/src/WalletV4R2LedgerAdapter.ts b/demo/v4ledger-adapter/src/WalletV4R2LedgerAdapter.ts index 3efa5fd11..d6d287e91 100644 --- a/demo/v4ledger-adapter/src/WalletV4R2LedgerAdapter.ts +++ b/demo/v4ledger-adapter/src/WalletV4R2LedgerAdapter.ts @@ -186,6 +186,10 @@ export class WalletV4R2LedgerAdapter implements WalletAdapter { } } + async getSignedSignMessage(): Promise { + throw new Error('WalletV4R2 does not support sign message signing. Use WalletV5R1.'); + } + /** * Get state init for wallet deployment */ diff --git a/demo/wallet-core/src/adapters/WalletV5SeqnoAdapter.ts b/demo/wallet-core/src/adapters/WalletV5SeqnoAdapter.ts deleted file mode 100644 index 9d8c5724c..000000000 --- a/demo/wallet-core/src/adapters/WalletV5SeqnoAdapter.ts +++ /dev/null @@ -1,176 +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 { - ActionSendMsg, - CallForSuccess, - ERROR_CODES, - packActionsList, - WalletKitError, - WalletV5R1Adapter, -} from '@ton/walletkit'; -import type { StateInit } from '@ton/core'; -import { Address, beginCell, Cell, external, internal, loadStateInit, SendMode, storeMessage } from '@ton/core'; -import type { Base64String, Network, TransactionRequest } from '@ton/walletkit'; -import type { WalletSigner } from '@ton/walletkit'; -import type { Maybe } from '@ton/core/dist/utils/maybe'; - -import { createComponentLogger } from '../utils/logger'; - -const log = createComponentLogger('WalletV5SeqnoAdapter'); - -/** Local seqno entry for fast send */ -export type LocalSeqnoEntry = { seqno: number; timestamp: number }; - -export interface WalletV5SeqnoAdapterOptions { - client: Parameters[1]['client']; - network: Network; - walletId?: number | bigint; - workchain?: number; - /** Wallet address for local seqno storage */ - walletAddress: string; - /** Get local seqno for address */ - getLocalSeqno: (address: string) => LocalSeqnoEntry | undefined; - /** Persist seqno after use (optimistic - called before send) */ - setLocalSeqno?: (address: string, seqno: number) => void; -} - -/** - * WalletV5 adapter with local seqno storage for fast send (prevents duplicate seqno on rapid clicks). - * Extends WalletV5R1Adapter and overrides getSignedSendTransaction. - */ -export class WalletV5SeqnoAdapter extends WalletV5R1Adapter { - private readonly walletAddress: string; - private readonly getLocalSeqno: (address: string) => LocalSeqnoEntry | undefined; - private readonly setLocalSeqno?: (address: string, seqno: number) => void; - - static async create(signer: WalletSigner, options: WalletV5SeqnoAdapterOptions): Promise { - const { walletAddress, getLocalSeqno, setLocalSeqno, client, network, walletId, workchain } = options; - const config: ConstructorParameters[0] = { - signer, - publicKey: signer.publicKey, - tonClient: client, - network, - walletId, - workchain, - }; - return new WalletV5SeqnoAdapter(config, walletAddress, getLocalSeqno, setLocalSeqno); - } - - private constructor( - config: ConstructorParameters[0], - walletAddress: string, - getLocalSeqno: (address: string) => LocalSeqnoEntry | undefined, - setLocalSeqno?: (address: string, seqno: number) => void, - ) { - super(config); - this.walletAddress = walletAddress; - this.getLocalSeqno = getLocalSeqno; - this.setLocalSeqno = setLocalSeqno; - } - - override async getSignedSendTransaction( - input: TransactionRequest, - options?: { fakeSignature: boolean }, - ): Promise { - const opts = options ?? { fakeSignature: false }; - const actions = packActionsList( - input.messages.map((m) => { - let bounce = true; - const parsedAddress = Address.parseFriendly(m.address); - if (parsedAddress.isBounceable === false) { - bounce = false; - } - - const msg = internal({ - to: m.address, - value: BigInt(m.amount), - bounce, - extracurrency: m.extraCurrency - ? Object.fromEntries(Object.entries(m.extraCurrency).map(([k, v]) => [Number(k), BigInt(v)])) - : undefined, - }); - - if (m.payload) { - try { - msg.body = Cell.fromBase64(m.payload); - } catch (error) { - log.warn('Failed to load payload', { error }); - throw WalletKitError.fromError( - ERROR_CODES.CONTRACT_VALIDATION_FAILED, - 'Failed to parse transaction payload', - error, - ); - } - } - if (m.stateInit) { - try { - msg.init = loadStateInit(Cell.fromBase64(m.stateInit).asSlice()); - } catch (error) { - log.warn('Failed to load state init', { error }); - throw WalletKitError.fromError( - ERROR_CODES.CONTRACT_VALIDATION_FAILED, - 'Failed to parse state init', - error, - ); - } - } - return new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, msg as never); - }), - ); - - const createBodyOptions: { validUntil: number | undefined; fakeSignature: boolean } = { - ...opts, - validUntil: undefined, - }; - if (input.validUntil) { - const now = Math.floor(Date.now() / 1000); - const maxValidUntil = now + 600; - if (input.validUntil < now) { - throw new WalletKitError( - ERROR_CODES.VALIDATION_ERROR, - 'Transaction validUntil timestamp is in the past', - undefined, - { validUntil: input.validUntil, currentTime: now }, - ); - } - if (input.validUntil > maxValidUntil) { - createBodyOptions.validUntil = maxValidUntil; - } else { - createBodyOptions.validUntil = input.validUntil; - } - } - - const networkSeqno = await CallForSuccess(async () => this.getSeqno(), 5, 1000); - const local = this.getLocalSeqno(this.walletAddress); - let seqno = networkSeqno; - if (local) { - const localSeqnoNext = local.seqno + 1; - const now = Date.now(); - if (now - local.timestamp < 5000) { - seqno = Math.max(networkSeqno, localSeqnoNext); - } - } - - this.setLocalSeqno?.(this.walletAddress, seqno); - - const walletId = (await this.walletContract.walletId).serialized; - if (!walletId) { - throw new Error('Failed to get seqno or walletId'); - } - - const transfer = await this.createBodyV5(seqno, walletId, actions, createBodyOptions); - - const ext = external({ - to: this.walletContract.address.toString(), - init: this.walletContract.init as Maybe, - body: transfer as unknown as Cell, - }); - return beginCell().store(storeMessage(ext)).endCell().toBoc().toString('base64') as Base64String; - } -} diff --git a/demo/wallet-core/src/adapters/index.ts b/demo/wallet-core/src/adapters/index.ts index 8bc774845..705af8389 100644 --- a/demo/wallet-core/src/adapters/index.ts +++ b/demo/wallet-core/src/adapters/index.ts @@ -7,4 +7,3 @@ */ export * from './storage'; -export * from './WalletV5SeqnoAdapter'; diff --git a/packages/mcp/src/__tests__/AgenticWalletAdapter.spec.ts b/packages/mcp/src/__tests__/AgenticWalletAdapter.spec.ts index ee7a031f8..620cd649f 100644 --- a/packages/mcp/src/__tests__/AgenticWalletAdapter.spec.ts +++ b/packages/mcp/src/__tests__/AgenticWalletAdapter.spec.ts @@ -10,6 +10,8 @@ import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; +import { Address, Cell, loadMessageRelaxed } from '@ton/core'; +import type { CommonMessageInfoInternal } from '@ton/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { Network } from '@ton/walletkit'; import type { ApiClient, Hex, WalletSigner } from '@ton/walletkit'; @@ -131,6 +133,59 @@ describe('AgenticWalletAdapter wallet NFT index caching', () => { }); }); +describe('AgenticWalletAdapter v5-style signing', () => { + it('creates a relaxed internal sign message for relaying', async () => { + const runGetMethod = vi.fn().mockResolvedValue({ + exitCode: 0, + stack: [{ type: 'num', value: '0' }], + }); + const adapter = await AgenticWalletAdapter.create(createSignerStub(), { + client: createClientStub(runGetMethod), + network: Network.mainnet(), + walletAddress: agentAddress, + walletNftIndex: 42n, + }); + + const boc = await adapter.getSignedSignMessage( + { + messages: [ + { + address: agentAddress, + amount: '1', + }, + ], + }, + { fakeSignature: true }, + ); + + const message = loadMessageRelaxed(Cell.fromBase64(boc).asSlice()); + const info = message.info as CommonMessageInfoInternal; + const body = message.body.beginParse(); + + expect(info.type).toBe('internal'); + expect(info.dest.toString()).toBe(Address.parse(agentAddress).toString()); + expect(body.loadUint(32)).toBe(0xbf235204); + expect(body.loadUintBig(256)).toBe(42n); + }); + + it('advertises v5-style transaction features', async () => { + const adapter = await AgenticWalletAdapter.create(createSignerStub(), { + client: createClientStub(vi.fn()), + network: Network.mainnet(), + walletAddress: agentAddress, + walletNftIndex: 42n, + }); + + expect(adapter.getSupportedFeatures()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'SendTransaction', maxMessages: 255 }), + expect.objectContaining({ name: 'SignMessage', maxMessages: 255 }), + expect.objectContaining({ name: 'EmbeddedRequest' }), + ]), + ); + }); +}); + describe('AgenticWalletAdapter back-fills persisted config via callback', () => { const originalConfigPath = process.env.TON_CONFIG_PATH; let tempDir = ''; diff --git a/packages/mcp/src/contracts/agentic_wallet/AgenticWalletAdapter.ts b/packages/mcp/src/contracts/agentic_wallet/AgenticWalletAdapter.ts index 29e2702c4..75e05b9d0 100644 --- a/packages/mcp/src/contracts/agentic_wallet/AgenticWalletAdapter.ts +++ b/packages/mcp/src/contracts/agentic_wallet/AgenticWalletAdapter.ts @@ -6,6 +6,7 @@ * */ +import type { CommonMessageInfoInternal } from '@ton/core'; import { Address, beginCell, @@ -15,6 +16,7 @@ import { loadStateInit, SendMode, storeMessage, + storeMessageRelaxed, storeStateInit, } from '@ton/core'; import { external, internal } from '@ton/core'; @@ -31,6 +33,7 @@ import type { Base64String, Feature, WalletId, + SignedSendTransactionOptions, } from '@ton/walletkit'; import { WalletKitError, @@ -48,6 +51,13 @@ import { ActionSendMsg, packActionsList } from './actions.js'; export const defaultAgenticWorkchain = 0; +type AgenticWalletAuthType = 'external' | 'internal'; + +interface CreateAgenticBodyOptions extends SignedSendTransactionOptions { + validUntil: number | undefined; + authType: AgenticWalletAuthType; +} + export interface AgenticWalletAdapterConfig { signer: WalletSigner; publicKey: Hex; @@ -228,83 +238,9 @@ export class AgenticWalletAdapter implements WalletAdapter { async getSignedSendTransaction( input: TransactionRequest, - options: { fakeSignature: boolean }, + options?: SignedSendTransactionOptions, ): Promise { - const actions = packActionsList( - input.messages.map((m) => { - let bounce = true; - const parsedAddress = Address.parseFriendly(m.address); - if (parsedAddress.isBounceable === false) { - bounce = false; - } - - const msg = internal({ - to: m.address, - value: BigInt(m.amount), - bounce, - extracurrency: m.extraCurrency - ? Object.fromEntries(Object.entries(m.extraCurrency).map(([k, v]) => [Number(k), BigInt(v)])) - : undefined, - }); - - if (m.payload) { - try { - msg.body = Cell.fromBase64(m.payload); - } catch (error) { - throw WalletKitError.fromError( - ERROR_CODES.CONTRACT_VALIDATION_FAILED, - 'Failed to parse transaction payload', - error, - ); - } - } - - if (m.stateInit) { - try { - msg.init = loadStateInit(Cell.fromBase64(m.stateInit).asSlice()); - } catch (error) { - throw WalletKitError.fromError( - ERROR_CODES.CONTRACT_VALIDATION_FAILED, - 'Failed to parse state init', - error, - ); - } - } - - return new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, msg); - }), - ); - - const createBodyOptions: { validUntil: number | undefined; fakeSignature: boolean } = { - ...options, - validUntil: undefined, - }; - - if (input.validUntil) { - const now = Math.floor(Date.now() / 1000); - const maxValidUntil = now + 600; - if (input.validUntil < now) { - throw new WalletKitError( - ERROR_CODES.VALIDATION_ERROR, - 'Transaction validUntil timestamp is in the past', - undefined, - { validUntil: input.validUntil, currentTime: now }, - ); - } - - createBodyOptions.validUntil = input.validUntil > maxValidUntil ? maxValidUntil : input.validUntil; - } - - let seqno = 0; - try { - seqno = await CallForSuccess(async () => this.getSeqno(), 5, 1000); - } catch (_) { - // allow seqno fallback to 0 for undeployed contracts - } - - const walletNftIndex = await this.getWalletNftIndex(); - const outActions = this.extractOutActions(actions); - const transfer = await this.createSignedBody(seqno, walletNftIndex, outActions, createBodyOptions); + const transfer = await this.createSignedTransferBody(input, options, 'external'); const walletInit = await this.ensureWalletInit(); const ext = external({ @@ -316,6 +252,31 @@ export class AgenticWalletAdapter implements WalletAdapter { return beginCell().store(storeMessage(ext)).endCell().toBoc().toString('base64') as Base64String; } + async getSignedSignMessage( + input: TransactionRequest, + options?: SignedSendTransactionOptions, + ): Promise { + const transfer = await this.createSignedTransferBody(input, options, 'internal'); + + const msg = internal({ + to: this.address, + value: 0n, + body: transfer, + bounce: false, + init: this.walletInit, + }); + msg.info = msg.info as CommonMessageInfoInternal; + msg.info.createdLt = 0n; + msg.info.createdAt = 0; + msg.info.ihrFee = 0n; + msg.info.forwardFee = 0n; + msg.info.ihrDisabled = false; + msg.info.bounce = false; + msg.info.bounced = false; + msg.info.src = new Address(0, Buffer.alloc(32)); + return beginCell().store(storeMessageRelaxed(msg)).endCell().toBoc().toString('base64') as Base64String; + } + async getSeqno(): Promise { const data = await this.client.runGetMethod(this.getAddress(), 'seqno'); if (data.exitCode !== 0) { @@ -347,22 +308,109 @@ export class AgenticWalletAdapter implements WalletAdapter { return nftIndex; } - private extractOutActions(actionsList: Cell): Cell | null { - const slice = actionsList.beginParse(); - return slice.loadMaybeRef(); + private async createSignedTransferBody( + input: TransactionRequest, + options: SignedSendTransactionOptions | undefined, + authType: AgenticWalletAuthType, + ): Promise { + const actions = packActionsList(input.messages.map((message) => this.createTransferAction(message))); + + let seqno = 0; + try { + seqno = await CallForSuccess(async () => this.getSeqno(), 5, 1000); + } catch (_) { + // allow seqno fallback to 0 for undeployed contracts + } + + const walletNftIndex = await this.getWalletNftIndex(); + return this.createSignedBody(seqno, walletNftIndex, actions, { + ...options, + authType, + validUntil: this.resolveValidUntil(input.validUntil), + }); + } + + private createTransferAction(message: TransactionRequest['messages'][number]): ActionSendMsg { + let bounce = true; + try { + const parsedAddress = Address.parseFriendly(message.address); + if (parsedAddress.isBounceable === false) { + bounce = false; + } + } catch { + // raw address has no bounceable flag, keep default true + } + + const msg = internal({ + to: message.address, + value: BigInt(message.amount), + bounce, + extracurrency: message.extraCurrency + ? Object.fromEntries(Object.entries(message.extraCurrency).map(([k, v]) => [Number(k), BigInt(v)])) + : undefined, + }); + + if (message.payload) { + try { + msg.body = Cell.fromBase64(message.payload); + } catch (error) { + throw WalletKitError.fromError( + ERROR_CODES.CONTRACT_VALIDATION_FAILED, + 'Failed to parse transaction payload', + error, + ); + } + } + + if (message.stateInit) { + try { + msg.init = loadStateInit(Cell.fromBase64(message.stateInit).asSlice()); + } catch (error) { + throw WalletKitError.fromError( + ERROR_CODES.CONTRACT_VALIDATION_FAILED, + 'Failed to parse state init', + error, + ); + } + } + + return new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, msg); + } + + private resolveValidUntil(validUntil: number | undefined): number | undefined { + if (!validUntil) { + return undefined; + } + + const now = Math.floor(Date.now() / 1000); + const maxValidUntil = now + 600; + if (validUntil < now) { + throw new WalletKitError( + ERROR_CODES.VALIDATION_ERROR, + 'Transaction validUntil timestamp is in the past', + undefined, + { validUntil, currentTime: now }, + ); + } + + return validUntil > maxValidUntil ? maxValidUntil : validUntil; } async createSignedBody( seqno: number, walletNftIndex: bigint, outActions: Cell | null, - options: { validUntil: number | undefined; fakeSignature: boolean }, + options: CreateAgenticBodyOptions, ): Promise { - const authSigned = 0xbf235204; + const Opcodes = { + auth_signed: 0xbf235204, + auth_signed_internal: 0xbf235204, + }; + const opcode = options.authType === 'internal' ? Opcodes.auth_signed_internal : Opcodes.auth_signed; const expireAt = options.validUntil ?? Math.floor(Date.now() / 1000) + 300; const payload = beginCell() - .storeUint(authSigned, 32) + .storeUint(opcode, 32) .storeUint(walletNftIndex, 256) .storeUint(expireAt, 32) .storeUint(seqno, 32) @@ -390,14 +438,23 @@ export class AgenticWalletAdapter implements WalletAdapter { getSupportedFeatures(): Feature[] | undefined { return [ + 'SendTransaction', { name: 'SendTransaction', maxMessages: 255, + extraCurrencySupported: true, + itemTypes: ['ton', 'jetton', 'nft'], + }, + { name: 'SignData', types: ['text', 'binary', 'cell'] }, + { + name: 'SignMessage', + maxMessages: 255, + extraCurrencySupported: true, + itemTypes: ['ton', 'jetton', 'nft'], }, { - name: 'SignData', - types: ['binary', 'cell', 'text'], + name: 'EmbeddedRequest', }, - ]; + ] as Feature[]; } } diff --git a/packages/walletkit-android-bridge/src/api/wallets.ts b/packages/walletkit-android-bridge/src/api/wallets.ts index bf189c45b..8cf3e95b4 100644 --- a/packages/walletkit-android-bridge/src/api/wallets.ts +++ b/packages/walletkit-android-bridge/src/api/wallets.ts @@ -75,6 +75,10 @@ class ProxyWalletAdapter implements WalletAdapter { return result as Base64String; } + async getSignedSignMessage(): Promise { + throw new Error('Sign message signing is not supported by the Android proxy wallet adapter.'); + } + async getSignedSignData(input: PreparedSignData, options?: { fakeSignature: boolean }): Promise { const result = await bridgeRequest('adapterSignData', { adapterId: this.adapterId, diff --git a/packages/walletkit-ios-bridge/src/SwiftWalletAdapter.ts b/packages/walletkit-ios-bridge/src/SwiftWalletAdapter.ts index bf129366a..c2cdf85a9 100644 --- a/packages/walletkit-ios-bridge/src/SwiftWalletAdapter.ts +++ b/packages/walletkit-ios-bridge/src/SwiftWalletAdapter.ts @@ -65,6 +65,15 @@ export class SwiftWalletAdapter implements WalletAdapter { return this.swiftWalletAdapter.getSignedSendTransaction(input, options); } + getSignedSignMessage( + input: TransactionRequest, + options?: { + fakeSignature: boolean; + }, + ): Promise { + return this.swiftWalletAdapter.getSignedSignMessage(input, options); + } + getSignedSignData( input: PreparedSignData, options?: { diff --git a/packages/walletkit/package.json b/packages/walletkit/package.json index 7c8161077..c6fcd9f86 100644 --- a/packages/walletkit/package.json +++ b/packages/walletkit/package.json @@ -88,7 +88,7 @@ "test:coverage": "vitest run --coverage", "test:mutation": "stryker run stryker.config.js", - "quality": "pnpm test:coverage", + "quality": "pnpm test", "lint": "eslint . --max-warnings 0", "lint:fix": "eslint . --max-warnings 0 --fix", "clean": "git clean -xdf dist node_modules .turbo coverage", diff --git a/packages/walletkit/src/api/interfaces/WalletAdapter.ts b/packages/walletkit/src/api/interfaces/WalletAdapter.ts index 7ae165336..328edc640 100644 --- a/packages/walletkit/src/api/interfaces/WalletAdapter.ts +++ b/packages/walletkit/src/api/interfaces/WalletAdapter.ts @@ -39,6 +39,10 @@ export interface WalletAdapter { /** Get the signed send transaction */ getSignedSendTransaction(input: TransactionRequest, options?: SignedSendTransactionOptions): Promise; + + /** Get the signed sign message */ + getSignedSignMessage(input: TransactionRequest, options?: SignedSendTransactionOptions): Promise; + getSignedSignData( input: PreparedSignData, options?: { diff --git a/packages/walletkit/src/api/models/transactions/SignedSendTransactionOptions.ts b/packages/walletkit/src/api/models/transactions/SignedSendTransactionOptions.ts index 0e7453ca2..8b4015e9d 100644 --- a/packages/walletkit/src/api/models/transactions/SignedSendTransactionOptions.ts +++ b/packages/walletkit/src/api/models/transactions/SignedSendTransactionOptions.ts @@ -8,6 +8,4 @@ export interface SignedSendTransactionOptions { fakeSignature?: boolean; - /** Use internal message opcode (0x73696e74) instead of external (0x7369676e) for gasless relaying */ - internal?: boolean; } diff --git a/packages/walletkit/src/clients/tonapi/ApiClientTonApi.ts b/packages/walletkit/src/clients/tonapi/ApiClientTonApi.ts index 82df46b6a..cd3ada409 100644 --- a/packages/walletkit/src/clients/tonapi/ApiClientTonApi.ts +++ b/packages/walletkit/src/clients/tonapi/ApiClientTonApi.ts @@ -174,10 +174,12 @@ export class ApiClientTonApi extends BaseApiClient implements ApiClient { } async fetchEmulation(messageBoc: Base64String, ignoreSignature?: boolean): Promise { - const result = await this.postJson('/v2/wallet/emulate', { - boc: messageBoc, - ignore_signature_check: ignoreSignature === true, - }); + const result = await this.postJson( + `/v2/traces/emulate?ignore_signature_check=${ignoreSignature === true ? 'true' : 'false'}`, + { + boc: messageBoc, + }, + ); return { result: 'success', emulationResult: mapTonApiEmulationResponse(result), diff --git a/packages/walletkit/src/clients/tonapi/mappers/map-emulation.spec.ts b/packages/walletkit/src/clients/tonapi/mappers/map-emulation.spec.ts index 7a9eccc28..d492af6f6 100644 --- a/packages/walletkit/src/clients/tonapi/mappers/map-emulation.spec.ts +++ b/packages/walletkit/src/clients/tonapi/mappers/map-emulation.spec.ts @@ -8,7 +8,6 @@ import { describe, expect, it } from 'vitest'; -import type { TonApiMessageConsequences } from '../types/emulation'; import type { TonApiTrace } from '../types/traces'; import { mapTonApiEmulationResponse } from './map-emulation'; @@ -20,20 +19,6 @@ 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 = { @@ -41,7 +26,13 @@ describe('mapTonApiEmulationResponse', () => { hash: ROOT_HASH, lt: '1000000', account: { address: SENDER }, - in_msg: { hash: EXT_MSG_HASH, source: undefined, destination: { address: SENDER } }, + in_msg: { + hash: EXT_MSG_HASH, + source: undefined, + destination: { address: SENDER }, + raw: '', + raw_body: '', + }, out_msgs: [], }, children: [ @@ -55,6 +46,8 @@ describe('mapTonApiEmulationResponse', () => { source: { address: SENDER }, destination: { address: RECIPIENT }, value: '100000000', + raw: '', + raw_body: '', }, out_msgs: [], }, @@ -62,7 +55,7 @@ describe('mapTonApiEmulationResponse', () => { ], }; - const result = mapTonApiEmulationResponse(makeConsequences(trace)); + const result = mapTonApiEmulationResponse(trace); // root tx has no in_msg source (external message) const rootTx = Object.values(result.transactions).find((tx) => !tx.inMsg?.source); @@ -77,19 +70,27 @@ describe('mapTonApiEmulationResponse', () => { hash: ROOT_HASH, lt: '1000000', account: { address: SENDER }, - in_msg: { hash: EXT_MSG_HASH, source: undefined, destination: { address: SENDER } }, + in_msg: { + hash: EXT_MSG_HASH, + source: undefined, + destination: { address: SENDER }, + raw: '', + raw_body: '', + }, out_msgs: [ { hash: INT_MSG_HASH, source: { address: SENDER }, destination: { address: RECIPIENT }, value: '100000000', + raw: '', + raw_body: '', }, ], }, }; - const result = mapTonApiEmulationResponse(makeConsequences(trace)); + const result = mapTonApiEmulationResponse(trace); const rootHashHex = Object.keys(result.transactions)[0]; const rootTx = result.transactions[rootHashHex]; @@ -102,12 +103,19 @@ describe('mapTonApiEmulationResponse', () => { hash: ROOT_HASH, lt: '1000000', account: { address: SENDER }, - in_msg: { hash: EXT_MSG_HASH, source: undefined, destination: { address: SENDER }, bounce: false }, + in_msg: { + hash: EXT_MSG_HASH, + source: undefined, + destination: { address: SENDER }, + bounce: false, + raw: '', + raw_body: '', + }, out_msgs: [], }, }; - const result = mapTonApiEmulationResponse(makeConsequences(trace)); + const result = mapTonApiEmulationResponse(trace); const rootHashHex = Object.keys(result.transactions)[0]; const rootTx = result.transactions[rootHashHex]; @@ -126,12 +134,14 @@ describe('mapTonApiEmulationResponse', () => { destination: { address: RECIPIENT }, value: '100000000', bounce: false, + raw: '', + raw_body: '', }, out_msgs: [], }, }; - const result = mapTonApiEmulationResponse(makeConsequences(trace)); + const result = mapTonApiEmulationResponse(trace); const txHashHex = Object.keys(result.transactions)[0]; const tx = result.transactions[txHashHex]; @@ -150,12 +160,14 @@ describe('mapTonApiEmulationResponse', () => { destination: { address: RECIPIENT }, value: '100000000', bounce: true, + raw: '', + raw_body: '', }, out_msgs: [], }, }; - const result = mapTonApiEmulationResponse(makeConsequences(trace)); + const result = mapTonApiEmulationResponse(trace); const txHashHex = Object.keys(result.transactions)[0]; const tx = result.transactions[txHashHex]; diff --git a/packages/walletkit/src/clients/tonapi/mappers/map-emulation.ts b/packages/walletkit/src/clients/tonapi/mappers/map-emulation.ts index 08aaf5bc1..3bed9424e 100644 --- a/packages/walletkit/src/clients/tonapi/mappers/map-emulation.ts +++ b/packages/walletkit/src/clients/tonapi/mappers/map-emulation.ts @@ -19,10 +19,12 @@ import type { EmulationAddressBookEntry, EmulationAccountStatus, Hex, + Base64String, } from '../../../api/models'; import { parseBlockRef, toHex } from './map-transactions'; import { asHex } from '../../../utils/hex'; import { asAddressFriendly, asMaybeAddressFriendly } from '../../../utils/address'; +import { HexToBase64 } from '../../../utils'; function mapTraceNode(trace: TonApiTrace): EmulationTraceNode { return { @@ -59,7 +61,7 @@ function mapMessage(raw: TonApiMessage, kind: 'in' | 'out'): EmulationMessage { kind === 'out' || !isExternal ? undefined : raw.import_fee != null ? String(raw.import_fee) : undefined, messageContent: { hash: undefined, - body: undefined, + body: HexToBase64(('0x' + raw.raw_body) as Hex) as Base64String, decoded: raw.decoded_body ?? undefined, }, initState: undefined, @@ -229,7 +231,7 @@ function normalizeJettonTransferDetails(payload: Record): Recor }; } -function mapAction(action: TonApiAction, event: TonApiAccountEvent, rootHash: Hex): EmulationAction { +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)); @@ -333,22 +335,37 @@ function mapAction(action: TonApiAction, event: TonApiAccountEvent, rootHash: He } export function mapTonApiEmulationResponse(result: TonApiMessageConsequences): EmulationResponse { - const rootTxHash = toHex(result.trace.transaction.hash); + const rootTxHash = toHex(result.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 externalHash = result.transaction.in_msg?.hash ? toHex(result.transaction.in_msg.hash) : rootTxHash; + const allTraces = flattenTrace(result); const transactions = Object.fromEntries( - allTraces.map((traceNode) => [toHex(traceNode.transaction.hash), mapTransaction(traceNode, externalHash)]), + allTraces.map((traceNode) => { + const hash = toHex(traceNode.transaction.hash); + const mappedTransaction = mapTransaction(traceNode, externalHash); + + if (traceNode.children) { + for (const child of traceNode.children) { + const childTransaction = mapTransaction(child, externalHash); + if (childTransaction.inMsg) { + // avoid duplicate outMsgs + if (!mappedTransaction.outMsgs.some((m) => m.hash === childTransaction?.inMsg?.hash)) { + mappedTransaction.outMsgs.push(childTransaction.inMsg); + } + } + } + } + + return [hash, mappedTransaction]; + }), ); - const actions = (result.event.actions ?? []).map((a) => mapAction(a, result.event, externalHash)); + // const actions = (result.event.actions ?? []).map((a) => mapAction(a, result.event, externalHash)); return { mcBlockSeqno: transactions[rootTxHash]?.mcBlockSeqno ?? 0, - trace: mapTraceNode(result.trace), + trace: mapTraceNode(result), transactions, - actions, + actions: [], randSeed: asHex('0x' + '0'.repeat(64)), isIncomplete: false, codeCells: {}, diff --git a/packages/walletkit/src/clients/tonapi/types/emulation.ts b/packages/walletkit/src/clients/tonapi/types/emulation.ts index 5a639a43b..fc4512472 100644 --- a/packages/walletkit/src/clients/tonapi/types/emulation.ts +++ b/packages/walletkit/src/clients/tonapi/types/emulation.ts @@ -6,8 +6,9 @@ * */ -import type { TonApiAccountEvent } from './events'; +// import type { TonApiAccountEvent } from './events'; import type { TonApiTrace } from './traces'; +// import type { TonApiTransaction } from './transactions'; export interface TonApiJettonQuantity { quantity: string; @@ -26,8 +27,15 @@ export interface TonApiRisk { nfts: unknown[]; } -export interface TonApiMessageConsequences { - trace: TonApiTrace; - risk: TonApiRisk; - event: TonApiAccountEvent; -} +export type TonApiMessageConsequences = TonApiTrace; +// { + +// transaction: TonApiTransaction; +// children: { +// transaction: TonApiTransaction; +// children: TonApiMessageConsequences[]; +// }[]; +// trace: TonApiTrace; +// risk: TonApiRisk; +// event: TonApiAccountEvent; +// } diff --git a/packages/walletkit/src/clients/tonapi/types/transactions.ts b/packages/walletkit/src/clients/tonapi/types/transactions.ts index 6de4b617e..2a43e2fb7 100644 --- a/packages/walletkit/src/clients/tonapi/types/transactions.ts +++ b/packages/walletkit/src/clients/tonapi/types/transactions.ts @@ -38,6 +38,12 @@ export interface TonApiMessage { bounced?: boolean | null; import_fee?: string | number | null; decoded_body?: unknown; + + // hex boc of inMessage + raw: string; + + // hex boc of inMessage.body + raw_body: string; } export interface TonApiPhaseStorage { diff --git a/packages/walletkit/src/contracts/v4r2/WalletV4R2Adapter.ts b/packages/walletkit/src/contracts/v4r2/WalletV4R2Adapter.ts index 6d60747b5..7879e5d60 100644 --- a/packages/walletkit/src/contracts/v4r2/WalletV4R2Adapter.ts +++ b/packages/walletkit/src/contracts/v4r2/WalletV4R2Adapter.ts @@ -47,6 +47,7 @@ import type { Base64String, SignedSendTransactionOptions, } from '../../api/models'; +import { FakeSignature } from '../../utils'; const log = globalLogger.createChild('WalletV4R2Adapter'); @@ -145,9 +146,6 @@ export class WalletV4R2Adapter implements WalletAdapter { input: TransactionRequest, options?: SignedSendTransactionOptions, ): Promise { - if (options?.internal) { - throw new Error('WalletV4R2 does not support internal message signing (gasless). Use WalletV5R1.'); - } if (input.messages.length === 0) { throw new Error('Ledger does not support empty messages'); } @@ -198,7 +196,9 @@ export class WalletV4R2Adapter implements WalletAdapter { const domainPrefix = this.domain ? signatureDomainPrefix(this.domain) : null; const signingData = domainPrefix ? Buffer.concat([domainPrefix, data.hash()]) : data.hash(); - const signature = await this.sign(Uint8Array.from(signingData)); + const signature = options?.fakeSignature + ? FakeSignature(signingData) + : await this.sign(Uint8Array.from(signingData)); const signedCell = beginCell() .storeBuffer(Buffer.from(HexToUint8Array(signature))) .storeSlice(data.asSlice()) @@ -217,6 +217,10 @@ export class WalletV4R2Adapter implements WalletAdapter { } } + async getSignedSignMessage(): Promise { + throw new Error('WalletV4R2 does not support sign message signing. Use WalletV5R1.'); + } + /** * Get state init for wallet deployment */ diff --git a/packages/walletkit/src/contracts/w5/WalletV5R1Adapter.spec.ts b/packages/walletkit/src/contracts/w5/WalletV5R1Adapter.spec.ts index b3f65b866..1eb52f27e 100644 --- a/packages/walletkit/src/contracts/w5/WalletV5R1Adapter.spec.ts +++ b/packages/walletkit/src/contracts/w5/WalletV5R1Adapter.spec.ts @@ -6,8 +6,8 @@ * */ -import { Cell, loadMessage } from '@ton/core'; -import type { CommonMessageInfoExternalIn } from '@ton/core/src/types/CommonMessageInfo'; +import { Cell, loadMessage, loadMessageRelaxed } from '@ton/core'; +import type { CommonMessageInfoInternal } from '@ton/core/src/types/CommonMessageInfo'; import { describe, it, expect, beforeEach } from 'vitest'; import { clearAllMocks, mocked } from '../../../mock.config'; @@ -141,6 +141,25 @@ describe('WalletV5R1Adapter', () => { { fakeSignature: false }, ); const message = loadMessage(Cell.fromBase64(boc).asSlice()); - expect((message.info as CommonMessageInfoExternalIn).dest.toString()).toEqual(addressV5r1.bounceable); + expect(message.info?.dest?.toString()).toEqual(addressV5r1.bounceable); + }); + + it('should create signed internal sign message', async () => { + const boc = await wallet.getSignedSignMessage( + { + messages: [ + { + address: addressV5r1.bounceableNot, + amount: '1', + }, + ], + }, + { fakeSignature: false }, + ); + const message = loadMessageRelaxed(Cell.fromBase64(boc).asSlice()); + const info = message.info as unknown as CommonMessageInfoInternal; + + expect(info.type).toEqual('internal'); + expect(info.dest.toString()).toEqual(addressV5r1.bounceable); }); }); diff --git a/packages/walletkit/src/contracts/w5/WalletV5R1Adapter.ts b/packages/walletkit/src/contracts/w5/WalletV5R1Adapter.ts index 3421c05dd..cbc3d6c10 100644 --- a/packages/walletkit/src/contracts/w5/WalletV5R1Adapter.ts +++ b/packages/walletkit/src/contracts/w5/WalletV5R1Adapter.ts @@ -52,6 +52,12 @@ import type { Feature } from '../../types/jsBridge'; const log = globalLogger.createChild('WalletV5R1Adapter'); export const defaultWalletIdV5R1 = 2147483409; +type WalletV5AuthType = 'external' | 'internal'; + +interface CreateBodyV5Options extends SignedSendTransactionOptions { + validUntil: number | undefined; + authType: WalletV5AuthType; +} /** * Configuration for creating a WalletV5R1 adapter @@ -173,112 +179,7 @@ export class WalletV5R1Adapter implements WalletAdapter { input: TransactionRequest, options?: SignedSendTransactionOptions, ): Promise { - const actions = packActionsList( - input.messages.map((m) => { - let bounce = true; - try { - const parsedAddress = Address.parseFriendly(m.address); - if (parsedAddress.isBounceable === false) { - bounce = false; - } - } catch { - // raw address — no bounceable flag, keep default true - } - - const msg = internal({ - to: m.address, - value: BigInt(m.amount), - bounce: bounce, - extracurrency: m.extraCurrency - ? Object.fromEntries(Object.entries(m.extraCurrency).map(([k, v]) => [Number(k), BigInt(v)])) - : undefined, - }); - - if (m.payload) { - try { - msg.body = Cell.fromBase64(m.payload); - } catch (error) { - log.warn('Failed to load payload', { error }); - throw WalletKitError.fromError( - ERROR_CODES.CONTRACT_VALIDATION_FAILED, - 'Failed to parse transaction payload', - error, - ); - } - } - - if (m.stateInit) { - try { - msg.init = loadStateInit(Cell.fromBase64(m.stateInit).asSlice()); - } catch (error) { - log.warn('Failed to load state init', { error }); - throw WalletKitError.fromError( - ERROR_CODES.CONTRACT_VALIDATION_FAILED, - 'Failed to parse state init', - error, - ); - } - } - return new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, msg); - }), - ); - - const createBodyOptions: { validUntil: number | undefined; fakeSignature?: boolean; internal?: boolean } = { - ...options, - validUntil: undefined, - }; - // add valid untill - if (input.validUntil) { - const now = Math.floor(Date.now() / 1000); - const maxValidUntil = now + 600; - if (input.validUntil < now) { - throw new WalletKitError( - ERROR_CODES.VALIDATION_ERROR, - 'Transaction validUntil timestamp is in the past', - undefined, - { validUntil: input.validUntil, currentTime: now }, - ); - } else if (input.validUntil > maxValidUntil) { - createBodyOptions.validUntil = maxValidUntil; - } else { - createBodyOptions.validUntil = input.validUntil; - } - } - - let seqno = 0; - try { - seqno = await CallForSuccess(async () => this.getSeqno(), 5, 1000); - } catch (_) { - // - } - const walletId = (await this.walletContract.walletId).serialized; - if (!walletId) { - throw new Error('Failed to get seqno or walletId'); - } - - const transfer = await this.createBodyV5(seqno, walletId, actions, createBodyOptions); - - if (options?.internal) { - // For gasless relaying, the signed body (auth_signed_internal opcode) must be - // delivered to the wallet via an internal message from a relayer contract. - const msg = internal({ - to: this.walletContract.address, - value: 0n, - body: transfer, - bounce: false, - }); - msg.info = msg.info as CommonMessageInfoInternal; - msg.info.createdLt = 0n; - msg.info.createdAt = 0; - msg.info.ihrFee = 0n; - msg.info.forwardFee = 0n; - msg.info.ihrDisabled = false; - msg.info.bounce = false; - msg.info.bounced = false; - msg.info.src = new Address(0, Buffer.alloc(32)); - return beginCell().store(storeMessageRelaxed(msg)).endCell().toBoc().toString('base64') as Base64String; - } - + const transfer = await this.createSignedTransferBody(input, options, 'external'); const ext = external({ to: this.walletContract.address, init: this.walletContract.init, @@ -287,6 +188,33 @@ export class WalletV5R1Adapter implements WalletAdapter { return beginCell().store(storeMessage(ext)).endCell().toBoc().toString('base64') as Base64String; } + async getSignedSignMessage( + input: TransactionRequest, + options?: SignedSendTransactionOptions, + ): Promise { + const transfer = await this.createSignedTransferBody(input, options, 'internal'); + + // For gasless relaying, the signed body (auth_signed_internal opcode) must be + // delivered to the wallet via an internal message from a relayer contract. + const msg = internal({ + to: this.walletContract.address, + value: 0n, + body: transfer, + bounce: false, + init: this.walletContract.init, + }); + msg.info = msg.info as CommonMessageInfoInternal; + msg.info.createdLt = 0n; + msg.info.createdAt = 0; + msg.info.ihrFee = 0n; + msg.info.forwardFee = 0n; + msg.info.ihrDisabled = false; + msg.info.bounce = false; + msg.info.bounced = false; + msg.info.src = new Address(0, Buffer.alloc(32)); + return beginCell().store(storeMessageRelaxed(msg)).endCell().toBoc().toString('base64') as Base64String; + } + /** * Get state init for wallet deployment */ @@ -348,12 +276,102 @@ export class WalletV5R1Adapter implements WalletAdapter { } } - async createBodyV5( - seqno: number, - walletId: bigint, - actionsList: Cell, - options: { validUntil: number | undefined; fakeSignature?: boolean; internal?: boolean }, - ) { + private async createSignedTransferBody( + input: TransactionRequest, + options: SignedSendTransactionOptions | undefined, + authType: WalletV5AuthType, + ): Promise { + const actions = packActionsList(input.messages.map((message) => this.createTransferAction(message))); + + let seqno = 0; + try { + seqno = await CallForSuccess(async () => this.getSeqno(), 5, 1000); + } catch (_) { + // + } + + const walletId = (await this.walletContract.walletId).serialized; + if (!walletId) { + throw new Error('Failed to get seqno or walletId'); + } + + return this.createBodyV5(seqno, walletId, actions, { + ...options, + authType, + validUntil: this.resolveValidUntil(input.validUntil), + }); + } + + private createTransferAction(message: TransactionRequest['messages'][number]): ActionSendMsg { + let bounce = true; + try { + const parsedAddress = Address.parseFriendly(message.address); + if (parsedAddress.isBounceable === false) { + bounce = false; + } + } catch { + // raw address — no bounceable flag, keep default true + } + + const msg = internal({ + to: message.address, + value: BigInt(message.amount), + bounce, + extracurrency: message.extraCurrency + ? Object.fromEntries(Object.entries(message.extraCurrency).map(([k, v]) => [Number(k), BigInt(v)])) + : undefined, + }); + + if (message.payload) { + try { + msg.body = Cell.fromBase64(message.payload); + } catch (error) { + log.warn('Failed to load payload', { error }); + throw WalletKitError.fromError( + ERROR_CODES.CONTRACT_VALIDATION_FAILED, + 'Failed to parse transaction payload', + error, + ); + } + } + + if (message.stateInit) { + try { + msg.init = loadStateInit(Cell.fromBase64(message.stateInit).asSlice()); + } catch (error) { + log.warn('Failed to load state init', { error }); + throw WalletKitError.fromError( + ERROR_CODES.CONTRACT_VALIDATION_FAILED, + 'Failed to parse state init', + error, + ); + } + } + + return new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, msg); + } + + private resolveValidUntil(validUntil: number | undefined): number | undefined { + if (!validUntil) { + return undefined; + } + + const now = Math.floor(Date.now() / 1000); + const maxValidUntil = now + 600; + + if (validUntil < now) { + throw new WalletKitError( + ERROR_CODES.VALIDATION_ERROR, + 'Transaction validUntil timestamp is in the past', + undefined, + { validUntil, currentTime: now }, + ); + } + + return validUntil > maxValidUntil ? maxValidUntil : validUntil; + } + + async createBodyV5(seqno: number, walletId: bigint, actionsList: Cell, options: CreateBodyV5Options) { // Opcodes defined in the WalletV5R1 contract spec, confirmed in @ton/ton WalletContractV5R1.js const Opcodes = { auth_signed: 0x7369676e, // external auth ("sign") @@ -361,9 +379,9 @@ export class WalletV5R1Adapter implements WalletAdapter { }; // Use internal opcode for gasless relaying (signOnly / signMsg intent) - const opcode = options.internal ? Opcodes.auth_signed_internal : Opcodes.auth_signed; + const opcode = options.authType === 'internal' ? Opcodes.auth_signed_internal : Opcodes.auth_signed; log.debug('createBodyV5 signing with opcode', { - internal: options.internal, + authType: options.authType, opcode: `0x${opcode.toString(16)}`, }); diff --git a/packages/walletkit/src/core/RequestProcessor.ts b/packages/walletkit/src/core/RequestProcessor.ts index 88c1aeb64..8f29525ec 100644 --- a/packages/walletkit/src/core/RequestProcessor.ts +++ b/packages/walletkit/src/core/RequestProcessor.ts @@ -621,7 +621,7 @@ export class RequestProcessor { { eventId: event.id }, ); } - const internalBoc = await wallet.getSignedSendTransaction(event.request, { internal: true }); + const internalBoc = await wallet.getSignedSignMessage(event.request); const actionResult = { internalBoc: internalBoc }; const tonConnectResponse = { diff --git a/packages/walletkit/src/core/TonWalletKit.spec.ts b/packages/walletkit/src/core/TonWalletKit.spec.ts index 8c9837605..8ced97df7 100644 --- a/packages/walletkit/src/core/TonWalletKit.spec.ts +++ b/packages/walletkit/src/core/TonWalletKit.spec.ts @@ -45,6 +45,7 @@ describe('TonWalletKit', () => { }, eventProcessor: { disableEvents: true, + disableTransactionEmulation: true, }, // Ensure we have storage in node env storage: { diff --git a/packages/walletkit/src/utils/transactionPreview.ts b/packages/walletkit/src/utils/transactionPreview.ts index 0c19a680c..5d11baf0a 100644 --- a/packages/walletkit/src/utils/transactionPreview.ts +++ b/packages/walletkit/src/utils/transactionPreview.ts @@ -28,16 +28,16 @@ const SIGN_MODE_EMULATION_VALUE = 2_000_000_000n; export async function createTransactionPreview( client: ApiClient, request: TransactionRequest, - wallet?: Wallet, + wallet: Wallet, options: TransactionPreviewOptions = {}, ): Promise { const mode: TransactionPreviewMode = options.mode ?? 'send'; const isSignMode = mode === 'sign'; - // const txData = await wallet?.getSignedSendTransaction(request, { fakeSignature: true, internal: isSignMode }); - const signedBoc = await wallet?.getSignedSendTransaction(request, { + const getSignedTransaction = isSignMode ? wallet?.getSignedSignMessage : wallet?.getSignedSendTransaction; + + const signedBoc = await getSignedTransaction.call(wallet, request, { fakeSignature: true, - internal: isSignMode, }); if (!signedBoc) { @@ -98,7 +98,7 @@ export async function createTransactionPreviewIfPossible( config: TonWalletKitOptions, client: ApiClient, request: TransactionRequest, - wallet?: Wallet, + wallet: Wallet, options: TransactionPreviewOptions = {}, ): Promise { if (config.eventProcessor?.disableTransactionEmulation) { diff --git a/packages/walletkit/vitest.config.ts b/packages/walletkit/vitest.config.ts index a8573da31..6f0ed258a 100644 --- a/packages/walletkit/vitest.config.ts +++ b/packages/walletkit/vitest.config.ts @@ -21,14 +21,14 @@ const config: ViteUserConfig = defineConfig({ include: ['src/**/*.spec.ts', 'src/**/*.test.ts'], exclude: ['node_modules', 'dist', 'build', 'coverage', '.stryker-tmp', '**/*.config.ts', '**/*.config.js'], // WebStorm compatibility - reporter: process.env.JETBRAINS_IDE ? ['verbose'] : ['default'], + // reporter: process.env.JETBRAINS_IDE ? ['verbose'] : ['default'], // Disable WebStorm-specific reporter onConsoleLog: () => false, coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], - all: true, - perFile: true, + // all: true, + // perFile: true, reportOnFailure: true, thresholds: { statements: target.coverage,