@meshconnect/uwc-ton-connector 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/namespaced-storage.d.ts +22 -0
- package/dist/namespaced-storage.d.ts.map +1 -0
- package/dist/namespaced-storage.js +52 -0
- package/dist/namespaced-storage.js.map +1 -0
- package/dist/ton-transaction-utils.d.ts +81 -0
- package/dist/ton-transaction-utils.d.ts.map +1 -0
- package/dist/ton-transaction-utils.js +120 -0
- package/dist/ton-transaction-utils.js.map +1 -0
- package/dist/validation.d.ts +2 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +5 -0
- package/dist/validation.js.map +1 -0
- package/package.json +36 -0
- package/src/index.ts +10 -0
- package/src/namespaced-storage.test.ts +135 -0
- package/src/namespaced-storage.ts +57 -0
- package/src/ton-transaction-utils.test.ts +300 -0
- package/src/ton-transaction-utils.ts +185 -0
- package/src/validation.test.ts +51 -0
- package/src/validation.ts +4 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { NamespacedStorage, type TonConnectStorage } from './namespaced-storage';
|
|
2
|
+
export { isValidBase64 } from './validation';
|
|
3
|
+
export { buildTonTransactionRequest, buildSignDataPayload, executeSignData, unwrapBoc, bocToHash, toFriendlyAddress } from './ton-transaction-utils';
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAChF,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,EACL,0BAA0B,EAC1B,oBAAoB,EACpB,eAAe,EACf,SAAS,EACT,SAAS,EACT,iBAAiB,EAClB,MAAM,yBAAyB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { NamespacedStorage } from './namespaced-storage';
|
|
2
|
+
export { isValidBase64 } from './validation';
|
|
3
|
+
export { buildTonTransactionRequest, buildSignDataPayload, executeSignData, unwrapBoc, bocToHash, toFriendlyAddress } from './ton-transaction-utils';
|
|
4
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAA0B,MAAM,sBAAsB,CAAA;AAChF,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,EACL,0BAA0B,EAC1B,oBAAoB,EACpB,eAAe,EACf,SAAS,EACT,SAAS,EACT,iBAAiB,EAClB,MAAM,yBAAyB,CAAA"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Mirrors @tonconnect/sdk IStorage — avoids leaking SDK types in published .d.ts. */
|
|
2
|
+
export interface TonConnectStorage {
|
|
3
|
+
setItem(key: string, value: string): Promise<void>;
|
|
4
|
+
getItem(key: string): Promise<string | null>;
|
|
5
|
+
removeItem(key: string): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Prefixed localStorage wrapper required by @tonconnect/sdk.
|
|
9
|
+
* The SDK requires the dApp to provide storage for persisting connection state across reloads.
|
|
10
|
+
* Keys are prefixed per wallet (e.g. "tonkeeper:session") to avoid collisions.
|
|
11
|
+
* Falls back to in-memory Map when localStorage is blocked (Safari ITP in iframes).
|
|
12
|
+
*/
|
|
13
|
+
export declare class NamespacedStorage implements TonConnectStorage {
|
|
14
|
+
private prefix;
|
|
15
|
+
private memoryFallback;
|
|
16
|
+
constructor(prefix: string);
|
|
17
|
+
setItem(key: string, value: string): Promise<void>;
|
|
18
|
+
getItem(key: string): Promise<string | null>;
|
|
19
|
+
removeItem(key: string): Promise<void>;
|
|
20
|
+
private prefixed;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=namespaced-storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"namespaced-storage.d.ts","sourceRoot":"","sources":["../src/namespaced-storage.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClD,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IAC5C,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACvC;AAED;;;;;GAKG;AACH,qBAAa,iBAAkB,YAAW,iBAAiB;IAG7C,OAAO,CAAC,MAAM;IAF1B,OAAO,CAAC,cAAc,CAA4B;gBAE9B,MAAM,EAAE,MAAM;IAE5B,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYlD,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAW5C,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY5C,OAAO,CAAC,QAAQ;CAGjB"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prefixed localStorage wrapper required by @tonconnect/sdk.
|
|
3
|
+
* The SDK requires the dApp to provide storage for persisting connection state across reloads.
|
|
4
|
+
* Keys are prefixed per wallet (e.g. "tonkeeper:session") to avoid collisions.
|
|
5
|
+
* Falls back to in-memory Map when localStorage is blocked (Safari ITP in iframes).
|
|
6
|
+
*/
|
|
7
|
+
export class NamespacedStorage {
|
|
8
|
+
prefix;
|
|
9
|
+
memoryFallback = new Map();
|
|
10
|
+
constructor(prefix) {
|
|
11
|
+
this.prefix = prefix;
|
|
12
|
+
}
|
|
13
|
+
async setItem(key, value) {
|
|
14
|
+
if (typeof localStorage === 'undefined') {
|
|
15
|
+
this.memoryFallback.set(this.prefixed(key), value);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
localStorage.setItem(this.prefixed(key), value);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
this.memoryFallback.set(this.prefixed(key), value);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async getItem(key) {
|
|
26
|
+
if (typeof localStorage === 'undefined') {
|
|
27
|
+
return this.memoryFallback.get(this.prefixed(key)) ?? null;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
return localStorage.getItem(this.prefixed(key));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return this.memoryFallback.get(this.prefixed(key)) ?? null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async removeItem(key) {
|
|
37
|
+
if (typeof localStorage === 'undefined') {
|
|
38
|
+
this.memoryFallback.delete(this.prefixed(key));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
localStorage.removeItem(this.prefixed(key));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
this.memoryFallback.delete(this.prefixed(key));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
prefixed(key) {
|
|
49
|
+
return `${this.prefix}:${key}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=namespaced-storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"namespaced-storage.js","sourceRoot":"","sources":["../src/namespaced-storage.ts"],"names":[],"mappings":"AAOA;;;;;GAKG;AACH,MAAM,OAAO,iBAAiB;IAGR;IAFZ,cAAc,GAAG,IAAI,GAAG,EAAkB,CAAA;IAElD,YAAoB,MAAc;QAAd,WAAM,GAAN,MAAM,CAAQ;IAAG,CAAC;IAEtC,KAAK,CAAC,OAAO,CAAC,GAAW,EAAE,KAAa;QACtC,IAAI,OAAO,YAAY,KAAK,WAAW,EAAE,CAAC;YACxC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAA;YAClD,OAAM;QACR,CAAC;QACD,IAAI,CAAC;YACH,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAA;QACjD,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAA;QACpD,CAAC;IACH,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,GAAW;QACvB,IAAI,OAAO,YAAY,KAAK,WAAW,EAAE,CAAC;YACxC,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI,CAAA;QAC5D,CAAC;QACD,IAAI,CAAC;YACH,OAAO,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAA;QACjD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI,CAAA;QAC5D,CAAC;IACH,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,GAAW;QAC1B,IAAI,OAAO,YAAY,KAAK,WAAW,EAAE,CAAC;YACxC,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAA;YAC9C,OAAM;QACR,CAAC;QACD,IAAI,CAAC;YACH,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAA;QAC7C,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAA;QAChD,CAAC;IACH,CAAC;IAEO,QAAQ,CAAC,GAAW;QAC1B,OAAO,GAAG,IAAI,CAAC,MAAM,IAAI,GAAG,EAAE,CAAA;IAChC,CAAC;CACF"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { TonNativeTransferRequest, TonSignDataPayload, SignatureType } from '@meshconnect/uwc-types';
|
|
2
|
+
/** Mirrors @tonconnect/sdk SendTransactionRequest. */
|
|
3
|
+
interface TonConnectTransactionRequest {
|
|
4
|
+
validUntil: number;
|
|
5
|
+
network?: string;
|
|
6
|
+
from?: string;
|
|
7
|
+
messages: {
|
|
8
|
+
address: string;
|
|
9
|
+
amount: string;
|
|
10
|
+
stateInit?: string;
|
|
11
|
+
payload?: string;
|
|
12
|
+
}[];
|
|
13
|
+
}
|
|
14
|
+
/** Mirrors @tonconnect/sdk SignDataResponse. Not all wallets support SignData (e.g. OKX). */
|
|
15
|
+
interface SignDataResult {
|
|
16
|
+
signature: string;
|
|
17
|
+
address: string;
|
|
18
|
+
timestamp: number;
|
|
19
|
+
domain: string;
|
|
20
|
+
payload: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Convert raw TON address to user-friendly format.
|
|
24
|
+
* TON has two formats:
|
|
25
|
+
* Raw: "0:6e7ee4a1b2c3d4..." (workchain:hex, used by the SDK)
|
|
26
|
+
* User-friendly: "UQBx7kobs..." (base64url, used in UIs and explorers)
|
|
27
|
+
*/
|
|
28
|
+
export declare function toFriendlyAddress(rawAddress: string, converter: (rawWorkchainAddress: string) => string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Build a single TON message from a transfer request.
|
|
31
|
+
* Example: { address: "UQCy9m...", amount: "500000000", payload: "te6cckEBAQ..." }
|
|
32
|
+
*
|
|
33
|
+
* - `amount` is in nanotons as a decimal string (1 TON = 1_000_000_000)
|
|
34
|
+
* - `payload` is a base64-encoded BOC (Bag of Cells) — for Jetton transfers, on-chain comments, etc.
|
|
35
|
+
* - `stateInit` is a base64 BOC that deploys a smart contract alongside the transfer
|
|
36
|
+
*/
|
|
37
|
+
export declare function buildTonTransactionMessage(request: TonNativeTransferRequest): {
|
|
38
|
+
stateInit?: string;
|
|
39
|
+
payload?: string;
|
|
40
|
+
address: string;
|
|
41
|
+
amount: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Transform a UWC transfer request into @tonconnect/sdk's SendTransactionRequest.
|
|
45
|
+
*
|
|
46
|
+
* @param senderRawAddress - Raw 0:hex address from sdk.account.address.
|
|
47
|
+
* The SDK expects `from` in raw format; the UI's user-friendly address (UQ...) is a fallback only.
|
|
48
|
+
*
|
|
49
|
+
* `validUntil` defaults to now + 5 min (TX_EXPIRY_SECONDS). TON transactions require an expiry.
|
|
50
|
+
* `sendMode` — bit flags for message handling (e.g. 128 = send entire balance).
|
|
51
|
+
* See: https://docs.ton.org/v3/documentation/smart-contracts/message-management/message-modes-cookbook
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildTonTransactionRequest(request: TonNativeTransferRequest, senderRawAddress?: string): TonConnectTransactionRequest & {
|
|
54
|
+
sendMode?: number;
|
|
55
|
+
};
|
|
56
|
+
export declare function buildSignDataPayload(message: string, override?: TonSignDataPayload): TonSignDataPayload;
|
|
57
|
+
/**
|
|
58
|
+
* Execute signData on the SDK, normalizing errors for wallets that don't support it.
|
|
59
|
+
* OKX and some other wallets do not implement SignData — this gives a clear error message.
|
|
60
|
+
*/
|
|
61
|
+
export declare function executeSignData(sdk: {
|
|
62
|
+
signData?: (payload: TonSignDataPayload) => Promise<SignDataResult>;
|
|
63
|
+
}, payload: TonSignDataPayload): Promise<SignatureType>;
|
|
64
|
+
/**
|
|
65
|
+
* Extract BOC (Bag of Cells) string from wallet response.
|
|
66
|
+
* TON wallets return the serialized signed transaction as base64, not a tx hash.
|
|
67
|
+
* Some wallets return a plain string, others wrap it in { boc: "..." } — handle both.
|
|
68
|
+
*/
|
|
69
|
+
export declare function unwrapBoc(result: string | {
|
|
70
|
+
boc: string;
|
|
71
|
+
} | null | undefined): string;
|
|
72
|
+
/**
|
|
73
|
+
* Convert a base64 BOC to a hex transaction hash for use in explorers.
|
|
74
|
+
*
|
|
75
|
+
* @ton/core (268 KB JS, CJS — no tree-shaking) is lazy-loaded via dynamic import.
|
|
76
|
+
* Impact: 0 KB for non-TON users, ~22ms cold load + <1ms hash on first TON transaction.
|
|
77
|
+
* Subsequent calls: <0.1ms (module cached by runtime).
|
|
78
|
+
*/
|
|
79
|
+
export declare function bocToHash(boc: string): Promise<string>;
|
|
80
|
+
export {};
|
|
81
|
+
//# sourceMappingURL=ton-transaction-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ton-transaction-utils.d.ts","sourceRoot":"","sources":["../src/ton-transaction-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,wBAAwB,EACxB,kBAAkB,EAClB,aAAa,EACd,MAAM,wBAAwB,CAAA;AAa/B,sDAAsD;AACtD,UAAU,4BAA4B;IACpC,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE;QACR,OAAO,EAAE,MAAM,CAAA;QACf,MAAM,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,OAAO,CAAC,EAAE,MAAM,CAAA;KACjB,EAAE,CAAA;CACJ;AAED,6FAA6F;AAC7F,UAAU,cAAc;IACtB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,CAAC,mBAAmB,EAAE,MAAM,KAAK,MAAM,GACjD,MAAM,CAKR;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,wBAAwB;;;;;EAkB3E;AAED;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,wBAAwB,EACjC,gBAAgB,CAAC,EAAE,MAAM,GACxB,4BAA4B,GAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAatD;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,kBAAkB,GAC5B,kBAAkB,CAEpB;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE;IAAE,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,OAAO,CAAC,cAAc,CAAC,CAAA;CAAE,EAC5E,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,aAAa,CAAC,CA4BxB;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,GAClD,MAAM,CASR;AAED;;;;;;GAMG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI5D"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { TON_MAINNET_CHAIN_ID } from '@meshconnect/uwc-constants';
|
|
2
|
+
import { isValidBase64 } from './validation';
|
|
3
|
+
/** Default transaction expiry window in seconds. TON transactions must include an expiry timestamp. */
|
|
4
|
+
const TX_EXPIRY_SECONDS = 300;
|
|
5
|
+
/**
|
|
6
|
+
* Convert raw TON address to user-friendly format.
|
|
7
|
+
* TON has two formats:
|
|
8
|
+
* Raw: "0:6e7ee4a1b2c3d4..." (workchain:hex, used by the SDK)
|
|
9
|
+
* User-friendly: "UQBx7kobs..." (base64url, used in UIs and explorers)
|
|
10
|
+
*/
|
|
11
|
+
export function toFriendlyAddress(rawAddress, converter) {
|
|
12
|
+
if (rawAddress.includes(':')) {
|
|
13
|
+
return converter(rawAddress);
|
|
14
|
+
}
|
|
15
|
+
return rawAddress;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Build a single TON message from a transfer request.
|
|
19
|
+
* Example: { address: "UQCy9m...", amount: "500000000", payload: "te6cckEBAQ..." }
|
|
20
|
+
*
|
|
21
|
+
* - `amount` is in nanotons as a decimal string (1 TON = 1_000_000_000)
|
|
22
|
+
* - `payload` is a base64-encoded BOC (Bag of Cells) — for Jetton transfers, on-chain comments, etc.
|
|
23
|
+
* - `stateInit` is a base64 BOC that deploys a smart contract alongside the transfer
|
|
24
|
+
*/
|
|
25
|
+
export function buildTonTransactionMessage(request) {
|
|
26
|
+
if (request.payload && !isValidBase64(request.payload)) {
|
|
27
|
+
throw new Error('payload must be a base64-encoded BOC. If you want to include a text comment, ' +
|
|
28
|
+
'encode it as a Cell first using @ton/core.');
|
|
29
|
+
}
|
|
30
|
+
if (request.stateInit && !isValidBase64(request.stateInit)) {
|
|
31
|
+
throw new Error('stateInit must be a base64-encoded BOC.');
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
address: request.to,
|
|
35
|
+
amount: request.amount,
|
|
36
|
+
...(request.payload && { payload: request.payload }),
|
|
37
|
+
...(request.stateInit && { stateInit: request.stateInit })
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Transform a UWC transfer request into @tonconnect/sdk's SendTransactionRequest.
|
|
42
|
+
*
|
|
43
|
+
* @param senderRawAddress - Raw 0:hex address from sdk.account.address.
|
|
44
|
+
* The SDK expects `from` in raw format; the UI's user-friendly address (UQ...) is a fallback only.
|
|
45
|
+
*
|
|
46
|
+
* `validUntil` defaults to now + 5 min (TX_EXPIRY_SECONDS). TON transactions require an expiry.
|
|
47
|
+
* `sendMode` — bit flags for message handling (e.g. 128 = send entire balance).
|
|
48
|
+
* See: https://docs.ton.org/v3/documentation/smart-contracts/message-management/message-modes-cookbook
|
|
49
|
+
*/
|
|
50
|
+
export function buildTonTransactionRequest(request, senderRawAddress) {
|
|
51
|
+
const message = buildTonTransactionMessage(request);
|
|
52
|
+
const txRequest = {
|
|
53
|
+
validUntil: request.validUntil ?? Math.floor(Date.now() / 1000) + TX_EXPIRY_SECONDS,
|
|
54
|
+
network: TON_MAINNET_CHAIN_ID,
|
|
55
|
+
from: senderRawAddress ?? request.from,
|
|
56
|
+
messages: [message]
|
|
57
|
+
};
|
|
58
|
+
if (request.sendMode !== undefined)
|
|
59
|
+
txRequest.sendMode = request.sendMode;
|
|
60
|
+
return txRequest;
|
|
61
|
+
}
|
|
62
|
+
export function buildSignDataPayload(message, override) {
|
|
63
|
+
return override ?? { type: 'text', text: message };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Execute signData on the SDK, normalizing errors for wallets that don't support it.
|
|
67
|
+
* OKX and some other wallets do not implement SignData — this gives a clear error message.
|
|
68
|
+
*/
|
|
69
|
+
export async function executeSignData(sdk, payload) {
|
|
70
|
+
if (typeof sdk.signData !== 'function') {
|
|
71
|
+
throw new Error('This wallet does not support message signing via TON Connect.');
|
|
72
|
+
}
|
|
73
|
+
let result;
|
|
74
|
+
try {
|
|
75
|
+
result = await sdk.signData(payload);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
79
|
+
if (msg.includes('SignData') || msg.includes('not support')) {
|
|
80
|
+
throw new Error(`This wallet does not support message signing via TON Connect. ` +
|
|
81
|
+
`Not all TON wallets implement the SignData feature.`);
|
|
82
|
+
}
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
type: 'tvm',
|
|
87
|
+
signature: result.signature,
|
|
88
|
+
timestamp: result.timestamp,
|
|
89
|
+
domain: result.domain,
|
|
90
|
+
payload
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Extract BOC (Bag of Cells) string from wallet response.
|
|
95
|
+
* TON wallets return the serialized signed transaction as base64, not a tx hash.
|
|
96
|
+
* Some wallets return a plain string, others wrap it in { boc: "..." } — handle both.
|
|
97
|
+
*/
|
|
98
|
+
export function unwrapBoc(result) {
|
|
99
|
+
if (!result) {
|
|
100
|
+
throw new Error('Empty transaction result from TON wallet');
|
|
101
|
+
}
|
|
102
|
+
const boc = typeof result === 'string' ? result : result.boc;
|
|
103
|
+
if (!boc) {
|
|
104
|
+
throw new Error('No BOC in transaction result');
|
|
105
|
+
}
|
|
106
|
+
return boc;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Convert a base64 BOC to a hex transaction hash for use in explorers.
|
|
110
|
+
*
|
|
111
|
+
* @ton/core (268 KB JS, CJS — no tree-shaking) is lazy-loaded via dynamic import.
|
|
112
|
+
* Impact: 0 KB for non-TON users, ~22ms cold load + <1ms hash on first TON transaction.
|
|
113
|
+
* Subsequent calls: <0.1ms (module cached by runtime).
|
|
114
|
+
*/
|
|
115
|
+
export async function bocToHash(boc) {
|
|
116
|
+
const { Cell } = await import('@ton/core');
|
|
117
|
+
const cell = Cell.fromBase64(boc);
|
|
118
|
+
return cell.hash().toString('hex');
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=ton-transaction-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ton-transaction-utils.js","sourceRoot":"","sources":["../src/ton-transaction-utils.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AACA,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEpD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,MAAM,YAAY,GAAG,2BAA2B,CAAA;AAChD,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;AAC/E,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@meshconnect/uwc-ton-connector",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "TON Connect connector for Universal Wallet Connector — mobile QR/URI wallet connections",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@ton/core": "^0.63.1",
|
|
20
|
+
"@meshconnect/uwc-constants": "0.5.0",
|
|
21
|
+
"@meshconnect/uwc-types": "0.7.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"typescript": "^5.9.3"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc",
|
|
31
|
+
"dev": "tsc --watch",
|
|
32
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
33
|
+
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
|
34
|
+
"type-check": "tsc --noEmit"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { NamespacedStorage, type TonConnectStorage } from './namespaced-storage'
|
|
2
|
+
export { isValidBase64 } from './validation'
|
|
3
|
+
export {
|
|
4
|
+
buildTonTransactionRequest,
|
|
5
|
+
buildSignDataPayload,
|
|
6
|
+
executeSignData,
|
|
7
|
+
unwrapBoc,
|
|
8
|
+
bocToHash,
|
|
9
|
+
toFriendlyAddress
|
|
10
|
+
} from './ton-transaction-utils'
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { NamespacedStorage } from './namespaced-storage'
|
|
3
|
+
|
|
4
|
+
describe('NamespacedStorage', () => {
|
|
5
|
+
describe('key prefixing', () => {
|
|
6
|
+
it('isolates namespaces from each other', async () => {
|
|
7
|
+
const injected = new NamespacedStorage('uwc-ton-injected')
|
|
8
|
+
const remote = new NamespacedStorage('uwc-ton-remote')
|
|
9
|
+
|
|
10
|
+
await injected.setItem('session', 'injected-data')
|
|
11
|
+
await remote.setItem('session', 'remote-data')
|
|
12
|
+
|
|
13
|
+
expect(await injected.getItem('session')).toBe('injected-data')
|
|
14
|
+
expect(await remote.getItem('session')).toBe('remote-data')
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('CRUD operations', () => {
|
|
19
|
+
it('setItem and getItem round-trip', async () => {
|
|
20
|
+
const storage = new NamespacedStorage('test')
|
|
21
|
+
await storage.setItem('key', 'value')
|
|
22
|
+
expect(await storage.getItem('key')).toBe('value')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('getItem returns null for missing keys', async () => {
|
|
26
|
+
const storage = new NamespacedStorage('test')
|
|
27
|
+
expect(await storage.getItem('nonexistent')).toBeNull()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('removeItem deletes the key', async () => {
|
|
31
|
+
const storage = new NamespacedStorage('test')
|
|
32
|
+
await storage.setItem('key', 'value')
|
|
33
|
+
await storage.removeItem('key')
|
|
34
|
+
expect(await storage.getItem('key')).toBeNull()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('setItem overwrites existing values', async () => {
|
|
38
|
+
const storage = new NamespacedStorage('test')
|
|
39
|
+
await storage.setItem('key', 'old')
|
|
40
|
+
await storage.setItem('key', 'new')
|
|
41
|
+
expect(await storage.getItem('key')).toBe('new')
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('localStorage throws (Safari ITP / iframe)', () => {
|
|
46
|
+
let originalLocalStorage: Storage
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
originalLocalStorage = globalThis.localStorage
|
|
50
|
+
Object.defineProperty(globalThis, 'localStorage', {
|
|
51
|
+
value: {
|
|
52
|
+
getItem: () => {
|
|
53
|
+
throw new DOMException('access denied')
|
|
54
|
+
},
|
|
55
|
+
setItem: () => {
|
|
56
|
+
throw new DOMException('access denied')
|
|
57
|
+
},
|
|
58
|
+
removeItem: () => {
|
|
59
|
+
throw new DOMException('access denied')
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
writable: true,
|
|
63
|
+
configurable: true
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
Object.defineProperty(globalThis, 'localStorage', {
|
|
69
|
+
value: originalLocalStorage,
|
|
70
|
+
writable: true,
|
|
71
|
+
configurable: true
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('falls back to memory on setItem/getItem', async () => {
|
|
76
|
+
const storage = new NamespacedStorage('test')
|
|
77
|
+
await storage.setItem('key', 'value')
|
|
78
|
+
expect(await storage.getItem('key')).toBe('value')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('falls back to memory on removeItem', async () => {
|
|
82
|
+
const storage = new NamespacedStorage('test')
|
|
83
|
+
await storage.setItem('key', 'value')
|
|
84
|
+
await storage.removeItem('key')
|
|
85
|
+
expect(await storage.getItem('key')).toBeNull()
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('localStorage undefined (non-browser env)', () => {
|
|
90
|
+
let originalLocalStorage: Storage
|
|
91
|
+
|
|
92
|
+
beforeEach(() => {
|
|
93
|
+
originalLocalStorage = globalThis.localStorage
|
|
94
|
+
// @ts-expect-error -- simulating non-browser environment
|
|
95
|
+
delete globalThis.localStorage
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
afterEach(() => {
|
|
99
|
+
Object.defineProperty(globalThis, 'localStorage', {
|
|
100
|
+
value: originalLocalStorage,
|
|
101
|
+
writable: true,
|
|
102
|
+
configurable: true
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('setItem and getItem use memory fallback', async () => {
|
|
107
|
+
const storage = new NamespacedStorage('test')
|
|
108
|
+
await storage.setItem('key', 'value')
|
|
109
|
+
expect(await storage.getItem('key')).toBe('value')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('getItem returns null for missing keys', async () => {
|
|
113
|
+
const storage = new NamespacedStorage('test')
|
|
114
|
+
expect(await storage.getItem('nope')).toBeNull()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('removeItem deletes from memory', async () => {
|
|
118
|
+
const storage = new NamespacedStorage('test')
|
|
119
|
+
await storage.setItem('key', 'value')
|
|
120
|
+
await storage.removeItem('key')
|
|
121
|
+
expect(await storage.getItem('key')).toBeNull()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('memory fallback is per-instance (not shared)', async () => {
|
|
125
|
+
const a = new NamespacedStorage('a')
|
|
126
|
+
const b = new NamespacedStorage('b')
|
|
127
|
+
|
|
128
|
+
await a.setItem('key', 'from-a')
|
|
129
|
+
await b.setItem('key', 'from-b')
|
|
130
|
+
|
|
131
|
+
expect(await a.getItem('key')).toBe('from-a')
|
|
132
|
+
expect(await b.getItem('key')).toBe('from-b')
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Mirrors @tonconnect/sdk IStorage — avoids leaking SDK types in published .d.ts. */
|
|
2
|
+
export interface TonConnectStorage {
|
|
3
|
+
setItem(key: string, value: string): Promise<void>
|
|
4
|
+
getItem(key: string): Promise<string | null>
|
|
5
|
+
removeItem(key: string): Promise<void>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Prefixed localStorage wrapper required by @tonconnect/sdk.
|
|
10
|
+
* The SDK requires the dApp to provide storage for persisting connection state across reloads.
|
|
11
|
+
* Keys are prefixed per wallet (e.g. "tonkeeper:session") to avoid collisions.
|
|
12
|
+
* Falls back to in-memory Map when localStorage is blocked (Safari ITP in iframes).
|
|
13
|
+
*/
|
|
14
|
+
export class NamespacedStorage implements TonConnectStorage {
|
|
15
|
+
private memoryFallback = new Map<string, string>()
|
|
16
|
+
|
|
17
|
+
constructor(private prefix: string) {}
|
|
18
|
+
|
|
19
|
+
async setItem(key: string, value: string): Promise<void> {
|
|
20
|
+
if (typeof localStorage === 'undefined') {
|
|
21
|
+
this.memoryFallback.set(this.prefixed(key), value)
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
localStorage.setItem(this.prefixed(key), value)
|
|
26
|
+
} catch {
|
|
27
|
+
this.memoryFallback.set(this.prefixed(key), value)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async getItem(key: string): Promise<string | null> {
|
|
32
|
+
if (typeof localStorage === 'undefined') {
|
|
33
|
+
return this.memoryFallback.get(this.prefixed(key)) ?? null
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
return localStorage.getItem(this.prefixed(key))
|
|
37
|
+
} catch {
|
|
38
|
+
return this.memoryFallback.get(this.prefixed(key)) ?? null
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async removeItem(key: string): Promise<void> {
|
|
43
|
+
if (typeof localStorage === 'undefined') {
|
|
44
|
+
this.memoryFallback.delete(this.prefixed(key))
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
localStorage.removeItem(this.prefixed(key))
|
|
49
|
+
} catch {
|
|
50
|
+
this.memoryFallback.delete(this.prefixed(key))
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private prefixed(key: string): string {
|
|
55
|
+
return `${this.prefix}:${key}`
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
buildTonTransactionRequest,
|
|
4
|
+
buildSignDataPayload,
|
|
5
|
+
unwrapBoc,
|
|
6
|
+
bocToHash,
|
|
7
|
+
executeSignData,
|
|
8
|
+
toFriendlyAddress
|
|
9
|
+
} from './ton-transaction-utils'
|
|
10
|
+
import { TON_MAINNET_CHAIN_ID } from '@meshconnect/uwc-constants'
|
|
11
|
+
import type {
|
|
12
|
+
TonNativeTransferRequest,
|
|
13
|
+
TonSignDataPayload
|
|
14
|
+
} from '@meshconnect/uwc-types'
|
|
15
|
+
|
|
16
|
+
describe('buildTonTransactionRequest', () => {
|
|
17
|
+
const baseRequest: TonNativeTransferRequest = {
|
|
18
|
+
to: 'UQBExample123',
|
|
19
|
+
amount: '1000000000',
|
|
20
|
+
from: 'UQBSender456'
|
|
21
|
+
} as TonNativeTransferRequest
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.useFakeTimers()
|
|
25
|
+
vi.setSystemTime(new Date('2026-01-15T12:00:00Z'))
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
vi.useRealTimers()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('builds correct transaction structure', () => {
|
|
33
|
+
const result = buildTonTransactionRequest(baseRequest)
|
|
34
|
+
|
|
35
|
+
expect(result.network).toBe(TON_MAINNET_CHAIN_ID)
|
|
36
|
+
expect(result.from).toBe('UQBSender456')
|
|
37
|
+
expect(result.messages).toHaveLength(1)
|
|
38
|
+
expect(result.messages[0].address).toBe('UQBExample123')
|
|
39
|
+
expect(result.messages[0].amount).toBe('1000000000')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('sets validUntil to now + 300s when not provided', () => {
|
|
43
|
+
const result = buildTonTransactionRequest(baseRequest)
|
|
44
|
+
const expectedTs = Math.floor(Date.now() / 1000) + 300
|
|
45
|
+
|
|
46
|
+
expect(result.validUntil).toBe(expectedTs)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('uses provided validUntil when specified', () => {
|
|
50
|
+
const result = buildTonTransactionRequest({
|
|
51
|
+
...baseRequest,
|
|
52
|
+
validUntil: 9999999
|
|
53
|
+
} as TonNativeTransferRequest)
|
|
54
|
+
|
|
55
|
+
expect(result.validUntil).toBe(9999999)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('includes sendMode when provided', () => {
|
|
59
|
+
const result = buildTonTransactionRequest({
|
|
60
|
+
...baseRequest,
|
|
61
|
+
sendMode: 3
|
|
62
|
+
} as TonNativeTransferRequest)
|
|
63
|
+
|
|
64
|
+
expect(result.sendMode).toBe(3)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('omits sendMode when not provided', () => {
|
|
68
|
+
const result = buildTonTransactionRequest(baseRequest)
|
|
69
|
+
|
|
70
|
+
expect(result).not.toHaveProperty('sendMode')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('overrides from with senderRawAddress when provided', () => {
|
|
74
|
+
const result = buildTonTransactionRequest(baseRequest, '0:abc123')
|
|
75
|
+
|
|
76
|
+
expect(result.from).toBe('0:abc123')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('uses request.from when senderRawAddress is undefined', () => {
|
|
80
|
+
const result = buildTonTransactionRequest(baseRequest)
|
|
81
|
+
|
|
82
|
+
expect(result.from).toBe('UQBSender456')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('includes payload and stateInit in message when valid base64', () => {
|
|
86
|
+
const result = buildTonTransactionRequest({
|
|
87
|
+
...baseRequest,
|
|
88
|
+
payload: 'dGVzdA==',
|
|
89
|
+
stateInit: 'AQIDBA=='
|
|
90
|
+
} as TonNativeTransferRequest)
|
|
91
|
+
|
|
92
|
+
expect(result.messages[0].payload).toBe('dGVzdA==')
|
|
93
|
+
expect(result.messages[0].stateInit).toBe('AQIDBA==')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('throws when payload is not valid base64', () => {
|
|
97
|
+
expect(() =>
|
|
98
|
+
buildTonTransactionRequest({
|
|
99
|
+
...baseRequest,
|
|
100
|
+
payload: 'not valid base64!!'
|
|
101
|
+
} as TonNativeTransferRequest)
|
|
102
|
+
).toThrow('payload must be a base64-encoded BOC')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('throws when stateInit is not valid base64', () => {
|
|
106
|
+
expect(() =>
|
|
107
|
+
buildTonTransactionRequest({
|
|
108
|
+
...baseRequest,
|
|
109
|
+
stateInit: 'bad data ##'
|
|
110
|
+
} as TonNativeTransferRequest)
|
|
111
|
+
).toThrow('stateInit must be a base64-encoded BOC')
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('buildSignDataPayload', () => {
|
|
116
|
+
it('returns override payload when provided', () => {
|
|
117
|
+
const override: TonSignDataPayload = { type: 'binary', bytes: 'AQID' }
|
|
118
|
+
const result = buildSignDataPayload('ignored message', override)
|
|
119
|
+
|
|
120
|
+
expect(result).toBe(override)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('builds text payload when no override provided', () => {
|
|
124
|
+
const result = buildSignDataPayload('Please sign this')
|
|
125
|
+
|
|
126
|
+
expect(result).toEqual({ type: 'text', text: 'Please sign this' })
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('returns cell override as-is', () => {
|
|
130
|
+
const override: TonSignDataPayload = {
|
|
131
|
+
type: 'cell',
|
|
132
|
+
schema: 'some-schema',
|
|
133
|
+
cell: 'AQID'
|
|
134
|
+
}
|
|
135
|
+
const result = buildSignDataPayload('msg', override)
|
|
136
|
+
|
|
137
|
+
expect(result).toBe(override)
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('unwrapBoc', () => {
|
|
142
|
+
it('returns string directly when result is a string', () => {
|
|
143
|
+
expect(unwrapBoc('te6cckEBAQ==')).toBe('te6cckEBAQ==')
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('extracts boc property when result is an object', () => {
|
|
147
|
+
expect(unwrapBoc({ boc: 'te6cckEBAQ==' })).toBe('te6cckEBAQ==')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('throws on null result', () => {
|
|
151
|
+
expect(() => unwrapBoc(null)).toThrow(
|
|
152
|
+
'Empty transaction result from TON wallet'
|
|
153
|
+
)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('throws on undefined result', () => {
|
|
157
|
+
expect(() => unwrapBoc(undefined)).toThrow(
|
|
158
|
+
'Empty transaction result from TON wallet'
|
|
159
|
+
)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('throws on object without boc property', () => {
|
|
163
|
+
expect(() => unwrapBoc({ hash: 'abc' })).toThrow(
|
|
164
|
+
'No BOC in transaction result'
|
|
165
|
+
)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('throws on empty string', () => {
|
|
169
|
+
expect(() => unwrapBoc('')).toThrow(
|
|
170
|
+
'Empty transaction result from TON wallet'
|
|
171
|
+
)
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('bocToHash', () => {
|
|
176
|
+
// BOC for: beginCell().storeUint(0, 32).storeStringTail('test').endCell()
|
|
177
|
+
const testBoc = 'te6cckEBAQEACgAAEAAAAAB0ZXN0cbBecQ=='
|
|
178
|
+
const expectedHash =
|
|
179
|
+
'f9b941877628a233a719e129140e93df1eeb5a65aeea5fbd16fb0b9af4004c94'
|
|
180
|
+
|
|
181
|
+
it('computes deterministic hash from a valid BOC', async () => {
|
|
182
|
+
expect(await bocToHash(testBoc)).toBe(expectedHash)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('returns the same hash for the same BOC', async () => {
|
|
186
|
+
expect(await bocToHash(testBoc)).toBe(await bocToHash(testBoc))
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('throws on invalid base64', async () => {
|
|
190
|
+
await expect(bocToHash('not-valid-boc')).rejects.toThrow()
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
describe('toFriendlyAddress', () => {
|
|
195
|
+
const mockConverter = (hex: string) => 'UQ' + hex.replace('0:', '')
|
|
196
|
+
|
|
197
|
+
it('converts raw 0:hex address using converter', () => {
|
|
198
|
+
expect(toFriendlyAddress('0:abc123', mockConverter)).toBe('UQabc123')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('returns already-friendly address as-is', () => {
|
|
202
|
+
expect(toFriendlyAddress('UQBExample123', mockConverter)).toBe(
|
|
203
|
+
'UQBExample123'
|
|
204
|
+
)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('returns EQ addresses as-is', () => {
|
|
208
|
+
expect(toFriendlyAddress('EQBExample456', mockConverter)).toBe(
|
|
209
|
+
'EQBExample456'
|
|
210
|
+
)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('returns empty string as-is (no colon)', () => {
|
|
214
|
+
expect(toFriendlyAddress('', mockConverter)).toBe('')
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('converts address with 0: prefix but no hex body', () => {
|
|
218
|
+
expect(toFriendlyAddress('0:', mockConverter)).toBe('UQ')
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('handles -1: workchain prefix', () => {
|
|
222
|
+
const converter = (raw: string) => 'EQ' + raw.split(':')[1]
|
|
223
|
+
expect(toFriendlyAddress('-1:abcdef', converter)).toBe('EQabcdef')
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
describe('executeSignData', () => {
|
|
228
|
+
it('throws early when signData is not a function', async () => {
|
|
229
|
+
const sdk = {} as { signData?: undefined }
|
|
230
|
+
const payload: TonSignDataPayload = { type: 'text', text: 'hello' }
|
|
231
|
+
|
|
232
|
+
await expect(executeSignData(sdk, payload)).rejects.toThrow(
|
|
233
|
+
'This wallet does not support message signing via TON Connect'
|
|
234
|
+
)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('returns signature result on success', async () => {
|
|
238
|
+
const mockResult = {
|
|
239
|
+
signature: 'abc123',
|
|
240
|
+
timestamp: 1700000000,
|
|
241
|
+
domain: 'example.com'
|
|
242
|
+
}
|
|
243
|
+
const sdk = { signData: vi.fn().mockResolvedValue(mockResult) }
|
|
244
|
+
const payload: TonSignDataPayload = { type: 'text', text: 'hello' }
|
|
245
|
+
|
|
246
|
+
const result = await executeSignData(sdk, payload)
|
|
247
|
+
|
|
248
|
+
expect(result.type).toBe('tvm')
|
|
249
|
+
expect(result.signature).toBe('abc123')
|
|
250
|
+
expect(result.timestamp).toBe(1700000000)
|
|
251
|
+
expect(result.domain).toBe('example.com')
|
|
252
|
+
expect(result.payload).toBe(payload)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('wraps error with user-friendly message when wallet does not support SignData', async () => {
|
|
256
|
+
const sdk = {
|
|
257
|
+
signData: vi
|
|
258
|
+
.fn()
|
|
259
|
+
.mockRejectedValue(new Error('SignData method not available'))
|
|
260
|
+
}
|
|
261
|
+
const payload: TonSignDataPayload = { type: 'text', text: 'hello' }
|
|
262
|
+
|
|
263
|
+
await expect(executeSignData(sdk, payload)).rejects.toThrow(
|
|
264
|
+
'This wallet does not support message signing via TON Connect'
|
|
265
|
+
)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('wraps error when message contains "not support"', async () => {
|
|
269
|
+
const sdk = {
|
|
270
|
+
signData: vi.fn().mockRejectedValue(new Error('Feature not supported'))
|
|
271
|
+
}
|
|
272
|
+
const payload: TonSignDataPayload = { type: 'text', text: 'hello' }
|
|
273
|
+
|
|
274
|
+
await expect(executeSignData(sdk, payload)).rejects.toThrow(
|
|
275
|
+
'This wallet does not support message signing via TON Connect'
|
|
276
|
+
)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('re-throws unrelated errors unchanged', async () => {
|
|
280
|
+
const sdk = {
|
|
281
|
+
signData: vi.fn().mockRejectedValue(new Error('Network timeout'))
|
|
282
|
+
}
|
|
283
|
+
const payload: TonSignDataPayload = { type: 'text', text: 'hello' }
|
|
284
|
+
|
|
285
|
+
await expect(executeSignData(sdk, payload)).rejects.toThrow(
|
|
286
|
+
'Network timeout'
|
|
287
|
+
)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('handles non-Error thrown values in the SignData path', async () => {
|
|
291
|
+
const sdk = {
|
|
292
|
+
signData: vi.fn().mockRejectedValue('not support this')
|
|
293
|
+
}
|
|
294
|
+
const payload: TonSignDataPayload = { type: 'text', text: 'hello' }
|
|
295
|
+
|
|
296
|
+
await expect(executeSignData(sdk, payload)).rejects.toThrow(
|
|
297
|
+
'This wallet does not support message signing via TON Connect'
|
|
298
|
+
)
|
|
299
|
+
})
|
|
300
|
+
})
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TonNativeTransferRequest,
|
|
3
|
+
TonSignDataPayload,
|
|
4
|
+
SignatureType
|
|
5
|
+
} from '@meshconnect/uwc-types'
|
|
6
|
+
import { TON_MAINNET_CHAIN_ID } from '@meshconnect/uwc-constants'
|
|
7
|
+
import { isValidBase64 } from './validation'
|
|
8
|
+
|
|
9
|
+
/** Default transaction expiry window in seconds. TON transactions must include an expiry timestamp. */
|
|
10
|
+
const TX_EXPIRY_SECONDS = 300
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* Local mirrors of @tonconnect/sdk types.
|
|
14
|
+
* Defined here so ton-connector has zero runtime dependency on @tonconnect/sdk,
|
|
15
|
+
* avoiding type leaks in the published .d.ts files.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Mirrors @tonconnect/sdk SendTransactionRequest. */
|
|
19
|
+
interface TonConnectTransactionRequest {
|
|
20
|
+
validUntil: number
|
|
21
|
+
network?: string
|
|
22
|
+
from?: string
|
|
23
|
+
messages: {
|
|
24
|
+
address: string
|
|
25
|
+
amount: string
|
|
26
|
+
stateInit?: string
|
|
27
|
+
payload?: string
|
|
28
|
+
}[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Mirrors @tonconnect/sdk SignDataResponse. Not all wallets support SignData (e.g. OKX). */
|
|
32
|
+
interface SignDataResult {
|
|
33
|
+
signature: string
|
|
34
|
+
address: string
|
|
35
|
+
timestamp: number
|
|
36
|
+
domain: string
|
|
37
|
+
payload: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Convert raw TON address to user-friendly format.
|
|
42
|
+
* TON has two formats:
|
|
43
|
+
* Raw: "0:6e7ee4a1b2c3d4..." (workchain:hex, used by the SDK)
|
|
44
|
+
* User-friendly: "UQBx7kobs..." (base64url, used in UIs and explorers)
|
|
45
|
+
*/
|
|
46
|
+
export function toFriendlyAddress(
|
|
47
|
+
rawAddress: string,
|
|
48
|
+
converter: (rawWorkchainAddress: string) => string
|
|
49
|
+
): string {
|
|
50
|
+
if (rawAddress.includes(':')) {
|
|
51
|
+
return converter(rawAddress)
|
|
52
|
+
}
|
|
53
|
+
return rawAddress
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Build a single TON message from a transfer request.
|
|
58
|
+
* Example: { address: "UQCy9m...", amount: "500000000", payload: "te6cckEBAQ..." }
|
|
59
|
+
*
|
|
60
|
+
* - `amount` is in nanotons as a decimal string (1 TON = 1_000_000_000)
|
|
61
|
+
* - `payload` is a base64-encoded BOC (Bag of Cells) — for Jetton transfers, on-chain comments, etc.
|
|
62
|
+
* - `stateInit` is a base64 BOC that deploys a smart contract alongside the transfer
|
|
63
|
+
*/
|
|
64
|
+
export function buildTonTransactionMessage(request: TonNativeTransferRequest) {
|
|
65
|
+
if (request.payload && !isValidBase64(request.payload)) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
'payload must be a base64-encoded BOC. If you want to include a text comment, ' +
|
|
68
|
+
'encode it as a Cell first using @ton/core.'
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (request.stateInit && !isValidBase64(request.stateInit)) {
|
|
73
|
+
throw new Error('stateInit must be a base64-encoded BOC.')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
address: request.to,
|
|
78
|
+
amount: request.amount,
|
|
79
|
+
...(request.payload && { payload: request.payload }),
|
|
80
|
+
...(request.stateInit && { stateInit: request.stateInit })
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Transform a UWC transfer request into @tonconnect/sdk's SendTransactionRequest.
|
|
86
|
+
*
|
|
87
|
+
* @param senderRawAddress - Raw 0:hex address from sdk.account.address.
|
|
88
|
+
* The SDK expects `from` in raw format; the UI's user-friendly address (UQ...) is a fallback only.
|
|
89
|
+
*
|
|
90
|
+
* `validUntil` defaults to now + 5 min (TX_EXPIRY_SECONDS). TON transactions require an expiry.
|
|
91
|
+
* `sendMode` — bit flags for message handling (e.g. 128 = send entire balance).
|
|
92
|
+
* See: https://docs.ton.org/v3/documentation/smart-contracts/message-management/message-modes-cookbook
|
|
93
|
+
*/
|
|
94
|
+
export function buildTonTransactionRequest(
|
|
95
|
+
request: TonNativeTransferRequest,
|
|
96
|
+
senderRawAddress?: string
|
|
97
|
+
): TonConnectTransactionRequest & { sendMode?: number } {
|
|
98
|
+
const message = buildTonTransactionMessage(request)
|
|
99
|
+
|
|
100
|
+
const txRequest: TonConnectTransactionRequest & { sendMode?: number } = {
|
|
101
|
+
validUntil:
|
|
102
|
+
request.validUntil ?? Math.floor(Date.now() / 1000) + TX_EXPIRY_SECONDS,
|
|
103
|
+
network: TON_MAINNET_CHAIN_ID,
|
|
104
|
+
from: senderRawAddress ?? request.from,
|
|
105
|
+
messages: [message]
|
|
106
|
+
}
|
|
107
|
+
if (request.sendMode !== undefined) txRequest.sendMode = request.sendMode
|
|
108
|
+
|
|
109
|
+
return txRequest
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function buildSignDataPayload(
|
|
113
|
+
message: string,
|
|
114
|
+
override?: TonSignDataPayload
|
|
115
|
+
): TonSignDataPayload {
|
|
116
|
+
return override ?? { type: 'text', text: message }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Execute signData on the SDK, normalizing errors for wallets that don't support it.
|
|
121
|
+
* OKX and some other wallets do not implement SignData — this gives a clear error message.
|
|
122
|
+
*/
|
|
123
|
+
export async function executeSignData(
|
|
124
|
+
sdk: { signData?: (payload: TonSignDataPayload) => Promise<SignDataResult> },
|
|
125
|
+
payload: TonSignDataPayload
|
|
126
|
+
): Promise<SignatureType> {
|
|
127
|
+
if (typeof sdk.signData !== 'function') {
|
|
128
|
+
throw new Error(
|
|
129
|
+
'This wallet does not support message signing via TON Connect.'
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let result: SignDataResult
|
|
134
|
+
try {
|
|
135
|
+
result = await sdk.signData(payload)
|
|
136
|
+
} catch (error) {
|
|
137
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
138
|
+
if (msg.includes('SignData') || msg.includes('not support')) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`This wallet does not support message signing via TON Connect. ` +
|
|
141
|
+
`Not all TON wallets implement the SignData feature.`
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
throw error
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
type: 'tvm',
|
|
149
|
+
signature: result.signature,
|
|
150
|
+
timestamp: result.timestamp,
|
|
151
|
+
domain: result.domain,
|
|
152
|
+
payload
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Extract BOC (Bag of Cells) string from wallet response.
|
|
158
|
+
* TON wallets return the serialized signed transaction as base64, not a tx hash.
|
|
159
|
+
* Some wallets return a plain string, others wrap it in { boc: "..." } — handle both.
|
|
160
|
+
*/
|
|
161
|
+
export function unwrapBoc(
|
|
162
|
+
result: string | { boc: string } | null | undefined
|
|
163
|
+
): string {
|
|
164
|
+
if (!result) {
|
|
165
|
+
throw new Error('Empty transaction result from TON wallet')
|
|
166
|
+
}
|
|
167
|
+
const boc = typeof result === 'string' ? result : result.boc
|
|
168
|
+
if (!boc) {
|
|
169
|
+
throw new Error('No BOC in transaction result')
|
|
170
|
+
}
|
|
171
|
+
return boc
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Convert a base64 BOC to a hex transaction hash for use in explorers.
|
|
176
|
+
*
|
|
177
|
+
* @ton/core (268 KB JS, CJS — no tree-shaking) is lazy-loaded via dynamic import.
|
|
178
|
+
* Impact: 0 KB for non-TON users, ~22ms cold load + <1ms hash on first TON transaction.
|
|
179
|
+
* Subsequent calls: <0.1ms (module cached by runtime).
|
|
180
|
+
*/
|
|
181
|
+
export async function bocToHash(boc: string): Promise<string> {
|
|
182
|
+
const { Cell } = await import('@ton/core')
|
|
183
|
+
const cell = Cell.fromBase64(boc)
|
|
184
|
+
return cell.hash().toString('hex')
|
|
185
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { isValidBase64 } from './validation'
|
|
3
|
+
|
|
4
|
+
describe('isValidBase64', () => {
|
|
5
|
+
it('accepts standard base64 strings', () => {
|
|
6
|
+
expect(isValidBase64('SGVsbG8gV29ybGQ=')).toBe(true)
|
|
7
|
+
expect(isValidBase64('dGVzdA==')).toBe(true)
|
|
8
|
+
expect(isValidBase64('AQIDBA==')).toBe(true)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('accepts URL-safe base64 strings (- and _ chars)', () => {
|
|
12
|
+
expect(isValidBase64('abc-def_ghi')).toBe(true)
|
|
13
|
+
expect(isValidBase64('A-B_CD')).toBe(true)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('accepts base64 without padding', () => {
|
|
17
|
+
expect(isValidBase64('SGVsbG8')).toBe(true)
|
|
18
|
+
expect(isValidBase64('YQ')).toBe(true)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('accepts base64 with single pad character', () => {
|
|
22
|
+
expect(isValidBase64('YWI=')).toBe(true)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('accepts base64 with double pad characters', () => {
|
|
26
|
+
expect(isValidBase64('YQ==')).toBe(true)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('returns false for empty string', () => {
|
|
30
|
+
expect(isValidBase64('')).toBe(false)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('returns false for strings with invalid characters', () => {
|
|
34
|
+
expect(isValidBase64('hello world')).toBe(false) // space
|
|
35
|
+
expect(isValidBase64('abc!def')).toBe(false) // exclamation
|
|
36
|
+
expect(isValidBase64('test@#$')).toBe(false) // special chars
|
|
37
|
+
expect(isValidBase64('data:image/png;base64,abc')).toBe(false) // colon, semicolon, comma
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('returns false for strings with padding in wrong position', () => {
|
|
41
|
+
expect(isValidBase64('=abc')).toBe(false)
|
|
42
|
+
expect(isValidBase64('a=bc')).toBe(false)
|
|
43
|
+
expect(isValidBase64('===abc')).toBe(false)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('rejects strings where length % 4 === 1 (undecodable)', () => {
|
|
47
|
+
expect(isValidBase64('A')).toBe(false)
|
|
48
|
+
expect(isValidBase64('0')).toBe(false)
|
|
49
|
+
expect(isValidBase64('ABCDE')).toBe(false)
|
|
50
|
+
})
|
|
51
|
+
})
|