Skip to content

Commit 1ffd208

Browse files
authored
Merge pull request #426 from tonkeeper/fix/transfer-links
Advanced transfer links & signData update
2 parents 248f396 + c79d27b commit 1ffd208

File tree

18 files changed

+344
-78
lines changed

18 files changed

+344
-78
lines changed

apps/extension/src/components/Notifications.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export const Notifications = () => {
8282
}}
8383
/>
8484
<SignDataNotification
85-
origin={data?.origin}
85+
origin={data?.kind === 'tonConnectSign' ? data.manifest.url : undefined}
8686
params={data?.kind === 'tonConnectSign' ? data.data : null}
8787
handleClose={(payload?: SignDataResponse) => {
8888
if (!data) return;

apps/extension/src/libs/event.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { EventEmitter, IEventEmitter } from '@tonkeeper/core/dist/entries/eventEmitter';
22
import {
3-
ConnectRequest,
3+
ConnectRequest, DAppManifest,
44
SignDataRequestPayload,
55
TonConnectTransactionPayload
6-
} from '@tonkeeper/core/dist/entries/tonConnect';
6+
} from "@tonkeeper/core/dist/entries/tonConnect";
77
import { ProxyConfiguration } from '../entries/proxy';
88
import { Account } from '@tonkeeper/core/dist/entries/account';
99
import { Network } from '@tonkeeper/core/dist/entries/network';
@@ -24,7 +24,7 @@ export type NotificationFields<Kind extends string, Value> = {
2424
logo?: string;
2525
origin: string;
2626
data: Value;
27-
};
27+
} & (Kind extends 'tonConnectRequest' ? {} : { manifest: DAppManifest });
2828

2929
export type NotificationData =
3030
| NotificationFields<'tonConnectRequest', ConnectRequest>

apps/extension/src/libs/service/dApp/tonConnectService.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ export const tonConnectTransaction = async (
155155
id,
156156
logo: await getActiveTabLogo(),
157157
origin,
158-
data
158+
data,
159+
manifest: connection.connection.manifest
159160
});
160161

161162
try {
@@ -200,7 +201,8 @@ export const tonConnectSignData = async (
200201
id,
201202
logo: await getActiveTabLogo(),
202203
origin,
203-
data
204+
data,
205+
manifest: connection.connection.manifest
204206
});
205207

206208
try {

apps/extension/src/provider/tonconnect.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
TonConnectEventPayload
1111
} from '@tonkeeper/core/dist/entries/tonConnect';
1212
import { TonProvider } from '../provider/index';
13-
import { typeOf } from 'react-is';
1413

