OmniConnect
OmniConnect是Bitget Wallet推出的面向开发者的软件开发工具包,旨在实现 Telegram 生态系统内的 Mini-Apps 与多链环境的无缝集成。集成此工具包后,Telegram Mini Apps 可以直接与 Bitget Wallet 进行跨多链的签名、交易和其他 DApp 操作。
- OmniConnect:一个开放协议,通过桥接服务器连接钱包和 DApps(Web3 应用程序),在 TG MiniApp 和/或 Bitget Wallet Lite 之间建立远程连接。
- OmniConnect 官方支持的链:
- EVM(以太坊虚拟机兼容区块链,如 Fantom、Base、Arbitrum 等)
- OmniConnect 支持的钱包:
- Bitget Wallet Lite
- 在线演示快速体验:
- NPM 包:
安装和初始化
要将 Omni Connect 集成到您的 DApp 中,您可以使用 pnpm:
pnpm add @bitget-wallet/omni-connect
在连接钱包之前,您需要实例化一个对象以进行后续操作,如连接钱包和发送交易。
const connector = new OmniConnect({ metadata, namespace })
请求参数
metaData
- object:应用程序元数据- name - string:应用程序名称
- iconUrl - string:应用程序图标 URL
- url - string:应用程序的主网站
- privacyPolicyUrl - string(可选):
- termsOfUseUrl - string(可选):
namespace
- object:请求连接的必要命名空间信息。值必须在支持范围内,否则连接将失败。- eip155 - object:EVM 值为”eip155”
- chains: - Array:链 ID 信息
- eip155 - object:EVM 值为”eip155”
示例
import { OmniConnect, PreventType } from '@bitget-wallet/omni-connect'
const connector = new OmniConnect({
metadata: {
name: '<您的DApp名称>',
iconUrl: '<您的图标URL>',
url: '<您的网站URL>',
privacyPolicyUrl: '', // 可选
termsOfUseUrl: '', // 可选
},
namespace: {
eip155: {
chains: ['1', '56'],
},
},
})
连接钱包
连接到钱包以获取钱包地址,该地址用作标识符,是签名交易所必需的;
connector.connect(options)
请求参数
options
- object(可选)- ret - string:PreventType.CLOSE 您可以指定连接后不关闭钱包(默认行为是关闭它)
恢复连接
如果用户之前已连接其钱包,请使用此方法恢复连接状态。(SDK 已默认恢复连接)
示例
connector.restoreConnection()
签名消息
向钱包签名消息的方法,支持签名;
connector.signMessage({ method, params: [address, message] })
请求参数
- method - string:请求方法名称值
eth_sign
|personal_sign
|eth_signTypedData
|eth_signTypedData_v4
- params - Array:
- [0] - address - string:钱包地址
- [1] - message - string | object:要签名的消息
- settings - object(可选):打开 TG 钱包配置
- preventPopup - string:PreventType.OPEN(防止打开新窗口)
示例
const address = '0x..'
try {
await connector.signMessage({
method: 'eth_sign',
params: [address, 'Hello World!'],
})
} catch (error) {
console.log(error)
}
发送交易
向钱包发送消息以支持交易的方法;
connector.sendTransaction({ method: "eth_sendTransaction", params: [txData], })
请求参数
method
- string:请求方法名称值eth_sendTransaction
params
- Array:- [0] - txData - object:
- chainId - hexstring:链 ID
- from - string:发送者地址。
- to - string:接收者地址,如果这是合约创建交易则为 null。
- value - hexstring:要转移的值,以 wei 为单位。
- nonce - hexstring(可选):防重放参数。
- gasLimit - hexstring:gasLimit
- maxPriorityFeePerGas - hexstring(可选):发送者愿意在基础费用之上为每个 gas 支付的最大费用,以 wei 为单位。
- maxFeePerGas - hexstring(可选):发送者愿意为每个 gas 支付的最大总费用(基础费用+优先费用),以 wei 为单位。
- [0] - txData - object:
- settings - object(可选):打开 TG 钱包配置
- preventPopup - string:PreventType.OPEN(防止打开新窗口)
示例
const address = '0x..'
try {
await connector.signTransaction({
method: 'eth_signTransaction',
params: [
{
chainId: '0x38',
data: '0x',
from: address,
to: '0x..',
value: '0x0',
gasLimit: '0x5208',
maxPriorityFeePerGas: '0x3b9aca00', //wei
maxFeePerGas: '0x2540be400', //wei
},
],
})
} catch (error) {
console.log(error)
}
监控钱包状态变化
钱包状态包括:连接状态、签名结果等。您可以使用此方法获取状态。
onStatusChange( callback: (walletInfo) => void, errorsHandler?: (err) => void ): () => void;
请求参数
callback
- (walletInfo) => void- id - number | string:请求 ID
- namespaceKey - string:‘eip155’
- event - string:事件类型
- connected - boolean:连接状态
- result - object
- address - string:钱包连接地址
- signature - string:签名结果
errorsHandler
- (err) => void- code - number:错误代码
- message - string:错误消息
示例
const subscription = connector.onStatusChange(
(walletInfo) => {
console.log('onStatusChange', walletInfo)
const { id, namespaceKey, event, connected, result } = walletInfo
switch (event) {
case 'connect':
case 'disconnect':
// 连接或断开连接逻辑..
break
case 'signMessage':
break
case 'signTransaction':
case 'sendTransaction':
// 处理result?.signature, result?.reciept
break
default:
break
}
},
(err) => {
const { code, message } = err
console.error(`错误流:代码:${code},消息:${message}`)
}
)
当您不再需要监听更新时,调用unsubscribe
以节省资源。
subscription?.unsubscribe()
断开连接
断开已连接的钱包
connector.disconnect()
错误代码
连接期间可能抛出的异常:
UNKNOWN_ERROR 未知异常
BAD_REQUEST_ERROR 请求错误
UNKNOWN_APP_ERROR 未知应用异常
USER_REJECTS_ERROR 用户拒绝
METHOD_NOT_SUPPORTED 方法不支持
export enum SIGN_DATA_ERROR_CODES {
UNKNOWN_ERROR = 0,
BAD_REQUEST_ERROR = 1,
UNKNOWN_APP_ERROR = 100,
USER_REJECTS_ERROR = 300,
METHOD_NOT_SUPPORTED = 400
}
export enum SEND_TRANSACTION_ERROR_CODES {
UNKNOWN_ERROR = 0,
BAD_REQUEST_ERROR = 1,
UNKNOWN_APP_ERROR = 100,
USER_REJECTS_ERROR = 300,
METHOD_NOT_SUPPORTED = 400
}
完整开发示例
"use client";
// import Image from "next/image";
import React from "react";
import {
Eip155SigningMethods,
OmniConnect,
PreventType,
RequestMethods,
} from "@bitget-wallet/omni-connect";
import { useState, useEffect, useMemo, useRef } from "react";
import { hashMessage } from "@ethersproject/hash";
import { recoverAddress, verifyMessage } from "ethers";
import { toChecksumAddress } from "ethereumjs-util";
import {
recoverTypedSignature,
SignTypedDataVersion,
} from "@metamask/eth-sig-util";
import { decodeUTF8, encodeUTF8 } from "tweetnacl-util";
import ethUtil from "ethereumjs-util";
export const Toast = ({ message, isVisible, onClose }) => {
if (!isVisible) return null;
return (
<div className="fixed inset-0 flex items-center justify-center bg-gray-800 bg-opacity-75 z-50">
<div className="bg-white rounded-lg shadow-lg p-4 text-center animate-fadeIn">
<p className="text-gray-800">{message}</p>
</div>
</div>
);
};
export default function Home() {
const [connector, setConnector] = useState({} as any);
const [connected, setConnected] = useState(false);
const [address, setAddress] = useState("");
const [signature, setSignature] = useState();
const [reciept, setReciept] = useState();
const [msg, setMsg] = useState({
eth_sign: "Hello World!",
personal_sign: "Personal:FOMO NUCN, just beyond FOMO3D",
eth_signTypedData: [
{
type: "string",
name: "Message",
value: "Hi, Alice!",
},
{
type: "uint32",
name: "A number",
value: "1337",
},
],
eth_signTypedData_v4: {
domain: {
chainId: 56,
// Give a user-friendly name to the specific contract you're signing for.
name: "Ether Mail",
// Add a verifying contract to make sure you're establishing contracts with the proper entity.
verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
// This identifies the latest version.
version: "1",
},
message: {
contents: "Hello, Bob!",
attachedMoneyInEth: 4.2,
from: {
name: "Cow",
wallets: [
"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826",
"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF",
],
},
to: [
{
name: "Bob",
wallets: [
"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB",
"0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57",
"0xB0B0b0b0b0b0B000000000000000000000000000",
],
},
],
},
primaryType: "Mail",
types: {
// This refers to the domain the contract is hosted on.
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
],
// Not an EIP712Domain definition.
Group: [
{ name: "name", type: "string" },
{ name: "members", type: "Person[]" },
],
// Refer to primaryType.
Mail: [
{ name: "from", type: "Person" },
{ name: "to", type: "Person[]" },
{ name: "contents", type: "string" },
],
// Not an EIP712Domain definition.
Person: [
{ name: "name", type: "string" },
{ name: "wallets", type: "address[]" },
],
},
},
});
const [msgSig, setMsgSig] = useState({} as any);
const [txData, setTxData] = useState({
chainId: "0x38",
data: "0x",
from: address,
to: "0x1A0A18AC4BECDDbd6389559687d1A73d8927E416",
value: "0x0",
// nonce: "0x1",
gasLimit: "0x5208",
maxPriorityFeePerGas: "0x3b9aca00", //wei
maxFeePerGas: "0x2540be400", //wei
});
const [chainId, setChainId] = useState("1");
const [isToastVisible, setToastVisible] = useState(false);
const [isClickToastVisible, setClickToastVisible] = useState(false);
const init = async (connector) => {
const result = await connector.provider.restoreConnection();
// setAddress(result?.account);
};
useEffect(() => {
const connector = new OmniConnect({
metadata: {
name: "<Your DAppName>",
iconUrl: "<Your iconUrl>",
url: "<Your website url>",
privacyPolicyUrl: "", // optional
termsOfUseUrl: "", // optional
},
namespace: {
eip155: {
chains: ["1", "56"],
},
},
});
setConnector(connector);
init(connector);
const subscription = connector.provider.onStatusChange(
(walletInfo) => {
console.log("onStatusChange", walletInfo);
const { id, namespaceKey, event, connected, result } = walletInfo;
switch (event) {
case RequestMethods.Connect:
case RequestMethods.Disconnect:
setConnected(connected);
setAddress(result?.address);
setTxData({
...txData,
from: result?.address,
});
break;
case RequestMethods.SignMessage:
setMsgSig(result);
break;
case RequestMethods.SignTransaction:
case RequestMethods.SendTransaction:
setSignature(result?.signature);
setReciept(result?.reciept);
break;
default:
break;
}
},
(err) => {
const { code, message } = err;
console.error(`error stream: code: ${code}, message: ${message}`);
}
);
return () => {
subscription?.unsubscribe();
};
}, []);
const onSelectChain = (e) => {
if (e.target.value !== chainId) {
setToastVisible(true);
setTimeout(() => {
setToastVisible(false);
}, 1300);
}
setChainId(e.target.value);
};
return (
<div className="w-full bg-[#efefef] box-border">
<div className="fixed w-full h-[55px] text-large font-bold text-white flex justify-center items-center bg-[#3894ff] ">
<img className="w-6 h-6 rounded-full mr-2" src={"/logo.png"} />
OmniConnect
</div>
<div className="flex flex-col max-w-[600px] mx-auto px-5 py-[55px] gap-3 font-[family-name:var(--font-geist-sans)]">
{/* connect */}
<div className="w-full p-5 bg-white rounded-md my-4">
<div className="flex flex-col justify-between">
<div className="mb-3">App namespace: Eip155</div>
<div className="mb-3">
Chain:
<select name="chain" id="chain" onChange={onSelectChain}>
<option value="1">ETH</option>
<option value="56">BNB</option>
</select>
</div>
<div className="mb-3">ChainId: {chainId}</div>
<div className="mb-3">
Connect Status: {connected ? "connected" : "unconnect"}
</div>
</div>
<div className="break-all">{connected && `address: ${address}`}</div>
<button
className={`w-full h-[48px] rounded-full bg-[#3894ff] text-white text-lg mt-6 `}
type="button"
onClick={async () => {
setClickToastVisible(true);
setTimeout(() => {
setClickToastVisible(false);
}, 500);
if (!!connected) {
connector.provider.disconnect();
} else {
try {
const { account, chainId } = await connector.provider.connect(
{
request: { chainId: "1" },
}
);
console.log("result", account, chainId);
} catch (error) {
console.log("error", error);
}
}
}}
>
{connected ? "disconnect" : "connect"}
</button>
</div>
<div className="w-full p-5 bg-white rounded-md my-4">
<h2 className="text-center text-lg font-bold mb-3">Eth Sign</h2>
<textarea
name="eth_sign"
rows={1}
className="w-full p-2 text-black border border-solid border-[#dedede] rounded-md"
value={msg.eth_sign}
onChange={(e) =>
setMsg({
...msg,
eth_sign: e.target.value,
})
}
/>
<button
className={`w-full h-[48px] rounded-full bg-[#3894ff] text-white text-lg mt-6`}
type="button"
onClick={async () => {
setClickToastVisible(true);
setTimeout(() => {
setClickToastVisible(false);
}, 500);
console.log("touched eth_sign");
try {
const signature = await connector.provider.signMessage({
method: Eip155SigningMethods.EthSign,
params: [address, msg.eth_sign],
});
// handle result...
console.log("signature", signature);
} catch (error) {
const { code, message } = error;
console.error(`error code: ${code}, message: ${message}`);
}
}}
>
eth_sign
</button>
<div className="mt-5">
<div style={{ wordBreak: "break-all" }} className="break-all mb-3">
signature:{" "}
{msgSig?.messageType === "eth_sign" && msgSig.signature}
</div>
<button
className={`w-full h-[48px] rounded-full bg-[#3894ff] text-white text-lg mt-4`}
type="button"
onClick={() => {
if (!connected) return;
const verifySigner = recoverAddress(
hashMessage(msg.eth_sign),
msgSig.signature
);
console.log("verifySigner", verifySigner);
console.log(
`equal to address: ${address} ? ${verifySigner === address}`
);
alert(
`equal to address: ${address} ? ${verifySigner === address}`
);
}}
>
verify eth_sign message
</button>
</div>
</div>
<div className="w-full p-5 bg-white rounded-md my-4">
{/* personal_sign */}
<div className="border-b border-[#dedede]">
<h2 className="text-center text-lg font-bold mb-3">
Sign Message personal
</h2>
<textarea
rows={1}
className="w-full min-h-10 p-2 text-black border border-solid border-[#dedede] rounded-md"
value={msg.personal_sign}
onChange={(e) =>
setMsg({
...msg,
personal_sign: e.target.value,
})
}
/>
<button
className={`w-full h-[48px] rounded-full bg-[#3894ff] text-white text-lg my-4`}
type="button"
onClick={async () => {
setClickToastVisible(true);
setTimeout(() => {
setClickToastVisible(false);
}, 500);
console.log("touched personal_sign");
try {
const signature = await connector.provider.signMessage({
method: Eip155SigningMethods.PersonalSign,
params: [
uint8ArraytoHex(decodeUTF8(msg.personal_sign)),
address,
],
});
console.log("signature", signature);
} catch (error) {
const { code, message } = error;
console.error(`error code: ${code}, message: ${message}`);
}
}}
>
personal_sign
</button>
<div className="mt-5">
<div
style={{ wordBreak: "break-all" }}
className="break-all mb-3"
>
signature:{" "}
{msgSig?.messageType === "personal_sign" && msgSig.signature}
</div>
<button
className={`w-full h-[48px] rounded-full bg-[#3894ff] text-white text-lg mt-4`}
type="button"
onClick={() => {
if (!connected) return;
const verifySigner = verifyMessage(
msg.personal_sign,
msgSig.signature
);
console.log("verifySigner", verifySigner);
console.log(
`equal to address: ${address} ? ${verifySigner === address}`
);
alert(
`equal to address: ${address} ? ${verifySigner === address}`
);
}}
>
verify personal_sign message
</button>
</div>
</div>
{/* eth_signTypedData*/}
<div className="border-b border-[#dedede] py-4">
<h2 className="text-center text-lg font-bold mb-3">
Sign Message signTypedData
</h2>
<textarea
rows={3}
className="w-full min-h-16 p-2 text-black border border-solid border-[#dedede] rounded-md mt-4"
readOnly
value={JSON.stringify(msg.eth_signTypedData)}
onChange={(e) =>
setMsg({
...msg,
eth_signTypedData: JSON.parse(e.target.value),
})
}
/>
<button
type="button"
className={`w-full h-[48px] rounded-full bg-[#3894ff] text-white text-lg mt-4`}
onClick={async () => {
setClickToastVisible(true);
setTimeout(() => {
setClickToastVisible(false);
}, 500);
console.log("touched eth_signTypedData");
try {
const signature = await connector.provider.signMessage({
method: Eip155SigningMethods.EthSignTypedData,
params: [address, msg.eth_signTypedData],
});
console.log("signature", signature);
} catch (error) {
const { code, message } = error;
console.error(`error code: ${code}, message: ${message}`);
}
}}
>
eth_signTypedData
</button>
<div className="mt-5">
<div
style={{ wordBreak: "break-all" }}
className="break-all mb-3"
>
signature:{" "}
{msgSig?.messageType === "eth_signTypedData" &&
msgSig.signature}
</div>
<button
className={`w-full h-[48px] rounded-full bg-[#3894ff] text-white text-lg mt-4`}
type="button"
onClick={() => {
if (!connected) return;
const verifySigner = recoverTypedSignature({
data: msg.eth_signTypedData,
signature: msgSig.signature,
version: SignTypedDataVersion.V1,
});
console.log("verifySigner", verifySigner);
alert(
`equal to address: ${toChecksumAddress(address)} ? ${
toChecksumAddress(verifySigner) ===
toChecksumAddress(address)
}`
);
}}
>
verify eth_signTypedData message
</button>
</div>
</div>
{/* eth_signTypedData_v4*/}
<div className=" py-4">
<h2 className="text-center text-lg font-bold mb-3">
Sign Message signTypedData_v4
</h2>
<textarea
readOnly
rows={6}
className="w-full min-h-16 p-2 text-black border border-solid border-[#dedede] rounded-md mt-4"
defaultValue={JSON.stringify(msg.eth_signTypedData_v4)}
/>
<button
type="button"
className={`w-full h-[48px] rounded-full bg-[#3894ff] text-white text-lg mt-6`}
onClick={async () => {
setClickToastVisible(true);
setTimeout(() => {
setClickToastVisible(false);
}, 500);
console.log("touched eth_signTypedData_v4");
try {
const signature = await connector.provider.signMessage({
method: Eip155SigningMethods.EthSignTypedDataV4,
params: [address, msg.eth_signTypedData_v4],
});
console.log("signature", signature);
} catch (error) {
const { code, message } = error;
console.error(`error code: ${code}, message: ${message}`);
}
}}
>
eth_signTypedData_v4
</button>
<div className="mt-5">
<div
style={{ wordBreak: "break-all" }}
className="break-all mb-3"
>
signature:{" "}
{msgSig?.messageType === "eth_signTypedData_v4" &&
msgSig.signature}
</div>
<button
className={`w-full h-[48px] rounded-full bg-[#3894ff] text-white text-lg mt-4`}
type="button"
onClick={() => {
if (!connected) return;
const verifySigner = recoverTypedSignature({
data: msg.eth_signTypedData_v4 as any,
signature: msgSig.signature,
version: SignTypedDataVersion.V4,
});
console.log("verifySigner", verifySigner);
alert(
`equal to address: ${toChecksumAddress(address)} ? ${
toChecksumAddress(verifySigner) ===
toChecksumAddress(address)
}`
);
}}
>
verify eth_signTypedData_v4 message
</button>
</div>
</div>
</div>
<div className="w-full p-5 bg-white rounded-md my-4">
<h2 className="text-center text-lg font-bold mb-3">
SignTransaction
</h2>
<textarea
rows={6}
name="txInput"
className="w-full min-h-10 p-2 text-black border border-solid border-[#dedede] rounded-md"
value={JSON.stringify(txData)}
onChange={(e) => {
setTxData(JSON.parse(e.target.value));
}}
/>
<div style={{ wordBreak: "break-all" }} className="break-all mb-3">
signature:{signature}
</div>
<button
className={`w-full h-[48px] rounded-full bg-[#3894ff] text-white text-lg my-4`}
type="button"
onClick={async () => {
setClickToastVisible(true);
setTimeout(() => {
setClickToastVisible(false);
}, 500);
console.log("touched signTransaction");
try {
const signature = await connector.provider.signTransaction({
method: Eip155SigningMethods.EthSignTransaction,
params: [txData],
});
console.log("signature", signature);
} catch (error) {
const { code, message } = error;
console.error(`error code: ${code}, message: ${message}`);
}
}}
>
signTransaction
</button>
</div>
<div className="w-full p-5 bg-white rounded-md my-4">
<h2 className="text-center text-lg font-bold mb-3">
SendTransaction
</h2>
<textarea
rows={6}
className="w-full min-h-10 p-2 text-black border border-solid border-[#dedede] rounded-md"
value={JSON.stringify(txData)}
onChange={(e) => {
setTxData(JSON.parse(e.target.value));
}}
/>
<div style={{ wordBreak: "break-all" }} className="break-all mb-3">
signature:{signature}
</div>
<div style={{ wordBreak: "break-all" }} className="break-all mb-3">
reciept:{reciept}
</div>
<button
className={`w-full h-[48px] rounded-full bg-[#3894ff] text-white text-lg my-4`}
type="button"
onClick={async () => {
setClickToastVisible(true);
setTimeout(() => {
setClickToastVisible(false);
}, 500);
console.log("touched sendTransaction");
try {
const reciept = await connector.provider.sendTransaction({
method: Eip155SigningMethods.EthSendTransaction,
params: [txData],
});
console.log("reciept", reciept);
} catch (error) {
const { code, message } = error;
console.error(`error code: ${code}, message: ${message}`);
}
}}
>
sendTransaction
</button>
</div>
{!connected && (
<div className="w-full p-5 bg-white rounded-md my-4">
<button
className={`w-full h-[48px] rounded-full bg-[#3894ff] text-white text-lg mt-5`}
type="button"
onClick={async () => {
setClickToastVisible(true);
setTimeout(() => {
setClickToastVisible(false);
}, 500);
console.log("touched connect & sign");
try {
const result = await connector.provider.connect({
ret: PreventType.Close,
});
console.log("result", result);
const signRes = await connector.provider.signMessage({
method: Eip155SigningMethods.EthSign,
params: [address, msg.eth_sign],
settings: {
preventPopup: PreventType.Open,
},
});
console.log("signRes", signRes);
} catch (error) {
console.error(error);
}
}}
>
connect & sign
</button>
</div>
)}
</div>
<Toast
message="Chain switched successfully"
isVisible={isToastVisible}
onClose={() => setToastVisible(false)}
/>
<Toast
message="Touched"
isVisible={isClickToastVisible}
onClose={() => setClickToastVisible(false)}
/>
</div>
);
}
支持的链 ID
参见支持的主网