Bitget Wallet OmniConnect
OmniConnect
是 Bitget Wallet
推出的一款面向开发者的软件开发套件,支持 Telegram 生态中的 Mini-App 无缝连接至多链生态系统,Telegram Mini App 集成该套件后,可以直接通过 Bitget Wallet,在多链上进行签名、交易等 DApp 交互操作。
- OmniConnect: 一个链接钱包和 DApps(Web3 应用程序)的开放协议,它使用一个桥梁服务器在 TG MiniApp 和/或 Bitget Wallet Lite 之间建立远程链接
- OmniConnect 官方支持的链:
- EVM(EVM 指代采用以太坊虚拟机技术的区块链网络,常见如 Fantom、Base、Arbitrum 等)
- OmniConnect 已支持的钱包:
- Bitget Wallet Lite
- 快速体验在线示例Demo:
- NPM Package:
安装及初始化
将 OmniConnect 集成到您的 DApp 中,使用 pnpm:
pnpm add @bitget-wallet/omni-connect
pnpm add @bitget-wallet/omni-connect
连接钱包之前,需要先实例化一个对象,用于后续连接钱包、发送交易等操作。
const connector = new OmniConnect({ metadata, namespace })
const connector = new OmniConnect({ metadata, namespace })
请求参数
metaData
- object: 应用的元数据- name - string: 应用名称
- iconUrl - string: 应用的图标地址
- url - string: 应用的主网站
- privacyPolicyUrl - string(optional):
- termsOfUseUrl - string(optional):
namespace
- object: 请求连接的必要 namespace 信息,取值必须在支持范围内,否则无法连接成功- 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: "<Your DAppName>",
iconUrl: "<Your iconUrl>",
url: "<Your website url>",
privacyPolicyUrl: "", // optional
termsOfUseUrl: "", // optional
},
namespace: {
eip155: {
chains: ["1", "56"],
},
},
});
import { OmniConnect, PreventType } from "@bitget-wallet/omni-connect";
const connector = new OmniConnect({
metadata: {
name: "<Your DAppName>",
iconUrl: "<Your iconUrl>",
url: "<Your website url>",
privacyPolicyUrl: "", // optional
termsOfUseUrl: "", // optional
},
namespace: {
eip155: {
chains: ["1", "56"],
},
},
});
连接钱包
连接钱包去获取钱包地址,作为标识符和用于签名交易的必要参数; connector.connect(options)
请求参数
options
- object(optional)- ret - string: PreventType.CLOSE 可指定连接钱包后不关闭钱包(默认是关闭行为)
恢复连接
如果用户已成功连接过钱包,再次进入或页面刷新时可调用此方法恢复连接状态。(sdk 已默认恢复连接)
示例
connector.restoreConnection();
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(optional): 打开 TG 钱包的配置
- preventPopup - string: PreventType.OPEN(阻止打开新窗口)
示例
const address = "0x..";
try {
await connector.signMessage({
method: "eth_sign",
params: [address, "Hello World!"],
});
} catch (error) {
console.log(error);
}
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: 接受者地址,如果是合约创建交易,则为空
- value - hexstring: 需要转移的值,以 wei 为单位
- nonce - hexstring(optional): 防重放参数
- gasLimit - hexstring: gasLimit
- maxPriorityFeePerGas - hexstring(optional): 最高费用,以 wei 为单位,表示发送者愿意为高于基本费用的每 gas 支付的费用。
- maxFeePerGas - hexstring(optional): 发送者愿意按每 gas 支付的最高总费用(基本费用 + 优先费用),以 wei 为单位。
- [0] - txData - object:
- settings - object(optional): 打开 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);
}
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":
// connected or disconnect logic..
break;
case "signMessage":
break;
case "signTransaction":
case "sendTransaction":
// handle result?.signature, result?.reciept
break;
default:
break;
}
},
(err) => {
const { code, message } = err;
console.error(`error stream: code: ${code}, message: ${message}`);
}
);
const subscription = connector.onStatusChange(
(walletInfo) => {
console.log("onStatusChange", walletInfo);
const { id, namespaceKey, event, connected, result } = walletInfo;
switch (event) {
case "connect":
case "disconnect":
// connected or disconnect logic..
break;
case "signMessage":
break;
case "signTransaction":
case "sendTransaction":
// handle result?.signature, result?.reciept
break;
default:
break;
}
},
(err) => {
const { code, message } = err;
console.error(`error stream: code: ${code}, message: ${message}`);
}
);
当不再需要监听更新时,调用 unsubscribe
节省资源。
subscription?.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
}
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>
);
}
"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>
);
}
支持的 ChainIds
见 已支持主网