@meshconnect/uwc-ton-connector 0.3.0 → 0.4.1

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 CHANGED
@@ -1,5 +1,6 @@
1
1
  export { NamespacedStorage } from './namespaced-storage';
2
2
  export { isValidBase64 } from './validation';
3
3
  export { buildTonTransactionRequest, buildSignDataPayload, executeSignData, unwrapBoc, bocToHash, toFriendlyAddress } from './ton-transaction-utils';
4
+ export { buildJettonTransferPayload, buildTonNativeRequestFromJettonTransfer } from './jetton-transfer';
4
5
  export { TonConnectConnector } from './ton-connect-connector';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AACxD,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;AAChC,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AACxD,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;AAChC,OAAO,EACL,0BAA0B,EAC1B,uCAAuC,EACxC,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAA"}
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { NamespacedStorage } from './namespaced-storage';
2
2
  export { isValidBase64 } from './validation';
3
3
  export { buildTonTransactionRequest, buildSignDataPayload, executeSignData, unwrapBoc, bocToHash, toFriendlyAddress } from './ton-transaction-utils';
4
+ export { buildJettonTransferPayload, buildTonNativeRequestFromJettonTransfer } from './jetton-transfer';
4
5
  export { TonConnectConnector } from './ton-connect-connector';
5
6
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AACxD,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;AAChC,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AACxD,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;AAChC,OAAO,EACL,0BAA0B,EAC1B,uCAAuC,EACxC,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAA"}
@@ -0,0 +1,27 @@
1
+ import type { TonJettonTransferParams, TonNativeTransferRequest } from '@meshconnect/uwc-types';
2
+ /**
3
+ * Build the TEP-74 Jetton transfer message body as a base64 BOC.
4
+ *
5
+ * Unlike EVM/Solana/TRON where wallets know how to encode token transfers,
6
+ * TON Connect wallets only accept raw messages with an optional payload.
7
+ * We must encode the Jetton transfer Cell (opcode, amount, destination, etc.)
8
+ * ourselves and pass it as the payload field.
9
+ *
10
+ * @param params - Logical jetton transfer parameters (destination, amount in base units, etc.)
11
+ * @returns Base64-encoded BOC of the transfer cell (use as payload in TonNativeTransferRequest)
12
+ */
13
+ export declare function buildJettonTransferPayload(params: TonJettonTransferParams): Promise<string>;
14
+ /**
15
+ * Build a TonNativeTransferRequest for a Jetton transfer.
16
+ *
17
+ * Wraps the encoded Cell into a standard TonNativeTransferRequest. The field mapping
18
+ * is intentionally different from a native TON transfer:
19
+ * - `to` = sender's Jetton wallet contract (not the recipient — the recipient is inside the Cell)
20
+ * - `amount` = gas in nanotons (not the token amount — that's also inside the Cell)
21
+ * - `payload` = the TEP-74 Cell built by buildJettonTransferPayload
22
+ *
23
+ * The caller must resolve the sender's Jetton wallet address (e.g. via get_wallet_address
24
+ * on the Jetton master, or computed locally from the wallet code + owner address).
25
+ */
26
+ export declare function buildTonNativeRequestFromJettonTransfer(params: TonJettonTransferParams, senderJettonWalletAddress: string, fromAddress: string, gasNanotons: string): Promise<TonNativeTransferRequest>;
27
+ //# sourceMappingURL=jetton-transfer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jetton-transfer.d.ts","sourceRoot":"","sources":["../src/jetton-transfer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,uBAAuB,EACvB,wBAAwB,EACzB,MAAM,wBAAwB,CAAA;AAgB/B;;;;;;;;;;GAUG;AACH,wBAAsB,0BAA0B,CAC9C,MAAM,EAAE,uBAAuB,GAC9B,OAAO,CAAC,MAAM,CAAC,CA8DjB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,uCAAuC,CAC3D,MAAM,EAAE,uBAAuB,EAC/B,yBAAyB,EAAE,MAAM,EACjC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,wBAAwB,CAAC,CAQnC"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * TEP-74 Jetton transfer op code. Identifies the message as a token transfer.
3
+ * This is a protocol-level constant defined in the standard — all Jetton contracts
4
+ * use this same value, so it's safe to hardcode.
5
+ * @see https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md
6
+ */
7
+ const JETTON_TRANSFER_OP = 0x0f8a7ea5;
8
+ /** TEP-74 forward_payload comment op code (0 = text comment). */
9
+ const FORWARD_PAYLOAD_OP_COMMENT = 0;
10
+ /** Max value for a 64-bit unsigned integer (TEP-74 query_id). */
11
+ const MAX_UINT64 = 18446744073709551615n;
12
+ /**
13
+ * Build the TEP-74 Jetton transfer message body as a base64 BOC.
14
+ *
15
+ * Unlike EVM/Solana/TRON where wallets know how to encode token transfers,
16
+ * TON Connect wallets only accept raw messages with an optional payload.
17
+ * We must encode the Jetton transfer Cell (opcode, amount, destination, etc.)
18
+ * ourselves and pass it as the payload field.
19
+ *
20
+ * @param params - Logical jetton transfer parameters (destination, amount in base units, etc.)
21
+ * @returns Base64-encoded BOC of the transfer cell (use as payload in TonNativeTransferRequest)
22
+ */
23
+ export async function buildJettonTransferPayload(params) {
24
+ const { beginCell, Address } = await import('@ton/core');
25
+ let queryId;
26
+ try {
27
+ queryId = BigInt(params.queryId ?? '0');
28
+ }
29
+ catch {
30
+ throw new Error('queryId must be a valid integer string');
31
+ }
32
+ if (queryId < 0n || queryId > MAX_UINT64) {
33
+ throw new Error(`queryId must be a uint64 (0 to ${MAX_UINT64})`);
34
+ }
35
+ let amount;
36
+ try {
37
+ amount = BigInt(params.amount);
38
+ }
39
+ catch {
40
+ throw new Error('amount must be a valid integer string');
41
+ }
42
+ if (amount < 0n) {
43
+ throw new Error('Jetton transfer amount must be non-negative');
44
+ }
45
+ const destination = Address.parse(params.destination);
46
+ const responseDestination = Address.parse(params.responseDestination);
47
+ let forwardTonAmount;
48
+ try {
49
+ forwardTonAmount = BigInt(params.forwardTonAmount ?? '0');
50
+ }
51
+ catch {
52
+ throw new Error('forwardTonAmount must be a valid integer string');
53
+ }
54
+ if (forwardTonAmount < 0n) {
55
+ throw new Error('forwardTonAmount must be non-negative');
56
+ }
57
+ const hasForwardComment = params.forwardComment != null &&
58
+ params.forwardComment.length > 0 &&
59
+ forwardTonAmount > 0n;
60
+ const body = beginCell()
61
+ .storeUint(JETTON_TRANSFER_OP, 32)
62
+ .storeUint(queryId, 64)
63
+ .storeCoins(amount)
64
+ .storeAddress(destination)
65
+ .storeAddress(responseDestination)
66
+ .storeBit(0) // no custom_payload
67
+ .storeCoins(forwardTonAmount);
68
+ if (hasForwardComment) {
69
+ const forwardPayload = beginCell()
70
+ .storeUint(FORWARD_PAYLOAD_OP_COMMENT, 32)
71
+ .storeStringTail(params.forwardComment)
72
+ .endCell();
73
+ body.storeBit(1);
74
+ body.storeRef(forwardPayload);
75
+ }
76
+ else {
77
+ body.storeBit(0);
78
+ }
79
+ const cell = body.endCell();
80
+ return cell.toBoc().toString('base64');
81
+ }
82
+ /**
83
+ * Build a TonNativeTransferRequest for a Jetton transfer.
84
+ *
85
+ * Wraps the encoded Cell into a standard TonNativeTransferRequest. The field mapping
86
+ * is intentionally different from a native TON transfer:
87
+ * - `to` = sender's Jetton wallet contract (not the recipient — the recipient is inside the Cell)
88
+ * - `amount` = gas in nanotons (not the token amount — that's also inside the Cell)
89
+ * - `payload` = the TEP-74 Cell built by buildJettonTransferPayload
90
+ *
91
+ * The caller must resolve the sender's Jetton wallet address (e.g. via get_wallet_address
92
+ * on the Jetton master, or computed locally from the wallet code + owner address).
93
+ */
94
+ export async function buildTonNativeRequestFromJettonTransfer(params, senderJettonWalletAddress, fromAddress, gasNanotons) {
95
+ const payload = await buildJettonTransferPayload(params);
96
+ return {
97
+ to: senderJettonWalletAddress,
98
+ amount: gasNanotons,
99
+ from: fromAddress,
100
+ payload
101
+ };
102
+ }
103
+ //# sourceMappingURL=jetton-transfer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jetton-transfer.js","sourceRoot":"","sources":["../src/jetton-transfer.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meshconnect/uwc-ton-connector",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "TON Connect connector for Universal Wallet Connector — mobile QR/URI wallet connections",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -18,8 +18,8 @@
18
18
  "dependencies": {
19
19
  "@ton/core": "^0.63.1",
20
20
  "@tonconnect/sdk": "^3.0.5",
21
- "@meshconnect/uwc-constants": "0.5.3",
22
- "@meshconnect/uwc-types": "0.9.0"
21
+ "@meshconnect/uwc-constants": "0.5.5",
22
+ "@meshconnect/uwc-types": "0.10.1"
23
23
  },
