@eos3/connect 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +246 -0
- package/dist/index.d.ts +225 -0
- package/dist/index.js +1386 -0
- package/package.json +40 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1386 @@
|
|
|
1
|
+
import { ABI, PrivateKey, Serializer, Transaction } from '@wharfkit/antelope';
|
|
2
|
+
export class EosConnectError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
rawMessage;
|
|
5
|
+
retryable;
|
|
6
|
+
constructor(code, message, options = {}) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'EosConnectError';
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.rawMessage = options.rawMessage ?? message;
|
|
11
|
+
this.retryable = options.retryable ?? false;
|
|
12
|
+
if (options.cause !== undefined) {
|
|
13
|
+
this.cause = options.cause;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function isEosConnectError(error) {
|
|
18
|
+
return error instanceof EosConnectError;
|
|
19
|
+
}
|
|
20
|
+
const defaultWalletSetupEnv = {
|
|
21
|
+
hasTelegramSession: true,
|
|
22
|
+
canStorePayKey: true
|
|
23
|
+
};
|
|
24
|
+
export const EOS_CONNECT_PRIVATE_KEY_STORAGE = 'tg_eos_wallet_pay_key_v1';
|
|
25
|
+
export const EOS_CONNECT_DEFAULT_API_BASE_URL = 'https://eospasskey.aiexsat.com';
|
|
26
|
+
export const EOS_CONNECT_NETWORKS = {
|
|
27
|
+
testnet: {
|
|
28
|
+
id: 'testnet',
|
|
29
|
+
name: 'Jungle4 Testnet',
|
|
30
|
+
apiBaseUrl: 'https://eospasskey.aiexsat.com',
|
|
31
|
+
chainId: '73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d',
|
|
32
|
+
rpcUrls: [
|
|
33
|
+
'https://jungle4.cryptolions.io:443',
|
|
34
|
+
'http://jungle4.cryptolions.io:80',
|
|
35
|
+
'https://15.204.22.80:443',
|
|
36
|
+
'https://jungle4.eosphere.io:443',
|
|
37
|
+
'https://jungle4.api.eosnation.io:443',
|
|
38
|
+
'https://104.37.219.148:443'
|
|
39
|
+
],
|
|
40
|
+
balanceAsset: {
|
|
41
|
+
tokenContract: 'core.vaulta',
|
|
42
|
+
symbol: 'A',
|
|
43
|
+
precision: 4
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
mainnet: {
|
|
47
|
+
id: 'mainnet',
|
|
48
|
+
name: 'EOS Mainnet',
|
|
49
|
+
apiBaseUrl: 'https://eospasskey.aiexsat.com',
|
|
50
|
+
chainId: 'aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906',
|
|
51
|
+
rpcUrls: [
|
|
52
|
+
'https://eos.eoseyes.com',
|
|
53
|
+
'https://api.eossupport.io',
|
|
54
|
+
'https://vaulta-hyperion.eosphere.io',
|
|
55
|
+
'https://eos.eosusa.io',
|
|
56
|
+
'https://eos.greymass.com'
|
|
57
|
+
],
|
|
58
|
+
balanceAsset: {
|
|
59
|
+
tokenContract: 'core.vaulta',
|
|
60
|
+
symbol: 'A',
|
|
61
|
+
precision: 4
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const SECURE_KEY_VERSION = 1;
|
|
66
|
+
const TELEGRAM_MOBILE_REQUIRED = 'Open this wallet in the Telegram mobile app to use secure biometric key storage.';
|
|
67
|
+
const providers = [
|
|
68
|
+
{
|
|
69
|
+
id: 'telegram',
|
|
70
|
+
name: 'Telegram secure binding',
|
|
71
|
+
state: 'available',
|
|
72
|
+
description: 'Create or connect an EOS wallet through Telegram SecureStorage and passkeys.'
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'tokenpocket',
|
|
76
|
+
name: 'TokenPocket',
|
|
77
|
+
state: 'available',
|
|
78
|
+
description: 'Open TokenPocket and bind an existing EOS wallet to Telegram payments.'
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'anchor',
|
|
82
|
+
name: 'Anchor',
|
|
83
|
+
state: 'coming_soon',
|
|
84
|
+
description: 'Anchor connection is reserved for a later wallet adapter.'
|
|
85
|
+
}
|
|
86
|
+
];
|
|
87
|
+
const idleState = {
|
|
88
|
+
status: 'idle',
|
|
89
|
+
provider: null,
|
|
90
|
+
account: null,
|
|
91
|
+
balance: null,
|
|
92
|
+
bindId: null,
|
|
93
|
+
bindUrl: null,
|
|
94
|
+
error: null
|
|
95
|
+
};
|
|
96
|
+
function normalizeBaseUrl(apiBaseUrl) {
|
|
97
|
+
return apiBaseUrl.replace(/\/+$/, '');
|
|
98
|
+
}
|
|
99
|
+
function providerDisplayName(provider) {
|
|
100
|
+
return providers.find((item) => item.id === provider)?.name ?? provider;
|
|
101
|
+
}
|
|
102
|
+
function titleCase(value) {
|
|
103
|
+
if (!value)
|
|
104
|
+
return value;
|
|
105
|
+
return value.slice(0, 1).toUpperCase() + value.slice(1);
|
|
106
|
+
}
|
|
107
|
+
function telegramDeviceLabel(app) {
|
|
108
|
+
const platform = app?.platform ? titleCase(app.platform) : '';
|
|
109
|
+
const version = app?.version ?? '';
|
|
110
|
+
return ['Telegram', platform, version].filter(Boolean).join(' ') || 'Telegram device';
|
|
111
|
+
}
|
|
112
|
+
export function eosConnectTelegramPayStorageDiagnostics(app) {
|
|
113
|
+
const hasInitData = Boolean(app.initData);
|
|
114
|
+
const hasSecureStorage = Boolean(app.SecureStorage);
|
|
115
|
+
const hasSecureSet = typeof app.SecureStorage?.setItem === 'function';
|
|
116
|
+
const hasSecureGet = typeof app.SecureStorage?.getItem === 'function';
|
|
117
|
+
const hasBiometricAuth = typeof app.BiometricManager?.authenticate === 'function';
|
|
118
|
+
const hasBiometricAccess = Boolean(app.BiometricManager?.isAccessGranted) ||
|
|
119
|
+
typeof app.BiometricManager?.requestAccess === 'function';
|
|
120
|
+
const hasAvailableBiometrics = app.BiometricManager?.isBiometricAvailable !== false;
|
|
121
|
+
const biometricAvailableDetail = app.BiometricManager?.isBiometricAvailable === true
|
|
122
|
+
? 'true'
|
|
123
|
+
: app.BiometricManager?.isBiometricAvailable === false
|
|
124
|
+
? 'false'
|
|
125
|
+
: '未返回';
|
|
126
|
+
const hasBiometricToken = typeof app.BiometricManager?.updateBiometricToken === 'function';
|
|
127
|
+
const hasBiometricSettings = typeof app.BiometricManager?.openSettings === 'function';
|
|
128
|
+
return [
|
|
129
|
+
{
|
|
130
|
+
label: 'Telegram 平台',
|
|
131
|
+
ok: true,
|
|
132
|
+
detail: `${app.platform ?? '未知平台'} · ${app.version ?? '未知版本'}`
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
label: 'Telegram 会话',
|
|
136
|
+
ok: hasInitData,
|
|
137
|
+
detail: hasInitData ? 'initData 已存在' : '缺少 initData'
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
label: 'SecureStorage',
|
|
141
|
+
ok: hasSecureStorage,
|
|
142
|
+
detail: hasSecureStorage ? '有' : '没有'
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
label: 'SecureStorage.setItem',
|
|
146
|
+
ok: hasSecureSet,
|
|
147
|
+
detail: hasSecureSet ? '有' : '没有'
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
label: 'SecureStorage.getItem',
|
|
151
|
+
ok: hasSecureGet,
|
|
152
|
+
detail: hasSecureGet ? '有' : '没有'
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
label: '生物识别解锁',
|
|
156
|
+
ok: hasBiometricAuth,
|
|
157
|
+
detail: hasBiometricAuth ? 'authenticate 可用' : '缺少 BiometricManager.authenticate'
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
label: 'isBiometricAvailable',
|
|
161
|
+
ok: hasAvailableBiometrics,
|
|
162
|
+
detail: biometricAvailableDetail
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
label: '生物识别授权',
|
|
166
|
+
ok: hasBiometricAccess,
|
|
167
|
+
detail: hasBiometricAccess ? '已授权或可请求授权' : '缺少 BiometricManager.requestAccess'
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
label: '生物识别令牌保存',
|
|
171
|
+
ok: hasBiometricToken,
|
|
172
|
+
detail: hasBiometricToken
|
|
173
|
+
? 'updateBiometricToken 可用'
|
|
174
|
+
: '缺少 BiometricManager.updateBiometricToken'
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
label: 'BiometricManager.openSettings',
|
|
178
|
+
ok: true,
|
|
179
|
+
detail: hasBiometricSettings ? '有' : '没有'
|
|
180
|
+
}
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
export function supportsEosConnectTelegramPayStorage(app) {
|
|
184
|
+
return eosConnectTelegramPayStorageDiagnostics(app).every((item) => item.ok);
|
|
185
|
+
}
|
|
186
|
+
export async function initEosConnectTelegramBiometricManager(app, timeoutMs = 5000) {
|
|
187
|
+
const biometric = app.BiometricManager;
|
|
188
|
+
if (typeof biometric?.init !== 'function') {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
if (biometric.isInited) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
await new Promise((resolve) => {
|
|
195
|
+
let settled = false;
|
|
196
|
+
const finish = () => {
|
|
197
|
+
if (settled)
|
|
198
|
+
return;
|
|
199
|
+
settled = true;
|
|
200
|
+
clearTimeout(timer);
|
|
201
|
+
resolve();
|
|
202
|
+
};
|
|
203
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
204
|
+
biometric.init(finish);
|
|
205
|
+
});
|
|
206
|
+
return Boolean(biometric.isInited);
|
|
207
|
+
}
|
|
208
|
+
export async function openEosConnectTelegramBiometricSettings(app) {
|
|
209
|
+
const biometric = app.BiometricManager;
|
|
210
|
+
if (typeof biometric?.openSettings !== 'function') {
|
|
211
|
+
return {
|
|
212
|
+
opened: false,
|
|
213
|
+
message: '当前 Telegram 不支持打开生物识别设置。请确认 Telegram 已更新到支持 BiometricManager.openSettings 的版本。'
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
await initEosConnectTelegramBiometricManager(app);
|
|
218
|
+
if (biometric.isBiometricAvailable === false) {
|
|
219
|
+
return {
|
|
220
|
+
opened: false,
|
|
221
|
+
message: 'Telegram 返回 isBiometricAvailable=false,无法打开生物识别授权设置。请先在 Android 系统和 Telegram 中开启指纹、密码锁或本机锁屏。'
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
biometric.openSettings();
|
|
225
|
+
return {
|
|
226
|
+
opened: true,
|
|
227
|
+
message: '已请求打开 Telegram 生物识别设置。返回后请重新点击“刷新”。'
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
return {
|
|
232
|
+
opened: false,
|
|
233
|
+
message: `打开 Telegram 生物识别设置失败:${error instanceof Error ? error.message : '操作失败'}`
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
export function eosConnectWalletSetupState(wallet, env = defaultWalletSetupEnv) {
|
|
238
|
+
const setupEnv = {
|
|
239
|
+
...defaultWalletSetupEnv,
|
|
240
|
+
...env
|
|
241
|
+
};
|
|
242
|
+
if (!setupEnv.hasTelegramSession) {
|
|
243
|
+
return {
|
|
244
|
+
status: 'missing_telegram_session',
|
|
245
|
+
action: 'none',
|
|
246
|
+
account: null,
|
|
247
|
+
hasWallet: false,
|
|
248
|
+
bindId: null,
|
|
249
|
+
bindUrl: null,
|
|
250
|
+
isRecoverable: false,
|
|
251
|
+
showAction: false,
|
|
252
|
+
reason: 'Telegram initData is missing'
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
if (!setupEnv.canStorePayKey) {
|
|
256
|
+
return {
|
|
257
|
+
status: 'secure_storage_unavailable',
|
|
258
|
+
action: 'none',
|
|
259
|
+
account: null,
|
|
260
|
+
hasWallet: Boolean(wallet.hasWallet),
|
|
261
|
+
bindId: wallet.bindId ?? null,
|
|
262
|
+
bindUrl: wallet.bindUrl ?? null,
|
|
263
|
+
isRecoverable: false,
|
|
264
|
+
showAction: false,
|
|
265
|
+
reason: TELEGRAM_MOBILE_REQUIRED
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (wallet.status === 'account_created_waiting_tgpay' && wallet.pendingAccount) {
|
|
269
|
+
return {
|
|
270
|
+
status: 'pending_tgpay',
|
|
271
|
+
action: 'continue',
|
|
272
|
+
account: wallet.pendingAccount,
|
|
273
|
+
hasWallet: Boolean(wallet.hasWallet),
|
|
274
|
+
bindId: wallet.bindId ?? null,
|
|
275
|
+
bindUrl: wallet.bindUrl ?? null,
|
|
276
|
+
isRecoverable: true,
|
|
277
|
+
showAction: true,
|
|
278
|
+
reason: null
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (wallet.status === 'pay_binding_invalid' && wallet.eosAccount) {
|
|
282
|
+
return {
|
|
283
|
+
status: 'pay_binding_invalid',
|
|
284
|
+
action: 'rebind',
|
|
285
|
+
account: wallet.eosAccount,
|
|
286
|
+
hasWallet: true,
|
|
287
|
+
bindId: wallet.bindId ?? null,
|
|
288
|
+
bindUrl: wallet.bindUrl ?? null,
|
|
289
|
+
isRecoverable: false,
|
|
290
|
+
showAction: true,
|
|
291
|
+
reason: wallet.bindingError ?? null
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (wallet.hasWallet && wallet.eosAccount) {
|
|
295
|
+
return {
|
|
296
|
+
status: 'ready',
|
|
297
|
+
action: 'transfer',
|
|
298
|
+
account: wallet.eosAccount,
|
|
299
|
+
hasWallet: true,
|
|
300
|
+
bindId: null,
|
|
301
|
+
bindUrl: null,
|
|
302
|
+
isRecoverable: false,
|
|
303
|
+
showAction: false,
|
|
304
|
+
reason: null
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
status: 'not_connected',
|
|
309
|
+
action: 'create',
|
|
310
|
+
account: null,
|
|
311
|
+
hasWallet: false,
|
|
312
|
+
bindId: wallet.bindId ?? null,
|
|
313
|
+
bindUrl: wallet.bindUrl ?? null,
|
|
314
|
+
isRecoverable: false,
|
|
315
|
+
showAction: true,
|
|
316
|
+
reason: wallet.bindingError ?? null
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function openExternalUrl(url, options) {
|
|
320
|
+
if (options.openExternal) {
|
|
321
|
+
options.openExternal(url);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (!url.startsWith('tpdapp://') && options.telegramWebApp?.openLink) {
|
|
325
|
+
options.telegramWebApp.openLink(url);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
globalThis.location?.assign(url);
|
|
329
|
+
}
|
|
330
|
+
function escapeConfirmHtml(value) {
|
|
331
|
+
return String(value ?? '')
|
|
332
|
+
.replace(/&/g, '&')
|
|
333
|
+
.replace(/</g, '<')
|
|
334
|
+
.replace(/>/g, '>')
|
|
335
|
+
.replace(/"/g, '"');
|
|
336
|
+
}
|
|
337
|
+
function paymentFromSummary(summary) {
|
|
338
|
+
return {
|
|
339
|
+
to: summary.to,
|
|
340
|
+
tokenContract: summary.tokenContract,
|
|
341
|
+
quantity: summary.quantity,
|
|
342
|
+
memo: summary.memo
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
function paymentsFromTransaction(transaction) {
|
|
346
|
+
const actions = Array.isArray(transaction.actions) ? transaction.actions : [];
|
|
347
|
+
for (const action of actions) {
|
|
348
|
+
if (!action || typeof action !== 'object')
|
|
349
|
+
continue;
|
|
350
|
+
const data = action.data;
|
|
351
|
+
if (!data || typeof data !== 'object')
|
|
352
|
+
continue;
|
|
353
|
+
const rawPayments = data.payments;
|
|
354
|
+
if (!Array.isArray(rawPayments))
|
|
355
|
+
continue;
|
|
356
|
+
return rawPayments
|
|
357
|
+
.map((payment) => {
|
|
358
|
+
if (!payment || typeof payment !== 'object')
|
|
359
|
+
return null;
|
|
360
|
+
const item = payment;
|
|
361
|
+
return {
|
|
362
|
+
to: String(item.to ?? ''),
|
|
363
|
+
tokenContract: String(item.tokenContract ?? item.token_contract ?? ''),
|
|
364
|
+
quantity: String(item.quantity ?? ''),
|
|
365
|
+
memo: String(item.memo ?? '')
|
|
366
|
+
};
|
|
367
|
+
})
|
|
368
|
+
.filter((payment) => Boolean(payment?.to && payment.quantity));
|
|
369
|
+
}
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
function paymentDetailsFromBuiltTransfer(built) {
|
|
373
|
+
const transactionPayments = paymentsFromTransaction(built.transaction);
|
|
374
|
+
const payments = built.payments?.length
|
|
375
|
+
? built.payments
|
|
376
|
+
: transactionPayments.length
|
|
377
|
+
? transactionPayments
|
|
378
|
+
: [paymentFromSummary(built.summary)];
|
|
379
|
+
const primary = payments[0] ?? paymentFromSummary(built.summary);
|
|
380
|
+
return {
|
|
381
|
+
intentId: built.intentId,
|
|
382
|
+
chainId: built.chainId,
|
|
383
|
+
from: built.summary.from,
|
|
384
|
+
to: primary.to,
|
|
385
|
+
tokenContract: primary.tokenContract,
|
|
386
|
+
quantity: primary.quantity,
|
|
387
|
+
memo: primary.memo,
|
|
388
|
+
payments
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
export function normalizeEosConnectError(error) {
|
|
392
|
+
if (isEosConnectError(error)) {
|
|
393
|
+
return error;
|
|
394
|
+
}
|
|
395
|
+
const rawMessage = rawErrorMessage(error);
|
|
396
|
+
const normalized = rawMessage.toLowerCase();
|
|
397
|
+
if (normalized.includes('daily limit exceeded')) {
|
|
398
|
+
return new EosConnectError('DAILY_LIMIT_EXCEEDED', '今日支付额度已用完,请明天再试或提高每日额度。', {
|
|
399
|
+
rawMessage,
|
|
400
|
+
cause: error
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
if (normalized.includes('per-transfer limit exceeded') ||
|
|
404
|
+
normalized.includes('per transfer limit exceeded') ||
|
|
405
|
+
normalized.includes('per_tx_limit') ||
|
|
406
|
+
normalized.includes('single transfer limit')) {
|
|
407
|
+
return new EosConnectError('PER_TX_LIMIT_EXCEEDED', '本次支付超过单笔额度,请降低金额或提高单笔额度。', {
|
|
408
|
+
rawMessage,
|
|
409
|
+
cause: error
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
if (normalized.includes('total budget exceeded')) {
|
|
413
|
+
return new EosConnectError('TOTAL_BUDGET_EXCEEDED', '该权限的总预算已用完,请调整总预算后再试。', {
|
|
414
|
+
rawMessage,
|
|
415
|
+
cause: error
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
if (normalized.includes('overdrawn balance') ||
|
|
419
|
+
normalized.includes('insufficient balance') ||
|
|
420
|
+
normalized.includes('balance is insufficient')) {
|
|
421
|
+
return new EosConnectError('INSUFFICIENT_BALANCE', '余额不足,请充值后再试。', {
|
|
422
|
+
rawMessage,
|
|
423
|
+
cause: error
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
if (normalized.includes('ram usage') ||
|
|
427
|
+
normalized.includes('billed ram') ||
|
|
428
|
+
normalized.includes('insufficient ram') ||
|
|
429
|
+
normalized.includes('current ram usage limit')) {
|
|
430
|
+
return new EosConnectError('RAM_INSUFFICIENT', '账号 RAM 不足,请补充 RAM 后再试。', {
|
|
431
|
+
rawMessage,
|
|
432
|
+
cause: error
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
if (normalized.includes('bot permission is inactive') ||
|
|
436
|
+
normalized.includes('permission does not match bot authorization') ||
|
|
437
|
+
normalized.includes('paylimit code authority is missing') ||
|
|
438
|
+
normalized.includes('missing from transfer permission') ||
|
|
439
|
+
normalized.includes('transaction declares authority') ||
|
|
440
|
+
normalized.includes('missing authority')) {
|
|
441
|
+
return new EosConnectError('PAYMENT_PERMISSION_MISSING', '快捷支付权限未绑定或已失效,请重新绑定后再试。', {
|
|
442
|
+
rawMessage,
|
|
443
|
+
cause: error
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
if (normalized.includes('not signed in') ||
|
|
447
|
+
normalized.includes('invalid session') ||
|
|
448
|
+
normalized.includes('session user not found') ||
|
|
449
|
+
normalized.includes('telegram initdata is missing') ||
|
|
450
|
+
normalized.includes('telegram session not found')) {
|
|
451
|
+
return new EosConnectError('SESSION_EXPIRED', '登录状态已失效,请重新打开小程序或重新登录。', {
|
|
452
|
+
rawMessage,
|
|
453
|
+
cause: error
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
if (normalized.includes('notallowederror') ||
|
|
457
|
+
normalized.includes('aborterror') ||
|
|
458
|
+
normalized.includes('payment confirmation was cancelled') ||
|
|
459
|
+
normalized.includes('biometric authentication failed') ||
|
|
460
|
+
normalized.includes('user cancelled')) {
|
|
461
|
+
return new EosConnectError('SIGNATURE_REJECTED', '已取消签名确认,支付未提交。', {
|
|
462
|
+
rawMessage,
|
|
463
|
+
cause: error
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
if (normalized.includes('failed to fetch') ||
|
|
467
|
+
normalized.includes('network') ||
|
|
468
|
+
normalized.includes('all eos rpc nodes failed') ||
|
|
469
|
+
normalized.includes('eos rpc') ||
|
|
470
|
+
normalized.includes('timeout')) {
|
|
471
|
+
return new EosConnectError('NETWORK_ERROR', '网络连接失败,请稍后重试。', {
|
|
472
|
+
rawMessage,
|
|
473
|
+
retryable: true,
|
|
474
|
+
cause: error
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return new EosConnectError('UNKNOWN_CHAIN_ERROR', '支付失败,请稍后重试或联系服务方。', {
|
|
478
|
+
rawMessage,
|
|
479
|
+
cause: error
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
function rawErrorMessage(error) {
|
|
483
|
+
if (error instanceof Error) {
|
|
484
|
+
return error.message;
|
|
485
|
+
}
|
|
486
|
+
if (typeof error === 'string') {
|
|
487
|
+
return error;
|
|
488
|
+
}
|
|
489
|
+
if (error && typeof error === 'object') {
|
|
490
|
+
const maybeError = error;
|
|
491
|
+
if (typeof maybeError.error === 'string') {
|
|
492
|
+
return maybeError.error;
|
|
493
|
+
}
|
|
494
|
+
if (typeof maybeError.message === 'string') {
|
|
495
|
+
return maybeError.message;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return 'EOS Connect request failed';
|
|
499
|
+
}
|
|
500
|
+
function ensurePaymentConfirmStyle(documentRef) {
|
|
501
|
+
if (documentRef.querySelector('[data-eos-connect-payment-confirm-style]'))
|
|
502
|
+
return;
|
|
503
|
+
const style = documentRef.createElement('style');
|
|
504
|
+
style.dataset.eosConnectPaymentConfirmStyle = 'true';
|
|
505
|
+
style.textContent = `
|
|
506
|
+
.eos-connect-payment-confirm-backdrop {
|
|
507
|
+
position: fixed;
|
|
508
|
+
inset: 0;
|
|
509
|
+
z-index: 2147483647;
|
|
510
|
+
display: flex;
|
|
511
|
+
align-items: flex-end;
|
|
512
|
+
justify-content: center;
|
|
513
|
+
background: rgba(17, 24, 39, 0.42);
|
|
514
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
515
|
+
animation: eosConnectPaymentBackdropIn 180ms ease-out both;
|
|
516
|
+
}
|
|
517
|
+
.eos-connect-payment-confirm-sheet {
|
|
518
|
+
width: min(100%, 520px);
|
|
519
|
+
max-height: calc(100vh - 32px);
|
|
520
|
+
overflow: auto;
|
|
521
|
+
box-sizing: border-box;
|
|
522
|
+
border-radius: 24px 24px 0 0;
|
|
523
|
+
background: #fff;
|
|
524
|
+
padding: 28px 24px max(24px, env(safe-area-inset-bottom));
|
|
525
|
+
color: #1f2937;
|
|
526
|
+
box-shadow: 0 -18px 50px rgba(15, 23, 42, 0.16);
|
|
527
|
+
animation: eosConnectPaymentSheetIn 220ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
|
528
|
+
}
|
|
529
|
+
.eos-connect-payment-confirm-header {
|
|
530
|
+
display: grid;
|
|
531
|
+
grid-template-columns: 44px 1fr 44px;
|
|
532
|
+
align-items: center;
|
|
533
|
+
margin-bottom: 24px;
|
|
534
|
+
}
|
|
535
|
+
.eos-connect-payment-confirm-header h2 {
|
|
536
|
+
margin: 0;
|
|
537
|
+
text-align: center;
|
|
538
|
+
font-size: 22px;
|
|
539
|
+
line-height: 1.2;
|
|
540
|
+
font-weight: 700;
|
|
541
|
+
}
|
|
542
|
+
.eos-connect-payment-confirm-close {
|
|
543
|
+
width: 44px;
|
|
544
|
+
height: 44px;
|
|
545
|
+
border: 0;
|
|
546
|
+
border-radius: 50%;
|
|
547
|
+
background: #f3f4f6;
|
|
548
|
+
color: #111827;
|
|
549
|
+
font-size: 30px;
|
|
550
|
+
line-height: 1;
|
|
551
|
+
}
|
|
552
|
+
.eos-connect-payment-confirm-amount {
|
|
553
|
+
margin: 0 0 24px;
|
|
554
|
+
border-radius: 12px;
|
|
555
|
+
background: #f5f6fa;
|
|
556
|
+
padding: 24px 12px;
|
|
557
|
+
text-align: center;
|
|
558
|
+
font-size: 24px;
|
|
559
|
+
line-height: 1.2;
|
|
560
|
+
font-weight: 700;
|
|
561
|
+
}
|
|
562
|
+
.eos-connect-payment-confirm-row {
|
|
563
|
+
display: grid;
|
|
564
|
+
grid-template-columns: minmax(72px, 0.42fr) minmax(0, 1fr);
|
|
565
|
+
gap: 18px;
|
|
566
|
+
padding: 18px 0;
|
|
567
|
+
border-bottom: 1px solid #eef0f4;
|
|
568
|
+
font-size: 17px;
|
|
569
|
+
line-height: 1.35;
|
|
570
|
+
}
|
|
571
|
+
.eos-connect-payment-confirm-row span {
|
|
572
|
+
color: #9ca3af;
|
|
573
|
+
}
|
|
574
|
+
.eos-connect-payment-confirm-row strong {
|
|
575
|
+
min-width: 0;
|
|
576
|
+
overflow-wrap: anywhere;
|
|
577
|
+
font-weight: 650;
|
|
578
|
+
color: #1f2937;
|
|
579
|
+
}
|
|
580
|
+
.eos-connect-payment-confirm-action {
|
|
581
|
+
width: 100%;
|
|
582
|
+
min-height: 56px;
|
|
583
|
+
margin-top: 34px;
|
|
584
|
+
border: 0;
|
|
585
|
+
border-radius: 14px;
|
|
586
|
+
background: #2f80ed;
|
|
587
|
+
color: #fff;
|
|
588
|
+
font-size: 19px;
|
|
589
|
+
font-weight: 700;
|
|
590
|
+
}
|
|
591
|
+
@keyframes eosConnectPaymentBackdropIn {
|
|
592
|
+
from {
|
|
593
|
+
opacity: 0;
|
|
594
|
+
}
|
|
595
|
+
to {
|
|
596
|
+
opacity: 1;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
@keyframes eosConnectPaymentSheetIn {
|
|
600
|
+
from {
|
|
601
|
+
opacity: 0;
|
|
602
|
+
transform: translateY(28px);
|
|
603
|
+
}
|
|
604
|
+
to {
|
|
605
|
+
opacity: 1;
|
|
606
|
+
transform: translateY(0);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
`;
|
|
610
|
+
documentRef.head.appendChild(style);
|
|
611
|
+
}
|
|
612
|
+
function defaultConfirmPayment(details) {
|
|
613
|
+
const documentRef = globalThis.document;
|
|
614
|
+
if (!documentRef?.body)
|
|
615
|
+
return Promise.resolve(true);
|
|
616
|
+
ensurePaymentConfirmStyle(documentRef);
|
|
617
|
+
const backdrop = documentRef.createElement('div');
|
|
618
|
+
backdrop.className = 'eos-connect-payment-confirm-backdrop';
|
|
619
|
+
backdrop.setAttribute('role', 'presentation');
|
|
620
|
+
const serviceRows = details.payments
|
|
621
|
+
.slice(1)
|
|
622
|
+
.map((payment) => `
|
|
623
|
+
<div class="eos-connect-payment-confirm-row">
|
|
624
|
+
<span>Gas</span>
|
|
625
|
+
<strong>${escapeConfirmHtml(payment.quantity)}</strong>
|
|
626
|
+
</div>
|
|
627
|
+
`)
|
|
628
|
+
.join('');
|
|
629
|
+
backdrop.innerHTML = `
|
|
630
|
+
<section class="eos-connect-payment-confirm-sheet" role="dialog" aria-modal="true" aria-labelledby="eos-connect-payment-confirm-title">
|
|
631
|
+
<header class="eos-connect-payment-confirm-header">
|
|
632
|
+
<span></span>
|
|
633
|
+
<h2 id="eos-connect-payment-confirm-title">交易详情</h2>
|
|
634
|
+
<button type="button" class="eos-connect-payment-confirm-close" aria-label="关闭">×</button>
|
|
635
|
+
</header>
|
|
636
|
+
<div class="eos-connect-payment-confirm-amount">-${escapeConfirmHtml(details.quantity)}</div>
|
|
637
|
+
<div class="eos-connect-payment-confirm-row">
|
|
638
|
+
<span>收款地址</span>
|
|
639
|
+
<strong>${escapeConfirmHtml(details.to)}</strong>
|
|
640
|
+
</div>
|
|
641
|
+
<div class="eos-connect-payment-confirm-row">
|
|
642
|
+
<span>备注</span>
|
|
643
|
+
<strong>${escapeConfirmHtml(details.memo || '-')}</strong>
|
|
644
|
+
</div>
|
|
645
|
+
${serviceRows}
|
|
646
|
+
<button type="button" class="eos-connect-payment-confirm-action">确定</button>
|
|
647
|
+
</section>
|
|
648
|
+
`;
|
|
649
|
+
return new Promise((resolve) => {
|
|
650
|
+
let settled = false;
|
|
651
|
+
const finish = (confirmed) => {
|
|
652
|
+
if (settled)
|
|
653
|
+
return;
|
|
654
|
+
settled = true;
|
|
655
|
+
backdrop.remove();
|
|
656
|
+
resolve(confirmed);
|
|
657
|
+
};
|
|
658
|
+
backdrop.querySelector('.eos-connect-payment-confirm-action')?.addEventListener('click', () => finish(true));
|
|
659
|
+
backdrop.querySelector('.eos-connect-payment-confirm-close')?.addEventListener('click', () => finish(false));
|
|
660
|
+
backdrop.addEventListener('click', (event) => {
|
|
661
|
+
if (event.target === backdrop)
|
|
662
|
+
finish(false);
|
|
663
|
+
});
|
|
664
|
+
documentRef.body.appendChild(backdrop);
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
function balanceQuery(asset) {
|
|
668
|
+
if (!asset)
|
|
669
|
+
return '';
|
|
670
|
+
const params = new URLSearchParams({
|
|
671
|
+
balanceTokenContract: asset.tokenContract,
|
|
672
|
+
balanceSymbol: asset.symbol
|
|
673
|
+
});
|
|
674
|
+
if (typeof asset.precision === 'number') {
|
|
675
|
+
params.set('balancePrecision', String(asset.precision));
|
|
676
|
+
}
|
|
677
|
+
return `?${params.toString()}`;
|
|
678
|
+
}
|
|
679
|
+
function parseRpcUrls(raw) {
|
|
680
|
+
const values = typeof raw === 'string' ? raw.split(',') : raw ?? [];
|
|
681
|
+
const seen = new Set();
|
|
682
|
+
const urls = [];
|
|
683
|
+
for (const item of values) {
|
|
684
|
+
const value = item.trim().replace(/\/+$/, '');
|
|
685
|
+
if (value && !seen.has(value)) {
|
|
686
|
+
seen.add(value);
|
|
687
|
+
urls.push(value);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return urls;
|
|
691
|
+
}
|
|
692
|
+
export function tokenPocketDappUrl(url) {
|
|
693
|
+
return `tpdapp://open?params=${encodeURIComponent(JSON.stringify({
|
|
694
|
+
url,
|
|
695
|
+
chain: 'EOS',
|
|
696
|
+
source: 'eospasskey'
|
|
697
|
+
}))}`;
|
|
698
|
+
}
|
|
699
|
+
export async function generateEosConnectPaymentKey() {
|
|
700
|
+
const privateKey = PrivateKey.generate('K1');
|
|
701
|
+
return {
|
|
702
|
+
privateKey: privateKey.toString(),
|
|
703
|
+
publicKey: privateKey.toPublic().toString()
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
export async function publicKeyFromEosPrivateKey(privateKey) {
|
|
707
|
+
return PrivateKey.fromString(privateKey).toPublic().toString();
|
|
708
|
+
}
|
|
709
|
+
function base64UrlEncode(bytes) {
|
|
710
|
+
const binary = String.fromCharCode(...bytes);
|
|
711
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
712
|
+
}
|
|
713
|
+
function base64UrlDecode(value) {
|
|
714
|
+
const base64 = value.replace(/-/g, '+').replace(/_/g, '/');
|
|
715
|
+
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=');
|
|
716
|
+
return Uint8Array.from(atob(padded), (char) => char.charCodeAt(0));
|
|
717
|
+
}
|
|
718
|
+
function arrayBuffer(bytes) {
|
|
719
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
720
|
+
copy.set(bytes);
|
|
721
|
+
return copy.buffer;
|
|
722
|
+
}
|
|
723
|
+
function randomBase64Url(byteLength) {
|
|
724
|
+
const bytes = new Uint8Array(byteLength);
|
|
725
|
+
crypto.getRandomValues(bytes);
|
|
726
|
+
return base64UrlEncode(bytes);
|
|
727
|
+
}
|
|
728
|
+
function requireSecureTelegramStorage(app) {
|
|
729
|
+
if (!app?.SecureStorage || !app.BiometricManager?.updateBiometricToken) {
|
|
730
|
+
throw new Error(TELEGRAM_MOBILE_REQUIRED);
|
|
731
|
+
}
|
|
732
|
+
return app;
|
|
733
|
+
}
|
|
734
|
+
async function importUnlockKey(unlockKey) {
|
|
735
|
+
return crypto.subtle.importKey('raw', arrayBuffer(base64UrlDecode(unlockKey)), { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
|
|
736
|
+
}
|
|
737
|
+
async function encryptPrivateKey(privateKey, unlockKey) {
|
|
738
|
+
const iv = new Uint8Array(12);
|
|
739
|
+
crypto.getRandomValues(iv);
|
|
740
|
+
const key = await importUnlockKey(unlockKey);
|
|
741
|
+
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, new TextEncoder().encode(privateKey));
|
|
742
|
+
return {
|
|
743
|
+
version: SECURE_KEY_VERSION,
|
|
744
|
+
alg: 'AES-GCM',
|
|
745
|
+
iv: base64UrlEncode(iv),
|
|
746
|
+
ciphertext: base64UrlEncode(new Uint8Array(ciphertext))
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
async function decryptPrivateKey(envelope, unlockKey) {
|
|
750
|
+
if (envelope.version !== SECURE_KEY_VERSION || envelope.alg !== 'AES-GCM') {
|
|
751
|
+
throw new Error('Unsupported encrypted signing key format');
|
|
752
|
+
}
|
|
753
|
+
const key = await importUnlockKey(unlockKey);
|
|
754
|
+
const plaintext = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: arrayBuffer(base64UrlDecode(envelope.iv)) }, key, arrayBuffer(base64UrlDecode(envelope.ciphertext)));
|
|
755
|
+
return new TextDecoder().decode(plaintext);
|
|
756
|
+
}
|
|
757
|
+
async function ensureBiometricAccess(app) {
|
|
758
|
+
const biometric = app.BiometricManager;
|
|
759
|
+
if (!biometric?.updateBiometricToken) {
|
|
760
|
+
throw new Error(TELEGRAM_MOBILE_REQUIRED);
|
|
761
|
+
}
|
|
762
|
+
if (!biometric.isInited) {
|
|
763
|
+
await new Promise((resolve) => biometric.init(resolve));
|
|
764
|
+
}
|
|
765
|
+
if (!biometric.isBiometricAvailable) {
|
|
766
|
+
throw new Error(TELEGRAM_MOBILE_REQUIRED);
|
|
767
|
+
}
|
|
768
|
+
if (!biometric.isAccessGranted) {
|
|
769
|
+
if (!biometric.requestAccess) {
|
|
770
|
+
throw new Error(TELEGRAM_MOBILE_REQUIRED);
|
|
771
|
+
}
|
|
772
|
+
const granted = await new Promise((resolve) => {
|
|
773
|
+
biometric.requestAccess?.({ reason: 'Protect EOS signing key' }, resolve);
|
|
774
|
+
});
|
|
775
|
+
if (!granted) {
|
|
776
|
+
throw new Error('Telegram biometric access was not granted');
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function secureStorageSet(app, key, value) {
|
|
781
|
+
return new Promise((resolve, reject) => {
|
|
782
|
+
app.SecureStorage?.setItem(key, value, (error, isStored) => {
|
|
783
|
+
if (error) {
|
|
784
|
+
reject(new Error(error));
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
if (!isStored) {
|
|
788
|
+
reject(new Error('Telegram SecureStorage did not save the signing key'));
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
resolve();
|
|
792
|
+
});
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
function secureStorageGet(app, key) {
|
|
796
|
+
return new Promise((resolve, reject) => {
|
|
797
|
+
app.SecureStorage?.getItem(key, (error, value) => {
|
|
798
|
+
if (error) {
|
|
799
|
+
reject(new Error(error));
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
resolve(value ?? null);
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
function updateBiometricToken(app, token) {
|
|
807
|
+
return new Promise((resolve, reject) => {
|
|
808
|
+
app.BiometricManager?.updateBiometricToken(token, (isUpdated) => {
|
|
809
|
+
if (isUpdated) {
|
|
810
|
+
resolve();
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
reject(new Error('Telegram did not save the biometric unlock key'));
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
function authenticateForUnlock(app) {
|
|
819
|
+
return new Promise((resolve, reject) => {
|
|
820
|
+
app.BiometricManager?.authenticate({ reason: 'Unlock EOS signing key' }, (isAuthenticated, biometricToken) => {
|
|
821
|
+
if (!isAuthenticated) {
|
|
822
|
+
reject(new Error('Biometric authentication failed'));
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (!biometricToken) {
|
|
826
|
+
reject(new Error('Biometric unlock key is missing on this device'));
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
resolve(biometricToken);
|
|
830
|
+
});
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
function secureStorageRemove(app, key) {
|
|
834
|
+
return new Promise((resolve, reject) => {
|
|
835
|
+
if (app.SecureStorage?.removeItem) {
|
|
836
|
+
app.SecureStorage.removeItem(key, (error, isRemoved) => {
|
|
837
|
+
if (error) {
|
|
838
|
+
reject(new Error(error));
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
if (isRemoved === false) {
|
|
842
|
+
reject(new Error('Telegram SecureStorage did not remove the signing key'));
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
resolve();
|
|
846
|
+
});
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
app.SecureStorage?.setItem(key, '', (error, isStored) => {
|
|
850
|
+
if (error) {
|
|
851
|
+
reject(new Error(error));
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (!isStored) {
|
|
855
|
+
reject(new Error('Telegram SecureStorage did not clear the signing key'));
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
resolve();
|
|
859
|
+
});
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
export async function saveEosConnectPaymentKey(privateKey, app) {
|
|
863
|
+
const telegram = requireSecureTelegramStorage(app);
|
|
864
|
+
await ensureBiometricAccess(telegram);
|
|
865
|
+
const unlockKey = randomBase64Url(32);
|
|
866
|
+
const envelope = await encryptPrivateKey(privateKey, unlockKey);
|
|
867
|
+
envelope.publicKey = await publicKeyFromEosPrivateKey(privateKey);
|
|
868
|
+
envelope.createdAt = new Date().toISOString();
|
|
869
|
+
await secureStorageSet(telegram, EOS_CONNECT_PRIVATE_KEY_STORAGE, JSON.stringify(envelope));
|
|
870
|
+
await updateBiometricToken(telegram, unlockKey);
|
|
871
|
+
}
|
|
872
|
+
export async function loadEosConnectStoredPublicKey(app) {
|
|
873
|
+
if (!app?.SecureStorage) {
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
876
|
+
const encrypted = await secureStorageGet(app, EOS_CONNECT_PRIVATE_KEY_STORAGE);
|
|
877
|
+
if (!encrypted) {
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
try {
|
|
881
|
+
const envelope = JSON.parse(encrypted);
|
|
882
|
+
return envelope.publicKey ?? null;
|
|
883
|
+
}
|
|
884
|
+
catch {
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
export async function loadEosConnectPaymentSigningKey(app) {
|
|
889
|
+
const telegram = requireSecureTelegramStorage(app);
|
|
890
|
+
await ensureBiometricAccess(telegram);
|
|
891
|
+
const encrypted = await secureStorageGet(telegram, EOS_CONNECT_PRIVATE_KEY_STORAGE);
|
|
892
|
+
if (!encrypted) {
|
|
893
|
+
throw new Error('Quick key is missing on this device');
|
|
894
|
+
}
|
|
895
|
+
const envelope = JSON.parse(encrypted);
|
|
896
|
+
const unlockKey = await authenticateForUnlock(telegram);
|
|
897
|
+
const privateKey = await decryptPrivateKey(envelope, unlockKey);
|
|
898
|
+
if (!privateKey) {
|
|
899
|
+
throw new Error('Quick key is missing on this device');
|
|
900
|
+
}
|
|
901
|
+
const publicKey = await publicKeyFromEosPrivateKey(privateKey);
|
|
902
|
+
if (envelope.publicKey && envelope.publicKey !== publicKey) {
|
|
903
|
+
throw new Error(`Local payment key is inconsistent. Stored: ${envelope.publicKey}, derived: ${publicKey}. Rebind payment key before paying.`);
|
|
904
|
+
}
|
|
905
|
+
return {
|
|
906
|
+
privateKey,
|
|
907
|
+
publicKey,
|
|
908
|
+
storedPublicKey: envelope.publicKey ?? null
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
export async function removeEosConnectPaymentKey(app) {
|
|
912
|
+
const telegram = requireSecureTelegramStorage(app);
|
|
913
|
+
await Promise.all([
|
|
914
|
+
secureStorageRemove(telegram, EOS_CONNECT_PRIVATE_KEY_STORAGE),
|
|
915
|
+
updateBiometricToken(telegram, '')
|
|
916
|
+
]);
|
|
917
|
+
}
|
|
918
|
+
export async function signEosConnectTransaction(transaction, context) {
|
|
919
|
+
const { privateKey } = await loadEosConnectPaymentSigningKey(context.telegramWebApp);
|
|
920
|
+
return signTransactionWithPrivateKey(transaction, privateKey, context.rpcUrls, context.fetch ?? fetch);
|
|
921
|
+
}
|
|
922
|
+
async function signTransactionWithPrivateKey(transaction, privateKey, rpcUrls, fetchImpl) {
|
|
923
|
+
if (rpcUrls.length === 0) {
|
|
924
|
+
throw new Error('Missing EOS RPC URL for EOS Connect signing');
|
|
925
|
+
}
|
|
926
|
+
const failures = [];
|
|
927
|
+
for (const rpcUrl of rpcUrls) {
|
|
928
|
+
try {
|
|
929
|
+
const prepared = await prepareEosConnectTransaction(rpcUrl, transaction, fetchImpl);
|
|
930
|
+
const signature = PrivateKey.fromString(privateKey)
|
|
931
|
+
.signDigest(prepared.transaction.signingDigest(prepared.chainId))
|
|
932
|
+
.toString();
|
|
933
|
+
if (!signature) {
|
|
934
|
+
throw new Error('EOS transaction signing failed');
|
|
935
|
+
}
|
|
936
|
+
return {
|
|
937
|
+
signatures: [signature],
|
|
938
|
+
serializedTransaction: Array.from(prepared.serializedTransaction),
|
|
939
|
+
transaction
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
catch (error) {
|
|
943
|
+
failures.push(`${rpcUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
throw new Error(`All EOS RPC nodes failed: ${failures.join('; ')}`);
|
|
947
|
+
}
|
|
948
|
+
async function prepareEosConnectTransaction(rpcUrl, transaction, fetchImpl) {
|
|
949
|
+
const [info, abis] = await Promise.all([
|
|
950
|
+
postChain(rpcUrl, 'get_info', {}, fetchImpl),
|
|
951
|
+
loadTransactionAbis(rpcUrl, transaction, fetchImpl)
|
|
952
|
+
]);
|
|
953
|
+
if (!info.chain_id) {
|
|
954
|
+
throw new Error('EOS RPC did not return a chain id');
|
|
955
|
+
}
|
|
956
|
+
const parsedTransaction = Transaction.from(transaction, abis);
|
|
957
|
+
const serializedTransaction = Serializer.encode({ object: parsedTransaction }).array;
|
|
958
|
+
if (!serializedTransaction.length) {
|
|
959
|
+
throw new Error('EOS transaction serialization failed');
|
|
960
|
+
}
|
|
961
|
+
return {
|
|
962
|
+
chainId: info.chain_id,
|
|
963
|
+
transaction: parsedTransaction,
|
|
964
|
+
serializedTransaction
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
async function postChain(rpcUrl, method, body, fetchImpl) {
|
|
968
|
+
const response = await fetchImpl(`${rpcUrl}/v1/chain/${method}`, {
|
|
969
|
+
method: 'POST',
|
|
970
|
+
headers: {
|
|
971
|
+
'content-type': 'application/json'
|
|
972
|
+
},
|
|
973
|
+
body: JSON.stringify(body)
|
|
974
|
+
});
|
|
975
|
+
if (!response.ok) {
|
|
976
|
+
throw new Error(`EOS RPC ${method} failed with ${response.status}`);
|
|
977
|
+
}
|
|
978
|
+
return response.json();
|
|
979
|
+
}
|
|
980
|
+
async function loadTransactionAbis(rpcUrl, transaction, fetchImpl) {
|
|
981
|
+
const contracts = transactionActionContracts(transaction);
|
|
982
|
+
return Promise.all(contracts.map(async (contract) => {
|
|
983
|
+
const response = await postChain(rpcUrl, 'get_abi', { account_name: contract }, fetchImpl);
|
|
984
|
+
if (!response.abi) {
|
|
985
|
+
throw new Error(`EOS RPC did not return an ABI for ${contract}`);
|
|
986
|
+
}
|
|
987
|
+
return { contract, abi: ABI.from(response.abi) };
|
|
988
|
+
}));
|
|
989
|
+
}
|
|
990
|
+
function transactionActionContracts(transaction) {
|
|
991
|
+
const seen = new Set();
|
|
992
|
+
for (const action of [
|
|
993
|
+
...actionList(transaction.context_free_actions),
|
|
994
|
+
...actionList(transaction.actions)
|
|
995
|
+
]) {
|
|
996
|
+
const account = typeof action.account === 'string' ? action.account : '';
|
|
997
|
+
if (account) {
|
|
998
|
+
seen.add(account);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return Array.from(seen);
|
|
1002
|
+
}
|
|
1003
|
+
function actionList(value) {
|
|
1004
|
+
if (!Array.isArray(value)) {
|
|
1005
|
+
return [];
|
|
1006
|
+
}
|
|
1007
|
+
return value.filter((item) => Boolean(item && typeof item === 'object'));
|
|
1008
|
+
}
|
|
1009
|
+
function walletCapability(wallet, localPublicKey) {
|
|
1010
|
+
if (wallet.status === 'account_created_waiting_tgpay') {
|
|
1011
|
+
return {
|
|
1012
|
+
status: 'pending',
|
|
1013
|
+
hasWallet: Boolean(wallet.hasWallet),
|
|
1014
|
+
canUse: false,
|
|
1015
|
+
needsLocalKey: false,
|
|
1016
|
+
account: wallet.pendingAccount ?? wallet.eosAccount,
|
|
1017
|
+
balance: wallet.balance,
|
|
1018
|
+
bindId: wallet.bindId ?? null,
|
|
1019
|
+
bindUrl: wallet.bindUrl ?? null,
|
|
1020
|
+
expectedPublicKey: wallet.quickPublicKey ?? null,
|
|
1021
|
+
localPublicKey,
|
|
1022
|
+
reason: null,
|
|
1023
|
+
wallet
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
if (!wallet.hasWallet || !wallet.eosAccount) {
|
|
1027
|
+
return {
|
|
1028
|
+
status: 'not_connected',
|
|
1029
|
+
hasWallet: false,
|
|
1030
|
+
canUse: false,
|
|
1031
|
+
needsLocalKey: false,
|
|
1032
|
+
account: null,
|
|
1033
|
+
balance: wallet.balance,
|
|
1034
|
+
bindId: wallet.bindId ?? null,
|
|
1035
|
+
bindUrl: wallet.bindUrl ?? null,
|
|
1036
|
+
expectedPublicKey: wallet.quickPublicKey ?? null,
|
|
1037
|
+
localPublicKey,
|
|
1038
|
+
reason: wallet.bindingError ?? null,
|
|
1039
|
+
wallet
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
const expectedPublicKey = wallet.quickPublicKey ?? null;
|
|
1043
|
+
if (!expectedPublicKey || localPublicKey !== expectedPublicKey) {
|
|
1044
|
+
return {
|
|
1045
|
+
status: 'needs_local_key',
|
|
1046
|
+
hasWallet: true,
|
|
1047
|
+
canUse: false,
|
|
1048
|
+
needsLocalKey: true,
|
|
1049
|
+
account: wallet.eosAccount,
|
|
1050
|
+
balance: wallet.balance,
|
|
1051
|
+
bindId: null,
|
|
1052
|
+
bindUrl: null,
|
|
1053
|
+
expectedPublicKey,
|
|
1054
|
+
localPublicKey,
|
|
1055
|
+
reason: 'Local quick payment key is missing or does not match this wallet',
|
|
1056
|
+
wallet
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
return {
|
|
1060
|
+
status: 'ready',
|
|
1061
|
+
hasWallet: true,
|
|
1062
|
+
canUse: true,
|
|
1063
|
+
needsLocalKey: false,
|
|
1064
|
+
account: wallet.eosAccount,
|
|
1065
|
+
balance: wallet.balance,
|
|
1066
|
+
bindId: null,
|
|
1067
|
+
bindUrl: null,
|
|
1068
|
+
expectedPublicKey,
|
|
1069
|
+
localPublicKey,
|
|
1070
|
+
reason: null,
|
|
1071
|
+
wallet
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
function walletToState(wallet) {
|
|
1075
|
+
if (wallet.status === 'account_created_waiting_tgpay') {
|
|
1076
|
+
return {
|
|
1077
|
+
status: 'pending',
|
|
1078
|
+
provider: 'telegram',
|
|
1079
|
+
account: wallet.pendingAccount ?? wallet.eosAccount,
|
|
1080
|
+
balance: wallet.balance,
|
|
1081
|
+
bindId: wallet.bindId ?? null,
|
|
1082
|
+
bindUrl: wallet.bindUrl ?? null,
|
|
1083
|
+
error: null
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
if (wallet.hasWallet && wallet.eosAccount) {
|
|
1087
|
+
return {
|
|
1088
|
+
status: 'connected',
|
|
1089
|
+
provider: 'telegram',
|
|
1090
|
+
account: wallet.eosAccount,
|
|
1091
|
+
balance: wallet.balance,
|
|
1092
|
+
bindId: null,
|
|
1093
|
+
bindUrl: null,
|
|
1094
|
+
error: null
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
return {
|
|
1098
|
+
status: 'not_connected',
|
|
1099
|
+
provider: null,
|
|
1100
|
+
account: null,
|
|
1101
|
+
balance: wallet.balance,
|
|
1102
|
+
bindId: null,
|
|
1103
|
+
bindUrl: null,
|
|
1104
|
+
error: wallet.bindingError ?? null
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
function walletCapabilityToState(capability) {
|
|
1108
|
+
const base = capability.status === 'unsupported'
|
|
1109
|
+
? {
|
|
1110
|
+
...idleState,
|
|
1111
|
+
status: 'unsupported',
|
|
1112
|
+
error: capability.reason
|
|
1113
|
+
}
|
|
1114
|
+
: capability.status === 'pending'
|
|
1115
|
+
? {
|
|
1116
|
+
status: 'pending',
|
|
1117
|
+
provider: 'telegram',
|
|
1118
|
+
account: capability.account,
|
|
1119
|
+
balance: capability.balance,
|
|
1120
|
+
bindId: capability.bindId,
|
|
1121
|
+
bindUrl: capability.bindUrl,
|
|
1122
|
+
error: null
|
|
1123
|
+
}
|
|
1124
|
+
: capability.hasWallet
|
|
1125
|
+
? {
|
|
1126
|
+
status: 'connected',
|
|
1127
|
+
provider: 'telegram',
|
|
1128
|
+
account: capability.account,
|
|
1129
|
+
balance: capability.balance,
|
|
1130
|
+
bindId: null,
|
|
1131
|
+
bindUrl: null,
|
|
1132
|
+
error: null
|
|
1133
|
+
}
|
|
1134
|
+
: {
|
|
1135
|
+
status: 'not_connected',
|
|
1136
|
+
provider: null,
|
|
1137
|
+
account: null,
|
|
1138
|
+
balance: capability.balance,
|
|
1139
|
+
bindId: capability.bindId,
|
|
1140
|
+
bindUrl: capability.bindUrl,
|
|
1141
|
+
error: capability.reason
|
|
1142
|
+
};
|
|
1143
|
+
return {
|
|
1144
|
+
...base,
|
|
1145
|
+
hasWallet: capability.hasWallet,
|
|
1146
|
+
canUse: capability.canUse,
|
|
1147
|
+
needsLocalKey: capability.needsLocalKey,
|
|
1148
|
+
expectedPublicKey: capability.expectedPublicKey,
|
|
1149
|
+
localPublicKey: capability.localPublicKey
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
export function createEosConnect(options) {
|
|
1153
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
1154
|
+
const networkConfig = options.network ? EOS_CONNECT_NETWORKS[options.network] : null;
|
|
1155
|
+
const apiBaseUrl = normalizeBaseUrl(options.apiBaseUrl ?? networkConfig?.apiBaseUrl ?? EOS_CONNECT_DEFAULT_API_BASE_URL);
|
|
1156
|
+
const balanceAsset = options.balanceAsset ?? networkConfig?.balanceAsset;
|
|
1157
|
+
const rpcUrls = parseRpcUrls(options.rpcUrls ?? networkConfig?.rpcUrls);
|
|
1158
|
+
const listeners = new Set();
|
|
1159
|
+
let state = { ...idleState };
|
|
1160
|
+
function publish(next) {
|
|
1161
|
+
state = next;
|
|
1162
|
+
listeners.forEach((listener) => listener(state));
|
|
1163
|
+
return state;
|
|
1164
|
+
}
|
|
1165
|
+
function initData() {
|
|
1166
|
+
return options.telegramWebApp?.initData ?? '';
|
|
1167
|
+
}
|
|
1168
|
+
async function request(path, requestOptions) {
|
|
1169
|
+
let response;
|
|
1170
|
+
try {
|
|
1171
|
+
response = await fetchImpl(`${apiBaseUrl}${path}`, {
|
|
1172
|
+
...requestOptions,
|
|
1173
|
+
headers: {
|
|
1174
|
+
'content-type': 'application/json',
|
|
1175
|
+
...(requestOptions?.headers ?? {})
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
catch (error) {
|
|
1180
|
+
throw normalizeEosConnectError(error);
|
|
1181
|
+
}
|
|
1182
|
+
const body = (await response.json());
|
|
1183
|
+
if (!response.ok) {
|
|
1184
|
+
throw normalizeEosConnectError(body);
|
|
1185
|
+
}
|
|
1186
|
+
return body;
|
|
1187
|
+
}
|
|
1188
|
+
return {
|
|
1189
|
+
getProviders() {
|
|
1190
|
+
return providers.map((provider) => ({ ...provider }));
|
|
1191
|
+
},
|
|
1192
|
+
getSnapshot() {
|
|
1193
|
+
return { ...state };
|
|
1194
|
+
},
|
|
1195
|
+
subscribe(listener) {
|
|
1196
|
+
listeners.add(listener);
|
|
1197
|
+
return () => listeners.delete(listener);
|
|
1198
|
+
},
|
|
1199
|
+
async restore() {
|
|
1200
|
+
if (!initData()) {
|
|
1201
|
+
return publish({
|
|
1202
|
+
...idleState,
|
|
1203
|
+
status: 'unsupported',
|
|
1204
|
+
error: 'Telegram initData is missing'
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
const wallet = await request(`/api/tg-wallet/me${balanceQuery(balanceAsset)}`, {
|
|
1208
|
+
headers: {
|
|
1209
|
+
'x-telegram-init-data': initData()
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
return publish(walletToState(wallet));
|
|
1213
|
+
},
|
|
1214
|
+
async checkWallet() {
|
|
1215
|
+
if (!initData()) {
|
|
1216
|
+
const capability = {
|
|
1217
|
+
status: 'unsupported',
|
|
1218
|
+
hasWallet: false,
|
|
1219
|
+
canUse: false,
|
|
1220
|
+
needsLocalKey: false,
|
|
1221
|
+
account: null,
|
|
1222
|
+
balance: null,
|
|
1223
|
+
bindId: null,
|
|
1224
|
+
bindUrl: null,
|
|
1225
|
+
expectedPublicKey: null,
|
|
1226
|
+
localPublicKey: null,
|
|
1227
|
+
reason: 'Telegram initData is missing',
|
|
1228
|
+
wallet: null
|
|
1229
|
+
};
|
|
1230
|
+
publish(walletCapabilityToState(capability));
|
|
1231
|
+
return capability;
|
|
1232
|
+
}
|
|
1233
|
+
const wallet = await request(`/api/tg-wallet/me${balanceQuery(balanceAsset)}`, {
|
|
1234
|
+
headers: {
|
|
1235
|
+
'x-telegram-init-data': initData()
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
1238
|
+
const localPublicKey = wallet.quickPublicKey
|
|
1239
|
+
? await loadEosConnectStoredPublicKey(options.telegramWebApp)
|
|
1240
|
+
: null;
|
|
1241
|
+
const capability = walletCapability(wallet, localPublicKey);
|
|
1242
|
+
publish(walletCapabilityToState(capability));
|
|
1243
|
+
return capability;
|
|
1244
|
+
},
|
|
1245
|
+
async connect(connectOptions) {
|
|
1246
|
+
if (connectOptions.provider !== 'telegram' && connectOptions.provider !== 'tokenpocket') {
|
|
1247
|
+
throw new Error(`${providerDisplayName(connectOptions.provider)} is not supported yet`);
|
|
1248
|
+
}
|
|
1249
|
+
if (!initData()) {
|
|
1250
|
+
return publish({
|
|
1251
|
+
...idleState,
|
|
1252
|
+
status: 'unsupported',
|
|
1253
|
+
provider: connectOptions.provider,
|
|
1254
|
+
error: 'Telegram initData is missing'
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
if (!connectOptions.publicKey) {
|
|
1258
|
+
throw new Error(`${providerDisplayName(connectOptions.provider)} provider requires a publicKey`);
|
|
1259
|
+
}
|
|
1260
|
+
const path = connectOptions.provider === 'tokenpocket'
|
|
1261
|
+
? '/api/tg-wallet/tp-bind/start'
|
|
1262
|
+
: '/api/tg-wallet/bind/start';
|
|
1263
|
+
const bind = await request(path, {
|
|
1264
|
+
method: 'POST',
|
|
1265
|
+
body: JSON.stringify({
|
|
1266
|
+
initData: initData(),
|
|
1267
|
+
botPublicKey: connectOptions.provider === 'telegram' ? connectOptions.publicKey : undefined,
|
|
1268
|
+
tgpayPublicKey: connectOptions.provider === 'tokenpocket' ? connectOptions.publicKey : undefined,
|
|
1269
|
+
botUsername: options.botUsername || undefined,
|
|
1270
|
+
deviceLabel: options.deviceLabel || telegramDeviceLabel(options.telegramWebApp),
|
|
1271
|
+
assetLimits: connectOptions.assetLimits,
|
|
1272
|
+
replaceWallet: connectOptions.provider === 'telegram' ? connectOptions.replaceWallet || undefined : undefined
|
|
1273
|
+
})
|
|
1274
|
+
});
|
|
1275
|
+
if (bind.hasWallet) {
|
|
1276
|
+
return publish({
|
|
1277
|
+
status: 'connected',
|
|
1278
|
+
provider: connectOptions.provider,
|
|
1279
|
+
account: bind.eosAccount ?? null,
|
|
1280
|
+
balance: null,
|
|
1281
|
+
bindId: null,
|
|
1282
|
+
bindUrl: null,
|
|
1283
|
+
hasWallet: true,
|
|
1284
|
+
reused: bind.reused,
|
|
1285
|
+
error: null
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
const next = publish({
|
|
1289
|
+
status: 'pending',
|
|
1290
|
+
provider: connectOptions.provider,
|
|
1291
|
+
account: bind.accountName ?? bind.eosAccount ?? null,
|
|
1292
|
+
balance: null,
|
|
1293
|
+
bindId: bind.bindId,
|
|
1294
|
+
bindUrl: bind.bindUrl,
|
|
1295
|
+
hasWallet: false,
|
|
1296
|
+
reused: bind.reused,
|
|
1297
|
+
error: null
|
|
1298
|
+
});
|
|
1299
|
+
if (connectOptions.openLink !== false) {
|
|
1300
|
+
openExternalUrl(connectOptions.provider === 'tokenpocket' ? tokenPocketDappUrl(bind.bindUrl) : bind.bindUrl, options);
|
|
1301
|
+
}
|
|
1302
|
+
return next;
|
|
1303
|
+
},
|
|
1304
|
+
async connectTelegram(telegramOptions = {}) {
|
|
1305
|
+
const paymentKey = await generateEosConnectPaymentKey();
|
|
1306
|
+
const next = await this.connect({
|
|
1307
|
+
provider: 'telegram',
|
|
1308
|
+
publicKey: paymentKey.publicKey,
|
|
1309
|
+
assetLimits: telegramOptions.assetLimits,
|
|
1310
|
+
replaceWallet: telegramOptions.replaceWallet,
|
|
1311
|
+
openLink: false
|
|
1312
|
+
});
|
|
1313
|
+
if (next.hasWallet) {
|
|
1314
|
+
return next;
|
|
1315
|
+
}
|
|
1316
|
+
if (!next.bindUrl || !next.bindId || !next.account) {
|
|
1317
|
+
throw new Error('EOS Connect did not return a binding request');
|
|
1318
|
+
}
|
|
1319
|
+
if (!next.reused) {
|
|
1320
|
+
await saveEosConnectPaymentKey(paymentKey.privateKey, options.telegramWebApp);
|
|
1321
|
+
}
|
|
1322
|
+
if (telegramOptions.openLink !== false) {
|
|
1323
|
+
openExternalUrl(next.bindUrl, options);
|
|
1324
|
+
}
|
|
1325
|
+
return next;
|
|
1326
|
+
},
|
|
1327
|
+
async connectTokenPocket(tokenPocketOptions = {}) {
|
|
1328
|
+
const paymentKey = await generateEosConnectPaymentKey();
|
|
1329
|
+
const next = await this.connect({
|
|
1330
|
+
provider: 'tokenpocket',
|
|
1331
|
+
publicKey: paymentKey.publicKey,
|
|
1332
|
+
assetLimits: tokenPocketOptions.assetLimits,
|
|
1333
|
+
openLink: false
|
|
1334
|
+
});
|
|
1335
|
+
if (!next.bindUrl || !next.bindId) {
|
|
1336
|
+
throw new Error('EOS Connect did not return a TokenPocket binding request');
|
|
1337
|
+
}
|
|
1338
|
+
await saveEosConnectPaymentKey(paymentKey.privateKey, options.telegramWebApp);
|
|
1339
|
+
if (tokenPocketOptions.openLink !== false) {
|
|
1340
|
+
openExternalUrl(tokenPocketDappUrl(next.bindUrl), options);
|
|
1341
|
+
}
|
|
1342
|
+
return next;
|
|
1343
|
+
},
|
|
1344
|
+
async pay(payOptions) {
|
|
1345
|
+
if (!initData()) {
|
|
1346
|
+
throw new Error('Telegram initData is missing');
|
|
1347
|
+
}
|
|
1348
|
+
const built = await request('/api/tg-wallet/transfer/build', {
|
|
1349
|
+
method: 'POST',
|
|
1350
|
+
body: JSON.stringify({
|
|
1351
|
+
initData: initData(),
|
|
1352
|
+
to: payOptions.to,
|
|
1353
|
+
amount: payOptions.amount,
|
|
1354
|
+
memo: payOptions.memo ?? '',
|
|
1355
|
+
tokenContract: payOptions.tokenContract,
|
|
1356
|
+
symbol: payOptions.symbol
|
|
1357
|
+
})
|
|
1358
|
+
});
|
|
1359
|
+
const confirmPayment = options.confirmPayment === false ? null : options.confirmPayment ?? defaultConfirmPayment;
|
|
1360
|
+
if (confirmPayment) {
|
|
1361
|
+
const confirmed = await confirmPayment(paymentDetailsFromBuiltTransfer(built));
|
|
1362
|
+
if (!confirmed) {
|
|
1363
|
+
throw new Error('Payment confirmation was cancelled');
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
const signer = options.signTransaction ?? signEosConnectTransaction;
|
|
1367
|
+
const signedTransaction = await signer(built.transaction, {
|
|
1368
|
+
telegramWebApp: options.telegramWebApp,
|
|
1369
|
+
rpcUrls,
|
|
1370
|
+
fetch: fetchImpl
|
|
1371
|
+
});
|
|
1372
|
+
return request('/api/tg-wallet/transfer/push', {
|
|
1373
|
+
method: 'POST',
|
|
1374
|
+
body: JSON.stringify({
|
|
1375
|
+
initData: initData(),
|
|
1376
|
+
intentId: built.intentId,
|
|
1377
|
+
signedTransaction,
|
|
1378
|
+
summary: built.summary
|
|
1379
|
+
})
|
|
1380
|
+
});
|
|
1381
|
+
},
|
|
1382
|
+
disconnectLocal() {
|
|
1383
|
+
return publish({ ...idleState });
|
|
1384
|
+
}
|
|
1385
|
+
};
|
|
1386
|
+
}
|