SHiNE-server/SHiNE-browser-plugin-wallet/provider-bridge.js

193 lines
5.7 KiB
JavaScript

import { PublicKey, Transaction, VersionedTransaction } from './js/lib/vendor/solana-publickey-bundle.js';
const PAGE_REQUEST = 'shine-wallet-page-request';
const PAGE_RESPONSE = 'shine-wallet-page-response';
function bytesToBase64(bytes) {
let binary = '';
const chunk = 0x8000;
for (let i = 0; i < bytes.length; i += chunk) {
const slice = bytes.subarray(i, i + chunk);
binary += String.fromCharCode(...slice);
}
return btoa(binary);
}
function base64ToBytes(value) {
const binary = atob(String(value || '').trim());
const out = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i += 1) {
out[i] = binary.charCodeAt(i);
}
return out;
}
function createProviderError(message, code = '') {
const error = new Error(String(message || 'Wallet provider error'));
if (code === 'USER_REJECTED' || code === 'NOT_TRUSTED') {
error.code = 4001;
} else if (code) {
error.code = code;
}
return error;
}
function createRequest(method, params = {}) {
const id = `shine-wallet-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
return new Promise((resolve, reject) => {
const onMessage = (event) => {
if (event.source !== window) return;
const data = event.data || {};
if (data?.target !== PAGE_RESPONSE || String(data?.id || '') !== id) return;
window.removeEventListener('message', onMessage);
if (!data?.ok) {
reject(createProviderError(data?.error || 'Wallet request failed', String(data?.code || '')));
return;
}
resolve(data?.result || {});
};
window.addEventListener('message', onMessage);
window.postMessage({
target: PAGE_REQUEST,
id,
method,
params,
}, window.location.origin);
});
}
function serializeTransactionBase64(transaction) {
if (!transaction || typeof transaction.serialize !== 'function') {
throw createProviderError('Unsupported transaction object', 'UNSUPPORTED_TRANSACTION');
}
let raw;
try {
raw = transaction.serialize({ requireAllSignatures: false, verifySignatures: false });
} catch {
raw = transaction.serialize();
}
const bytes = raw instanceof Uint8Array ? raw : new Uint8Array(raw);
return bytesToBase64(bytes);
}
function deserializeSignedTransaction(base64, originalTransaction) {
const bytes = base64ToBytes(base64);
const ctor = originalTransaction?.constructor;
if (ctor && typeof ctor.deserialize === 'function') {
return ctor.deserialize(bytes);
}
if (ctor && typeof ctor.from === 'function') {
return ctor.from(bytes);
}
if (typeof originalTransaction?.version === 'number') {
return VersionedTransaction.deserialize(bytes);
}
return Transaction.from(bytes);
}
class ShineSolanaProvider {
constructor() {
this.isSHiNE = true;
this.isPhantom = true;
this.publicKey = null;
this.isConnected = false;
this._listeners = new Map();
}
on(event, handler) {
const key = String(event || '');
if (!this._listeners.has(key)) {
this._listeners.set(key, new Set());
}
this._listeners.get(key).add(handler);
return this;
}
off(event, handler) {
const bucket = this._listeners.get(String(event || ''));
if (!bucket) return this;
bucket.delete(handler);
if (!bucket.size) {
this._listeners.delete(String(event || ''));
}
return this;
}
removeListener(event, handler) {
return this.off(event, handler);
}
emit(event, payload) {
const bucket = this._listeners.get(String(event || ''));
if (!bucket?.size) return;
for (const handler of [...bucket]) {
try {
handler(payload);
} catch {}
}
}
async connect(options = {}) {
const onlyIfTrusted = !!options?.onlyIfTrusted;
if (!onlyIfTrusted) {
const confirmed = window.confirm(`Connect SHiNE Wallet to ${window.location.origin}?`);
if (!confirmed) {
throw createProviderError('User rejected wallet connection', 'USER_REJECTED');
}
}
const result = await createRequest('connect', { onlyIfTrusted });
const nextKey = new PublicKey(String(result?.publicKeyBase58 || '').trim());
this.publicKey = nextKey;
this.isConnected = true;
this.emit('connect', nextKey);
this.emit('accountChanged', nextKey);
return { publicKey: nextKey };
}
async disconnect() {
await createRequest('disconnect', {});
this.isConnected = false;
this.publicKey = null;
this.emit('disconnect');
this.emit('accountChanged', null);
}
async signTransaction(transaction) {
if (!this.publicKey) {
await this.connect();
}
const transactionBase64 = serializeTransactionBase64(transaction);
const comment = `Site ${window.location.origin} requested transaction signature`;
const result = await createRequest('signTransaction', {
publicKeyBase58: this.publicKey?.toBase58?.() || '',
transactionBase64,
comment,
});
return deserializeSignedTransaction(String(result?.signedTransactionBase64 || ''), transaction);
}
async request(args = {}) {
const method = String(args?.method || '');
const params = args?.params;
if (method === 'connect') {
return this.connect(Array.isArray(params) ? params[0] : params || {});
}
if (method === 'disconnect') {
return this.disconnect();
}
if (method === 'signTransaction') {
const tx = Array.isArray(params) ? params[0] : params?.transaction || params;
return this.signTransaction(tx);
}
throw createProviderError(`Unsupported request method: ${method}`, 'UNSUPPORTED_METHOD');
}
}
if (!window.solana) {
const provider = new ShineSolanaProvider();
window.solana = provider;
window.phantom = window.phantom || {};
window.phantom.solana = provider;
window.dispatchEvent(new Event('solana#initialized'));
}