@meshconnect/uwc-tron-connector 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/dist/events/state-machine.d.ts +26 -0
- package/dist/events/state-machine.d.ts.map +1 -0
- package/dist/events/state-machine.js +21 -0
- package/dist/events/state-machine.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/rest/abi.d.ts +23 -0
- package/dist/rest/abi.d.ts.map +1 -0
- package/dist/rest/abi.js +28 -0
- package/dist/rest/abi.js.map +1 -0
- package/dist/rest/address.d.ts +45 -0
- package/dist/rest/address.d.ts.map +1 -0
- package/dist/rest/address.js +124 -0
- package/dist/rest/address.js.map +1 -0
- package/dist/rest/trongrid-client.d.ts +57 -0
- package/dist/rest/trongrid-client.d.ts.map +1 -0
- package/dist/rest/trongrid-client.js +133 -0
- package/dist/rest/trongrid-client.js.map +1 -0
- package/dist/shared/error-utils.d.ts +9 -0
- package/dist/shared/error-utils.d.ts.map +1 -0
- package/dist/shared/error-utils.js +52 -0
- package/dist/shared/error-utils.js.map +1 -0
- package/dist/tron-connector.d.ts +85 -0
- package/dist/tron-connector.d.ts.map +1 -0
- package/dist/tron-connector.js +456 -0
- package/dist/tron-connector.js.map +1 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/wallets/base.d.ts +87 -0
- package/dist/wallets/base.d.ts.map +1 -0
- package/dist/wallets/base.js +118 -0
- package/dist/wallets/base.js.map +1 -0
- package/dist/wallets/bitget.d.ts +8 -0
- package/dist/wallets/bitget.d.ts.map +1 -0
- package/dist/wallets/bitget.js +14 -0
- package/dist/wallets/bitget.js.map +1 -0
- package/dist/wallets/okx.d.ts +4 -0
- package/dist/wallets/okx.d.ts.map +1 -0
- package/dist/wallets/okx.js +10 -0
- package/dist/wallets/okx.js.map +1 -0
- package/dist/wallets/registry.d.ts +8 -0
- package/dist/wallets/registry.d.ts.map +1 -0
- package/dist/wallets/registry.js +18 -0
- package/dist/wallets/registry.js.map +1 -0
- package/dist/wallets/tokenpocket.d.ts +9 -0
- package/dist/wallets/tokenpocket.d.ts.map +1 -0
- package/dist/wallets/tokenpocket.js +15 -0
- package/dist/wallets/tokenpocket.js.map +1 -0
- package/dist/wallets/tronlink.d.ts +8 -0
- package/dist/wallets/tronlink.d.ts.map +1 -0
- package/dist/wallets/tronlink.js +15 -0
- package/dist/wallets/tronlink.js.map +1 -0
- package/dist/wallets/trust.d.ts +9 -0
- package/dist/wallets/trust.d.ts.map +1 -0
- package/dist/wallets/trust.js +15 -0
- package/dist/wallets/trust.js.map +1 -0
- package/package.json +34 -0
- package/src/events/state-machine.ts +44 -0
- package/src/index.ts +17 -0
- package/src/rest/abi.test.ts +25 -0
- package/src/rest/abi.ts +33 -0
- package/src/rest/address.test.ts +55 -0
- package/src/rest/address.ts +140 -0
- package/src/rest/trongrid-client.test.ts +169 -0
- package/src/rest/trongrid-client.ts +205 -0
- package/src/shared/error-utils.ts +60 -0
- package/src/tron-connector.test.ts +612 -0
- package/src/tron-connector.ts +568 -0
- package/src/types.ts +11 -0
- package/src/wallets/base.ts +184 -0
- package/src/wallets/bitget.ts +17 -0
- package/src/wallets/okx.ts +10 -0
- package/src/wallets/registry.ts +26 -0
- package/src/wallets/tokenpocket.ts +15 -0
- package/src/wallets/tronlink.ts +15 -0
- package/src/wallets/trust.ts +18 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AvailableAddress,
|
|
3
|
+
ConnectorResult,
|
|
4
|
+
DetectedTronWalletInfo,
|
|
5
|
+
Namespace,
|
|
6
|
+
Network,
|
|
7
|
+
NetworkId,
|
|
8
|
+
SignatureType,
|
|
9
|
+
SwitchNetworkResult,
|
|
10
|
+
TransactionRequest,
|
|
11
|
+
TronNativeTransferRequest,
|
|
12
|
+
TronTRC20TransferRequest,
|
|
13
|
+
WalletMetadata
|
|
14
|
+
} from '@meshconnect/uwc-types'
|
|
15
|
+
import { WalletConnectorError } from '@meshconnect/uwc-types'
|
|
16
|
+
import { parseError } from './shared/error-utils'
|
|
17
|
+
import {
|
|
18
|
+
TronStateMachine,
|
|
19
|
+
type TronStateListener
|
|
20
|
+
} from './events/state-machine'
|
|
21
|
+
import {
|
|
22
|
+
TronGridClient,
|
|
23
|
+
type UnsignedTronTransaction
|
|
24
|
+
} from './rest/trongrid-client'
|
|
25
|
+
import { assertValidTronChecksum, tronAddressToHex } from './rest/address'
|
|
26
|
+
import {
|
|
27
|
+
encodeTrc20TransferParams,
|
|
28
|
+
TRC20_TRANSFER_SELECTOR_HASH
|
|
29
|
+
} from './rest/abi'
|
|
30
|
+
import {
|
|
31
|
+
detectProvider,
|
|
32
|
+
waitForWalletAddress,
|
|
33
|
+
type InjectedTronProvider
|
|
34
|
+
} from './wallets/base'
|
|
35
|
+
import {
|
|
36
|
+
ALL_TRON_WALLET_IDS,
|
|
37
|
+
TRON_WALLET_REGISTRY,
|
|
38
|
+
isTronWalletId,
|
|
39
|
+
type TronWalletId
|
|
40
|
+
} from './wallets/registry'
|
|
41
|
+
import type { TronConnectorConfig } from './types'
|
|
42
|
+
|
|
43
|
+
/** Wallet events we mirror into the state machine while connected. */
|
|
44
|
+
const WALLET_EVENTS = ['accountsChanged', 'disconnect'] as const
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Default probe window for `getAvailableWallets`. Short on purpose — see the
|
|
48
|
+
* rationale in that method. Override via `TronConnectorConfig.discoveryTimeoutMs`.
|
|
49
|
+
*/
|
|
50
|
+
const DEFAULT_DISCOVERY_TIMEOUT_MS = 300
|
|
51
|
+
|
|
52
|
+
interface ActiveTronConnection {
|
|
53
|
+
walletId: TronWalletId
|
|
54
|
+
provider: InjectedTronProvider
|
|
55
|
+
address: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Mesh-owned Tron wallet connector. Zero third-party Tron runtime deps.
|
|
60
|
+
*
|
|
61
|
+
* One instance fronts all 5 supported wallets — TronLink, Bitget, OKX,
|
|
62
|
+
* TokenPocket, Trust — selected per-connect by the wallet id. Per-wallet
|
|
63
|
+
* enablement is controlled by `config.enabledWallets`.
|
|
64
|
+
*
|
|
65
|
+
* Transactions are built and broadcast against the Tron full-node HTTP API
|
|
66
|
+
* (`/wallet/*`); signing goes through the wallet's own injected
|
|
67
|
+
* `provider.tronWeb.trx.sign(unsigned)`, so the crypto stays in the wallet and
|
|
68
|
+
* costs us no bundle.
|
|
69
|
+
*/
|
|
70
|
+
export class TronConnector {
|
|
71
|
+
private readonly config: TronConnectorConfig
|
|
72
|
+
private readonly enabled: ReadonlySet<TronWalletId>
|
|
73
|
+
private readonly clientByNetwork = new Map<NetworkId, TronGridClient>()
|
|
74
|
+
private readonly state = new TronStateMachine()
|
|
75
|
+
|
|
76
|
+
private active: ActiveTronConnection | null = null
|
|
77
|
+
private activeNetworkId: NetworkId | null = null
|
|
78
|
+
/** Listeners attached to the active provider, kept so we can detach on cleanup. */
|
|
79
|
+
private boundListeners: Array<{
|
|
80
|
+
event: string
|
|
81
|
+
handler: (...args: unknown[]) => void
|
|
82
|
+
}> = []
|
|
83
|
+
|
|
84
|
+
constructor(config: TronConnectorConfig) {
|
|
85
|
+
this.config = config
|
|
86
|
+
this.enabled = new Set(Array.from(config.enabledWallets))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Subscribe to connection-state transitions (decision D3). */
|
|
90
|
+
subscribe(listener: TronStateListener): () => void {
|
|
91
|
+
return this.state.subscribe(listener)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Returns the wallets that are (a) enabled in config and (b) currently
|
|
96
|
+
* injected into the page. Conforms to `Connector.getAvailableWallets` for
|
|
97
|
+
* namespace `'tron'`.
|
|
98
|
+
*/
|
|
99
|
+
async getAvailableWallets(
|
|
100
|
+
namespace: Namespace,
|
|
101
|
+
expectedWallets?: WalletMetadata[]
|
|
102
|
+
): Promise<DetectedTronWalletInfo[]> {
|
|
103
|
+
if (namespace !== 'tron') return []
|
|
104
|
+
|
|
105
|
+
// Discovery uses a deliberately SHORT timeout, distinct from connect()'s
|
|
106
|
+
// longer `readyStateTimeoutMs`. `detectProvider` only resolves early when a
|
|
107
|
+
// wallet *appears* — an absent wallet always waits out the full timeout. In
|
|
108
|
+
// discovery every enabled-but-not-installed wallet is probed in parallel, so
|
|
109
|
+
// a 3s timeout would make a user who has only TronLink installed wait ~3s
|
|
110
|
+
// for the wallet list on every call. A short window still catches wallets
|
|
111
|
+
// that inject slightly after page load (injection is async) without
|
|
112
|
+
// stalling the UI; connect(), which targets one already-chosen wallet and
|
|
113
|
+
// can afford to wait, keeps the longer timeout.
|
|
114
|
+
const timeout =
|
|
115
|
+
this.config.discoveryTimeoutMs ?? DEFAULT_DISCOVERY_TIMEOUT_MS
|
|
116
|
+
const interval = this.config.readyStatePollIntervalMs ?? 50
|
|
117
|
+
const allowedIds = this.resolveAllowedIds(expectedWallets)
|
|
118
|
+
|
|
119
|
+
// Probe in parallel but place each result by its index, so the returned
|
|
120
|
+
// order is stable (matches resolveAllowedIds) regardless of which wallet's
|
|
121
|
+
// probe resolves first.
|
|
122
|
+
const detected = await Promise.all(
|
|
123
|
+
allowedIds.map(async (id): Promise<DetectedTronWalletInfo | null> => {
|
|
124
|
+
const wrapper = TRON_WALLET_REGISTRY[id]
|
|
125
|
+
const provider = await detectProvider(wrapper, timeout, interval)
|
|
126
|
+
return provider ? { uuid: id, name: wrapper.displayName } : null
|
|
127
|
+
})
|
|
128
|
+
)
|
|
129
|
+
return detected.filter((w): w is DetectedTronWalletInfo => w !== null)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Connect to a Tron wallet on the given network. The wallet is identified by
|
|
134
|
+
* `walletProvider.uuid` (or `.walletId`), which must be an enabled
|
|
135
|
+
* `TronWalletId`. On success this connector holds the active provider until
|
|
136
|
+
* `disconnect()` or the next `connect()`.
|
|
137
|
+
*/
|
|
138
|
+
async connect(
|
|
139
|
+
network: Network,
|
|
140
|
+
walletProvider?: { uuid?: string } | { walletId?: string }
|
|
141
|
+
): Promise<ConnectorResult> {
|
|
142
|
+
this.assertTronNetwork(network, 'connect')
|
|
143
|
+
const walletId = this.requireWalletId(walletProvider)
|
|
144
|
+
const wrapper = TRON_WALLET_REGISTRY[walletId]
|
|
145
|
+
|
|
146
|
+
this.state.set({ status: 'connecting', walletId })
|
|
147
|
+
|
|
148
|
+
const provider = await detectProvider(
|
|
149
|
+
wrapper,
|
|
150
|
+
this.config.readyStateTimeoutMs ?? 3000,
|
|
151
|
+
this.config.readyStatePollIntervalMs ?? 50
|
|
152
|
+
)
|
|
153
|
+
if (!provider) {
|
|
154
|
+
// Same consistency guard as the catch below: a failed reconnect (target
|
|
155
|
+
// not installed) must not leave `active` pointing at the prior wallet.
|
|
156
|
+
this.cleanupActive()
|
|
157
|
+
this.state.set({ status: 'disconnected' })
|
|
158
|
+
throw new WalletConnectorError({
|
|
159
|
+
type: 'unknown',
|
|
160
|
+
message: `Tron wallet "${wrapper.displayName}" is not installed`
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
// Prompts the wallet to expose the account. The wallet populates its
|
|
166
|
+
// `tronWeb` + account a tick after this resolves, so poll briefly.
|
|
167
|
+
await provider.request?.({ method: 'tron_requestAccounts' })
|
|
168
|
+
const address = await waitForWalletAddress(
|
|
169
|
+
provider,
|
|
170
|
+
this.config.readyStateTimeoutMs ?? 3000,
|
|
171
|
+
this.config.readyStatePollIntervalMs ?? 50
|
|
172
|
+
)
|
|
173
|
+
if (!address) {
|
|
174
|
+
throw new WalletConnectorError({
|
|
175
|
+
type: 'unknown',
|
|
176
|
+
message: `Tron wallet "${wrapper.displayName}" connected but exposed no address`
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Pre-warm the per-network client so the first send doesn't pay the cost.
|
|
181
|
+
this.getClient(network.id)
|
|
182
|
+
this.cleanupActive()
|
|
183
|
+
this.active = { walletId, provider, address }
|
|
184
|
+
this.activeNetworkId = network.id
|
|
185
|
+
this.bindWalletEvents(provider)
|
|
186
|
+
this.state.set({ status: 'connected', walletId, address })
|
|
187
|
+
|
|
188
|
+
const availableAddresses: AvailableAddress[] = [
|
|
189
|
+
{ networkId: network.id, address }
|
|
190
|
+
]
|
|
191
|
+
return { networkId: network.id, address, availableAddresses }
|
|
192
|
+
} catch (error) {
|
|
193
|
+
// Clear any prior/partial connection so `active` can't be left pointing
|
|
194
|
+
// at a stale wallet while we report 'disconnected' (e.g. a failed
|
|
195
|
+
// reconnect after a previous connect succeeded).
|
|
196
|
+
this.cleanupActive()
|
|
197
|
+
this.state.set({ status: 'disconnected' })
|
|
198
|
+
parseError(error)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Tron network selection is wallet-driven (the user picks the chain in the
|
|
204
|
+
* wallet UI); there is no standardised programmatic switch across these
|
|
205
|
+
* injected providers. We repoint the build/broadcast endpoint to the target
|
|
206
|
+
* network and report the current address.
|
|
207
|
+
*/
|
|
208
|
+
async switchNetwork(network: Network): Promise<SwitchNetworkResult> {
|
|
209
|
+
this.assertTronNetwork(network, 'switchNetwork')
|
|
210
|
+
if (!this.active) {
|
|
211
|
+
throw new WalletConnectorError({
|
|
212
|
+
type: 'unknown',
|
|
213
|
+
message: 'TronConnector.switchNetwork called before connect()'
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
this.getClient(network.id)
|
|
218
|
+
} catch (error) {
|
|
219
|
+
// Surface a missing-endpoint (or similar) failure as a WalletConnectorError,
|
|
220
|
+
// consistent with connect/sendTransaction/signMessage.
|
|
221
|
+
parseError(error)
|
|
222
|
+
}
|
|
223
|
+
this.activeNetworkId = network.id
|
|
224
|
+
return { networkId: network.id, address: this.active.address }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async disconnect(): Promise<void> {
|
|
228
|
+
this.cleanupActive()
|
|
229
|
+
this.state.set({ status: 'disconnected' })
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async signMessage(message: string): Promise<SignatureType> {
|
|
233
|
+
const active = this.requireActive('signMessage')
|
|
234
|
+
const tronWeb = active.provider.tronWeb
|
|
235
|
+
const signMessageV2 = tronWeb?.trx.signMessageV2
|
|
236
|
+
if (!tronWeb || !signMessageV2) {
|
|
237
|
+
throw new WalletConnectorError({
|
|
238
|
+
type: 'unknown',
|
|
239
|
+
message: 'Active Tron wallet does not support message signing'
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
const signature = await signMessageV2.call(tronWeb.trx, message)
|
|
244
|
+
// Match the existing injected-connector tron path, which wraps the
|
|
245
|
+
// signature as a StandardSignature rather than the TronSignature variant.
|
|
246
|
+
return { type: 'standard', signature }
|
|
247
|
+
} catch (error) {
|
|
248
|
+
parseError(error)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async sendTransaction(request: TransactionRequest): Promise<string> {
|
|
253
|
+
const active = this.requireActive('sendTransaction')
|
|
254
|
+
if (!this.activeNetworkId) {
|
|
255
|
+
throw new WalletConnectorError({
|
|
256
|
+
type: 'unknown',
|
|
257
|
+
message: 'TronConnector.sendTransaction: no active network'
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
const client = this.getClient(this.activeNetworkId)
|
|
261
|
+
|
|
262
|
+
// Build, sign, and broadcast all run inside the try so node/network errors
|
|
263
|
+
// from the build step surface as WalletConnectorError (via parseError),
|
|
264
|
+
// consistent with the sign/broadcast path; the validation/guard throws below
|
|
265
|
+
// are already WalletConnectorError and pass through parseError unchanged.
|
|
266
|
+
try {
|
|
267
|
+
let unsigned: UnsignedTronTransaction
|
|
268
|
+
if (isTronTRC20(request)) {
|
|
269
|
+
this.assertValidAmount(request.amount)
|
|
270
|
+
this.assertFromMatchesActive(request.from, active.address)
|
|
271
|
+
// TRC20 encodes `to`/`contract` as raw hex, so the node never sees (or
|
|
272
|
+
// checksum-validates) the base58 forms — verify both here so a typo'd
|
|
273
|
+
// recipient or contract can't silently misdirect funds.
|
|
274
|
+
await this.assertValidRecipient(request.to)
|
|
275
|
+
await this.assertValidRecipient(request.contractAddress)
|
|
276
|
+
unsigned = await client.createTrc20Transfer({
|
|
277
|
+
from: request.from,
|
|
278
|
+
to: request.to,
|
|
279
|
+
amount: request.amount,
|
|
280
|
+
contractAddress: request.contractAddress
|
|
281
|
+
})
|
|
282
|
+
} else if (isTronNative(request)) {
|
|
283
|
+
// The node validates `to` (sent as base58 with visible:true), so no
|
|
284
|
+
// checksum check is needed on this path.
|
|
285
|
+
this.assertValidAmount(request.amount)
|
|
286
|
+
this.assertFromMatchesActive(request.from, active.address)
|
|
287
|
+
unsigned = await client.createTransaction({
|
|
288
|
+
from: request.from,
|
|
289
|
+
to: request.to,
|
|
290
|
+
amount: request.amount
|
|
291
|
+
})
|
|
292
|
+
} else {
|
|
293
|
+
throw new WalletConnectorError({
|
|
294
|
+
type: 'unknown',
|
|
295
|
+
message: 'TronConnector.sendTransaction: unsupported request shape'
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Don't blindly sign what the node returned: confirm the built tx encodes
|
|
300
|
+
// the requested recipient/amount/contract. A compromised or MITM'd
|
|
301
|
+
// endpoint could otherwise swap the destination, with the wallet's confirm
|
|
302
|
+
// UI as the only backstop.
|
|
303
|
+
this.assertBuiltTxMatchesRequest(unsigned, request)
|
|
304
|
+
|
|
305
|
+
// A wallet can lock/disconnect without firing an event, dropping
|
|
306
|
+
// `tronWeb`. Guard so we surface a clear error, not a raw TypeError.
|
|
307
|
+
const tronWeb = active.provider.tronWeb
|
|
308
|
+
if (!tronWeb) {
|
|
309
|
+
throw new WalletConnectorError({
|
|
310
|
+
type: 'unknown',
|
|
311
|
+
message: 'Active Tron wallet is unavailable; reconnect and try again'
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const signed = await tronWeb.trx.sign(unsigned)
|
|
316
|
+
return await client.broadcastTransaction(signed)
|
|
317
|
+
} catch (error) {
|
|
318
|
+
parseError(error)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Verify the node-built transaction encodes exactly what we asked for, before
|
|
324
|
+
* handing it to the wallet to sign. Reads `raw_data.contract[0].parameter.value`:
|
|
325
|
+
* native → owner/to/amount; TRC20 → owner/contract + the full `transfer` call
|
|
326
|
+
* data. Addresses are compared hex-normalized (base58/hex agnostic), falling
|
|
327
|
+
* back to exact string match for non-decodable placeholders.
|
|
328
|
+
*/
|
|
329
|
+
private assertBuiltTxMatchesRequest(
|
|
330
|
+
unsigned: UnsignedTronTransaction,
|
|
331
|
+
request: TronNativeTransferRequest | TronTRC20TransferRequest
|
|
332
|
+
): void {
|
|
333
|
+
const mismatch = (): never => {
|
|
334
|
+
throw new WalletConnectorError({
|
|
335
|
+
type: 'unknown',
|
|
336
|
+
message:
|
|
337
|
+
'TronConnector.sendTransaction: the node-built transaction does not match the request (possible tampered/MITM endpoint)'
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
const value = (
|
|
341
|
+
unsigned.raw_data as
|
|
342
|
+
| {
|
|
343
|
+
contract?: Array<{
|
|
344
|
+
parameter?: { value?: Record<string, unknown> }
|
|
345
|
+
}>
|
|
346
|
+
}
|
|
347
|
+
| undefined
|
|
348
|
+
)?.contract?.[0]?.parameter?.value
|
|
349
|
+
if (!value) {
|
|
350
|
+
throw new WalletConnectorError({
|
|
351
|
+
type: 'unknown',
|
|
352
|
+
message:
|
|
353
|
+
'TronConnector.sendTransaction: the node-built transaction does not match the request (possible tampered/MITM endpoint)'
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const sameAddress = (built: unknown, requested: string): boolean => {
|
|
358
|
+
const a = String(built ?? '')
|
|
359
|
+
if (a === requested) return true
|
|
360
|
+
try {
|
|
361
|
+
return tronAddressToHex(a) === tronAddressToHex(requested)
|
|
362
|
+
} catch {
|
|
363
|
+
return false
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!sameAddress(value['owner_address'], request.from)) mismatch()
|
|
368
|
+
if (isTronTRC20(request)) {
|
|
369
|
+
if (!sameAddress(value['contract_address'], request.contractAddress)) {
|
|
370
|
+
mismatch()
|
|
371
|
+
}
|
|
372
|
+
const expectedData = (
|
|
373
|
+
TRC20_TRANSFER_SELECTOR_HASH +
|
|
374
|
+
encodeTrc20TransferParams(request.to, request.amount)
|
|
375
|
+
).toLowerCase()
|
|
376
|
+
if (String(value['data'] ?? '').toLowerCase() !== expectedData) mismatch()
|
|
377
|
+
} else {
|
|
378
|
+
if (!sameAddress(value['to_address'], request.to)) mismatch()
|
|
379
|
+
if (Number(value['amount']) !== request.amount) mismatch()
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** The wallet only signs for its own key — reject a mismatched `from` early. */
|
|
384
|
+
private assertFromMatchesActive(from: string, activeAddress: string): void {
|
|
385
|
+
if (from !== activeAddress) {
|
|
386
|
+
throw new WalletConnectorError({
|
|
387
|
+
type: 'unknown',
|
|
388
|
+
message: `TronConnector.sendTransaction: "from" (${from}) does not match the connected address (${activeAddress})`
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ---- internals -----------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
private assertTronNetwork(network: Network, method: string): void {
|
|
396
|
+
if (network.namespace !== 'tron') {
|
|
397
|
+
throw new Error(
|
|
398
|
+
`TronConnector.${method}: expected a 'tron' network, got '${network.namespace}'`
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private requireActive(method: string): ActiveTronConnection {
|
|
404
|
+
if (!this.active) {
|
|
405
|
+
throw new WalletConnectorError({
|
|
406
|
+
type: 'unknown',
|
|
407
|
+
message: `TronConnector.${method} called before connect()`
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
return this.active
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Reject amounts that aren't safe non-negative integers. Two reasons, both on
|
|
415
|
+
* the money path: a fractional value makes `BigInt(amount)` throw a raw
|
|
416
|
+
* `RangeError` (and the build runs outside the sign/broadcast try/catch, so it
|
|
417
|
+
* would escape unwrapped); and a value beyond `Number.MAX_SAFE_INTEGER` can't
|
|
418
|
+
* be represented exactly, so it would silently transfer the wrong amount.
|
|
419
|
+
* `Number.isSafeInteger` covers both.
|
|
420
|
+
*/
|
|
421
|
+
private assertValidAmount(amount: number): void {
|
|
422
|
+
if (!Number.isSafeInteger(amount) || amount < 0) {
|
|
423
|
+
throw new WalletConnectorError({
|
|
424
|
+
type: 'unknown',
|
|
425
|
+
message: `TronConnector.sendTransaction: amount must be a non-negative integer within the safe range, got ${amount}`
|
|
426
|
+
})
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Verify a TRC20 recipient's base58 checksum, surfaced as a typed error. */
|
|
431
|
+
private async assertValidRecipient(to: string): Promise<void> {
|
|
432
|
+
try {
|
|
433
|
+
await assertValidTronChecksum(to)
|
|
434
|
+
} catch (error) {
|
|
435
|
+
throw new WalletConnectorError({
|
|
436
|
+
type: 'unknown',
|
|
437
|
+
message:
|
|
438
|
+
error instanceof Error
|
|
439
|
+
? error.message
|
|
440
|
+
: 'TronConnector.sendTransaction: invalid recipient address'
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private resolveAllowedIds(
|
|
446
|
+
expectedWallets?: WalletMetadata[]
|
|
447
|
+
): TronWalletId[] {
|
|
448
|
+
if (!expectedWallets || expectedWallets.length === 0) {
|
|
449
|
+
return ALL_TRON_WALLET_IDS.filter(id => this.enabled.has(id))
|
|
450
|
+
}
|
|
451
|
+
const expectedIds = new Set(expectedWallets.map(wallet => wallet.id))
|
|
452
|
+
return ALL_TRON_WALLET_IDS.filter(
|
|
453
|
+
id => this.enabled.has(id) && expectedIds.has(id)
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private requireWalletId(
|
|
458
|
+
provider: { uuid?: string } | { walletId?: string } | undefined
|
|
459
|
+
): TronWalletId {
|
|
460
|
+
const raw =
|
|
461
|
+
(provider as { uuid?: string } | undefined)?.uuid ??
|
|
462
|
+
(provider as { walletId?: string } | undefined)?.walletId
|
|
463
|
+
if (!raw) {
|
|
464
|
+
throw new WalletConnectorError({
|
|
465
|
+
type: 'unknown',
|
|
466
|
+
message: 'TronConnector.connect: missing wallet identifier'
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
if (!isTronWalletId(raw)) {
|
|
470
|
+
throw new WalletConnectorError({
|
|
471
|
+
type: 'unknown',
|
|
472
|
+
message: `TronConnector.connect: unknown wallet "${raw}"`
|
|
473
|
+
})
|
|
474
|
+
}
|
|
475
|
+
if (!this.enabled.has(raw)) {
|
|
476
|
+
throw new WalletConnectorError({
|
|
477
|
+
type: 'unknown',
|
|
478
|
+
message: `TronConnector.connect: wallet "${raw}" is not enabled in config`
|
|
479
|
+
})
|
|
480
|
+
}
|
|
481
|
+
return raw
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private getClient(networkId: NetworkId): TronGridClient {
|
|
485
|
+
let client = this.clientByNetwork.get(networkId)
|
|
486
|
+
if (!client) {
|
|
487
|
+
client = new TronGridClient(networkId, this.config.tronGrid)
|
|
488
|
+
this.clientByNetwork.set(networkId, client)
|
|
489
|
+
}
|
|
490
|
+
return client
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private bindWalletEvents(provider: InjectedTronProvider): void {
|
|
494
|
+
if (!provider.on) return
|
|
495
|
+
for (const event of WALLET_EVENTS) {
|
|
496
|
+
const handler = (...args: unknown[]) => this.onWalletEvent(event, args[0])
|
|
497
|
+
provider.on(event, handler)
|
|
498
|
+
this.boundListeners.push({ event, handler })
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private onWalletEvent(event: string, payload: unknown): void {
|
|
503
|
+
if (!this.active) return
|
|
504
|
+
if (event === 'disconnect') {
|
|
505
|
+
this.cleanupActive()
|
|
506
|
+
this.state.set({ status: 'disconnected' })
|
|
507
|
+
return
|
|
508
|
+
}
|
|
509
|
+
// accountsChanged payload shape varies by wallet: TronLink/TIP-1193 emit a
|
|
510
|
+
// bare address string, while the EIP-1193-style providers (OKX, Trust,
|
|
511
|
+
// Bitget) emit an array. An empty array is the logout signal. We normalise
|
|
512
|
+
// both, falling back to the wallet's current default address only when the
|
|
513
|
+
// payload carries nothing usable.
|
|
514
|
+
let nextAddress: string | undefined
|
|
515
|
+
if (typeof payload === 'string') {
|
|
516
|
+
nextAddress = payload
|
|
517
|
+
} else if (Array.isArray(payload)) {
|
|
518
|
+
nextAddress = payload[0] as string | undefined
|
|
519
|
+
} else {
|
|
520
|
+
nextAddress =
|
|
521
|
+
this.active.provider.tronWeb?.defaultAddress?.base58 || undefined
|
|
522
|
+
}
|
|
523
|
+
if (!nextAddress) {
|
|
524
|
+
this.cleanupActive()
|
|
525
|
+
this.state.set({ status: 'disconnected' })
|
|
526
|
+
return
|
|
527
|
+
}
|
|
528
|
+
this.active = { ...this.active, address: nextAddress }
|
|
529
|
+
this.state.set({
|
|
530
|
+
status: 'connected',
|
|
531
|
+
walletId: this.active.walletId,
|
|
532
|
+
address: nextAddress
|
|
533
|
+
})
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private cleanupActive(): void {
|
|
537
|
+
if (this.active?.provider.removeListener) {
|
|
538
|
+
for (const { event, handler } of this.boundListeners) {
|
|
539
|
+
this.active.provider.removeListener(event, handler)
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
this.boundListeners = []
|
|
543
|
+
this.active = null
|
|
544
|
+
this.activeNetworkId = null
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function isTronNative(
|
|
549
|
+
request: TransactionRequest
|
|
550
|
+
): request is TronNativeTransferRequest {
|
|
551
|
+
return (
|
|
552
|
+
'from' in request &&
|
|
553
|
+
'to' in request &&
|
|
554
|
+
typeof (request as TronNativeTransferRequest).amount === 'number' &&
|
|
555
|
+
!('contractAddress' in request)
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function isTronTRC20(
|
|
560
|
+
request: TransactionRequest
|
|
561
|
+
): request is TronTRC20TransferRequest {
|
|
562
|
+
return (
|
|
563
|
+
'from' in request &&
|
|
564
|
+
'to' in request &&
|
|
565
|
+
'contractAddress' in request &&
|
|
566
|
+
typeof (request as TronTRC20TransferRequest).amount === 'number'
|
|
567
|
+
)
|
|
568
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public configuration types for the Tron connector now live in
|
|
3
|
+
* `@meshconnect/uwc-types` (alongside `TonConnectConfig` / `WalletConnectConfig`)
|
|
4
|
+
* so they can thread through `UniversalWalletConnector`. Re-exported here for
|
|
5
|
+
* convenience and backward compatibility.
|
|
6
|
+
*/
|
|
7
|
+
export type {
|
|
8
|
+
TronConnectorConfig,
|
|
9
|
+
TronGridConfig,
|
|
10
|
+
TronWalletId
|
|
11
|
+
} from '@meshconnect/uwc-types'
|