Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 37 additions & 0 deletions packages/walletkit/GLOBALOPTIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# KitGlobalOptions

`KitGlobalOptions` is a static class for global WalletKit configuration that affects all instances. Currently provides time synchronization functionality, with more global settings planned for future releases.

## Time Provider

Configure how WalletKit obtains current time for transaction validation. Useful for avoiding clock skew issues and ensuring accurate `validUntil` validation.

### API

```typescript
class KitGlobalOptions {
static setGetCurrentTime(fn: () => Promise<number> | number): void;
static getCurrentTime(): Promise<number>;
}
```

### Usage

```typescript
import { KitGlobalOptions } from '@ton/walletkit';

// Set custom time provider (optional, before creating TonWalletKit)
KitGlobalOptions.setGetCurrentTime(async () => {
const response = await fetch('https://your-api.com/time');
const { timestamp } = await response.json();
return timestamp; // Unix timestamp in seconds
});
```

**Default behavior**: Uses `Math.floor(Date.now() / 1000)` if not configured.

## Notes

- **Global scope**: Affects all `TonWalletKit` instances
- **Time format**: Unix timestamp in seconds (not milliseconds)
- **Set once**: Configure at app initialization
20 changes: 20 additions & 0 deletions packages/walletkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ A production-ready wallet-side integration layer for TON Connect, designed for b

