App sends requests to the wallet. Wallet sends responses and events to the app.
type AppMessage = ConnectRequest | AppRequest;
type WalletMessage = WalletResponse | WalletEvent;
App needs to have its manifest to pass meta information to the wallet. Manifest is a JSON file named as tonconnect-manifest.json
following format:
{
"url": "<app-url>", // required
"name": "<app-name>", // required
"iconUrl": "<app-icon-url>", // required
"termsOfUseUrl": "<terms-of-use-url>", // optional
"privacyPolicyUrl": "<privacy-policy-url>" // optional
}
Best practice is to place the manifest in the root of your app, e.g. https://myapp.com/tonconnect-manifest.json
. It allows the wallet to handle your app better and improve the UX connected to your app.
Make sure that manifest is available to GET by its URL.
url
-- app URL. Will be used as the dapp identifier. Will be used to open the dapp after click to its icon in the wallet. It is recommended to pass url without closing slash, e.g. 'https://mydapp.com' instead of 'https://mydapp.com/'.name
-- app name. Might be simple, will not be used as identifier.iconUrl
-- Url to the app icon. Must be PNG, ICO, ... format. SVG icons are not supported. Perfectly pass url to a 180x180px PNG icon.termsOfUseUrl
-- (optional) url to the Terms Of Use document. Optional for usual apps, but required for the apps which is placed in the Tonkeeper recommended apps list.privacyPolicyUrl
-- (optional) url to the Privacy Policy document. Optional for usual apps, but required for the apps which is placed in the Tonkeeper recommended apps list.
App’s request message is InitialRequest.
type ConnectRequest = {
manifestUrl: string;
items: ConnectItem[], // data items to share with the app
}
// In the future we may add other personal items.
// Or, instead of the wallet address we may ask for per-service ID.
type ConnectItem = TonAddressItem | TonProofItem | ...;
type TonAddressItem = {
name: "ton_addr";
}
type TonProofItem = {
name: "ton_proof";
payload: string; // arbitrary payload, e.g. nonce + expiration timestamp.
}
ConnectRequest description:
manifestUrl
: link to the app's tonconnect-manifest.jsonitems
: data items to share with the app.
Wallet responds with ConnectEvent message if the user approves the request.
type ConnectEvent = ConnectEventSuccess | ConnectEventError;
type ConnectEventSuccess = {
event: "connect";
id: number; // increasing event counter
payload: {
items: ConnectItemReply[];
device: DeviceInfo;
}
}
type ConnectEventError = {
event: "connect_error",
id: number; // increasing event counter
payload: {
code: number;
message: string;
}
}
type DeviceInfo = {
platform: "iphone" | "ipad" | "android" | "windows" | "mac" | "linux";
appName: string; // e.g. "Tonkeeper"
appVersion: string; // e.g. "2.3.367"
maxProtocolVersion: number;
features: Feature[]; // list of supported features and methods in RPC
// Currently there is only one feature -- 'SendTransaction';
}
type Feature = { name: 'SendTransaction', maxMessages: number } | // `maxMessages` is maximum number of messages in one `SendTransaction` that the wallet supports
{ name: 'SignData' };
type ConnectItemReply = TonAddressItemReply | TonProofItemReply ...;
// Untrusted data returned by the wallet.
// If you need a guarantee that the user owns this address and public key, you need to additionally request a ton_proof.
type TonAddressItemReply = {
name: "ton_addr";
address: string; // TON address raw (`0:<hex>`)
network: NETWORK; // network global_id
publicKey: string; // HEX string without 0x
walletStateInit: string; // Base64 (not url safe) encoded stateinit cell for the wallet contract
}
type TonProofItemReply = TonProofItemReplySuccess | TonProofItemReplyError;
type TonProofItemReplySuccess = {
name: "ton_proof";
proof: {
timestamp: string; // 64-bit unix epoch time of the signing operation (seconds)
domain: {
lengthBytes: number; // AppDomain Length
value: string; // app domain name (as url part, without encoding)
};
signature: string; // base64-encoded signature
payload: string; // payload from the request
}
}
type TonProofItemReplyError = {
name: "ton_addr";
error: {
code: ConnectItemErrorCode;
message?: string;
}
}
enum NETWORK {
MAINNET = '-239',
TESTNET = '-3'
}
Connect event error codes:
code | description |
---|---|
0 | Unknown error |
1 | Bad request |
2 | App manifest not found |
3 | App manifest content error |
100 | Unknown app |
300 | User declined the connection |
Connect item error codes:
code | description |
---|---|
0 | Unknown error |
400 | Method is not supported |
If the wallet doesn't support the requested ConnectItem
(e.g. "ton_proof"), it must send reply with the following ConnectItemReply corresponding to the requested item.
with following structure:
type ConnectItemReplyError = {
name: "<requested-connect-item-name>";
error: {
code: 400;
message?: string;
}
}
If TonProofItem
is requested, wallet proves ownership of the selected account’s key. The signed message is bound to:
- Unique prefix to separate messages from on-chain messages. (
ton-connect
) - Wallet address.
- App domain
- Signing timestamp
- App’s custom payload (where server may put its nonce, cookie id, expiration time).
message = utf8_encode("ton-proof-item-v2/") ++
Address ++
AppDomain ++
Timestamp ++
Payload
signature = Ed25519Sign(privkey, sha256(0xffff ++ utf8_encode("ton-connect") ++ sha256(message)))
where:
Address
is the wallet address encoded as a sequence:workchain
: 32-bit signed integer big endian;hash
: 256-bit unsigned integer big endian;
AppDomain
is Length ++ EncodedDomainNameLength
is 32-bit value of utf-8 encoded app domain name length in bytesEncodedDomainName
idLength
-byte utf-8 encoded app domain name
Timestamp
64-bit unix epoch time of the signing operationPayload
is a variable-length binary string.
Note: payload is variable-length untrusted data. To avoid using unnecessary length prefixes we simply put it last in the message.
The signature must be verified by public key:
-
First, try to obtain public key via
get_public_key
get-method on smart contract deployed atAddress
. -
If the smart contract is not deployed yet, or the get-method is missing, you need:
2.1. Parse
TonAddressItemReply.walletStateInit
and get public key from stateInit. You can compare thewalletStateInit.code
with the code of standard wallets contracts and parse the data according to the found wallet version.2.2. Check that
TonAddressItemReply.publicKey
equals to obtained public key2.3. Check that
TonAddressItemReply.walletStateInit.hash()
equals toTonAddressItemReply.address
..hash()
means BoC hash.
- All messages from the app to the wallet are requests for an operation.
- Messages from the wallet to the application can be either responses to app requests or events triggered by user actions on the side of the wallet.
Available operations:
- sendTransaction
- signData
- disconnect
Available events:
- connect
- connect_error
- disconnect
All app requests have the following structure (like json-rpc 2.0)
interface AppRequest {
method: string;
params: string[];
id: string;
}
Where
method
: name of the operation ('sendTransaction', 'signMessage', ...)params
: array of the operation specific parametersid
: increasing identifier that allows to match requests and responses
Wallet messages are responses or events.
Response is an object formatted as a json-rpc 2.0 response. Response id
must match request's id.
Wallet doesn't accept any request with an id that does not greater the last processed request id of that session.
type WalletResponse = WalletResponseSuccess | WalletResponseError;
interface WalletResponseSuccess {
result: string;
id: string;
}
interface WalletResponseError {
error: { code: number; message: string; data?: unknown };
id: string;
}
Event is an object with property event
that is equal to event's name, id
that is increasing event counter (not related to request.id
because there is no request for an event), and payload
that contains event additional data.
interface WalletEvent {
event: WalletEventName;
id: number; // increasing event counter
payload: <event-payload>; // specific payload for each event
}
type WalletEventName = 'connect' | 'connect_error' | 'disconnect';
Wallet must increase id
while generating a new event. (Every next event must have id
> previous event id
)
DApp doesn't accept any event with an id that does not greater the last processed event id of that session.
App sends SendTransactionRequest:
interface SendTransactionRequest {
method: 'sendTransaction';
params: [<transaction-payload>];
id: string;
}
Where <transaction-payload>
is JSON with following properties:
valid_until
(integer, optional): unix timestamp. after th moment transaction will be invalid.network
(NETWORK, optional): The network (mainnet or testnet) where DApp intends to send the transaction. If not set, the transaction is sent to the network currently set in the wallet, but this is not safe and DApp should always strive to set the network. If thenetwork
parameter is set, but the wallet has a different network set, the wallet should show an alert and DO NOT ALLOW TO SEND this transaction.from
(string in : format, optional) - The sender address from which DApp intends to send the transaction. If not set, wallet allows user to select the sender's address at the moment of transaction approval. Iffrom
parameter is set, the wallet should DO NOT ALLOW user to select the sender's address; If sending from the specified address is impossible, the wallet should show an alert and DO NOT ALLOW TO SEND this transaction.messages
(array of messages): 1-4 outgoing messages from the wallet contract to other accounts. All messages are sent out in order, however the wallet cannot guarantee that messages will be delivered and executed in same order.
Message structure:
address
(string): message destination in user-friendly formatamount
(decimal string): number of nanocoins to send.payload
(string base64, optional): raw one-cell BoC encoded in Base64.stateInit
(string base64, optional): raw once-cell BoC encoded in Base64.
Common cases
1. No payload, no stateInit: simple transfer without a message. 2. payload is prefixed with 32 zero bits, no stateInit: simple transfer with a text message. 3. No payload or prefixed with 32 zero bits; stateInit is present: deployment of the contract.Example
{
"valid_until": 1658253458,
"network": "-239", // enum NETWORK { MAINNET = '-239', TESTNET = '-3'}
"from": "0:348bcf827469c5fc38541c77fdd91d4e347eac200f6f2d9fd62dc08885f0415f",
"messages": [
{
"address": "EQBBJBB3HagsujBqVfqeDUPJ0kXjgTPLWPFFffuNXNiJL0aA",
"amount": "20000000",
"stateInit": "base64bocblahblahblah==" //deploy contract
},{
"address": "EQDmnxDMhId6v1Ofg_h5KR5coWlFG6e86Ro3pc7Tq4CA0-Jn",
"amount": "60000000",
"payload": "base64bocblahblahblah==" //transfer nft to new deployed account 0:412410771DA82CBA306A55FA9E0D43C9D245E38133CB58F1457DFB8D5CD8892F
}
]
}
Wallet replies with SendTransactionResponse:
type SendTransactionResponse = SendTransactionResponseSuccess | SendTransactionResponseError;
interface SendTransactionResponseSuccess {
result: <boc>;
id: string;
}
interface SendTransactionResponseError {
error: { code: number; message: string };
id: string;
}
Error codes:
code | description |
---|---|
0 | Unknown error |
1 | Bad request |
100 | Unknown app |
300 | User declined the transaction |
400 | Method not supported |
WARNING: this is currently an experimental method and its signature may change in the future
App sends SignDataRequest:
interface SignDataRequest {
method: 'signData';
params: [<sign-data-payload>];
id: string;
}
Where <sign-data-payload>
is JSON with following properties:
schema_crc
(integer): indicates the layout of payload cell that in turn defines domain separation.cell
(string, base64 encoded Cell): contains arbitrary data per its TL-B definition.publicKey
(HEX string without 0x, optional): The public key of key pair from which DApp intends to sign the data. If not set, the wallet is not limited in what keypair to sign. IfpublicKey
parameter is set, the wallet SHOULD to sign by keypair corresponding this public key; If sign by this specified keypair is impossible, the wallet should show an alert and DO NOT ALLOW TO SIGN this data.
The signature will be computed in the following way:
ed25519(uint32be(schema_crc) ++ uint64be(timestamp) ++ cell_hash(X), privkey)
Wallet should decode the cell in accordance with the schema_crc and show corresponding data to the user. If the schema_crc is unknown to the wallet, the wallet should show danger notification/UI to the user.
Wallet replies with SignDataResponse:
type SignDataResponse = SignDataResponseSuccess | SignDataResponseError;
interface SignDataResponseSuccess {
result: {
signature: string; // base64 encoded signature
timestamp: string; // UNIX timestamp in seconds (UTC) at the moment on creating the signature.
};
id: string;
}
interface SignDataResponseError {
error: { code: number; message: string };
id: string;
}
Error codes:
code | description |
---|---|
0 | Unknown error |
1 | Bad request |
100 | Unknown app |
300 | User declined the request |
400 | Method not supported |
When user disconnects the wallet in the dApp, dApp should inform the wallet to help the wallet save resources and delete unnecessary session. Allows the wallet to update its interface to the disconnected state.
interface DisconnectRequest {
method: 'disconnect';
params: [];
id: string;
}
Wallet replies with DisconnectResponse:
type DisconnectResponse = DisconnectResponseSuccess | DisconnectResponseError;
interface DisconnectResponseSuccess {
id: string;
result: { };
}
interface DisconnectResponseError {
error: { code: number; message: string };
id: string;
}
Wallet shouldn't emit a 'Disconnect' event if disconnect was initialized by the dApp.
Error codes:
code | description |
---|---|
0 | Unknown error |
1 | Bad request |
100 | Unknown app |
400 | Method not supported |
Disconnect
The event fires when the user deletes the app in the wallet. The app must react to the event and delete the saved session. If the user disconnects the wallet on the app side, then the event does not fire, and the session information remains in the localstorage
interface DisconnectEvent {
type: "disconnect",
id: number; // increasing event counter
payload: { }
}
Connect
type ConnectEventSuccess = {
event: "connect";
id: number; // increasing event counter
payload: {
items: ConnectItemReply[];
device: DeviceInfo;
}
}
type ConnectEventError = {
event: "connect_error",
id: number; // increasing event counter
payload: {
code: number;
message: string;
}
}