@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,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
|
+
}
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
|
|
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
|
-
}
|
|
1
|
+
import type { IStorage } from '@tonconnect/sdk'
|
|
7
2
|
|
|
8
3
|
/**
|
|
9
4
|
* Prefixed localStorage wrapper required by @tonconnect/sdk.
|
|
@@ -11,7 +6,7 @@ export interface TonConnectStorage {
|
|
|
11
6
|
* Keys are prefixed per wallet (e.g. "tonkeeper:session") to avoid collisions.
|
|
12
7
|
* Falls back to in-memory Map when localStorage is blocked (Safari ITP in iframes).
|
|
13
8
|
*/
|
|
14
|
-
export class NamespacedStorage implements
|
|
9
|
+
export class NamespacedStorage implements IStorage {
|
|
15
10
|
private memoryFallback = new Map<string, string>()
|
|
16
11
|
|
|
17
12
|
constructor(private prefix: string) {}
|