@meshconnect/uwc-ton-connector 0.3.0 → 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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -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/package.json +3 -3
- package/src/index.ts +4 -0
- package/src/jetton-transfer.test.ts +199 -0
- package/src/jetton-transfer.ts +122 -0
- package/src/ton-connect-connector.test.ts +36 -0
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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
|
+
"version": "0.4.0",
|
|
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.
|
|
22
|
-
"@meshconnect/uwc-types": "0.
|
|
21
|
+
"@meshconnect/uwc-constants": "0.5.4",
|
|
22
|
+
"@meshconnect/uwc-types": "0.10.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"typescript": "^5.9.3"
|
package/src/index.ts
CHANGED
|
@@ -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)
|