193 lines
5.7 KiB
JavaScript
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'));
|
|
}
|