Skip to main content

sui

OpenBlock loads Dapp via an iframe, and the Dapp interacts with the OpenBlock wallet using the standard web3 rpc methods

tip
  1. use window.obsui replace window.sui.

  2. OpenBlock will send a message to the DApp when the Dapp is loaded. The Dapp can use this message to determine if the host application is OpenBlock and then load 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();
}