跳到主要内容

sui

Dapp 开发者集成OpenBlock内嵌dappsdk,OpenBlock内部通过iframe加载Dapp url,Dapp使用OpenBlock Wallet Provider(window.obsui) OpenBlock钱包交互通信

提示
  1. Dapp使用全局变量window.obsui 为钱包provider

  2. OpenBlock会在DApp加载完毕之后给Dapp发送一条消息,Dapp可以根据此消息来判断宿主应用是否为OpenBlock再加载innerdappsdk:

// openblock side:
dappPageIframe.onload = () => {
dappPageIframe.contentWindow?.postMessage(
{
from: 'openblock-content-integration',
target: 'dapp-inpage-integration',
value: 'dapploaded',
},
'*',
);
};
// dapp side:
window.addEventListener(
'message',
(event) => {
if (event.data.target === 'dapp-inpage-integration' && event.data.from === 'openblock-content-integration') {
// fom openblock wallet
return;
}
},
false,
);

Wallet Provider Methods

Connect

const provider = window.obsui;

response: {
accounts: ReadonlyWalletAccount[];
}
await provider.features['standard:connect'].connect();

Listen

const provider = window.obsui;

request: ...[event: string, listener: StandardEventsListener]
response: () => void;
await provider.features['standard:events'].on();

Execute Transaction

const provider = window.obsui;

request: SuiSignAndExecuteTransactionBlockInput {
transactionBlock: TransactionBlock;
}
response: SuiSignAndExecuteTransactionBlockOutput {
digest: TransactionDigest;
effects: TransactionEffects;
}
await provider.features['sui:signAndExecuteTransactionBlock'].signAndExecuteTransactionBlock();

Sign Message

const provider = window.obsui;

request: SuiSignMessageInput {
message: Uint8Array;
}
response: SuiSignMessageOutput {
signature: string;
}
await provider.features['sui:signMessage'].signMessage();

Source code

inner-dappsdk.js

import mitt from 'mitt';
import { TransactionBlock, toB64 } from '@mysten/sui.js';
import {
SUI_CHAINS,
SUI_DEVNET_CHAIN,
SUI_TESTNET_CHAIN,
SUI_LOCALNET_CHAIN,
ReadonlyWalletAccount,
} from '@mysten/wallet-standard';

const DAPP_INPAGE = 'dapp-inpage-integration';
const OPENBLOCK_CONTENT = 'openblock-content-integration';

const guid = () => {
function S4() {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
}
return S4() + S4() + S4() + S4() + S4() + S4() + S4() + S4();
};

const API_ENV = {
local: 'local',
devNet: 'devNet',
testNet: 'testNet',
customRPC: 'customRPC',
};

const API_ENV_TO_CHAIN = {
[API_ENV.local]: SUI_LOCALNET_CHAIN,
[API_ENV.devNet]: SUI_DEVNET_CHAIN,
[API_ENV.testNet]: SUI_TESTNET_CHAIN,
};

