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:
odiinnn committed 2025-11-05 21:36:10 +03:00
1 parent deae91a3e6
commit 4b7a39d332
12 files changed
+1325 -182

No files matched your search

+2
View File
@@ -10,6 +10,8 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@gear-js/api": "^0.44.2",
"@polkadot/api": "^16.4.9",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1" "react-dom": "^19.1.1"
}, },
+726 -8
View File
File diff suppressed because it is too large. Load diff
+19 -1
View File
@@ -1,5 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import './App.css' import './App.css'
import { GearApi } from '@gear-js/api';
const RAW_TRANSACTION = '0xd10204680359836ffbbdaee026cac6008a4c1f68e9fee5fc2949e0ab3ac3f478b7446a8a0dd90114506f6b657220526567697374657218506c617965724cd84143c22eb3da602607c1bd992dbd6db170e21d1b45b99278dbca78831af7028be58d11229c7b53a181793ada6c38754c3b30cc27cb607741d99e7bbadc7c687d165102b787e3e17957eee3435455613ff857a91389b1ae0a24e0618f1afcd6266268020000000000000000000000000000000000000001' 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 signTransaction = async () => {
const extension = await varanWallet.enable?.('current'); const extension = await varanWallet.enable?.('current');
const address = (await extension?.accounts.get())?.[0].address; const address = (await extension?.accounts.get())?.[0].address;
@@ -34,7 +51,7 @@ function App() {
const signature = await extension?.signer?.signRaw?.({ const signature = await extension?.signer?.signRaw?.({
address: address, address: address,
data: RAW_TRANSACTION, data: RAW_TRANSACTION,
type: 'payload', type: 'bytes',
}); });
console.log('[VARANINJECT] signature', signature); console.log('[VARANINJECT] signature', signature);
setSignature(signature?.signature || ''); setSignature(signature?.signature || '');
@@ -53,6 +70,7 @@ function App() {
<button onClick={checkData}>Check Data</button> <button onClick={checkData}>Check Data</button>
<div>DATA</div> <div>DATA</div>
<pre>{data}</pre> <pre>{data}</pre>
<button onClick={test}>Test</button>
{isConnected && {isConnected &&
<> <>
<button onClick={signTransaction}>Sign Transaction</button> <button onClick={signTransaction}>Sign Transaction</button>
+1 -1
View File
@@ -5,7 +5,7 @@ import App from './App.tsx'
import { injectVaranWallet } from 'varan-inject' import { injectVaranWallet } from 'varan-inject'
import WebApp from '@twa-dev/sdk' import WebApp from '@twa-dev/sdk'
injectVaranWallet(WebApp, { injectVaranWallet(WebApp, {
callbackUrl: 'test_y_maxim_bbbbot/start?startapp=' callbackUrl: 'test_y_maxim_bbbbot/start?startapp=',
// callbackUrl: 'ZkPokerTestBot/zkPoker?startapp=' // callbackUrl: 'ZkPokerTestBot/zkPoker?startapp='
}) })
+1
View File
@@ -56,6 +56,7 @@
}, },
"homepage": "https://gitlab.mind-dev.com/pb/varan-connect#readme", "homepage": "https://gitlab.mind-dev.com/pb/varan-connect#readme",
"dependencies": { "dependencies": {
"@polkadot/util": "13.5.7",
"@twa-dev/sdk": "^8.0.2" "@twa-dev/sdk": "^8.0.2"
}, },
"devDependencies": { "devDependencies": {
+65
View File
@@ -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;
}
}
+8
View File
@@ -1 +1,9 @@
export { injectVaranWallet } from './varan-wallet'; export { injectVaranWallet } from './varan-wallet';
export { sendConnect, sendSignedTransaction } from './api';
export type {
ConnectRequest,
SignTransactionRequest,
ConnectResponseMessage,
SignTransactionResponseMessage,
ServerMessage
} from './socket';
+351
View File
@@ -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;
}
}
-68
View File
@@ -21,16 +21,11 @@ const defaultStore: WalletStoreType = {
export class WalletStore { export class WalletStore {
private readonly store: WalletStorage; private readonly store: WalletStorage;
private readonly storeSubscribe: (cb: (ev: StorageEvent) => void) => void;
private _store: WalletStoreType; private _store: WalletStoreType;
constructor() { constructor() {
this.store = window.localStorage as WalletStorage; this.store = window.localStorage as WalletStorage;
this._store = Object.assign(defaultStore, JSON.parse(this.store.getItem(VARAN_WALLET_LS_KEY) || '{}')); 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: { private callbacks: {
@@ -44,69 +39,6 @@ export class WalletStore {
transactions: [], 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 { private dispatch<Key extends keyof WalletStoreType>(key: Key): void {
const callbacks = this.callbacks[key]; const callbacks = this.callbacks[key];
if (!callbacks || callbacks.length === 0) return; if (!callbacks || callbacks.length === 0) return;
+3 -9
View File
@@ -7,12 +7,6 @@ declare global {
export type WalletStorage = Storage; export type WalletStorage = Storage;
export type InjectionConfig = {
callbackUrl: string;
connectTimeout?: number;
closeAppOnActions?: boolean;
}
export type WebApp = typeof WebAppConst; export type WebApp = typeof WebAppConst;
export interface ConnectRequest { export interface ConnectRequest {
method: 'connect'; method: 'connect';
@@ -20,10 +14,10 @@ export interface ConnectRequest {
} }
export interface SignTransactionRequest { export interface SignTransactionRequest {
method: 'signTransaction'; method: 'sT';
id: number; id: number;
tx: string; tx: `p:${string}` | `bytes:${string}`;
wallet: string; wallet?: string;
callback: string; callback: string;
} }
+3 -15
View File
@@ -1,24 +1,12 @@
import type { WebApp, InjectionConfig } from './types'; import type { WebApp } from './types';
import { Wallet } from './wallet'; import { Wallet } from './wallet';
const defaultInjectionConfig: Required<InjectionConfig> = { const injectVaranWallet = (webApp: WebApp): void => {
callbackUrl: '',
connectTimeout: 30_000,
closeAppOnActions: true,
}
const injectVaranWallet = (webApp: WebApp, config: InjectionConfig = defaultInjectionConfig): void => {
const isInTelegram = webApp.platform !== 'unknown'; const isInTelegram = webApp.platform !== 'unknown';
if (!isInTelegram) return; if (!isInTelegram) return;
const callbackUrl = config.callbackUrl || defaultInjectionConfig.callbackUrl; const wallet = new Wallet(webApp);
if (!callbackUrl) {
throw new Error('Callback URL must be provided to injectVaranWallet');
}
const wallet = new Wallet(webApp, { ...defaultInjectionConfig, ...config });
window.injectedWeb3 = { window.injectedWeb3 = {
varan: wallet, varan: wallet,
+146 -80
View File
@@ -1,30 +1,28 @@
import type { ConnectRequest, SignTransactionRequest, WebApp } from "./types"; import type { WebApp } from "./types";
import type { InjectionConfig } from "./types";
import { WalletStore } from "./store"; import { WalletStore } from "./store";
import type { Injected, InjectedAccount, InjectedExtension, InjectedMetadataKnown, InjectedWindowProvider } from "@polkadot/extension-inject/types"; import type { Injected, InjectedAccount, InjectedExtension, InjectedMetadataKnown, InjectedWindowProvider } from "@polkadot/extension-inject/types";
import { encodeRequest } from "./encryption"; import { SignerPayloadJSON, SignerPayloadRaw, SignerResult } from "@polkadot/types/types";
import { SignerPayloadRaw, SignerResult } from "@polkadot/types/types"; import { generateRequestId } from "./helpers";
import { generateRequestId, parseSignedTxFromStartParam, parseWalletsFromStartParam } from "./helpers";
import { mockAccountsSubscribe } from "./constants"; import { mockAccountsSubscribe } from "./constants";
import {
WalletSocket,
type ConnectResponseMessage,
type SignTransactionResponseMessage,
type ConnectRequest,
type SignTransactionRequest
} from "./socket";
export class Wallet implements InjectedWindowProvider { export class Wallet implements InjectedWindowProvider {
private readonly varanWalletUrl!: string;
private readonly store: WalletStore; private readonly store: WalletStore;
private readonly socket: WalletSocket;
private readonly name = 'Varan Wallet'; private readonly name = 'Varan Wallet';
private readonly callbackUrl: string; private readonly varanWalletUrl = 'https://t.me/varanwalletbot/start?startapp=';
private readonly connectTimeout: number;
private readonly closeAppOnActions: boolean;
constructor( constructor(
private readonly webApp: WebApp, private readonly webApp: WebApp,
{ callbackUrl, connectTimeout, closeAppOnActions }: Required<InjectionConfig>,
) { ) {
this.varanWalletUrl = 'https://t.me/varanwalletbot/start?startapp=';
this.store = new WalletStore(); this.store = new WalletStore();
this.callbackUrl = callbackUrl; this.socket = new WalletSocket();
this.connectTimeout = connectTimeout; void this._initializeSocket();
this.closeAppOnActions = closeAppOnActions;
void this._startParamsCheck();
} }
get version(): string { get version(): string {
@@ -34,48 +32,44 @@ export class Wallet implements InjectedWindowProvider {
private get signer(): InjectedExtension['signer'] { private get signer(): InjectedExtension['signer'] {
return { return {
signRaw: (raw) => this.signRawTransaction(raw), signRaw: (raw) => this.signRawTransaction(raw),
signPayload: (payload) => this.signPayloadTransaction(payload),
}; };
} }
private async _startParamsCheck(): Promise<void> { private async _initializeSocket(): Promise<void> {
let actions = 0; try {
const newWalletAddresses = parseWalletsFromStartParam(this.webApp); await this.socket.connect();
const currentAccounts = await this.accounts;
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( const allWallets = Array.from(
new Set( new Set(
currentAccounts this.store.get('accounts').concat(data.wallets)
.map(a => a.address)
.concat(newWalletAddresses)
) )
) );
this.store.set('accounts', allWallets); this.store.set('accounts', allWallets);
actions += 1;
} }
});
const tx = parseSignedTxFromStartParam(this.webApp); // Listen for transaction signing responses
this.socket.onMessage<SignTransactionResponseMessage>('signTransaction', (data) => {
if (tx) {
const currentTransactions = this.store.get('transactions'); 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) {
if (pendingTransactionIndex === -1) return;
currentTransactions[pendingTransactionIndex] = { currentTransactions[pendingTransactionIndex] = {
...currentTransactions[pendingTransactionIndex], ...currentTransactions[pendingTransactionIndex],
status: tx.success ? 'success' : 'failed', status: data.success ? 'success' : 'failed',
signedTx: tx.signedTx ? tx.signedTx : '', signedTx: data.signedTx || '',
}; };
this.store.set('transactions', currentTransactions); this.store.set('transactions', currentTransactions);
actions += 1;
} }
});
} catch (error) {
if (actions && this.closeAppOnActions) { console.error('[VARANINJECT] Error initializing socket:', error);
this.webApp.close();
} }
} }
@@ -100,7 +94,7 @@ export class Wallet implements InjectedWindowProvider {
private get extensionMetadata(): InjectedExtension['metadata'] { private get extensionMetadata(): InjectedExtension['metadata'] {
const metadata = this.store.get('injectedMetadataKnown'); const metadata = this.store.get('injectedMetadataKnown');
const setMetadata = async (metadata: InjectedMetadataKnown) => { const setMetadata = async (metadata: InjectedMetadataKnown): Promise<boolean> => {
await this.store.set('injectedMetadataKnown', metadata); await this.store.set('injectedMetadataKnown', metadata);
return true; 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> { 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; const accounts = await this.accounts;
if (!accounts.length) throw new Error('No accounts found'); if (!accounts.length) throw new Error('No accounts found');
const address = accounts.find(a => a.address === raw.address); const address = accounts.find(a => a.address === raw.address);
if (!address) throw new Error('Provided account was not found'); if (!address) throw new Error('Provided account was not found');
const requestId = generateRequestId();
const request: SignTransactionRequest = { const request: SignTransactionRequest = {
method: 'signTransaction', method: 'sT',
tx: raw.data, tx: `bytes:${raw.data}`,
wallet: raw.address, wallet: raw.address,
callback: this.callbackUrl, id: requestId,
id: generateRequestId(),
}; };
const encodedRequest = encodeRequest(request); // Store transaction as pending
const botUrl = `${this.varanWalletUrl}${encodedRequest}`; const currentTransactions = this.store.get('transactions');
this.store.set('transactions', [{ currentTransactions.push({
id: request.id, id: requestId,
signedTx: '', signedTx: '',
status: 'signing', status: 'signing',
}]); });
this.store.set('transactions', currentTransactions);
this.webApp.openTelegramLink(botUrl); // Send via WebSocket
await this.socket.sendViaAPI(request);
return new Promise<SignerResult>((resolve) => { // Wait for response
const id = this.store.subscribe('transactions', (transactions) => { return new Promise<SignerResult>((resolve, reject) => {
const pendingTransaction = transactions.find(t => t.id === request.id && t.status !== 'signing'); const unsubscribe = this.socket.onMessage<SignTransactionResponseMessage>('signTransaction', (data) => {
if (pendingTransaction) { if (data.id === requestId) {
this.store.unsubscribe('transactions', id); 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({ resolve({
id: pendingTransaction.id, id: transaction.id,
signature: pendingTransaction.signedTx, signature: transaction.signedTx,
} as SignerResult); } as SignerResult);
} else {
reject(new Error('Transaction signing failed'));
}
}
} }
}); });
}); });
} }
private async _connect(): Promise<void> { private async _connect(): Promise<void> {
const request: ConnectRequest = { try {
method: 'connect', // Ensure socket is connected
callback: this.callbackUrl, await this.socket.connect();
};
const encodedRequest = encodeRequest(request); // Send connect request via WebSocket
console.log('encodedRequest', encodedRequest); await this.socket.sendViaAPI({
const botUrl = `${this.varanWalletUrl}${encodedRequest}`; method: 'connect',
console.log('botUrl', botUrl); } as ConnectRequest);
const botUrl = `${this.varanWalletUrl}${this.socket.getUniqueId()}`;
await this.webApp.openTelegramLink(botUrl); await this.webApp.openTelegramLink(botUrl);
// Wait for accounts to be populated
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
const id = this.store.subscribe('accounts', (accounts) => { const unsubscribe = this.socket.onMessage<ConnectResponseMessage>('connect', (data) => {
if (accounts.length) { if (data.success && data.wallets && data.wallets.length > 0) {
this.store.unsubscribe('accounts', id); unsubscribe();
resolve(); resolve();
} }
}); });
}); });
} catch (error) {
console.error('[VARANINJECT] Connect error:', error);
throw error;
}
} }
async connect(): Promise<InjectedExtension> { async connect(): Promise<InjectedExtension> {
const accounts = await this.accounts; const accounts = await this.accounts;
if (!accounts.length) { if (!accounts.length) {
await Promise.race([ await this._connect();
this._connect(),
new Promise<void>((_, reject) => {
setTimeout(() => {
reject(new Error('Connect timeout'));
}, this.connectTimeout);
})
]);
} }
return Promise.resolve({ return {
name: this.name, name: this.name,
version: this.version, version: this.version,
accounts: this.extensionAccounts, accounts: this.extensionAccounts,
signer: this.signer, signer: this.signer,
metadata: this.extensionMetadata, metadata: this.extensionMetadata,
}); };
} }
async enable(): Promise<Injected> { async enable(): Promise<Injected> {