Skip to content

Advanced transfer links & signData update #426

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/extension/src/components/Notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const Notifications = () => {
}}
/>
<SignDataNotification
origin={data?.origin}
origin={data?.kind === 'tonConnectSign' ? data.manifest.url : undefined}
params={data?.kind === 'tonConnectSign' ? data.data : null}
handleClose={(payload?: SignDataResponse) => {
if (!data) return;
Expand Down
6 changes: 3 additions & 3 deletions apps/extension/src/libs/event.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { EventEmitter, IEventEmitter } from '@tonkeeper/core/dist/entries/eventEmitter';
import {
ConnectRequest,
ConnectRequest, DAppManifest,
SignDataRequestPayload,
TonConnectTransactionPayload
} from '@tonkeeper/core/dist/entries/tonConnect';
} from "@tonkeeper/core/dist/entries/tonConnect";
import { ProxyConfiguration } from '../entries/proxy';
import { Account } from '@tonkeeper/core/dist/entries/account';
import { Network } from '@tonkeeper/core/dist/entries/network';
Expand All @@ -24,7 +24,7 @@ export type NotificationFields<Kind extends string, Value> = {
logo?: string;
origin: string;
data: Value;
};
} & (Kind extends 'tonConnectRequest' ? {} : { manifest: DAppManifest });

export type NotificationData =
| NotificationFields<'tonConnectRequest', ConnectRequest>
Expand Down
6 changes: 4 additions & 2 deletions apps/extension/src/libs/service/dApp/tonConnectService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ export const tonConnectTransaction = async (
id,
logo: await getActiveTabLogo(),
origin,
data
data,
manifest: connection.connection.manifest
});

try {
Expand Down Expand Up @@ -200,7 +201,8 @@ export const tonConnectSignData = async (
id,
logo: await getActiveTabLogo(),
origin,
data
data,
manifest: connection.connection.manifest
});

try {
Expand Down
3 changes: 1 addition & 2 deletions apps/extension/src/provider/tonconnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
TonConnectEventPayload
} from '@tonkeeper/core/dist/entries/tonConnect';
import { TonProvider } from '../provider/index';
import { typeOf } from 'react-is';