1514
const formatConnectEventError = (error: TonConnectError): ConnectEventError => {
1615
return {
@@ -171,7 +170,7 @@ export class TonConnect implements TonConnectBridge {
171170
try {
172171
const payload = Array.isArray(message.params)
173172
? message.params.map(item => JSON.parse(item))
174-
: message.params;
173+
: JSON.parse(message.params);
175174

176175
const result = await this.provider.send<string>(
177176
`tonConnect_${message.method}`,

packages/core/src/entries/tonConnect.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ export type SignDataFeature = {
230230
export interface SignDataRpcRequest {
231231
id: string;
232232
method: 'signData';
233-
params: SignDataRequestPayload;
233+
params: [string];
234234
}
235235

236236
export type SignDataRequestPayload = SignDataRequestPayloadKind;

packages/core/src/service/deeplinkingService.ts

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
import queryString from 'query-string';
22
import { seeIfValidTonAddress, seeIfValidTronAddress } from '../utils/common';
3+
import { TON_CONNECT_MSG_VARIANTS_ID, TonConnectTransactionPayload } from '../entries/tonConnect';
4+
import {
5+
Address,
6+
beginCell,
7+
Cell,
8+
comment,
9+
CommonMessageInfoRelaxedInternal,
10+
storeStateInit
11+
} from '@ton/core';
12+
import { DNSApi } from '../tonApiV2';
13+
import { APIConfig } from '../entries/apis';
14+
import { JettonEncoder } from './ton-blockchain/encoder/jetton-encoder';
315

416
export function seeIfBringToFrontLink(options: { url: string }) {
517
const { query } = queryString.parseUrl(options.url);
@@ -58,6 +70,163 @@ export function parseTonTransferWithAddress(options: { url: string }) {
5870
}
5971
}
6072

73+
// eslint-disable-next-line complexity
74+
export async function parseTonTransaction(
75+
url: string,
76+
{
77+
api,
78+
walletAddress,
79+
batteryResponse,
80+
gaslessResponse
81+
}: {
82+
api: APIConfig;
83+
walletAddress: string;
84+
batteryResponse: string;
85+
gaslessResponse: string;
86+
}
87+
): Promise<
88+
| {
89+
type: 'complex';
90+
params: TonConnectTransactionPayload;
91+
}
92+
| {
93+
type: 'simple';
94+
params: Omit<TonTransferParams, 'address'> & { address: string };
95+
}
96+
| null
97+
> {
98+
try {
99+
const data = queryString.parseUrl(url);
100+
101+
let paths = data.url.split('/');
102+
paths = paths.slice(2);
103+
104+
if (paths.length !== 2 || paths[0] !== 'transfer') {
105+
throw new Error('Unsupported link');
106+
}
107+
108+
const addrParam = paths[1];
109+
if (typeof addrParam !== 'string') {
110+
throw new Error('Unsupported link: wrong address');
111+
}
112+
113+
let to = undefined;
114+
if (seeIfValidTonAddress(addrParam)) {
115+
to = addrParam;
116+
} else {
117+
const result = await new DNSApi(api.tonApiV2).dnsResolve({ domainName: addrParam });
118+
if (result.wallet?.address && seeIfValidTonAddress(result.wallet?.address)) {
119+
to = result.wallet?.address;
120+
} else {
121+
throw new Error('Unsupported link: wrong dns');
122+
}
123+
}
124+
125+
let value: string;
126+
if (data.query.amount && typeof data.query.amount === 'string') {
127+
if (isFinite(parseInt(data.query.amount))) {
128+
value = data.query.amount;
129+
} else {
130+
throw new Error('Unsupported link: no amount');
131+
}
132+
} else {
133+
return {
134+
type: 'simple',
135+
params: {
136+
address: to,
137+
text: typeof data.query.text === 'string' ? data.query.text : undefined,
138+
jetton: typeof data.query.jetton === 'string' ? data.query.jetton : undefined
139+
}
140+
};
141+
}
142+
143+
let validUntil = Math.ceil(Date.now() / 1000 + 5 * 60);
144+
if (
145+
data.query.exp &&
146+
typeof data.query.exp === 'string' &&
147+
isFinite(parseInt(data.query.exp))
148+
) {
149+
validUntil = parseInt(data.query.exp);
150+
if (validUntil - 2000 < Date.now() / 1000) {
151+
throw new Error('Unsupported link: expired');
152+
}
153+
}
154+
155+
let payload = undefined;
156+
if (data.query.bin && typeof data.query.bin === 'string') {
157+
payload = data.query.bin;
158+
}
159+
160+
if (data.query.text && typeof data.query.text === 'string') {
161+
if (payload !== undefined) {
162+
throw new Error('Unsupported link: payload and text');
163+
}
164+
165+
payload = comment(data.query.text).toBoc().toString('base64');
166+
}
167+
168+
if (data.query.jetton && typeof data.query.jetton === 'string') {
169+
const jetton = data.query.jetton;
170+
if (!seeIfValidTonAddress(jetton)) {
171+
throw new Error('Unsupported link: wrong jetton address');
172+
}
173+
174+
const params = {
175+
valid_until: validUntil,
176+
messages: await encodeJettonMessage(
177+
{ to, value, payload, jetton },
178+
{ api, walletAddress }
179+
),
180+
messagesVariants: {
181+
[TON_CONNECT_MSG_VARIANTS_ID.BATTERY]: {
182+
messages: await encodeJettonMessage(
183+
{ to, value, payload, jetton, responseAddress: batteryResponse },
184+
{ api, walletAddress }
185+
)
186+
},
187+
[TON_CONNECT_MSG_VARIANTS_ID.GASLESS]: {
188+
messages: await encodeJettonMessage(
189+
{ to, value, payload, jetton, responseAddress: gaslessResponse },
190+
{ api, walletAddress }
191+
),
192+
options: {
193+
asset: jetton
194+
}
195+
}
196+
}
197+
} satisfies TonConnectTransactionPayload;
198+
return {
199+
type: 'complex',
200+
params
201+
};
202+
}
203+
204+
let stateInit = undefined;
205+
if (data.query.init && typeof data.query.init === 'string') {
206+
stateInit = data.query.init;
207+
}
208+
209+
const params = {
210+
valid_until: validUntil,
211+
messages: [
212+
{
213+
address: to,
214+
amount: value,
215+
payload,
216+
stateInit
217+
}
218+
]
219+
} satisfies TonConnectTransactionPayload;
220+
return {
221+
type: 'complex',
222+
params
223+
};
224+
} catch (e) {
225+
console.error(e);
226+
return null;
227+
}
228+
}
229+
61230
export function parseTronTransferWithAddress(options: { url: string }) {
62231
try {
63232
const data = queryString.parseUrl(options.url);
@@ -90,3 +259,56 @@ export function parseTronTransferWithAddress(options: { url: string }) {
90259
return null;
91260
}
92261
}
262+
263+
async function encodeJettonMessage(
264+
{
265+
to,
266+
value,
267+
payload,
268+
jetton,
269+
responseAddress
270+
}: { to: string; value: string; payload?: string; jetton: string; responseAddress?: string },
271+
{
272+
api,
273+
walletAddress
274+
}: {
275+
api: APIConfig;
276+
walletAddress: string;
277+
}
278+
) {
279+
const je = new JettonEncoder(api, walletAddress);
280+
const jettonTransfer = await je.encodeTransfer({
281+
to,
282+
amount: {
283+
asset: {
284+
address: Address.parse(jetton)
285+
},
286+
stringWeiAmount: value
287+
},
288+
responseAddress,
289+
payload: payload
290+
? {
291+
type: 'raw',
292+
value: Cell.fromBase64(payload)
293+
}
294+
: undefined
295+
});
296+
297+
if (jettonTransfer.messages.length !== 1) {
298+
throw new Error('Unsupported link: wrong jetton encoding result');
299+
}
300+
301+
const msg = jettonTransfer.messages[0];
302+
const info = msg.info as CommonMessageInfoRelaxedInternal;
303+
304+
return [
305+
{
306+
address: info.dest.toRawString(),
307+
amount: info.value.coins.toString(),
308+
payload: msg.body.toBoc().toString('base64'),
309+
stateInit: msg.init
310+
? beginCell().store(storeStateInit(msg.init)).endCell().toBoc().toString('base64')
311+
: undefined
312+
}
313+
] satisfies TonConnectTransactionPayload['messages'];
314+
}

packages/core/src/service/sign/signUtils.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
/**
1313
* Creates hash for text or binary payload.
1414
* Message format:
15-
* message = "sign-data/" || workchain || address_hash || domain_len || domain || timestamp || payload
15+
* message = 0xffff ++ "ton-connect/sign-data/" ++ workchain ++ address_hash ++ domain_len ++ domain ++ timestamp ++ payload
1616
* finalMessage = 0xffff || "ton-connect" || sha256(message)
1717
*/
1818
export function createTextBinaryHash(
@@ -44,9 +44,9 @@ export function createTextBinaryHash(
4444
const payloadLenBuffer = Buffer.alloc(4);
4545
payloadLenBuffer.writeUInt32BE(payloadBuffer.length);
4646

47-
// Build message
4847
const message = Buffer.concat([
49-
Buffer.from('sign-data/'),
48+
Buffer.from([0xff, 0xff]),
49+
Buffer.from('ton-connect/sign-data/'),
5050
wcBuffer,
5151
parsedAddr.hash,
5252
domainLenBuffer,
@@ -57,17 +57,7 @@ export function createTextBinaryHash(
5757
payloadBuffer
5858
]);
5959

60-
// Hash message
61-
const messageHash = crypto.createHash('sha256').update(message).digest();
62-
63-
// Create final message with prefix
64-
const finalMessage = Buffer.concat([
65-
Buffer.from([0xff, 0xff]),
66-
Buffer.from('ton-connect'),
67-
messageHash
68-
]);
69-
70-
return crypto.createHash('sha256').update(finalMessage).digest();
60+
return crypto.createHash('sha256').update(message).digest();
7161
}
7262

7363
/**

0 commit comments

Comments
 (0)