@meshconnect/uwc-ton-connector 0.2.2 → 0.4.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 +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/jetton-transfer.d.ts +27 -0
- package/dist/jetton-transfer.d.ts.map +1 -0
- package/dist/jetton-transfer.js +103 -0
- package/dist/jetton-transfer.js.map +1 -0
- package/dist/namespaced-storage.d.ts +2 -7
- package/dist/namespaced-storage.d.ts.map +1 -1
- package/dist/namespaced-storage.js.map +1 -1
- package/dist/ton-connect-connector.d.ts +36 -0
- package/dist/ton-connect-connector.d.ts.map +1 -0
- package/dist/ton-connect-connector.js +249 -0
- package/dist/ton-connect-connector.js.map +1 -0
- package/dist/ton-transaction-utils.d.ts +2 -14
- package/dist/ton-transaction-utils.d.ts.map +1 -1
- package/dist/ton-transaction-utils.js.map +1 -1
- package/package.json +4 -3
- package/src/index.ts +6 -1
- package/src/jetton-transfer.test.ts +199 -0
- package/src/jetton-transfer.ts +122 -0
- package/src/namespaced-storage.ts +2 -7
- package/src/ton-connect-connector.test.ts +663 -0
- package/src/ton-connect-connector.ts +347 -0
- package/src/ton-transaction-utils.ts +3 -21
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Network,
|
|
3
|
+
Connector,
|
|
4
|
+
ConnectorResult,
|
|
5
|
+
SwitchNetworkResult,
|
|
6
|
+
TonConnectConfig,
|
|
7
|
+
TonConnectWalletProvider,
|
|
8
|
+
TonNativeTransferRequest,
|
|
9
|
+
TransactionRequest,
|
|
10
|
+
TransactionResult,
|
|
11
|
+
SignatureType,
|
|
12
|
+
NetworkId,
|
|
13
|
+
AvailableAddress
|
|
14
|
+
} from '@meshconnect/uwc-types'
|
|
15
|
+
import type {
|
|
16
|
+
ITonConnect,
|
|
17
|
+
WalletInfoRemote,
|
|
18
|
+
TonConnectOptions
|
|
19
|
+
} from '@tonconnect/sdk'
|
|
20
|
+
import { NamespacedStorage } from './namespaced-storage'
|
|
21
|
+
import {
|
|
22
|
+
buildTonTransactionRequest,
|
|
23
|
+
buildSignDataPayload,
|
|
24
|
+
executeSignData,
|
|
25
|
+
unwrapBoc,
|
|
26
|
+
bocToHash,
|
|
27
|
+
toFriendlyAddress
|
|
28
|
+
} from './ton-transaction-utils'
|
|
29
|
+
|
|
30
|
+
// 5 minutes matches WalletConnect's proposal expiry. The TonConnect protocol
|
|
31
|
+
// has no built-in timeout — without this, a pending connect hangs indefinitely.
|
|
32
|
+
const CONNECT_TIMEOUT_MS = 5 * 60 * 1000
|
|
33
|
+
|
|
34
|
+
// Shape of the error object passed to onStatusChange's errorCallback.
|
|
35
|
+
// Not exported by @tonconnect/sdk — the SDK wraps the raw bridge RPC error internally.
|
|
36
|
+
interface TonConnectErrorEvent {
|
|
37
|
+
payload?: { code?: number; message?: string }
|
|
38
|
+
message?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class TonConnectConnector implements Connector {
|
|
42
|
+
private config: TonConnectConfig
|
|
43
|
+
private sdk: ITonConnect | null = null
|
|
44
|
+
private connectionURI: string | undefined
|
|
45
|
+
private connectedAddress: string | null = null
|
|
46
|
+
private connectedNetworkId: NetworkId | null = null
|
|
47
|
+
private visibilityHandler: (() => void) | null = null
|
|
48
|
+
private connectTimeout: ReturnType<typeof setTimeout> | null = null
|
|
49
|
+
private toUserFriendlyAddressFn:
|
|
50
|
+
| ((hex: string, testOnly?: boolean) => string)
|
|
51
|
+
| null = null
|
|
52
|
+
private UserRejectsErrorClass: (new (...args: never[]) => Error) | null = null
|
|
53
|
+
private connectInFlight = false
|
|
54
|
+
|
|
55
|
+
constructor(config: TonConnectConfig) {
|
|
56
|
+
this.config = config
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Connect to a TON wallet via HTTP Bridge (QR code / deeplink). */
|
|
60
|
+
async connect(
|
|
61
|
+
network: Network,
|
|
62
|
+
provider?: TonConnectWalletProvider
|
|
63
|
+
): Promise<ConnectorResult> {
|
|
64
|
+
if (!provider) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
'TonConnectWalletProvider is required for TON Connect connection'
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
if (!provider.walletListAppName) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
'walletListAppName is required — set it on TonConnectWalletProvider, or ensure the wallet has a jsBridgeKey in extensionInjectedProvider.tvm'
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (this.connectInFlight) {
|
|
76
|
+
throw new Error('TON Connect connection already in progress')
|
|
77
|
+
}
|
|
78
|
+
this.connectInFlight = true
|
|
79
|
+
|
|
80
|
+
if (this.sdk) {
|
|
81
|
+
await this.cleanupSdk(this.sdk)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const { TonConnect, toUserFriendlyAddress, UserRejectsError } =
|
|
86
|
+
await import('@tonconnect/sdk')
|
|
87
|
+
this.toUserFriendlyAddressFn = toUserFriendlyAddress
|
|
88
|
+
this.UserRejectsErrorClass = UserRejectsError
|
|
89
|
+
|
|
90
|
+
const sdkOptions: TonConnectOptions = {
|
|
91
|
+
storage: new NamespacedStorage('uwc-ton-remote'),
|
|
92
|
+
manifestUrl: this.config.manifestUrl
|
|
93
|
+
}
|
|
94
|
+
if (this.config.walletsListSource) {
|
|
95
|
+
sdkOptions.walletsListSource = this.config.walletsListSource
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.sdk = new TonConnect(sdkOptions) as ITonConnect
|
|
99
|
+
|
|
100
|
+
// ONC-2717: The SDK supports restoreConnection() to resume a previous
|
|
101
|
+
// session from NamespacedStorage without a new QR scan. Currently we
|
|
102
|
+
// always start fresh (disconnect stale sessions below). Adding session
|
|
103
|
+
// resumption would let users stay connected across page reloads —
|
|
104
|
+
// call sdk.restoreConnection() here and skip the QR flow if it succeeds.
|
|
105
|
+
|
|
106
|
+
if (this.sdk.connected) {
|
|
107
|
+
// Can't reuse: the SDK binds one provider (JS or HTTP bridge) per instance.
|
|
108
|
+
// A stale HTTP session from a previous wallet would corrupt the new one.
|
|
109
|
+
try {
|
|
110
|
+
await this.sdk.disconnect()
|
|
111
|
+
} catch {
|
|
112
|
+
/* best effort — clear stale session from NamespacedStorage */
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const sdkRef = this.sdk
|
|
117
|
+
|
|
118
|
+
// Resolve bridge URLs from the SDK's wallet list (built-in or custom walletsListSource)
|
|
119
|
+
const wallets = await sdkRef.getWallets()
|
|
120
|
+
const remoteWallets = wallets.filter(
|
|
121
|
+
(w): w is WalletInfoRemote => 'bridgeUrl' in w && 'universalLink' in w
|
|
122
|
+
)
|
|
123
|
+
const walletInfo = remoteWallets.find(
|
|
124
|
+
w => w.appName === provider.walletListAppName
|
|
125
|
+
)
|
|
126
|
+
if (!walletInfo) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Wallet '${provider.walletListAppName}' not found in TonConnect wallet list, or missing bridge/universalLink`
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const address = await this.waitForWalletApproval(
|
|
133
|
+
sdkRef,
|
|
134
|
+
walletInfo.universalLink,
|
|
135
|
+
walletInfo.bridgeUrl
|
|
136
|
+
)
|
|
137
|
+
this.connectionURI = undefined // URI consumed — clear before notify fires
|
|
138
|
+
|
|
139
|
+
const friendlyAddress = this.toUserFriendlyAddressFn
|
|
140
|
+
? toFriendlyAddress(address, this.toUserFriendlyAddressFn)
|
|
141
|
+
: address
|
|
142
|
+
|
|
143
|
+
this.connectedAddress = friendlyAddress
|
|
144
|
+
this.connectedNetworkId = network.id
|
|
145
|
+
this.setupVisibilityHandler(sdkRef)
|
|
146
|
+
|
|
147
|
+
const availableAddresses = this.buildAvailableAddresses(
|
|
148
|
+
provider.supportedNetworkIds,
|
|
149
|
+
friendlyAddress
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
networkId: network.id,
|
|
154
|
+
address: friendlyAddress,
|
|
155
|
+
availableAddresses
|
|
156
|
+
}
|
|
157
|
+
} finally {
|
|
158
|
+
this.connectInFlight = false
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Return the connection URI generated by the last connect() call. */
|
|
163
|
+
getConnectionURI(): string {
|
|
164
|
+
if (!this.connectionURI) {
|
|
165
|
+
throw new Error('No connection URI available. Call connect() first.')
|
|
166
|
+
}
|
|
167
|
+
return this.connectionURI
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Sign a message using TON Connect signData. */
|
|
171
|
+
async signMessage(message: string): Promise<SignatureType> {
|
|
172
|
+
if (!this.sdk) {
|
|
173
|
+
throw new Error('No active TON connection')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const payload = buildSignDataPayload(message)
|
|
177
|
+
return executeSignData(this.sdk, payload)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Send a TON transaction via the connected wallet. */
|
|
181
|
+
async sendTransaction(
|
|
182
|
+
request: TransactionRequest
|
|
183
|
+
): Promise<TransactionResult> {
|
|
184
|
+
if (!this.sdk) {
|
|
185
|
+
throw new Error('No active TON connection')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const tonRequest = request as TonNativeTransferRequest
|
|
189
|
+
const senderRawAddress = this.sdk.account?.address
|
|
190
|
+
const txRequest = buildTonTransactionRequest(tonRequest, senderRawAddress)
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const result = await this.sdk.sendTransaction(txRequest)
|
|
194
|
+
const boc = unwrapBoc(result as string | { boc: string })
|
|
195
|
+
return bocToHash(boc)
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (
|
|
198
|
+
this.UserRejectsErrorClass &&
|
|
199
|
+
error instanceof this.UserRejectsErrorClass
|
|
200
|
+
) {
|
|
201
|
+
throw new Error('Transaction was rejected by the user')
|
|
202
|
+
}
|
|
203
|
+
throw error
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** TON is single-chain — switchNetwork returns the current state. */
|
|
208
|
+
async switchNetwork(network: Network): Promise<SwitchNetworkResult> {
|
|
209
|
+
return {
|
|
210
|
+
networkId: this.connectedNetworkId ?? network.id,
|
|
211
|
+
address: this.connectedAddress ?? ''
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Disconnect from the wallet and clean up resources. */
|
|
216
|
+
async disconnect(): Promise<void> {
|
|
217
|
+
if (this.sdk) {
|
|
218
|
+
try {
|
|
219
|
+
await this.sdk.disconnect()
|
|
220
|
+
} catch {
|
|
221
|
+
/* best effort */
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
this.sdk = null
|
|
225
|
+
this.connectedAddress = null
|
|
226
|
+
this.connectedNetworkId = null
|
|
227
|
+
this.connectionURI = undefined
|
|
228
|
+
this.connectInFlight = false
|
|
229
|
+
this.clearConnectTimeout()
|
|
230
|
+
this.removeVisibilityHandler()
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Tear down SDK, event listeners, and pending timeouts. */
|
|
234
|
+
async destroy(): Promise<void> {
|
|
235
|
+
await this.disconnect()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private async waitForWalletApproval(
|
|
239
|
+
sdk: ITonConnect,
|
|
240
|
+
universalLink: string,
|
|
241
|
+
bridgeUrl: string
|
|
242
|
+
): Promise<string> {
|
|
243
|
+
return new Promise<string>((resolve, reject) => {
|
|
244
|
+
let settled = false
|
|
245
|
+
let unsubscribe: () => void
|
|
246
|
+
|
|
247
|
+
const cleanup = (): void => {
|
|
248
|
+
this.clearConnectTimeout()
|
|
249
|
+
unsubscribe()
|
|
250
|
+
this.connectionURI = undefined
|
|
251
|
+
this.cleanupSdk(sdk)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
this.connectTimeout = setTimeout(() => {
|
|
255
|
+
if (settled) return
|
|
256
|
+
settled = true
|
|
257
|
+
cleanup()
|
|
258
|
+
reject(new Error('TON wallet connection timed out'))
|
|
259
|
+
}, CONNECT_TIMEOUT_MS)
|
|
260
|
+
|
|
261
|
+
unsubscribe = sdk.onStatusChange(
|
|
262
|
+
wallet => {
|
|
263
|
+
if (settled) return
|
|
264
|
+
settled = true
|
|
265
|
+
if (wallet?.account?.address) {
|
|
266
|
+
this.clearConnectTimeout()
|
|
267
|
+
unsubscribe()
|
|
268
|
+
resolve(wallet.account.address)
|
|
269
|
+
} else {
|
|
270
|
+
cleanup()
|
|
271
|
+
reject(new Error('TON wallet connection was rejected'))
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
error => {
|
|
275
|
+
if (settled) return
|
|
276
|
+
settled = true
|
|
277
|
+
const errorObj = error as TonConnectErrorEvent
|
|
278
|
+
const message =
|
|
279
|
+
errorObj?.payload?.message ||
|
|
280
|
+
this.extractSdkErrorMessage(error) ||
|
|
281
|
+
'TON wallet connection was rejected'
|
|
282
|
+
cleanup()
|
|
283
|
+
reject(new Error(message))
|
|
284
|
+
}
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
this.connectionURI = sdk.connect({ universalLink, bridgeUrl })
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private extractSdkErrorMessage(error: unknown): string | undefined {
|
|
292
|
+
if (error && typeof error === 'object' && 'message' in error) {
|
|
293
|
+
const msg = (error as { message: string }).message
|
|
294
|
+
const lines = msg.split('\n')
|
|
295
|
+
if (lines.length > 1) {
|
|
296
|
+
return lines[lines.length - 1]
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return undefined
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private async cleanupSdk(sdk: ITonConnect): Promise<void> {
|
|
303
|
+
try {
|
|
304
|
+
await sdk.disconnect()
|
|
305
|
+
} catch {
|
|
306
|
+
/* best effort */
|
|
307
|
+
}
|
|
308
|
+
if (this.sdk === sdk) {
|
|
309
|
+
this.sdk = null
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private setupVisibilityHandler(sdk: ITonConnect): void {
|
|
314
|
+
this.removeVisibilityHandler()
|
|
315
|
+
this.visibilityHandler = () => {
|
|
316
|
+
if (document.visibilityState === 'visible') {
|
|
317
|
+
sdk.unPauseConnection()
|
|
318
|
+
} else {
|
|
319
|
+
sdk.pauseConnection()
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
document.addEventListener('visibilitychange', this.visibilityHandler)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private removeVisibilityHandler(): void {
|
|
326
|
+
if (this.visibilityHandler) {
|
|
327
|
+
document.removeEventListener('visibilitychange', this.visibilityHandler)
|
|
328
|
+
this.visibilityHandler = null
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private clearConnectTimeout(): void {
|
|
333
|
+
if (this.connectTimeout) {
|
|
334
|
+
clearTimeout(this.connectTimeout)
|
|
335
|
+
this.connectTimeout = null
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private buildAvailableAddresses(
|
|
340
|
+
supportedNetworkIds: string[],
|
|
341
|
+
address: string
|
|
342
|
+
): AvailableAddress[] {
|
|
343
|
+
return supportedNetworkIds
|
|
344
|
+
.filter(id => id.startsWith('tvm:'))
|
|
345
|
+
.map(networkId => ({ address, networkId: networkId as NetworkId }))
|
|
346
|
+
}
|
|
347
|
+
}
|
|
@@ -3,31 +3,13 @@ import type {
|
|
|
3
3
|
TonSignDataPayload,
|
|
4
4
|
SignatureType
|
|
5
5
|
} from '@meshconnect/uwc-types'
|
|
6
|
+
import type { SendTransactionRequest } from '@tonconnect/sdk'
|
|
6
7
|
import { TON_MAINNET_CHAIN_ID } from '@meshconnect/uwc-constants'
|
|
7
8
|
import { isValidBase64 } from './validation'
|
|
8
9
|
|
|
9
10
|
/** Default transaction expiry window in seconds. TON transactions must include an expiry timestamp. */
|
|
10
11
|
const TX_EXPIRY_SECONDS = 300
|
|
11
12
|
|
|
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
13
|
/**
|
|
32
14
|
* Convert raw TON address to user-friendly format.
|
|
33
15
|
* TON has two formats:
|
|
@@ -85,10 +67,10 @@ export function buildTonTransactionMessage(request: TonNativeTransferRequest) {
|
|
|
85
67
|
export function buildTonTransactionRequest(
|
|
86
68
|
request: TonNativeTransferRequest,
|
|
87
69
|
senderRawAddress?: string
|
|
88
|
-
):
|
|
70
|
+
): SendTransactionRequest & { sendMode?: number } {
|
|
89
71
|
const message = buildTonTransactionMessage(request)
|
|
90
72
|
|
|
91
|
-
const txRequest:
|
|
73
|
+
const txRequest: SendTransactionRequest & { sendMode?: number } = {
|
|
92
74
|
validUntil:
|
|
93
75
|
request.validUntil ?? Math.floor(Date.now() / 1000) + TX_EXPIRY_SECONDS,
|
|
94
76
|
network: TON_MAINNET_CHAIN_ID,
|