24
24
  "devDependencies": {
25
25
  "typescript": "^5.9.3"
package/src/index.ts CHANGED
@@ -8,4 +8,8 @@ export {
8
8
  bocToHash,
9
9
  toFriendlyAddress
10
10
  } from './ton-transaction-utils'
11
+ export {
12
+ buildJettonTransferPayload,
13
+ buildTonNativeRequestFromJettonTransfer
14
+ } from './jetton-transfer'
11
15
  export { TonConnectConnector } from './ton-connect-connector'
@@ -0,0 +1,199 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { Cell } from '@ton/core'
3
+ import {
4
+ buildJettonTransferPayload,
5
+ buildTonNativeRequestFromJettonTransfer
6
+ } from './jetton-transfer'
7
+ import type { TonJettonTransferParams } from '@meshconnect/uwc-types'
8
+
9
+ // Any valid TON address works for testing Cell encoding
10
+ const TEST_ADDRESS = 'EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs'
11
+ const DEST = TEST_ADDRESS
12
+ const RESPONSE_DEST = TEST_ADDRESS
13
+
14
+ describe('buildJettonTransferPayload', () => {
15
+ it('returns base64 BOC with correct op code and amount', async () => {
16
+ const params: TonJettonTransferParams = {
17
+ destination: DEST,
18
+ amount: '1000000',
19
+ responseDestination: RESPONSE_DEST
20
+ }
21
+ const payload = await buildJettonTransferPayload(params)
22
+ expect(typeof payload).toBe('string')
23
+ expect(payload.length).toBeGreaterThan(0)
24
+ const cell = Cell.fromBase64(payload)
25
+ const slice = cell.beginParse()
26
+ const op = slice.loadUint(32)
27
+ expect(op).toBe(0x0f8a7ea5)
28
+ const queryId = slice.loadUintBig(64)
29
+ expect(queryId).toBe(0n)
30
+ const amount = slice.loadCoins()
31
+ expect(amount).toBe(1000000n)
32
+ })
33
+
34
+ it('includes forward comment when forwardTonAmount > 0', async () => {
35
+ const params: TonJettonTransferParams = {
36
+ destination: DEST,
37
+ amount: '500000',
38
+ responseDestination: RESPONSE_DEST,
39
+ forwardTonAmount: '20000000',
40
+ forwardComment: 'for coffee'
41
+ }
42
+ const payload = await buildJettonTransferPayload(params)
43
+ const cell = Cell.fromBase64(payload)
44
+ const slice = cell.beginParse()
45
+ slice.loadUint(32) // op
46
+ slice.loadUintBig(64) // query_id
47
+ slice.loadCoins() // amount
48
+ slice.loadAddress() // destination
49
+ slice.loadAddress() // response_destination
50
+ slice.loadBit() // custom_payload
51
+ const forwardTon = slice.loadCoins()
52
+ expect(forwardTon).toBe(20000000n)
53
+ const hasForwardRef = slice.loadBit()
54
+ expect(hasForwardRef).toBe(true)
55
+ const ref = slice.loadRef()
56
+ const refSlice = ref.beginParse()
57
+ const commentOp = refSlice.loadUint(32)
58
+ expect(commentOp).toBe(0)
59
+ const comment = refSlice.loadStringTail()
60
+ expect(comment).toBe('for coffee')
61
+ })
62
+
63
+ it('uses custom queryId when provided', async () => {
64
+ const params: TonJettonTransferParams = {
65
+ destination: DEST,
66
+ amount: '1',
67
+ responseDestination: RESPONSE_DEST,
68
+ queryId: '12345'
69
+ }
70
+ const payload = await buildJettonTransferPayload(params)
71
+ const cell = Cell.fromBase64(payload)
72
+ const slice = cell.beginParse()
73
+ slice.loadUint(32)
74
+ const queryId = slice.loadUintBig(64)
75
+ expect(queryId).toBe(12345n)
76
+ })
77
+
78
+ it('throws when forwardTonAmount is negative', async () => {
79
+ const params: TonJettonTransferParams = {
80
+ destination: DEST,
81
+ amount: '1000000',
82
+ responseDestination: RESPONSE_DEST,
83
+ forwardTonAmount: '-1'
84
+ }
85
+ await expect(buildJettonTransferPayload(params)).rejects.toThrow(
86
+ 'forwardTonAmount must be non-negative'
87
+ )
88
+ })
89
+
90
+ it('throws when queryId exceeds uint64 max', async () => {
91
+ const params: TonJettonTransferParams = {
92
+ destination: DEST,
93
+ amount: '1000000',
94
+ responseDestination: RESPONSE_DEST,
95
+ queryId: '18446744073709551616' // 2^64, one above max
96
+ }
97
+ await expect(buildJettonTransferPayload(params)).rejects.toThrow(
98
+ 'queryId must be a uint64'
99
+ )
100
+ })
101
+
102
+ it('throws when queryId is negative', async () => {
103
+ const params: TonJettonTransferParams = {
104
+ destination: DEST,
105
+ amount: '1000000',
106
+ responseDestination: RESPONSE_DEST,
107
+ queryId: '-1'
108
+ }
109
+ await expect(buildJettonTransferPayload(params)).rejects.toThrow(
110
+ 'queryId must be a uint64'
111
+ )
112
+ })
113
+
114
+ it('accepts zero amount', async () => {
115
+ const params: TonJettonTransferParams = {
116
+ destination: DEST,
117
+ amount: '0',
118
+ responseDestination: RESPONSE_DEST
119
+ }
120
+ const payload = await buildJettonTransferPayload(params)
121
+ const cell = Cell.fromBase64(payload)
122
+ const slice = cell.beginParse()
123
+ slice.loadUint(32) // op
124
+ slice.loadUintBig(64) // queryId
125
+ const amount = slice.loadCoins()
126
+ expect(amount).toBe(0n)
127
+ })
128
+
129
+ it('throws when amount is not a valid integer string', async () => {
130
+ const params: TonJettonTransferParams = {
131
+ destination: DEST,
132
+ amount: 'abc',
133
+ responseDestination: RESPONSE_DEST
134
+ }
135
+ await expect(buildJettonTransferPayload(params)).rejects.toThrow(
136
+ 'amount must be a valid integer string'
137
+ )
138
+ })
139
+
140
+ it('throws when queryId is not a valid integer string', async () => {
141
+ const params: TonJettonTransferParams = {
142
+ destination: DEST,
143
+ amount: '1000000',
144
+ responseDestination: RESPONSE_DEST,
145
+ queryId: 'not-a-number'
146
+ }
147
+ await expect(buildJettonTransferPayload(params)).rejects.toThrow(
148
+ 'queryId must be a valid integer string'
149
+ )
150
+ })
151
+
152
+ it('throws when forwardTonAmount is not a valid integer string', async () => {
153
+ const params: TonJettonTransferParams = {
154
+ destination: DEST,
155
+ amount: '1000000',
156
+ responseDestination: RESPONSE_DEST,
157
+ forwardTonAmount: 'xyz'
158
+ }
159
+ await expect(buildJettonTransferPayload(params)).rejects.toThrow(
160
+ 'forwardTonAmount must be a valid integer string'
161
+ )
162
+ })
163
+
164
+ it('throws when amount is negative', async () => {
165
+ const params: TonJettonTransferParams = {
166
+ destination: DEST,
167
+ amount: '-1',
168
+ responseDestination: RESPONSE_DEST
169
+ }
170
+ await expect(buildJettonTransferPayload(params)).rejects.toThrow(
171
+ 'Jetton transfer amount must be non-negative'
172
+ )
173
+ })
174
+ })
175
+
176
+ describe('buildTonNativeRequestFromJettonTransfer', () => {
177
+ it('builds request with to = sender jetton wallet and payload from params', async () => {
178
+ const params: TonJettonTransferParams = {
179
+ destination: DEST,
180
+ amount: '1000000',
181
+ responseDestination: RESPONSE_DEST
182
+ }
183
+ const senderJettonWallet = TEST_ADDRESS
184
+ const from = TEST_ADDRESS
185
+ const gas = '100000000'
186
+
187
+ const request = await buildTonNativeRequestFromJettonTransfer(
188
+ params,
189
+ senderJettonWallet,
190
+ from,
191
+ gas
192
+ )
193
+
194
+ expect(request.to).toBe(senderJettonWallet)
195
+ expect(request.amount).toBe(gas)
196
+ expect(request.from).toBe(from)
197
+ expect(request.payload).toBe(await buildJettonTransferPayload(params))
198
+ })
199
+ })
@@ -0,0 +1,122 @@
1
+ import type {
2
+ TonJettonTransferParams,
3
+ TonNativeTransferRequest
4
+ } from '@meshconnect/uwc-types'
5
+
6
+ /**
7
+ * TEP-74 Jetton transfer op code. Identifies the message as a token transfer.
8
+ * This is a protocol-level constant defined in the standard — all Jetton contracts
9
+ * use this same value, so it's safe to hardcode.
10
+ * @see https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md
11
+ */
12
+ const JETTON_TRANSFER_OP = 0x0f8a7ea5
13
+
14
+ /** TEP-74 forward_payload comment op code (0 = text comment). */
15
+ const FORWARD_PAYLOAD_OP_COMMENT = 0
16
+
17
+ /** Max value for a 64-bit unsigned integer (TEP-74 query_id). */
18
+ const MAX_UINT64 = 18446744073709551615n
19
+
20
+ /**
21
+ * Build the TEP-74 Jetton transfer message body as a base64 BOC.
22
+ *
23
+ * Unlike EVM/Solana/TRON where wallets know how to encode token transfers,
24
+ * TON Connect wallets only accept raw messages with an optional payload.
25
+ * We must encode the Jetton transfer Cell (opcode, amount, destination, etc.)
26
+ * ourselves and pass it as the payload field.
27
+ *
28
+ * @param params - Logical jetton transfer parameters (destination, amount in base units, etc.)
29
+ * @returns Base64-encoded BOC of the transfer cell (use as payload in TonNativeTransferRequest)
30
+ */
31
+ export async function buildJettonTransferPayload(
32
+ params: TonJettonTransferParams
33
+ ): Promise<string> {
34
+ const { beginCell, Address } = await import('@ton/core')
35
+
36
+ let queryId: bigint
37
+ try {
38
+ queryId = BigInt(params.queryId ?? '0')
39
+ } catch {
40
+ throw new Error('queryId must be a valid integer string')
41
+ }
42
+ if (queryId < 0n || queryId > MAX_UINT64) {
43
+ throw new Error(`queryId must be a uint64 (0 to ${MAX_UINT64})`)
44
+ }
45
+
46
+ let amount: bigint
47
+ try {
48
+ amount = BigInt(params.amount)
49
+ } catch {
50
+ throw new Error('amount must be a valid integer string')
51
+ }
52
+ if (amount < 0n) {
53
+ throw new Error('Jetton transfer amount must be non-negative')
54
+ }
55
+
56
+ const destination = Address.parse(params.destination)
57
+ const responseDestination = Address.parse(params.responseDestination)
58
+
59
+ let forwardTonAmount: bigint
60
+ try {
61
+ forwardTonAmount = BigInt(params.forwardTonAmount ?? '0')
62
+ } catch {
63
+ throw new Error('forwardTonAmount must be a valid integer string')
64
+ }
65
+ if (forwardTonAmount < 0n) {
66
+ throw new Error('forwardTonAmount must be non-negative')
67
+ }
68
+ const hasForwardComment =
69
+ params.forwardComment != null &&
70
+ params.forwardComment.length > 0 &&
71
+ forwardTonAmount > 0n
72
+
73
+ const body = beginCell()
74
+ .storeUint(JETTON_TRANSFER_OP, 32)
75
+ .storeUint(queryId, 64)
76
+ .storeCoins(amount)
77
+ .storeAddress(destination)
78
+ .storeAddress(responseDestination)
79
+ .storeBit(0) // no custom_payload
80
+ .storeCoins(forwardTonAmount)
81
+
82
+ if (hasForwardComment) {
83
+ const forwardPayload = beginCell()
84
+ .storeUint(FORWARD_PAYLOAD_OP_COMMENT, 32)
85
+ .storeStringTail(params.forwardComment as string)
86
+ .endCell()
87
+ body.storeBit(1)
88
+ body.storeRef(forwardPayload)
89
+ } else {
90
+ body.storeBit(0)
91
+ }
92
+
93
+ const cell = body.endCell()
94
+ return cell.toBoc().toString('base64')
95
+ }
96
+
97
+ /**
98
+ * Build a TonNativeTransferRequest for a Jetton transfer.
99
+ *
100
+ * Wraps the encoded Cell into a standard TonNativeTransferRequest. The field mapping
101
+ * is intentionally different from a native TON transfer:
102
+ * - `to` = sender's Jetton wallet contract (not the recipient — the recipient is inside the Cell)
103
+ * - `amount` = gas in nanotons (not the token amount — that's also inside the Cell)
104
+ * - `payload` = the TEP-74 Cell built by buildJettonTransferPayload
105
+ *
106
+ * The caller must resolve the sender's Jetton wallet address (e.g. via get_wallet_address
107
+ * on the Jetton master, or computed locally from the wallet code + owner address).
108
+ */
109
+ export async function buildTonNativeRequestFromJettonTransfer(
110
+ params: TonJettonTransferParams,
111
+ senderJettonWalletAddress: string,
112
+ fromAddress: string,
113
+ gasNanotons: string
114
+ ): Promise<TonNativeTransferRequest> {
115
+ const payload = await buildJettonTransferPayload(params)
116
+ return {
117
+ to: senderJettonWalletAddress,
118
+ amount: gasNanotons,
119
+ from: fromAddress,
120
+ payload
121
+ }
122
+ }
@@ -501,6 +501,42 @@ describe('TonConnectConnector', () => {
501
501
  expect(txArg.from).toBe('0:sender_raw')
502
502
  })
503
503
 
504
+ it('sends a pre-built Jetton transfer (payload in TonNativeTransferRequest)', async () => {
505
+ simulateSuccessfulConnect()
506
+ await connector.connect(TON_NETWORK, PROVIDER)
507
+
508
+ getLastSdkInstance()!.account = {
509
+ address: '0:abcdef1234567890',
510
+ chain: '-239'
511
+ }
512
+ mockSendTransaction.mockResolvedValue({ boc: 'te6cckJettonBoc...' })
513
+
514
+ // Simulate what the app layer does: buildTonNativeRequestFromJettonTransfer
515
+ // wraps a Jetton Cell into a TonNativeTransferRequest where:
516
+ // to = sender's Jetton wallet (NOT the recipient)
517
+ // amount = gas nanotons (NOT the token amount)
518
+ // payload = base64 BOC of the TEP-74 Cell
519
+ // Use a real base64 string to pass payload validation
520
+ const fakeJettonPayload = btoa('jetton-cell-boc')
521
+
522
+ const result = await connector.sendTransaction!({
523
+ to: 'EQBsenderJettonWallet',
524
+ amount: '100000000', // 0.1 TON gas
525
+ from: 'UQBsender',
526
+ payload: fakeJettonPayload
527
+ })
528
+
529
+ expect(mockSendTransaction).toHaveBeenCalled()
530
+ const txArg = mockSendTransaction.mock.calls[0][0]
531
+ // The SDK message should target the Jetton wallet, not a recipient
532
+ expect(txArg.messages[0].address).toBe('EQBsenderJettonWallet')
533
+ // Amount is gas, not token amount
534
+ expect(txArg.messages[0].amount).toBe('100000000')
535
+ // Payload (Jetton Cell BOC) is passed through
536
+ expect(txArg.messages[0].payload).toBe(fakeJettonPayload)
537
+ expect(result).toBe('hash_of_te6cckJettonBoc...')
538
+ })
539
+
504
540
  it('wraps UserRejectsError with clear message', async () => {
505
541
  simulateSuccessfulConnect()
506
542
  await connector.connect(TON_NETWORK, PROVIDER)