与 JavaScript SDK 的集成手册
在本教程 中,我们将创建一个示例网页应用,支持 TON Connect 2.0 认证。这将允许进行签名验证,以消除在各方之间未建立协议时的身份冒用的可能性。
文档链接
必要条件
为了使应用和钱包之间的连接流畅,网页应用必须使用可通过钱包应用访问的 manifest。完成此项的必要条件通常是静态文件的主机。例如,假如开发者想利用 GitHub 页面,或使用托管在他们电脑上的 TON Sites 部署他们的网站。这将意味着他们的网页应用站点是公开可访问的。
获取钱包支持列表
为了提高 TON 区块链的整体采用率,TON Connect 2.0 需要能够促进大量应用和钱包连接集成。近期,TON Connect 2.0 的持续开发使得连接 Tonkeeper、TonHub、MyTonWallet 和其他钱包与各种 TON 生态系统应用成为可能。我们的使命是最终允许通过 TON Connect 协议在基于 TON 构 建的所有钱包类型与应用之间交换数据。目前,这是通过为TON Connect提供加载当前在TON生态系统中运行的可用钱包的广泛列表的能力来实现的。
目前我们的示例网页应用能够实现以下功能:
- 加载 TON Connect SDK(旨在简化集成的库),
- 创建一个连接器(当前没有应用 manifest),
- 加载支持的钱包列表(来自 GitHub 上的 wallets.json)。
为了学习目的,让我们来看看以下代码描述的 HTML 页面:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://unpkg.com/@tonconnect/sdk@latest/dist/tonconnect-sdk.min.js" defer></script> <!-- (1) -->
</head>
<body>
<script>
window.onload = async () => {
const connector = new TonConnectSDK.TonConnect(); // (2)
const walletsList = await connector.getWallets(); // (3)
console.log(walletsList);
}
</script>
</body>
</html>
如果您在浏览器中加载此页面并查看控制台,可能会得到类似以下内容:
> Array [ {…}, {…} ]
0: Object { name: "Tonkeeper", imageUrl: "https://tonkeeper.com/assets/tonconnect-icon.png", aboutUrl: "https://tonkeeper.com", … }
aboutUrl: "https://tonkeeper.com"
bridgeUrl: "https://bridge.tonapi.io/bridge"
deepLink: undefined
embedded: false
imageUrl: "https://tonkeeper.com/assets/tonconnect-icon.png"
injected: false
jsBridgeKey: "tonkeeper"
name: "Tonkeeper"
tondns: "tonkeeper.ton"
universalLink: "https://app.tonkeeper.com/ton-connect"
根据 TON Connect 2.0 规范,钱包应用信息总是使用以下格式:
{
name: string;
imageUrl: string;
tondns?: string;
aboutUrl: string;
universalLink?: string;
deepLink?: string;
bridgeUrl?: string;
jsBridgeKey?: string;
injected?: boolean; // true if this wallet is injected to the webpage
embedded?: boolean; // true if the DAppis opened inside this wallet's browser
}
不同钱包应用的按钮显示
按钮可能会根据您的网页应用设计而变化。 当前页面产生以下结果:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://unpkg.com/@tonconnect/sdk@latest/dist/tonconnect-sdk.min.js" defer></script>
<style>
body {
width: 1000px;
margin: 0 auto;
font-family: Roboto, sans-serif;
}
.section {
padding: 20px; margin: 20px;
border: 2px #AEFF6A solid; border-radius: 8px;
}
#tonconnect-buttons>button {
display: block;
padding: 8px; margin-bottom: 8px;
font-size: 18px; font-family: inherit;
}
.featured {
font-weight: 800;
}
</style>
</head>
<body>
<div class="section" id="tonconnect-buttons">
</div>
<script>
const $ = document.querySelector.bind(document);
window.onload = async () => {
const connector = new TonConnectSDK.TonConnect();
const walletsList = await connector.getWallets();
let buttonsContainer = $('#tonconnect-buttons');
for (let wallet of walletsList) {
let connectButton = document.createElement('button');
connectButton.innerText = 'Connect with ' + wallet.name;
if (wallet.embedded) {
// `embedded` means we are browsing the app from wallet application
// we need to mark this sign-in option somehow
connectButton.classList.add('featured');
}
if (!wallet.bridgeUrl && !wallet.injected && !wallet.embedded) {
// no `bridgeUrl` means this wallet app is injecting JS code
// no `injected` and no `embedded` -> app is inaccessible on this page
connectButton.disabled = true;
}
buttonsContainer.appendChild(connectButton);
}
};
</script>
</body>
</html>
请注意以下几点:
- 如果网页通过钱包应用显示,它会将
embedded
选项设置为true
。这意味着标记这个登录选项很重要,因为它是最常使用的。 - 如果一个特定的钱包只使用 JavaScript 构建(它没有
bridgeUrl
) ,并且它没有设置injected
属性(或embedded
,为了安全),那么它显然是不可访问的,按钮应该被禁用。
无应用 manifest 的连接
在没有应用 manifest 的情况下进行连接时,脚本应该如下更改:
const $ = document.querySelector.bind(document);
window.onload = async () => {
const connector = new TonConnectSDK.TonConnect();
const walletsList = await connector.getWallets();
const unsubscribe = connector.onStatusChange(
walletInfo => {
console.log('Connection status:', walletInfo);
}
);
let buttonsContainer = $('#tonconnect-buttons');
for (let wallet of walletsList) {
let connectButton = document.createElement('button');
connectButton.innerText = 'Connect with ' + wallet.name;
if (wallet.embedded) {
// `embedded` means we are browsing the app from wallet application
// we need to mark this sign-in option somehow
connectButton.classList.add('featured');
}
if (wallet.embedded || wallet.injected) {
connectButton.onclick = () => {
connectButton.disabled = true;
connector.connect({jsBridgeKey: wallet.jsBridgeKey});
};
} else if (wallet.bridgeUrl) {
connectButton.onclick = () => {
connectButton.disabled = true;
console.log('Connection link:', connector.connect({
universalLink: wallet.universalLink,
bridgeUrl: wallet.bridgeUrl
}));
};
} else {
// wallet app does not provide any auth method
connectButton.disabled = true;
}
buttonsContainer.appendChild(connectButton);
}
};
现在已经进行了上述操作,正在记录状态变化(以查看 TON Connect 是否工作)。展示用于连接的 QR 代码的modals超出了本手册的范围。为了测试目的,可以使用浏览器扩展或通过任何必要的手段将连接请求链接发送到用户的手机(例如,使用 Telegram)。 注意:我们还没有创建应用 manifest。目前,如果未满足此要求,最佳做法是分析最终结果。
使用 Tonkeeper 登录
为了用 Tonkeeper 登录,创建了以下用于认证的链接(下面提供参考):
https://app.tonkeeper.com/ton-connect?v=2&id=3c12f5311be7e305094ffbf5c9b830e53a4579b40485137f29b0ca0c893c4f31&r=%7B%22manifestUrl%22%3A%22null%2Ftonconnect-manifest.json%22%2C%22items%22%3A%5B%7B%22name%22%3A%22ton_addr%22%7D%5D%7D
当解码时,r
参数产生以下 JSON 格式:
{"manifestUrl":"null/tonconnect-manifest.json","items":[{"name":"ton_addr"}]}
点击手机链接后,Tonkeeper 自动打开然后关闭,忽略请求。此外,在网页应用页面的控制台出现以下错误:
Error: [TON_CONNECT_SDK_ERROR] Can't get null/tonconnect-manifest.json
。
这意味着应用 manifest 必须可供下载。
使用应用清单连接
从现在开始,需要在某处托管用户文件(主要是tonconnect-manifest.json)。在这个例子中,我们将使用另一个Web应用程序的清单。然而,这不推荐用于生产环境,但允许用于测试目的。
以下代码片段:
window.onload = async () => {
const connector = new TonConnectSDK.TonConnect();
const walletsList = await connector.getWallets();
const unsubscribe = connector.onStatusChange(
walletInfo => {
console.log('Connection status:', walletInfo);
}
);
必须被这个版本替换:
window.onload = async () => {
const connector = new TonConnectSDK.TonConnect({manifestUrl: 'https://ratingers.pythonanywhere.com/ratelance/tonconnect-manifest.json'});
window.connector = connector; // for experimenting in browser console
const walletsList = await connector.getWallets();
const unsubscribe = connector.onStatusChange(
walletInfo => {
console.log('Connection status:', walletInfo);
}
);
connector.restoreConnection();
在上方的新版本中,添加了将 connector
变量存储在 window
中,使其在浏览器控制台中可以访问。此外,添加了 restoreConnection
,这样用户就不必在每个Web应用程序页面都登录。
用Tonkeeper登录
如果我们拒绝钱包的请求,控制台显示的结果将是Error: [TON_CONNECT_SDK_ERROR] Wallet declined the request
。
因此,如果保存了链接,用户能够接受相同的登录请求。这意味着Web应用程序应该能够将认证拒绝视为非最终状态,以确保其正确工作。
之后,接受登录请求,浏览器控制台立即反映如下:
22:40:13.887 Connection status:
Object { device: {…}, provider: "http", account: {…} }
account: Object { address: "0:b2a1ec...", chain: "-239", walletStateInit: "te6cckECFgEAAwQAAgE0ARUBFP8A9..." }
device: Object {platform: "android", appName: "Tonkeeper", appVersion: "2.8.0.261", …}
provider: "http"
以上结果考虑了以下内容:
- 账户:包含地址(工作链+哈希)、网络(主网/测试网)以及用于提取公钥的walletStateInit的信息。
- 设备:包含请求时的名称和钱包应用程序版本(名称应该与最初请求的相同,但这可以进行验证以确保真实性),以及平台名称和支持功能列表。
- 提供者:包含http -- 这允许钱包与Web应用程序之间进行的所有请求与响应通过bridge进行服务。
登出并请求TonProof
现在我们已经登录了我们的Mini App,但是...后端如何知道它是正确的一方呢?为了验证这一点,我们必须请求钱包所有权证明。
这只能通过认证来完成,所以我们必须登出。因此,我们在控制台中运行以下代码:
connector.disconnect();
当断开连接过程完成时,将显示 Connection status: null
。
在添加TonProof之前,让我们更改代码以表明当前实现是不安全的:
let connHandler = connector.statusChangeSubscriptions[0];
connHandler({
device: {
appName: "Uber Singlesig Cold Wallet App",
appVersion: "4.0.1",
features: [],
maxProtocolVersion: 3,
platform: "ios"
},
account: {
/* TON Foundation address */
address: '0:83dfd552e63729b472fcbcc8c45ebcc6691702558b68ec7527e1ba403a0f31a8',
chain: '-239',
walletStateInit: 'te6ccsEBAwEAoAAFcSoCATQBAgDe/wAg3SCCAUyXuiGCATOcurGfcbDtRNDTH9MfMdcL/+ME4KTyYIMI1xgg0x/TH9Mf+CMTu/Jj7UTQ0x/TH9P/0VEyuvKhUUS68qIE+QFUEFX5EPKj+ACTINdKltMH1AL7AOjRAaTIyx/LH8v/ye1UAFAAAAAAKamjF3LJ7WtipuLroUqTuQRi56Nnd3vrijj7FbnzOETSLOL/HqR30Q=='
},
provider: 'http'
});
控制台显示的代码行几乎与最初启动连接时显示的一样。因此,如果后端不按预期正确执行用户认证,需要一个方法来测试它是否工作正确。为了实现这一点,可以在控制台中充当TON Foundation,以便可以测试令牌余额和令牌所有权参数的合法性。自然,提供的代码不会更改连接器中的任何变量,但是用户可以根据自己的意愿使用应用程序,除非该连接器受到闭包的保护。即使是这种情况,使用调试器和编码断点也不难提取它。
现在用户的认证已经得到验证,让我们继续写代码。