class SuiProvider {
constructor() {
this._events = mitt();
this._accounts = [];
this._activeChain = null;
this.callBackList = {};
this.eventListener = {};
this.isOpenBlock = () => true;

this.hasPermissions = this.hasPermissions.bind(this);
this.requestPermissions = this.requestPermissions.bind(this);
this.getAccounts = this.getAccounts.bind(this);
this._signAndExecuteTransactionBlock = this._signAndExecuteTransactionBlock.bind(this);
this._signTransactionBlock = this._signTransactionBlock.bind(this);
this._stake = this._stake.bind(this);
this._signMessage = this._signMessage.bind(this);
this._connected = this._connected.bind(this);
this._connect = this._connect.bind(this);
this._setAccounts = this._setAccounts.bind(this);
this._setActiveChain = this._setActiveChain.bind(this);
}

get version() {
return '1.0.0';
}

get name() {
return 'OpenBlock';
}

get icon() {
return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTUiIGhlaWdodD0iNTUiIHZpZXdCb3g9IjAgMCA1NSA1NSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjU1IiBoZWlnaHQ9IjU1IiBmaWxsPSIjRjlGQkZGIi8+CjxwYXRoIGQ9Ik0zNi44MTQ3IDEwLjc3NTFDNDMuNTQ0NSAxMC43NzUxIDQ5IDE2LjIzMDYgNDkgMjIuOTYwNFYyMy4yNDc2TDQ0Ljc0NDIgMjcuNUw0OSAzMS43NDgzVjMyLjAzOTZDNDkgMzguNzY5NCA0My41Mzk5IDQ0LjIyNDkgMzYuODEwMiA0NC4yMjQ5SDUuOTg4NTNMMTUuODQ0NSAzNy4xMTAzTDMyLjI3NTEgMzcuMjMwN0MzNy41NTMzIDM3LjIz MDcgNDEuOTAzMSAzMi43NzgyIDQxLjkwMzEgMjcuNUM0MS45MDMxIDIyLjIyMTggMzcuNTUzMyAxNy43NjEgMzIuMjc1MSAxNy43NjFIMTUuODQ0NUw1Ljk5MzA4IDEwLjc3NTFIMzYuODE0N1oiIGZpbGw9IiM0QTNERTYiLz4KPHBhdGggZD0iTTE4LjEzODQgMjMuNDM4MkMxNS44OTUyIDIzLjQzODIgMTQuMDc2NyAyNS4yNTY3IDE0LjA3NjcgMjcuNDk5OUMxNC4wNzY3IDI5Ljc0MzIgMTUuODk1MiAzMS41NjE3IDE4LjEzODQgMzEuNTYxN0gzMi4yODkyQzM0LjUzMjUgMzEuNTYxNyAzNi4zNTEgMjkuNzQzMiAzNi4zNTEgMjcuNDk5OUMzNi4zNTEgMjUuMjU2NyAzNC41MzI1IDIzLjQzODIgMzIuMjg5MiAyMy40MzgySDE4LjEzODRaIiBmaWxsPSIjNEEzREU2Ii8+Cjwvc3ZnPgo=';
}

get chains() {
return SUI_CHAINS;
}

async _connected() {
console.log('suiProvider::_connected');
this._setActiveChain(this._getActiveNetwork());
if (!(await this.hasPermissions(['viewAccount']))) {
return;
}
const accounts = await this.getAccounts();
this._setAccounts(accounts);
if (this._accounts.length) {
this._events.emit('change', { accounts: this.accounts });
}
}

async _connect(input) {
console.log('suiProvider::_connect, input:', input);
await this._connected();
if (!this._accounts.length) {
await this.requestPermissions();
await this._connected();
}

return { accounts: this.accounts };
}

get features() {
return {
'standard:connect': {
version: '1.0.0',
connect: this._connect,
},
'standard:events': {
version: '1.0.0',
on: (event, listener) => {
console.log('suiProvider::standard:events, event:', event, 'listener:', listener);
this._events.on(event, listener);
return () => this._events.off(event, listener);
},
},
'sui:signTransactionBlock': {
version: '1.1.0',
signTransactionBlock: this._signTransactionBlock,
},
'sui:signAndExecuteTransactionBlock': {
version: '1.0.0',
signAndExecuteTransactionBlock: this._signAndExecuteTransactionBlock,
},
'suiWallet:stake': {
version: '0.0.1',
stake: this._stake,
},
'sui:signMessage': {
version: '1.0.0',
signMessage: this._signMessage,
},
};
}

get accounts() {
return this._accounts;
}

async hasPermissions(permissions) {
return this._hookRequest({
method: 'sui_accounts',
params: permissions,
}).then((res) => {
return Promise.resolve(!!res);
});
}

async requestPermissions(permissions) {
return this._hookRequest({
method: 'sui_requestAccounts',
params: permissions,
}).then((res) => {
return Promise.resolve(!!res);
});
}

async getAccounts() {
return this._hookRequest({
method: 'sui_accounts',
});
}

async _signTransactionBlock(input) {
console.log('suiProvider::_signTransactionBlock, input:', input);
if (!TransactionBlock.is(input.transactionBlock)) {
throw new Error('Unexpect transaction format found. Ensure that you are using the `Transaction` class');
}

return this._hookRequest({
method: 'sui_signTransaction',
params: {
...input,
type: 'transaction',
account: input.account?.address || this._accounts[0]?.address || '',
data: input.transactionBlock.serialize(),
},
});
}

async _signAndExecuteTransactionBlock(input) {
console.log('suiProvider::_signAndExecuteTransactionBlock, input:', input);
if (!TransactionBlock.is(input.transactionBlock)) {
throw new Error('Unexpect transaction format found. Ensure that you are using the `Transaction` class');
}

const result = await this._hookRequest({
method: 'sui_signAndExecuteTransaction',
params: {
type: 'transaction',
data: input.transactionBlock.serialize(),
options: input.options,
account: input.account?.address || this._accounts[0]?.address || '',
},
});

if (!result?.digest) {
throw new Error('Failed to sign and execute transaction');
}

return result;
}

_stake({ validatorAddress }) {
console.log('suiProvider::_stake, validatorAddress:', validatorAddress);
}

async _signMessage({ message, account }) {
console.log('suiProvider::_signMessage, message:', message, 'account:', account);
const result = await this._hookRequest({
method: 'sui_signMessage',
params: {
message: toB64(message),
accountAddress: account?.address || this._accounts[0]?.address,
},
});

if (!result || typeof result !== 'string') {
throw new Error('Failed to sign message');
}

return {
signature: result,
};
}

_setAccounts(addresses) {
this._accounts = addresses.filter(Boolean).map(
(address) =>
new ReadonlyWalletAccount({
address,
publicKey: new Uint8Array(),
chains: this._activeChain ? [this._activeChain] : [],
features: ['sui:signAndExecuteTransaction'],
}),
);
}

_getActiveNetwork() {
return { env: API_ENV.testNet };
}

_setActiveChain({ env }) {
this._activeChain = env === API_ENV.customRPC ? 'sui:unknown' : API_ENV_TO_CHAIN[env];
}

_hookRequest(paramsObj) {
return new Promise((resolve, reject) => {
const _guid = guid();

if (typeof paramsObj === 'string') {
paramsObj = {
method: paramsObj,
};
}
paramsObj = {
...paramsObj,
mark: {
id: _guid,
method: paramsObj.method,
type: 'call_method',
},
provider: 'sui',
};

window.parent.postMessage(
{
from: DAPP_INPAGE,
target: OPENBLOCK_CONTENT,
data: paramsObj,
},
'*',
);

this.callBackList[_guid] = {
resolve,
reject,
};
});
}

_handleNormalMethod(mark, event) {
if (event.data.value.data?.error) {
this.callBackList[mark.id]?.['reject'](event.data.value.data?.error);
} else {
this.callBackList[mark.id]?.['resolve'](event.data.value.data?.result);
}
delete this.callBackList[mark.id];
}
}

const initializeProvider = () => {
const provider = new SuiProvider();

try {
delete window.suiWallet;
Object.defineProperty(window, 'obsui', {
enumerable: false,
configurable: false,
writable: false,
value: provider,
});
} catch (e) {
console.error(
`[sui-wallet] Unable to attach to window.suiWallet. There are likely multiple copies of the Sui Wallet installed.`,
);
}
};

if (typeof window !== 'undefined') {
initializeProvider();
}