Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
196fb2f
chore(repo): sync ignore rules and lockfiles for global dependencies
ArkadiyStena Mar 6, 2026
b191275
feat(agentic-wallets-dashboard): add a new dashboard application
ArkadiyStena Mar 6, 2026
d7af09e
feat(apps): update configs and agentic wallet integration in applicat…
ArkadiyStena Mar 6, 2026
b9632da
fix(agentic-wallets-dashboard): refine UI components and styles
ArkadiyStena Mar 6, 2026
13141bc
docs(agentic-wallets-dashboard): add README and update agent creation…
ArkadiyStena Mar 6, 2026
f669738
feat(agentic-wallets-dashboard): update config and hooks for create-a…
ArkadiyStena Mar 6, 2026
b11eb5b
feat(agentic-dashboard): improve card balance handling and agent oper…
ArkadiyStena Mar 6, 2026
67fda28
feat(mcp,cli): add by-address account tools and jetton resolvers
ArkadiyStena Mar 6, 2026
f9d39cf
fix(agentic-wallets-dashboard): update mobile ui
ArkadiyStena Mar 6, 2026
783bd24
mcp: accept 64-byte private keys in CLI
ArkadiyStena Mar 9, 2026
724080e
cli: import and adapt TON CLI package to current repo
ArkadiyStena Mar 9, 2026
85f2812
feat(walletkit): add collectionAddress filter to UserNFTsRequest
ArkadiyStena Apr 23, 2026
8d2c48e
chore: fix lint errors
ArkadiyStena Apr 23, 2026
23443d5
fix(walletkit): enforce 32-byte hash length in toHex
ArkadiyStena Apr 23, 2026
f025796
Preserve dashboard walletkit behavior in source
ArkadiyStena May 5, 2026
86a0957
Integrate upstream main into dashboard walletkit branch
ArkadiyStena May 5, 2026
2b48764
fix(walletkit): update bulk clients
ArkadiyStena May 5, 2026
7be54cf
fix ci
ArkadiyStena May 5, 2026
0ff83f1
remove dashboard example
ArkadiyStena May 5, 2026
6153a0d
remove cli package
ArkadiyStena May 5, 2026
3d06120
revert unintentional changes
ArkadiyStena May 5, 2026
af9cb75
fix ci
ArkadiyStena May 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import type {
GetEventsResponse,
MasterchainInfo,
Network,
BulkAccountState,
} from '@ton/walletkit';

import { error } from '../utils/logger';
Expand Down Expand Up @@ -159,6 +160,10 @@ export class AndroidAPIClientAdapter implements ApiClient {
}
}

async getBulkAccounts(_addresses: string[]): Promise<BulkAccountState[]> {
throw new Error('getBulkAccounts is not implemented yet');
}

async getAccountTransactions(_request: TransactionsByAddressRequest): Promise<TransactionsResponse> {
throw new Error('getAccountTransactions is not implemented yet');
}
Expand Down
5 changes: 5 additions & 0 deletions packages/walletkit-ios-bridge/src/SwiftAPIClientAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
GetEventsRequest,
GetEventsResponse,
MasterchainInfo,
BulkAccountState,
} from '@ton/walletkit';

