Update dependencies and enhance App component functionality
- Added new dependencies: @polkadot/util and @gear-js/api to package.json and example/package.json. - Implemented a test function in App component to interact with GearApi for balance transfer. - Updated transaction signing logic to support both raw and payload types. - Refactored Wallet class to improve socket connection handling and transaction management. - Enhanced WalletStore by removing unnecessary listener setup code.
This commit is contained in:
1 parent
deae91a3e6
commit
4b7a39d332
12 files changed
+1325
-182
No files matched your search
@@ -10,6 +10,8 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gear-js/api": "^0.44.2",
|
||||
"@polkadot/api": "^16.4.9",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1"
|
||||
},
|
||||
|
||||
Generated
+726
-8
File diff suppressed because it is too large.
Load diff
+19
-1
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import './App.css'
|
||||
import { GearApi } from '@gear-js/api';
|
||||
|
||||
const RAW_TRANSACTION = '0xd10204680359836ffbbdaee026cac6008a4c1f68e9fee5fc2949e0ab3ac3f478b7446a8a0dd90114506f6b657220526567697374657218506c617965724cd84143c22eb3da602607c1bd992dbd6db170e21d1b45b99278dbca78831af7028be58d11229c7b53a181793ada6c38754c3b30cc27cb607741d99e7bbadc7c687d165102b787e3e17957eee3435455613ff857a91389b1ae0a24e0618f1afcd6266268020000000000000000000000000000000000000001'
|
||||
|
||||
@@ -24,6 +25,22 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
const test = async () => {
|
||||
const extension = await varanWallet.enable?.('current');
|
||||
const address = (await extension?.accounts.get())?.[0].address;
|
||||
if (!address) {
|
||||
console.log('[VARANINJECT] address not found');
|
||||
return;
|
||||
}
|
||||
const apiPromise = new GearApi({ providerAddress: 'wss://rpc.vara.network' });
|
||||
await apiPromise.isReady;
|
||||
const tx = apiPromise.balance.transfer('kGi1Ui7VXBFmPmaoMD5xgWd2VHNixZ5BbLNhHFYD39T85rUi3', 100)
|
||||
console.log('[VARANINJECT] tx', tx);
|
||||
// @ts-expect-error - signer is not typed
|
||||
const result = await tx.signAndSend(address, { signer: extension?.signer })
|
||||
console.log('[VARANINJECT] result', result);
|
||||
}
|
||||
|
||||
const signTransaction = async () => {
|
||||
const extension = await varanWallet.enable?.('current');
|
||||
const address = (await extension?.accounts.get())?.[0].address;
|
||||
@@ -34,7 +51,7 @@ function App() {
|
||||
const signature = await extension?.signer?.signRaw?.({
|
||||
address: address,
|
||||
data: RAW_TRANSACTION,
|
||||
type: 'payload',
|
||||
type: 'bytes',
|
||||
});
|
||||
console.log('[VARANINJECT] signature', signature);
|
||||
setSignature(signature?.signature || '');
|
||||
@@ -53,6 +70,7 @@ function App() {
|
||||
<button onClick={checkData}>Check Data</button>
|
||||
<div>DATA</div>
|
||||
<pre>{data}</pre>
|
||||
<button onClick={test}>Test</button>
|
||||
{isConnected &&
|
||||
<>
|
||||
<button onClick={signTransaction}>Sign Transaction</button>
|
||||
|
||||
@@ -5,7 +5,7 @@ import App from './App.tsx'
|
||||
import { injectVaranWallet } from 'varan-inject'
|
||||
import WebApp from '@twa-dev/sdk'
|
||||
injectVaranWallet(WebApp, {
|
||||
callbackUrl: 'test_y_maxim_bbbbot/start?startapp='
|
||||
callbackUrl: 'test_y_maxim_bbbbot/start?startapp=',
|
||||
// callbackUrl: 'ZkPokerTestBot/zkPoker?startapp='
|
||||
})
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
},
|
||||
"homepage": "https://gitlab.mind-dev.com/pb/varan-connect#readme",
|
||||
"dependencies": {
|
||||
"@polkadot/util": "13.5.7",
|
||||
"@twa-dev/sdk": "^8.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import type {
|
||||
ServerMessage,
|
||||
ConnectResponseMessage,
|
||||
SignTransactionResponseMessage
|
||||
} from './socket';
|
||||
|
||||
const API_URL = 'https://connect.varan.app';
|
||||
|
||||
/**
|
||||
* Sends a connect response message to the API
|
||||
* This will be received by the client's WebSocket and handled by the socket
|
||||
* @param message The connect response message
|
||||
* @param connectionId The unique connection ID from WebSocket
|
||||
* @returns Promise resolving when the message is sent
|
||||
*/
|
||||
export async function sendConnect(
|
||||
message: ConnectResponseMessage,
|
||||
connectionId: string,
|
||||
): Promise<void> {
|
||||
await sendServerMessage(message, connectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a signed transaction response message to the API
|
||||
* This will be received by the client's WebSocket and handled by the socket
|
||||
* @param message The sign transaction response message
|
||||
* @param connectionId The unique connection ID from WebSocket
|
||||
* @returns Promise resolving when the message is sent
|
||||
*/
|
||||
export async function sendSignedTransaction(
|
||||
message: SignTransactionResponseMessage,
|
||||
connectionId: string,
|
||||
): Promise<void> {
|
||||
await sendServerMessage(message, connectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends any server message to the API
|
||||
* This will be received by the client's WebSocket and handled by the socket
|
||||
* @param message The server message to send
|
||||
* @param connectionId The unique connection ID from WebSocket
|
||||
* @returns Promise resolving when the message is sent
|
||||
*/
|
||||
async function sendServerMessage(
|
||||
message: ServerMessage,
|
||||
connectionId: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/send/${connectionId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(message),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[VARANINJECT] Error sending server message:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,9 @@
|
||||
export { injectVaranWallet } from './varan-wallet';
|
||||
export { sendConnect, sendSignedTransaction } from './api';
|
||||
export type {
|
||||
ConnectRequest,
|
||||
SignTransactionRequest,
|
||||
ConnectResponseMessage,
|
||||
SignTransactionResponseMessage,
|
||||
ServerMessage
|
||||
} from './socket';
|
||||
+351
@@ -0,0 +1,351 @@
|
||||
// Base WebSocket message structure
|
||||
export interface WebSocketMessage {
|
||||
type: 'connection' | 'acknowledgment' | 'server_message' | 'data';
|
||||
id?: string;
|
||||
data?: unknown;
|
||||
message?: unknown;
|
||||
}
|
||||
|
||||
// Connection message - sent by server when client connects
|
||||
export interface ConnectionMessage extends WebSocketMessage {
|
||||
type: 'connection';
|
||||
id: string;
|
||||
}
|
||||
|
||||
// Acknowledgment message - sent by server to acknowledge received data
|
||||
export interface AcknowledgmentMessage extends WebSocketMessage {
|
||||
type: 'acknowledgment';
|
||||
}
|
||||
|
||||
// Server message wrapper - contains actual message data
|
||||
export interface ServerMessageWrapper extends WebSocketMessage {
|
||||
type: 'server_message';
|
||||
message: ServerMessage;
|
||||
}
|
||||
|
||||
// Data message wrapper - contains data payload
|
||||
export interface DataMessageWrapper extends WebSocketMessage {
|
||||
type: 'data';
|
||||
data: ServerMessage;
|
||||
}
|
||||
|
||||
// Server messages that can be sent/received
|
||||
export type ServerMessage =
|
||||
| ConnectResponseMessage
|
||||
| SignTransactionResponseMessage
|
||||
| GenericServerMessage;
|
||||
|
||||
// Connect response - sent when wallet connection is established
|
||||
export interface ConnectResponseMessage {
|
||||
type: 'connect';
|
||||
success: boolean;
|
||||
wallets?: string[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Sign transaction response - sent when transaction signing is complete
|
||||
export interface SignTransactionResponseMessage {
|
||||
type: 'signTransaction';
|
||||
id: number;
|
||||
success: boolean;
|
||||
signedTx?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Generic server message for extensibility
|
||||
export interface GenericServerMessage {
|
||||
type: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// Client request messages sent via API
|
||||
export interface ConnectRequest {
|
||||
method: 'connect';
|
||||
}
|
||||
|
||||
export interface SignTransactionRequest {
|
||||
method: 'sT';
|
||||
id: number;
|
||||
tx: `p:${string}` | `bytes:${string}`;
|
||||
wallet?: string;
|
||||
}
|
||||
|
||||
export type ClientRequest = ConnectRequest | SignTransactionRequest;
|
||||
|
||||
// Message handler type
|
||||
export interface MessageHandler<T = ServerMessage> {
|
||||
(data: T): void;
|
||||
}
|
||||
|
||||
// Socket configuration
|
||||
export interface SocketConfig {
|
||||
wsUrl?: string;
|
||||
apiUrl?: string;
|
||||
}
|
||||
|
||||
export class WalletSocket {
|
||||
private ws: WebSocket | null = null;
|
||||
private uniqueId: string | null = null;
|
||||
private reconnectAttempts = 0;
|
||||
private maxReconnectAttempts = 5;
|
||||
private reconnectDelay = 1000;
|
||||
private isConnecting = false;
|
||||
private messageHandlers: Map<string, MessageHandler<ServerMessage>> = new Map();
|
||||
private connectionPromise: Promise<void> | null = null;
|
||||
private connectionResolve: (() => void) | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly wsUrl: string = 'wss://connect.varan.app',
|
||||
private readonly apiUrl: string = 'https://connect.varan.app'
|
||||
) {}
|
||||
|
||||
async connect(): Promise<string> {
|
||||
if (this.connectionPromise) {
|
||||
return this.connectionPromise.then(() => this.uniqueId!);
|
||||
}
|
||||
|
||||
this.connectionPromise = new Promise((resolve, reject) => {
|
||||
if (this.ws?.readyState === WebSocket.OPEN && this.uniqueId) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isConnecting) {
|
||||
// Wait for existing connection attempt
|
||||
const checkInterval = setInterval(() => {
|
||||
if (!this.isConnecting && this.uniqueId) {
|
||||
clearInterval(checkInterval);
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
this.isConnecting = true;
|
||||
this.reconnectAttempts = 0;
|
||||
|
||||
try {
|
||||
this.ws = new WebSocket(this.wsUrl);
|
||||
|
||||
this.ws.onopen = (): void => {
|
||||
console.log('[VARANINJECT] WebSocket connection opened');
|
||||
this.isConnecting = false;
|
||||
this.reconnectAttempts = 0;
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event: MessageEvent): void => {
|
||||
try {
|
||||
const message: WebSocketMessage = JSON.parse(event.data as string);
|
||||
this.handleMessage(message);
|
||||
} catch (error) {
|
||||
console.error('[VARANINJECT] Error parsing WebSocket message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onerror = (error: Event): void => {
|
||||
console.error('[VARANINJECT] WebSocket error:', error);
|
||||
this.isConnecting = false;
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.attemptReconnect();
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onclose = (): void => {
|
||||
console.log('[VARANINJECT] WebSocket connection closed');
|
||||
this.isConnecting = false;
|
||||
if (!this.uniqueId) {
|
||||
// Connection closed before we got an ID, attempt reconnect
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.attemptReconnect();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Store resolve function to call when connection message is received
|
||||
this.connectionResolve = (): void => {
|
||||
this.connectionResolve = null;
|
||||
resolve();
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.isConnecting = false;
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
await this.connectionPromise;
|
||||
return this.uniqueId!;
|
||||
}
|
||||
|
||||
private handleMessage(message: WebSocketMessage): void {
|
||||
if (message.type === 'connection' && message.id) {
|
||||
const connMessage = message as ConnectionMessage;
|
||||
this.uniqueId = connMessage.id;
|
||||
console.log(`[VARANINJECT] Unique ID received: ${this.uniqueId}`);
|
||||
// Resolve connection promise if pending
|
||||
if (this.connectionResolve) {
|
||||
this.connectionResolve();
|
||||
}
|
||||
} else if (message.type === 'acknowledgment') {
|
||||
console.log('[VARANINJECT] Data acknowledged by server');
|
||||
} else if (message.type === 'server_message') {
|
||||
console.log('[VARANINJECT] Received server message');
|
||||
const serverWrapper = message as ServerMessageWrapper;
|
||||
if (serverWrapper.message) {
|
||||
this.handleServerMessage(serverWrapper.message);
|
||||
}
|
||||
} else if (message.type === 'data' && message.data) {
|
||||
console.log('[VARANINJECT] Received data message');
|
||||
const dataWrapper = message as DataMessageWrapper;
|
||||
if (dataWrapper.data) {
|
||||
// Type guard to ensure data is a ServerMessage
|
||||
const serverData = dataWrapper.data as ServerMessage;
|
||||
this.handleServerMessage(serverData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleServerMessage(data: ServerMessage): void {
|
||||
// Check if there's a handler for this message type
|
||||
if (this.messageHandlers.has(data.type)) {
|
||||
const handler = this.messageHandlers.get(data.type);
|
||||
handler?.(data);
|
||||
}
|
||||
|
||||
// Also check for request ID handlers (for signTransaction messages)
|
||||
if ('id' in data && typeof data.id === 'number') {
|
||||
const requestId = String(data.id);
|
||||
const handler = this.messageHandlers.get(`request_${requestId}`);
|
||||
if (handler) {
|
||||
handler(data);
|
||||
this.messageHandlers.delete(`request_${requestId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async send(data: ClientRequest): Promise<void> {
|
||||
const id = await this.ensureConnected();
|
||||
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
throw new Error('WebSocket is not connected');
|
||||
}
|
||||
|
||||
const message = {
|
||||
...data,
|
||||
id,
|
||||
};
|
||||
|
||||
this.ws.send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
async sendViaAPI(data: ClientRequest): Promise<unknown> {
|
||||
const id = await this.ensureConnected();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiUrl}/send/${id}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('[VARANINJECT] Error sending data via API:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getData(): Promise<unknown> {
|
||||
const id = await this.ensureConnected();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiUrl}/data/${id}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('[VARANINJECT] Error getting data:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
onMessage<T extends ServerMessage = ServerMessage>(
|
||||
type: string,
|
||||
handler: MessageHandler<T>
|
||||
): () => void {
|
||||
this.messageHandlers.set(type, handler as MessageHandler<ServerMessage>);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
this.messageHandlers.delete(type);
|
||||
};
|
||||
}
|
||||
|
||||
waitForResponse<T extends ServerMessage = ServerMessage>(
|
||||
requestId: string | number,
|
||||
timeout: number = 30000
|
||||
): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.messageHandlers.delete(`request_${requestId}`);
|
||||
reject(new Error(`Timeout waiting for response to request ${requestId}`));
|
||||
}, timeout);
|
||||
|
||||
this.messageHandlers.set(`request_${requestId}`, (data: ServerMessage) => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve(data as T);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async ensureConnected(): Promise<string> {
|
||||
if (!this.uniqueId || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
return await this.connect();
|
||||
}
|
||||
return this.uniqueId;
|
||||
}
|
||||
|
||||
private attemptReconnect(): void {
|
||||
this.reconnectAttempts++;
|
||||
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
||||
|
||||
console.log(`[VARANINJECT] Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
||||
|
||||
setTimeout(() => {
|
||||
this.connectionPromise = null;
|
||||
this.connectionResolve = null;
|
||||
this.connect().catch(console.error);
|
||||
}, delay);
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
this.uniqueId = null;
|
||||
this.connectionPromise = null;
|
||||
this.connectionResolve = null;
|
||||
this.messageHandlers.clear();
|
||||
}
|
||||
|
||||
getUniqueId(): string | null {
|
||||
return this.uniqueId;
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
return this.ws !== null && this.ws.readyState === WebSocket.OPEN && !!this.uniqueId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,16 +21,11 @@ const defaultStore: WalletStoreType = {
|
||||
|
||||
export class WalletStore {
|
||||
private readonly store: WalletStorage;
|
||||
private readonly storeSubscribe: (cb: (ev: StorageEvent) => void) => void;
|
||||
private _store: WalletStoreType;
|
||||
|
||||
constructor() {
|
||||
this.store = window.localStorage as WalletStorage;
|
||||
this._store = Object.assign(defaultStore, JSON.parse(this.store.getItem(VARAN_WALLET_LS_KEY) || '{}'));
|
||||
this.storeSubscribe = (cb): void => {
|
||||
window.addEventListener('storage', cb);
|
||||
};
|
||||
this._setupListeners();
|
||||
}
|
||||
|
||||
private callbacks: {
|
||||
@@ -44,69 +39,6 @@ export class WalletStore {
|
||||
transactions: [],
|
||||
};
|
||||
|
||||
private _setupListeners(): void {
|
||||
let currentState = this._store;
|
||||
this.storeSubscribe(ev => {
|
||||
ev.stopImmediatePropagation();
|
||||
if (ev.key !== VARAN_WALLET_LS_KEY) return;
|
||||
|
||||
const data = JSON.parse(ev.newValue || '{}') as WalletStoreType;
|
||||
const keys = Object.keys(data) as (keyof WalletStoreType)[];
|
||||
for (const key of keys) {
|
||||
if (key === 'injectedMetadataKnown') continue;
|
||||
if (key === 'accounts') {
|
||||
const currentAccounts = currentState[key];
|
||||
const dataAccounts = data[key];
|
||||
if (currentAccounts.length !== dataAccounts.length) {
|
||||
currentState = data;
|
||||
this._store = data;
|
||||
this.dispatch(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
let shouldDispatch = false;
|
||||
dataAccounts.forEach(account => {
|
||||
if (!currentAccounts.includes(account)) {
|
||||
shouldDispatch = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (shouldDispatch) {
|
||||
currentState = data;
|
||||
this._store = data;
|
||||
this.dispatch(key);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (key === 'transactions') {
|
||||
const currentTransactions = currentState[key];
|
||||
const dataTransactions = data[key];
|
||||
if (currentTransactions.length !== dataTransactions.length) {
|
||||
currentState = data;
|
||||
this._store = data;
|
||||
this.dispatch(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
let shouldDispatch = false;
|
||||
dataTransactions.forEach(transaction => {
|
||||
const currentTransaction = currentTransactions.find(t => t.id === transaction.id);
|
||||
if (currentTransaction && currentTransaction.status !== transaction.status) {
|
||||
shouldDispatch = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (shouldDispatch) {
|
||||
currentState = data;
|
||||
this._store = data;
|
||||
this.dispatch(key);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private dispatch<Key extends keyof WalletStoreType>(key: Key): void {
|
||||
const callbacks = this.callbacks[key];
|
||||
if (!callbacks || callbacks.length === 0) return;
|
||||
|
||||
+3
-9
@@ -7,12 +7,6 @@ declare global {
|
||||
|
||||
export type WalletStorage = Storage;
|
||||
|
||||
export type InjectionConfig = {
|
||||
callbackUrl: string;
|
||||
connectTimeout?: number;
|
||||
closeAppOnActions?: boolean;
|
||||
}
|
||||
|
||||
export type WebApp = typeof WebAppConst;
|
||||
export interface ConnectRequest {
|
||||
method: 'connect';
|
||||
@@ -20,10 +14,10 @@ export interface ConnectRequest {
|
||||
}
|
||||
|
||||
export interface SignTransactionRequest {
|
||||
method: 'signTransaction';
|
||||
method: 'sT';
|
||||
id: number;
|
||||
tx: string;
|
||||
wallet: string;
|
||||
tx: `p:${string}` | `bytes:${string}`;
|
||||
wallet?: string;
|
||||
callback: string;
|
||||
}
|
||||
|
||||
|
||||
+3
-15
@@ -1,24 +1,12 @@
|
||||
import type { WebApp, InjectionConfig } from './types';
|
||||
import type { WebApp } from './types';
|
||||
import { Wallet } from './wallet';
|
||||
|
||||
const defaultInjectionConfig: Required<InjectionConfig> = {
|
||||
callbackUrl: '',
|
||||
connectTimeout: 30_000,
|
||||
closeAppOnActions: true,
|
||||
}
|
||||
|
||||
const injectVaranWallet = (webApp: WebApp, config: InjectionConfig = defaultInjectionConfig): void => {
|
||||
const injectVaranWallet = (webApp: WebApp): void => {
|
||||
const isInTelegram = webApp.platform !== 'unknown';
|
||||
|
||||
if (!isInTelegram) return;
|
||||
|
||||
const callbackUrl = config.callbackUrl || defaultInjectionConfig.callbackUrl;
|
||||
|
||||
if (!callbackUrl) {
|
||||
throw new Error('Callback URL must be provided to injectVaranWallet');
|
||||
}
|
||||
|
||||
const wallet = new Wallet(webApp, { ...defaultInjectionConfig, ...config });
|
||||
const wallet = new Wallet(webApp);
|
||||
|
||||
window.injectedWeb3 = {
|
||||
varan: wallet,
|
||||
|
||||
+146
-80
@@ -1,30 +1,28 @@
|
||||
import type { ConnectRequest, SignTransactionRequest, WebApp } from "./types";
|
||||
import type { InjectionConfig } from "./types";
|
||||
import type { WebApp } from "./types";
|
||||
import { WalletStore } from "./store";
|
||||
import type { Injected, InjectedAccount, InjectedExtension, InjectedMetadataKnown, InjectedWindowProvider } from "@polkadot/extension-inject/types";
|
||||
import { encodeRequest } from "./encryption";
|
||||
import { SignerPayloadRaw, SignerResult } from "@polkadot/types/types";
|
||||
import { generateRequestId, parseSignedTxFromStartParam, parseWalletsFromStartParam } from "./helpers";
|
||||
import { SignerPayloadJSON, SignerPayloadRaw, SignerResult } from "@polkadot/types/types";
|
||||
import { generateRequestId } from "./helpers";
|
||||
import { mockAccountsSubscribe } from "./constants";
|
||||
|
||||
import {
|
||||
WalletSocket,
|
||||
type ConnectResponseMessage,
|
||||
type SignTransactionResponseMessage,
|
||||
type ConnectRequest,
|
||||
type SignTransactionRequest
|
||||
} from "./socket";
|
||||
export class Wallet implements InjectedWindowProvider {
|
||||
private readonly varanWalletUrl!: string;
|
||||
private readonly store: WalletStore;
|
||||
private readonly socket: WalletSocket;
|
||||
private readonly name = 'Varan Wallet';
|
||||
private readonly callbackUrl: string;
|
||||
private readonly connectTimeout: number;
|
||||
private readonly closeAppOnActions: boolean;
|
||||
private readonly varanWalletUrl = 'https://t.me/varanwalletbot/start?startapp=';
|
||||
|
||||
constructor(
|
||||
private readonly webApp: WebApp,
|
||||
{ callbackUrl, connectTimeout, closeAppOnActions }: Required<InjectionConfig>,
|
||||
) {
|
||||
this.varanWalletUrl = 'https://t.me/varanwalletbot/start?startapp=';
|
||||
this.store = new WalletStore();
|
||||
this.callbackUrl = callbackUrl;
|
||||
this.connectTimeout = connectTimeout;
|
||||
this.closeAppOnActions = closeAppOnActions;
|
||||
void this._startParamsCheck();
|
||||
this.socket = new WalletSocket();
|
||||
void this._initializeSocket();
|
||||
}
|
||||
|
||||
get version(): string {
|
||||
@@ -34,48 +32,44 @@ export class Wallet implements InjectedWindowProvider {
|
||||
private get signer(): InjectedExtension['signer'] {
|
||||
return {
|
||||
signRaw: (raw) => this.signRawTransaction(raw),
|
||||
signPayload: (payload) => this.signPayloadTransaction(payload),
|
||||
};
|
||||
}
|
||||
|
||||
private async _startParamsCheck(): Promise<void> {
|
||||
let actions = 0;
|
||||
const newWalletAddresses = parseWalletsFromStartParam(this.webApp);
|
||||
const currentAccounts = await this.accounts;
|
||||
private async _initializeSocket(): Promise<void> {
|
||||
try {
|
||||
await this.socket.connect();
|
||||
|
||||
if (newWalletAddresses) {
|
||||
// Listen for wallet connection messages
|
||||
this.socket.onMessage<ConnectResponseMessage>('connect', (data) => {
|
||||
if (data.success && data.wallets && Array.isArray(data.wallets)) {
|
||||
const allWallets = Array.from(
|
||||
new Set(
|
||||
currentAccounts
|
||||
.map(a => a.address)
|
||||
.concat(newWalletAddresses)
|
||||
this.store.get('accounts').concat(data.wallets)
|
||||
)
|
||||
)
|
||||
|
||||
);
|
||||
this.store.set('accounts', allWallets);
|
||||
actions += 1;
|
||||
}
|
||||
});
|
||||
|
||||
const tx = parseSignedTxFromStartParam(this.webApp);
|
||||
|
||||
if (tx) {
|
||||
// Listen for transaction signing responses
|
||||
this.socket.onMessage<SignTransactionResponseMessage>('signTransaction', (data) => {
|
||||
const currentTransactions = this.store.get('transactions');
|
||||
const pendingTransactionIndex = currentTransactions.findIndex(
|
||||
t => t.id === data.id && t.status === 'signing'
|
||||
);
|
||||
|
||||
const pendingTransactionIndex = currentTransactions.findIndex(t => t.id === tx.id && t.status === 'signing');
|
||||
if (pendingTransactionIndex === -1) return;
|
||||
|
||||
if (pendingTransactionIndex !== -1) {
|
||||
currentTransactions[pendingTransactionIndex] = {
|
||||
...currentTransactions[pendingTransactionIndex],
|
||||
status: tx.success ? 'success' : 'failed',
|
||||
signedTx: tx.signedTx ? tx.signedTx : '',
|
||||
status: data.success ? 'success' : 'failed',
|
||||
signedTx: data.signedTx || '',
|
||||
};
|
||||
|
||||
this.store.set('transactions', currentTransactions);
|
||||
actions += 1;
|
||||
}
|
||||
|
||||
|
||||
if (actions && this.closeAppOnActions) {
|
||||
this.webApp.close();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[VARANINJECT] Error initializing socket:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +94,7 @@ export class Wallet implements InjectedWindowProvider {
|
||||
|
||||
private get extensionMetadata(): InjectedExtension['metadata'] {
|
||||
const metadata = this.store.get('injectedMetadataKnown');
|
||||
const setMetadata = async (metadata: InjectedMetadataKnown) => {
|
||||
const setMetadata = async (metadata: InjectedMetadataKnown): Promise<boolean> => {
|
||||
await this.store.set('injectedMetadataKnown', metadata);
|
||||
return true;
|
||||
};
|
||||
@@ -114,87 +108,159 @@ export class Wallet implements InjectedWindowProvider {
|
||||
};
|
||||
}
|
||||
|
||||
private async signPayloadTransaction(payload: SignerPayloadJSON): Promise<SignerResult> {
|
||||
const accounts = await this.accounts;
|
||||
if (!accounts.length) throw new Error('No accounts found');
|
||||
|
||||
const address = accounts.find(a => a.address === payload.address);
|
||||
if (!address) throw new Error('Provided account was not found');
|
||||
|
||||
const requestId = generateRequestId();
|
||||
const request: SignTransactionRequest = {
|
||||
method: 'sT',
|
||||
tx: `p:${JSON.stringify(payload)}`,
|
||||
id: requestId,
|
||||
};
|
||||
|
||||
console.log('[VARANINJECT] signPayloadTransaction request', request);
|
||||
|
||||
// Store transaction as pending
|
||||
const currentTransactions = this.store.get('transactions');
|
||||
currentTransactions.push({
|
||||
id: requestId,
|
||||
signedTx: '',
|
||||
status: 'signing',
|
||||
});
|
||||
this.store.set('transactions', currentTransactions);
|
||||
|
||||
// Send via WebSocket
|
||||
await this.socket.sendViaAPI(request);
|
||||
|
||||
const botUrl = `${this.varanWalletUrl}${this.socket.getUniqueId()}`;
|
||||
|
||||
this.webApp.openTelegramLink(botUrl);
|
||||
|
||||
// Wait for response
|
||||
return new Promise<SignerResult>((resolve, reject) => {
|
||||
const unsubscribe = this.socket.onMessage<SignTransactionResponseMessage>('signTransaction', (data) => {
|
||||
if (data.id === requestId) {
|
||||
unsubscribe();
|
||||
|
||||
const currentTransactions = this.store.get('transactions');
|
||||
const transaction = currentTransactions.find(t => t.id === requestId);
|
||||
|
||||
if (transaction && transaction.status !== 'signing') {
|
||||
if (transaction.status === 'success') {
|
||||
resolve({
|
||||
id: transaction.id,
|
||||
signature: transaction.signedTx,
|
||||
} as SignerResult);
|
||||
} else {
|
||||
reject(new Error('Transaction signing failed'));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async signRawTransaction(raw: SignerPayloadRaw): Promise<SignerResult> {
|
||||
if (raw.type === 'payload') {
|
||||
console.log('[VARANINJECT] signRawTransaction payload', raw.data);
|
||||
return this.signPayloadTransaction(JSON.parse(raw.data));
|
||||
}
|
||||
|
||||
const accounts = await this.accounts;
|
||||
if (!accounts.length) throw new Error('No accounts found');
|
||||
|
||||
const address = accounts.find(a => a.address === raw.address);
|
||||
if (!address) throw new Error('Provided account was not found');
|
||||
|
||||
const requestId = generateRequestId();
|
||||
const request: SignTransactionRequest = {
|
||||
method: 'signTransaction',
|
||||
tx: raw.data,
|
||||
method: 'sT',
|
||||
tx: `bytes:${raw.data}`,
|
||||
wallet: raw.address,
|
||||
callback: this.callbackUrl,
|
||||
id: generateRequestId(),
|
||||
id: requestId,
|
||||
};
|
||||
|
||||
const encodedRequest = encodeRequest(request);
|
||||
const botUrl = `${this.varanWalletUrl}${encodedRequest}`;
|
||||
this.store.set('transactions', [{
|
||||
id: request.id,
|
||||
// Store transaction as pending
|
||||
const currentTransactions = this.store.get('transactions');
|
||||
currentTransactions.push({
|
||||
id: requestId,
|
||||
signedTx: '',
|
||||
status: 'signing',
|
||||
}]);
|
||||
});
|
||||
this.store.set('transactions', currentTransactions);
|
||||
|
||||
this.webApp.openTelegramLink(botUrl);
|
||||
// Send via WebSocket
|
||||
await this.socket.sendViaAPI(request);
|
||||
|
||||
return new Promise<SignerResult>((resolve) => {
|
||||
const id = this.store.subscribe('transactions', (transactions) => {
|
||||
const pendingTransaction = transactions.find(t => t.id === request.id && t.status !== 'signing');
|
||||
if (pendingTransaction) {
|
||||
this.store.unsubscribe('transactions', id);
|
||||
// Wait for response
|
||||
return new Promise<SignerResult>((resolve, reject) => {
|
||||
const unsubscribe = this.socket.onMessage<SignTransactionResponseMessage>('signTransaction', (data) => {
|
||||
if (data.id === requestId) {
|
||||
unsubscribe();
|
||||
|
||||
const currentTransactions = this.store.get('transactions');
|
||||
const transaction = currentTransactions.find(t => t.id === requestId);
|
||||
|
||||
if (transaction && transaction.status !== 'signing') {
|
||||
if (transaction.status === 'success') {
|
||||
resolve({
|
||||
id: pendingTransaction.id,
|
||||
signature: pendingTransaction.signedTx,
|
||||
id: transaction.id,
|
||||
signature: transaction.signedTx,
|
||||
} as SignerResult);
|
||||
} else {
|
||||
reject(new Error('Transaction signing failed'));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async _connect(): Promise<void> {
|
||||
const request: ConnectRequest = {
|
||||
method: 'connect',
|
||||
callback: this.callbackUrl,
|
||||
};
|
||||
try {
|
||||
// Ensure socket is connected
|
||||
await this.socket.connect();
|
||||
|
||||
const encodedRequest = encodeRequest(request);
|
||||
console.log('encodedRequest', encodedRequest);
|
||||
const botUrl = `${this.varanWalletUrl}${encodedRequest}`;
|
||||
console.log('botUrl', botUrl);
|
||||
// Send connect request via WebSocket
|
||||
await this.socket.sendViaAPI({
|
||||
method: 'connect',
|
||||
} as ConnectRequest);
|
||||
|
||||
const botUrl = `${this.varanWalletUrl}${this.socket.getUniqueId()}`;
|
||||
await this.webApp.openTelegramLink(botUrl);
|
||||
|
||||
// Wait for accounts to be populated
|
||||
return new Promise<void>((resolve) => {
|
||||
const id = this.store.subscribe('accounts', (accounts) => {
|
||||
if (accounts.length) {
|
||||
this.store.unsubscribe('accounts', id);
|
||||
const unsubscribe = this.socket.onMessage<ConnectResponseMessage>('connect', (data) => {
|
||||
if (data.success && data.wallets && data.wallets.length > 0) {
|
||||
unsubscribe();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[VARANINJECT] Connect error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async connect(): Promise<InjectedExtension> {
|
||||
const accounts = await this.accounts;
|
||||
if (!accounts.length) {
|
||||
await Promise.race([
|
||||
this._connect(),
|
||||
new Promise<void>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error('Connect timeout'));
|
||||
}, this.connectTimeout);
|
||||
})
|
||||
]);
|
||||
await this._connect();
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
return {
|
||||
name: this.name,
|
||||
version: this.version,
|
||||
accounts: this.extensionAccounts,
|
||||
signer: this.signer,
|
||||
metadata: this.extensionMetadata,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
async enable(): Promise<Injected> {
|
||||
|
||||
Reference in new issue
Block a user