const formatConnectEventError = (error: TonConnectError): ConnectEventError => {
return {
Expand Down Expand Up @@ -171,7 +170,7 @@ export class TonConnect implements TonConnectBridge {
try {
const payload = Array.isArray(message.params)
? message.params.map(item => JSON.parse(item))
: message.params;
: JSON.parse(message.params);

const result = await this.provider.send<string>(
`tonConnect_${message.method}`,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/entries/tonConnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ export type SignDataFeature = {
export interface SignDataRpcRequest {
id: string;
method: 'signData';
params: SignDataRequestPayload;
params: [string];
}

export type SignDataRequestPayload = SignDataRequestPayloadKind;
Expand Down
222 changes: 222 additions & 0 deletions packages/core/src/service/deeplinkingService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import queryString from 'query-string';
import { seeIfValidTonAddress, seeIfValidTronAddress } from '../utils/common';
import { TON_CONNECT_MSG_VARIANTS_ID, TonConnectTransactionPayload } from '../entries/tonConnect';
import {
Address,
beginCell,
Cell,
comment,
CommonMessageInfoRelaxedInternal,
storeStateInit
} from '@ton/core';
import { DNSApi } from '../tonApiV2';
import { APIConfig } from '../entries/apis';
import { JettonEncoder } from './ton-blockchain/encoder/jetton-encoder';

export function seeIfBringToFrontLink(options: { url: string }) {
const { query } = queryString.parseUrl(options.url);
Expand Down Expand Up @@ -58,6 +70,163 @@ export function parseTonTransferWithAddress(options: { url: string }) {
}
}

// eslint-disable-next-line complexity
export async function parseTonTransaction(
url: string,
{
api,
walletAddress,
batteryResponse,
gaslessResponse
}: {
api: APIConfig;
walletAddress: string;
batteryResponse: string;
gaslessResponse: string;
}
): Promise<
| {
type: 'complex';
params: TonConnectTransactionPayload;
}
| {
type: 'simple';
params: Omit<TonTransferParams, 'address'> & { address: string };
}
| null
> {
try {
const data = queryString.parseUrl(url);

let paths = data.url.split('/');
paths = paths.slice(2);

if (paths.length !== 2 || paths[0] !== 'transfer') {
throw new Error('Unsupported link');
}

const addrParam = paths[1];
if (typeof addrParam !== 'string') {
throw new Error('Unsupported link: wrong address');
}

let to = undefined;
if (seeIfValidTonAddress(addrParam)) {
to = addrParam;
} else {
const result = await new DNSApi(api.tonApiV2).dnsResolve({ domainName: addrParam });
if (result.wallet?.address && seeIfValidTonAddress(result.wallet?.address)) {
to = result.wallet?.address;
} else {
throw new Error('Unsupported link: wrong dns');
}
}

let value: string;
if (data.query.amount && typeof data.query.amount === 'string') {
if (isFinite(parseInt(data.query.amount))) {
value = data.query.amount;
} else {
throw new Error('Unsupported link: no amount');
}
} else {
return {
type: 'simple',
params: {
address: to,
text: typeof data.query.text === 'string' ? data.query.text : undefined,
jetton: typeof data.query.jetton === 'string' ? data.query.jetton : undefined
}
};
}

let validUntil = Math.ceil(Date.now() / 1000 + 5 * 60);
if (
data.query.exp &&
typeof data.query.exp === 'string' &&
isFinite(parseInt(data.query.exp))
) {
validUntil = parseInt(data.query.exp);
if (validUntil - 2000 < Date.now() / 1000) {
throw new Error('Unsupported link: expired');
}
}

let payload = undefined;
if (data.query.bin && typeof data.query.bin === 'string') {
payload = data.query.bin;
}

if (data.query.text && typeof data.query.text === 'string') {
if (payload !== undefined) {
throw new Error('Unsupported link: payload and text');
}

payload = comment(data.query.text).toBoc().toString('base64');
}

if (data.query.jetton && typeof data.query.jetton === 'string') {
const jetton = data.query.jetton;
if (!seeIfValidTonAddress(jetton)) {
throw new Error('Unsupported link: wrong jetton address');
}

const params = {
valid_until: validUntil,
messages: await encodeJettonMessage(
{ to, value, payload, jetton },
{ api, walletAddress }
),
messagesVariants: {
[TON_CONNECT_MSG_VARIANTS_ID.BATTERY]: {
messages: await encodeJettonMessage(
{ to, value, payload, jetton, responseAddress: batteryResponse },
{ api, walletAddress }
)
},
[TON_CONNECT_MSG_VARIANTS_ID.GASLESS]: {
messages: await encodeJettonMessage(
{ to, value, payload, jetton, responseAddress: gaslessResponse },
{ api, walletAddress }
),
options: {
asset: jetton
}
}
}
} satisfies TonConnectTransactionPayload;
return {
type: 'complex',
params
};
}

let stateInit = undefined;
if (data.query.init && typeof data.query.init === 'string') {
stateInit = data.query.init;
}

const params = {
valid_until: validUntil,
messages: [
{
address: to,
amount: value,
payload,
stateInit
}
]
} satisfies TonConnectTransactionPayload;
return {
type: 'complex',
params
};
} catch (e) {
console.error(e);
return null;
}
}

export function parseTronTransferWithAddress(options: { url: string }) {
try {
const data = queryString.parseUrl(options.url);
Expand Down Expand Up @@ -90,3 +259,56 @@ export function parseTronTransferWithAddress(options: { url: string }) {
return null;
}
}

async function encodeJettonMessage(
{
to,
value,
payload,
jetton,
responseAddress
}: { to: string; value: string; payload?: string; jetton: string; responseAddress?: string },
{
api,
walletAddress
}: {
api: APIConfig;
walletAddress: string;
}
) {
const je = new JettonEncoder(api, walletAddress);
const jettonTransfer = await je.encodeTransfer({
to,
amount: {
asset: {
address: Address.parse(jetton)
},
stringWeiAmount: value
},
responseAddress,
payload: payload
? {
type: 'raw',
value: Cell.fromBase64(payload)
}
: undefined
});

if (jettonTransfer.messages.length !== 1) {
throw new Error('Unsupported link: wrong jetton encoding result');
}

const msg = jettonTransfer.messages[0];
const info = msg.info as CommonMessageInfoRelaxedInternal;

return [
{
address: info.dest.toRawString(),
amount: info.value.coins.toString(),
payload: msg.body.toBoc().toString('base64'),
stateInit: msg.init
? beginCell().store(storeStateInit(msg.init)).endCell().toBoc().toString('base64')
: undefined
}
] satisfies TonConnectTransactionPayload['messages'];
}
18 changes: 4 additions & 14 deletions packages/core/src/service/sign/signUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
/**
* Creates hash for text or binary payload.
* Message format:
* message = "sign-data/" || workchain || address_hash || domain_len || domain || timestamp || payload
* message = 0xffff ++ "ton-connect/sign-data/" ++ workchain ++ address_hash ++ domain_len ++ domain ++ timestamp ++ payload
* finalMessage = 0xffff || "ton-connect" || sha256(message)
*/
export function createTextBinaryHash(
Expand Down Expand Up @@ -44,9 +44,9 @@ export function createTextBinaryHash(
const payloadLenBuffer = Buffer.alloc(4);
payloadLenBuffer.writeUInt32BE(payloadBuffer.length);

// Build message
const message = Buffer.concat([
Buffer.from('sign-data/'),
Buffer.from([0xff, 0xff]),
Buffer.from('ton-connect/sign-data/'),
wcBuffer,
parsedAddr.hash,
domainLenBuffer,
Expand All @@ -57,17 +57,7 @@ export function createTextBinaryHash(
payloadBuffer
]);

// Hash message
const messageHash = crypto.createHash('sha256').update(message).digest();

// Create final message with prefix
const finalMessage = Buffer.concat([
Buffer.from([0xff, 0xff]),
Buffer.from('ton-connect'),
messageHash
]);

return crypto.createHash('sha256').update(finalMessage).digest();
return crypto.createHash('sha256').update(message).digest();
}

/**
Expand Down
Loading