export class SwiftAPIClientAdapter implements ApiClient {
Expand Down Expand Up @@ -74,6 +75,10 @@ export class SwiftAPIClientAdapter implements ApiClient {
throw new Error('getBalance is not implemented yet');
}

async getBulkAccounts(addresses: string[]): Promise<BulkAccountState[]> {
return this.swiftApiClient.getBulkAccounts(addresses);
}

async getAccountTransactions(_request: TransactionsByAddressRequest): Promise<TransactionsResponse> {
throw new Error('getAccountTransactions is not implemented yet');
}
Expand Down
4 changes: 4 additions & 0 deletions packages/walletkit/src/api/models/nfts/UserNFTsRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export interface UserNFTsRequest {
* Owner address of the NFTs
*/
ownerAddress: UserFriendlyAddress;
/**
* Collection address to filter NFTs by
*/
collectionAddress?: UserFriendlyAddress;
/**
* Pagination information
*/
Expand Down
34 changes: 34 additions & 0 deletions packages/walletkit/src/clients/tonapi/ApiClientTonApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const HEX_HASH = `0x${'11'.repeat(32)}`;
type ClientWithGetJson = ApiClientTonApi & {
getJson: (url: string, query?: Record<string, unknown>) => Promise<unknown>;
};
type ClientWithPostJson = ApiClientTonApi & {
postJson: (url: string, body: unknown) => Promise<unknown>;
};

function makeTransaction(overrides: Record<string, unknown> = {}): Record<string, unknown> {
return {
Expand Down Expand Up @@ -156,6 +159,37 @@ describe('ApiClientTonApi', () => {
expect(response.transactions[0]?.hash).toMatch(/^0x[0-9a-f]+$/);
});

it('fetches TonAPI bulk accounts', async () => {
const client = new ApiClientTonApi();
const postJsonSpy = vi.spyOn(client as ClientWithPostJson, 'postJson').mockResolvedValue({
accounts: [
{
address: TEST_ADDRESS,
balance: 1,
code: 'ff',
data: '00',
last_transaction_lt: 123,
last_transaction_hash: '11'.repeat(32),
frozen_hash: null,
status: 'active',
},
],
});

const result = await client.getBulkAccounts([TEST_ADDRESS]);

expect(postJsonSpy).toHaveBeenCalledWith('/v2/blockchain/accounts/_bulk', { account_ids: [TEST_ADDRESS] });
expect(result).toHaveLength(1);
expect(result[0]?.address).toBe(TEST_ADDRESS);
expect(result[0]?.balance).toBe('1');
expect(result[0]?.code).toBe('/w==');
expect(result[0]?.data).toBe('AA==');
expect(result[0]?.lastTransaction).toEqual({
lt: '123',
hash: `0x${'11'.repeat(32)}`,
});
});

it('resolves bodyHash via /transactions first to avoid message 404 noise', async () => {
const client = new ApiClientTonApi();
const getJsonSpy = vi.spyOn(client as ClientWithGetJson, 'getJson').mockImplementation(async (url: string) => {
Expand Down
55 changes: 54 additions & 1 deletion packages/walletkit/src/clients/tonapi/ApiClientTonApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Address } from '@ton/core';

import type {
ApiClient,
BulkAccountState,
GetEventsRequest,
GetEventsResponse,
GetJettonsByAddressRequest,
Expand Down Expand Up @@ -40,6 +41,7 @@ import type { ToncenterResponseJettonMasters, ToncenterTracesResponse } from '..
import { BaseApiClient } from '../BaseApiClient';
import type { BaseApiClientConfig } from '../BaseApiClient';
import { TonClientError } from '../TonClientError';
import { globalLogger } from '../../core/Logger';
import type { TonApiBlockchainAccount } from './types/accounts';
import { asAddressFriendly } from '../../utils/address';
import { mapAccountState } from './mappers/map-account-state';
Expand All @@ -61,6 +63,8 @@ import { mapTonApiTrace, mapTonApiTraceTransaction } from './mappers/map-traces'
import { mapTonApiEvent } from './mappers/map-events';
import { mapMasterchainInfo } from './mappers/map-masterchain-info';

const log = globalLogger.createChild('ApiClientTonApi');

/**
* @experimental
* This client implementation is experimental and currently has inconsistencies
Expand Down Expand Up @@ -113,6 +117,17 @@ export class ApiClientTonApi extends BaseApiClient implements ApiClient {
return state.balance;
}

async getBulkAccounts(addresses: string[]): Promise<BulkAccountState[]> {
const raw = await this.postJson<{ accounts: TonApiBlockchainAccount[] }>('/v2/blockchain/accounts/_bulk', {
account_ids: addresses,
});

return raw.accounts.map((account) => ({
address: account.address,
...mapAccountState(account),
}));
}

async jettonsByAddress(request: GetJettonsByAddressRequest): Promise<ToncenterResponseJettonMasters> {
const raw = await this.getJson<TonApiJettonInfo>(`/v2/jettons/${request.address}`);

Expand Down Expand Up @@ -145,14 +160,52 @@ export class ApiClientTonApi extends BaseApiClient implements ApiClient {

async nftItemsByOwner(request: UserNFTsRequest): Promise<NFTsResponse> {
const query: Record<string, unknown> = {};
if (request.collectionAddress) query.collection = request.collectionAddress;
if (request.pagination?.limit) query.limit = request.pagination.limit;
if (request.pagination?.offset) query.offset = request.pagination.offset;

const raw = await this.getJson<TonApiNftItems>(
`/v2/accounts/${this.normalizeAddress(request.ownerAddress)}/nfts`,
query,
);
return mapNftItemsResponse(raw.nft_items);
const result = mapNftItemsResponse(raw.nft_items);
await this.enrichAddressBookWithInterfaces(result);

return result;
}

private async enrichAddressBookWithInterfaces(result: NFTsResponse): Promise<void> {
if (result.nfts.length === 0) {
return;
}

const addresses = result.nfts.map((nft) => nft.address);
const addressBook = (result.addressBook ??= {});
const chunkSize = 100;

try {
for (let i = 0; i < addresses.length; i += chunkSize) {
const chunk = addresses.slice(i, i + chunkSize);
const bulkRaw = await this.postJson<{
accounts?: Array<TonApiBlockchainAccount & { interfaces?: string[] }>;
}>('/v2/blockchain/accounts/_bulk', { account_ids: chunk });

for (const account of bulkRaw.accounts ?? []) {
const address = asAddressFriendly(account.address);
if (addressBook[address]) {
addressBook[address].interfaces = account.interfaces ?? [];
} else {
addressBook[address] = {
address,
domain: undefined,
interfaces: account.interfaces ?? [],
};
}
}
}
} catch (error) {
log.warn('Failed to enrich NFT address book with interfaces', { error });
}
}

async sendBoc(boc: Base64String): Promise<string> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,9 @@ export function mapAccountState(raw: TonApiBlockchainAccount): FullAccountState
lastTransaction,
};

if (raw.frozen_hash) {
out.frozenHash = (raw.frozen_hash.startsWith('0x') ? raw.frozen_hash : `0x${raw.frozen_hash}`) as Hex;
}

return out;
}
104 changes: 81 additions & 23 deletions packages/walletkit/src/clients/toncenter/ApiClientToncenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
*
*/

import type { ExtraCurrency } from '@ton/core';
import type { AccountStatus, ExtraCurrency } from '@ton/core';
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 {
ApiClient,
BulkAccountState,
GetJettonsByOwnerRequest,
GetJettonsByAddressRequest,
GetPendingTraceRequest,
Expand Down Expand Up @@ -152,34 +153,25 @@ export class ApiClientToncenter extends BaseApiClient implements ApiClient {
const query: Record<string, unknown> = { include_boc: true, address: [address] };
if (typeof seqno === 'number') query.seqno = seqno.toString();
const raw = await this.getJson<V2AddressInformation>('/api/v3/addressInformation', query);
const balance = BigInt(raw.balance);
const extraCurrencies: ExtraCurrency = {};
for (const currency of raw.extra_currencies || []) {
extraCurrencies[currency.id] = BigInt(currency.amount);
}
// const code = Base64ToUint8Array(raw.code);
// const data = Base64ToUint8Array(raw.data);
const out: FullAccountState = {
status: raw.status,
balance: balance.toString(),
extraCurrencies,
code: raw.code,
data: raw.data,
lastTransaction: parseInternalTransactionId({
hash: raw.last_transaction_hash,
lt: raw.last_transaction_lt,
}),
};
if (raw.frozen_hash) {
out.frozenHash = Base64ToHex(raw.frozen_hash) ?? undefined;
}
return out;
return toFullAccountState(raw);
}

async getBalance(address: UserFriendlyAddress, seqno?: number): Promise<TokenAmount> {
return (await this.getAccountState(address, seqno)).balance;
}

async getBulkAccounts(addresses: string[]): Promise<BulkAccountState[]> {
const raw = await this.getJson<ToncenterAccountStatesResponse>('/api/v3/accountStates', {
address: addresses,
include_boc: true,
});

return (raw.accounts ?? []).map((account) => ({
address: account.address,
...toFullAccountState(account),
}));
}

async getAccountTransactions(request: TransactionsByAddressRequest): Promise<TransactionsResponse> {
const accounts = request.address?.map(prepareAddress);
let offset = request.offset ?? 0;
Expand Down Expand Up @@ -458,3 +450,69 @@ export class ApiClientToncenter extends BaseApiClient implements ApiClient {
};
}
}

interface ToncenterAccountStateResponse extends RawToncenterAccountState {
address: string;
}

interface ToncenterAccountStatesResponse {
accounts?: ToncenterAccountStateResponse[];
}

interface RawToncenterAccountState {
balance?: string | number;
code?: string | null;
data?: string | null;
frozen_hash?: string | null;
last_transaction_hash?: string | null;
last_transaction_lt?: string | number;
status?: string;
extra_currencies?: Array<{
id: number;
amount: string;
}>;
}

function toFullAccountState(raw: RawToncenterAccountState): FullAccountState {
const extraCurrencies: ExtraCurrency = {};
for (const currency of raw.extra_currencies ?? []) {
extraCurrencies[currency.id] = BigInt(currency.amount);
}

const out: FullAccountState = {
status: normalizeAccountStatus(raw.status),
balance: BigInt(raw.balance ?? '0').toString(),
extraCurrencies,
code: raw.code ?? null,
data: raw.data ?? null,
lastTransaction:
raw.last_transaction_hash && raw.last_transaction_lt
? parseInternalTransactionId({
hash: raw.last_transaction_hash,
lt: String(raw.last_transaction_lt),
})
: null,
};

if (raw.frozen_hash) {
out.frozenHash = Base64ToHex(raw.frozen_hash) ?? undefined;
}

return out;
}

function normalizeAccountStatus(status?: string): AccountStatus {
switch (status) {
case 'active':
case 'frozen':
case 'uninitialized':
case 'non-existing':
return status;
case 'uninit':
return 'uninitialized';
case 'nonexist':
return 'non-existing';
default:
return 'non-existing';
}
}
1 change: 1 addition & 0 deletions packages/walletkit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export type {
GetJettonsByAddressRequest,
GetEventsRequest,
GetEventsResponse,
BulkAccountState,
} from './types/toncenter/ApiClient';
export type { FullAccountState } from './types/toncenter/api';
export type { ToncenterEmulationResult } from './utils/toncenterEmulation';
Expand Down
1 change: 1 addition & 0 deletions packages/walletkit/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export type {
GetJettonsByAddressRequest,
GetEventsRequest,
GetEventsResponse,
BulkAccountState,
} from './toncenter/ApiClient';

export type { NftItem } from './toncenter/NftItem';
Expand Down
5 changes: 5 additions & 0 deletions packages/walletkit/src/types/toncenter/ApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ export interface GetEventsResponse {
hasNext: boolean;
}

export interface BulkAccountState extends FullAccountState {
address: string;
}

export interface ApiClient {
nftItemsByAddress(request: NFTsRequest): Promise<NFTsResponse>;
nftItemsByOwner(request: UserNFTsRequest): Promise<NFTsResponse>;
Expand All @@ -107,6 +111,7 @@ export interface ApiClient {
): Promise<GetMethodResult>; // TODO - Make serializable
getAccountState(address: UserFriendlyAddress, seqno?: number): Promise<FullAccountState>;
getBalance(address: UserFriendlyAddress, seqno?: number): Promise<TokenAmount>;
getBulkAccounts(addresses: string[]): Promise<BulkAccountState[]>;

getAccountTransactions(request: TransactionsByAddressRequest): Promise<TransactionsResponse>;
getTransactionsByHash(request: GetTransactionByHashRequest): Promise<TransactionsResponse>;
Expand Down
Loading