- **[Browser Extension Build](/apps/demo-wallet/EXTENSION.md)** - How to build and load the demo wallet as a Chrome extension
- **[JS Bridge Usage](/packages/walletkit/examples/js-bridge-usage.md)** - Implementing TonConnect JS Bridge for browser extension wallets
- **[KitGlobalOptions](/packages/walletkit/GLOBALOPTIONS.md)** - Configure global time provider for accurate transaction validation
- **[iOS WalletKit](https://github.com/ton-connect/kit-ios)** - Swift Package providing TON wallet capabilities for iOS and macOS
- **[Android WalletKit](https://github.com/ton-connect/kit-android)** - Kotlin/Java Package providing TON wallet capabilities for Android

Expand Down Expand Up @@ -315,6 +316,25 @@ const info = kit.jettons.getJettonInfo(jettonAddress);
// info?.name, info?.symbol, info?.image
```

## Advanced Configuration

### Custom Time Provider

For production wallets, it's recommended to use server-synchronized time instead of device time to avoid issues with clock skew and timezone differences:

```ts
import { KitGlobalOptions } from '@ton/walletkit';

// Set custom time provider before creating TonWalletKit
KitGlobalOptions.setGetCurrentTime(async () => {
const response = await fetch('https://your-api.com/time');
const { timestamp } = await response.json();
return timestamp; // Unix timestamp in seconds
});
```

This ensures accurate `validUntil` validation for transactions. See [KitGlobalOptions documentation](/packages/walletkit/GLOBALOPTIONS.md) for detailed usage and best practices.

## Sending assets programmatically

You can create transactions from your wallet app (not from dApps) and feed them into the regular approval flow via `handleNewTransaction`. This triggers your `onTransactionRequest` callback, allowing the same UI confirmation flow for both dApp and wallet-initiated transactions.
Expand Down
1 change: 1 addition & 0 deletions packages/walletkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"lint": "eslint . --max-warnings 0",
"lint:fix": "eslint . --max-warnings 0 --fix",
"clean": "git clean -xdf dist node_modules .turbo",
"typecheck": "tsc --noEmit",
"generate-openapi-spec": "src/api/scripts/generate-openapi-spec.sh"
},
"keywords": [
Expand Down
12 changes: 9 additions & 3 deletions packages/walletkit/src/contracts/v4r2/WalletV4R2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +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 { KitGlobalOptions } from '../../core/KitGlobalOptions';
import type { ApiClient } from '../../types/toncenter/ApiClient';
import { ParseStack } from '../../utils/tvmStack';
import { asAddressFriendly } from '../../utils';
Expand Down Expand Up @@ -142,8 +143,13 @@ export class WalletV4R2 implements Contract {
/**
* Create transfer message body
*/
createTransfer(args: { seqno: number; sendMode: SendMode; messages: MessageRelaxed[]; timeout?: number }): Cell {
const timeout = args.timeout ?? Math.floor(Date.now() / 1000) + 60;
async createTransfer(args: {
seqno: number;
sendMode: SendMode;
messages: MessageRelaxed[];
timeout?: number;
}): Promise<Cell> {
const timeout = args.timeout ?? (await KitGlobalOptions.getCurrentTime()) + 60;

let body = beginCell()
.storeUint(this.subwalletId, 32)
Expand Down Expand Up @@ -173,7 +179,7 @@ export class WalletV4R2 implements Contract {
timeout?: number;
},
): Promise<void> {
const transfer = this.createTransfer(args);
const transfer = await this.createTransfer(args);
await provider.internal(via, {
sendMode: SendMode.PAY_GAS_SEPARATELY,
body: transfer,
Expand Down
8 changes: 2 additions & 6 deletions packages/walletkit/src/contracts/v4r2/WalletV4R2Adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,6 @@ export class WalletV4R2Adapter implements WalletAdapter {
//
}

const timeout = input.validUntil
? Math.min(input.validUntil, Math.floor(Date.now() / 1000) + 600)
: Math.floor(Date.now() / 1000) + 60;

try {
const messages: MessageRelaxed[] = input.messages.map((m) => {
let bounce = true;
Expand All @@ -176,11 +172,11 @@ export class WalletV4R2Adapter implements WalletAdapter {
init: m.stateInit ? loadStateInit(Cell.fromBase64(m.stateInit).asSlice()) : undefined,
});
});
const data = this.walletContract.createTransfer({
const data = await this.walletContract.createTransfer({
seqno: seqno,
sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS,
messages,
timeout: timeout,
timeout: input.validUntil,
});

const signature = await this.sign(Uint8Array.from(data.hash()));
Expand Down
8 changes: 5 additions & 3 deletions packages/walletkit/src/contracts/w5/WalletV5R1Adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import type {
Hex,
Base64String,
} from '../../api/models';
import { KitGlobalOptions } from '../../core/KitGlobalOptions';
import type { Feature } from '../../types/jsBridge';

const log = globalLogger.createChild('WalletV5R1Adapter');
Expand Down Expand Up @@ -205,9 +206,10 @@ export class WalletV5R1Adapter implements WalletAdapter {
...options,
validUntil: undefined,
};
// add valid untill

// add validUntil
if (input.validUntil) {
const now = Math.floor(Date.now() / 1000);
const now = await KitGlobalOptions.getCurrentTime();
const maxValidUntil = now + 600;
if (input.validUntil < now) {
throw new WalletKitError(
Expand Down Expand Up @@ -315,7 +317,7 @@ export class WalletV5R1Adapter implements WalletAdapter {
auth_signed: 0x7369676e,
};

const expireAt = options.validUntil ?? Math.floor(Date.now() / 1000) + 300;
const expireAt = options.validUntil ?? (await KitGlobalOptions.getCurrentTime()) + 300;
const payload = beginCell()
.storeUint(Opcodes.auth_signed, 32)
.storeUint(walletId, 32)
Expand Down
3 changes: 2 additions & 1 deletion packages/walletkit/src/core/BridgeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type { Analytics, AnalyticsManager } from '../analytics';
import type { TonWalletKitOptions } from '../types/config';
import { TONCONNECT_BRIDGE_RESPONSE } from '../bridge/JSBridgeInjector';
import type { BridgeEvent } from '../api/models';
import { KitGlobalOptions } from '../core/KitGlobalOptions';

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

Expand Down Expand Up @@ -578,7 +579,7 @@ export class BridgeManager {
method: event.method || 'unknown',
params: event.params || event,
// sessionId: event.from,
timestamp: Date.now(),
timestamp: await KitGlobalOptions.getCurrentTime(),
from: event?.from,
domain: event?.domain,
isJsBridge: event?.isJsBridge,
Expand Down
23 changes: 23 additions & 0 deletions packages/walletkit/src/core/KitGlobalOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* 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 { getUnixtime } from '../utils/time';

type GetCurrentTimeFunc = () => Promise<number> | number;

export class KitGlobalOptions {
private static getCurrentTimeImpl: GetCurrentTimeFunc = getUnixtime;

static setGetCurrentTime(fn: GetCurrentTimeFunc): void {
KitGlobalOptions.getCurrentTimeImpl = fn;
}

static async getCurrentTime(): Promise<number> {
return KitGlobalOptions.getCurrentTimeImpl();
}
}
20 changes: 10 additions & 10 deletions packages/walletkit/src/core/RequestProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ import {
} from '@tonconnect/protocol';
import { getSecureRandomBytes } from '@ton/crypto';

import { KitGlobalOptions } from '../core/KitGlobalOptions';
import type { EventSignDataApproval, TonWalletKitOptions } from '../types';
import { isBefore } from '../utils/time';
import type { SessionManager } from './SessionManager';
import type { BridgeManager } from './BridgeManager';
import { globalLogger } from './Logger';
Expand Down Expand Up @@ -777,6 +779,7 @@ export class RequestProcessor {
{ eventId: event.id },
);
}

const wallet = this.getWalletFromEvent(event);
if (!wallet) {
throw new WalletKitError(
Expand All @@ -788,16 +791,13 @@ export class RequestProcessor {
}

const validUntil = event.request.validUntil;
if (validUntil) {
const now = Math.floor(Date.now() / 1000);
if (validUntil < now) {
throw new WalletKitError(
ERROR_CODES.VALIDATION_ERROR,
'Transaction valid_until timestamp is in the past',
undefined,
{ validUntil, currentTime: now },
);
}
if (validUntil && (await isBefore(validUntil))) {
throw new WalletKitError(
ERROR_CODES.VALIDATION_ERROR,
'Transaction expired: valid_until timestamp is in the past',
undefined,
{ validUntil, currentTime: await KitGlobalOptions.getCurrentTime() },
);
}

return await signTransactionInternal(wallet, event.request);
Expand Down
Loading
Loading