@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.
Files changed (80) hide show
  1. package/dist/events/state-machine.d.ts +26 -0
  2. package/dist/events/state-machine.d.ts.map +1 -0
  3. package/dist/events/state-machine.js +21 -0
  4. package/dist/events/state-machine.js.map +1 -0
  5. package/dist/index.d.ts +7 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +3 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/rest/abi.d.ts +23 -0
  10. package/dist/rest/abi.d.ts.map +1 -0
  11. package/dist/rest/abi.js +28 -0
  12. package/dist/rest/abi.js.map +1 -0
  13. package/dist/rest/address.d.ts +45 -0
  14. package/dist/rest/address.d.ts.map +1 -0
  15. package/dist/rest/address.js +124 -0
  16. package/dist/rest/address.js.map +1 -0
  17. package/dist/rest/trongrid-client.d.ts +57 -0
  18. package/dist/rest/trongrid-client.d.ts.map +1 -0
  19. package/dist/rest/trongrid-client.js +133 -0
  20. package/dist/rest/trongrid-client.js.map +1 -0
  21. package/dist/shared/error-utils.d.ts +9 -0
  22. package/dist/shared/error-utils.d.ts.map +1 -0
  23. package/dist/shared/error-utils.js +52 -0
  24. package/dist/shared/error-utils.js.map +1 -0
  25. package/dist/tron-connector.d.ts +85 -0
  26. package/dist/tron-connector.d.ts.map +1 -0
  27. package/dist/tron-connector.js +456 -0
  28. package/dist/tron-connector.js.map +1 -0
  29. package/dist/types.d.ts +8 -0
  30. package/dist/types.d.ts.map +1 -0
  31. package/dist/types.js +2 -0
  32. package/dist/types.js.map +1 -0
  33. package/dist/wallets/base.d.ts +87 -0
  34. package/dist/wallets/base.d.ts.map +1 -0
  35. package/dist/wallets/base.js +118 -0
  36. package/dist/wallets/base.js.map +1 -0
  37. package/dist/wallets/bitget.d.ts +8 -0
  38. package/dist/wallets/bitget.d.ts.map +1 -0
  39. package/dist/wallets/bitget.js +14 -0
  40. package/dist/wallets/bitget.js.map +1 -0
  41. package/dist/wallets/okx.d.ts +4 -0
  42. package/dist/wallets/okx.d.ts.map +1 -0
  43. package/dist/wallets/okx.js +10 -0
  44. package/dist/wallets/okx.js.map +1 -0
  45. package/dist/wallets/registry.d.ts +8 -0
  46. package/dist/wallets/registry.d.ts.map +1 -0
  47. package/dist/wallets/registry.js +18 -0
  48. package/dist/wallets/registry.js.map +1 -0
  49. package/dist/wallets/tokenpocket.d.ts +9 -0
  50. package/dist/wallets/tokenpocket.d.ts.map +1 -0
  51. package/dist/wallets/tokenpocket.js +15 -0
  52. package/dist/wallets/tokenpocket.js.map +1 -0
  53. package/dist/wallets/tronlink.d.ts +8 -0
  54. package/dist/wallets/tronlink.d.ts.map +1 -0
  55. package/dist/wallets/tronlink.js +15 -0
  56. package/dist/wallets/tronlink.js.map +1 -0
  57. package/dist/wallets/trust.d.ts +9 -0
  58. package/dist/wallets/trust.d.ts.map +1 -0
  59. package/dist/wallets/trust.js +15 -0
  60. package/dist/wallets/trust.js.map +1 -0
  61. package/package.json +34 -0
  62. package/src/events/state-machine.ts +44 -0
  63. package/src/index.ts +17 -0
  64. package/src/rest/abi.test.ts +25 -0
  65. package/src/rest/abi.ts +33 -0
  66. package/src/rest/address.test.ts +55 -0
  67. package/src/rest/address.ts +140 -0
  68. package/src/rest/trongrid-client.test.ts +169 -0
  69. package/src/rest/trongrid-client.ts +205 -0
  70. package/src/shared/error-utils.ts +60 -0
  71. package/src/tron-connector.test.ts +612 -0
  72. package/src/tron-connector.ts +568 -0
  73. package/src/types.ts +11 -0
  74. package/src/wallets/base.ts +184 -0
  75. package/src/wallets/bitget.ts +17 -0
  76. package/src/wallets/okx.ts +10 -0
  77. package/src/wallets/registry.ts +26 -0
  78. package/src/wallets/tokenpocket.ts +15 -0
  79. package/src/wallets/tronlink.ts +15 -0
  80. package/src/wallets/trust.ts +18 -0
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Tron address utilities — base58check ↔ hex, dependency-free.
3
+ *
4
+ * A Tron address has two on-the-wire forms:
5
+ * - base58check (`T...`): Base58Check of `0x41 ‖ <20-byte body> ‖ <4-byte checksum>`.
6
+ * - hex (`41` + 40 hex chars): the 21-byte payload (`0x41` prefix + 20-byte body).
7
+ *
8
+ * The full-node HTTP API accepts base58 directly when `visible: true` is set, so
9
+ * build/broadcast never needs conversion. The one place we DO need it is the
10
+ * TRC20 ABI parameter, which encodes the recipient as the bare 20-byte body.
11
+ *
12
+ * Decoding base58 is pure bigint math (no hashing), so the conversion helpers
13
+ * (`tronAddressToHex` / `tronAddressToEvmHex`) stay synchronous and do NOT
14
+ * verify the 4-byte checksum — that's the hot path used for already-validated
15
+ * addresses. Checksum verification is provided separately by the async
16
+ * `assertValidTronChecksum` (SubtleCrypto), which the connector calls on the
17
+ * TRC20 recipient — the one case the node never validates (it's encoded as raw
18
+ * hex, not sent as base58).
19
+ */
20
+
21
+ const BASE58_ALPHABET =
22
+ '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
23
+
24
+ /** Decode a Base58 string to bytes (big-endian). Throws on invalid characters. */
25
+ function base58Decode(input: string): Uint8Array {
26
+ if (input.length === 0) throw new Error('tron address: empty base58 string')
27
+
28
+ let value = 0n
29
+ for (const char of input) {
30
+ const index = BASE58_ALPHABET.indexOf(char)
31
+ if (index === -1) {
32
+ throw new Error(`tron address: invalid base58 character "${char}"`)
33
+ }
34
+ value = value * 58n + BigInt(index)
35
+ }
36
+
37
+ const bytes: number[] = []
38
+ while (value > 0n) {
39
+ bytes.unshift(Number(value & 0xffn))
40
+ value >>= 8n
41
+ }
42
+
43
+ // Each leading '1' in base58 represents a leading zero byte.
44
+ for (const char of input) {
45
+ if (char !== '1') break
46
+ bytes.unshift(0)
47
+ }
48
+
49
+ return Uint8Array.from(bytes)
50
+ }
51
+
52
+ function toHex(bytes: Uint8Array): string {
53
+ let out = ''
54
+ for (const byte of bytes) {
55
+ out += byte.toString(16).padStart(2, '0')
56
+ }
57
+ return out
58
+ }
59
+
60
+ /**
61
+ * Convert a base58 Tron address (`T...`) to its 21-byte hex form
62
+ * (`41` + 40 hex chars). Already-hex input (with or without `0x`) is normalised
63
+ * and returned as-is.
64
+ */
65
+ export function tronAddressToHex(address: string): string {
66
+ // Accept hex passthrough so callers can mix forms.
67
+ const stripped = address.startsWith('0x') ? address.slice(2) : address
68
+ if (/^41[0-9a-fA-F]{40}$/.test(stripped)) {
69
+ return stripped.toLowerCase()
70
+ }
71
+
72
+ const decoded = base58Decode(address)
73
+ // 25 bytes = 21-byte payload (0x41 + 20-byte body) + 4-byte checksum.
74
+ if (decoded.length !== 25) {
75
+ throw new Error(
76
+ `tron address: expected 25 bytes after base58 decode, got ${decoded.length}`
77
+ )
78
+ }
79
+ const payload = decoded.subarray(0, 21)
80
+ if (payload[0] !== 0x41) {
81
+ throw new Error('tron address: missing 0x41 Tron address prefix')
82
+ }
83
+ return toHex(payload)
84
+ }
85
+
86
+ /**
87
+ * Convert a Tron address to its bare 20-byte EVM-style hex body (no `0x41`
88
+ * prefix, no `0x`). This is the form used inside TRC20 ABI parameters.
89
+ */
90
+ export function tronAddressToEvmHex(address: string): string {
91
+ return tronAddressToHex(address).slice(2)
92
+ }
93
+
94
+ /**
95
+ * SHA-256 via SubtleCrypto. Re-wraps the input with `Uint8Array.from` so it is
96
+ * an ArrayBuffer-backed view (satisfies `digest`'s `BufferSource` param without
97
+ * naming the DOM type, which our lint's `no-undef` doesn't recognize).
98
+ */
99
+ async function sha256(bytes: Uint8Array): Promise<Uint8Array> {
100
+ const digest = await crypto.subtle.digest('SHA-256', Uint8Array.from(bytes))
101
+ return new Uint8Array(digest)
102
+ }
103
+
104
+ /**
105
+ * Validate a base58 Tron address's 0x41 prefix and 4-byte Base58Check checksum
106
+ * (first 4 bytes of `sha256(sha256(payload))`). Throws on mismatch.
107
+ *
108
+ * This matters for the TRC20 recipient: native transfers send the address as
109
+ * base58 with `visible:true`, so the node validates it — but TRC20 encodes the
110
+ * recipient as a raw 20-byte hex word, so the node never sees the base58 form.
111
+ * Without this check a typo'd-but-structurally-valid address would silently
112
+ * send funds to the wrong, unrecoverable destination.
113
+ *
114
+ * Hex inputs carry no base58 checksum, so they pass through unchecked.
115
+ * Async because it uses SubtleCrypto for SHA-256 (no hashing dependency).
116
+ */
117
+ export async function assertValidTronChecksum(address: string): Promise<void> {
118
+ const stripped = address.startsWith('0x') ? address.slice(2) : address
119
+ if (/^41[0-9a-fA-F]{40}$/.test(stripped)) return // hex form: no checksum to verify
120
+
121
+ const decoded = base58Decode(address)
122
+ if (decoded.length !== 25) {
123
+ throw new Error(
124
+ `tron address: expected 25 bytes after base58 decode, got ${decoded.length}`
125
+ )
126
+ }
127
+ const payload = decoded.subarray(0, 21)
128
+ if (payload[0] !== 0x41) {
129
+ throw new Error('tron address: missing 0x41 Tron address prefix')
130
+ }
131
+ const expected = decoded.subarray(21, 25)
132
+ const round2 = await sha256(await sha256(payload))
133
+ for (let i = 0; i < 4; i++) {
134
+ if (round2[i] !== expected[i]) {
135
+ throw new Error(
136
+ 'tron address: base58 checksum mismatch (possible typo in recipient address)'
137
+ )
138
+ }
139
+ }
140
+ }
@@ -0,0 +1,169 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
2
+ import { TronGridClient, decodeHexMessage } from './trongrid-client'
3
+
4
+ const MAINNET = 'tron:0x2b6653dc' as const
5
+
6
+ function mockFetch(responses: unknown[]): ReturnType<typeof vi.fn> {
7
+ const fn = vi.fn()
8
+ for (const body of responses) {
9
+ fn.mockResolvedValueOnce({
10
+ ok: true,
11
+ status: 200,
12
+ statusText: 'OK',
13
+ json: async () => body
14
+ })
15
+ }
16
+ return fn
17
+ }
18
+
19
+ describe('TronGridClient', () => {
20
+ let fetchMock: ReturnType<typeof vi.fn>
21
+
22
+ beforeEach(() => {
23
+ fetchMock = vi.fn()
24
+ vi.stubGlobal('fetch', fetchMock)
25
+ })
26
+ afterEach(() => {
27
+ vi.unstubAllGlobals()
28
+ })
29
+
30
+ it('throws when no endpoint is configured for the network', () => {
31
+ expect(() => new TronGridClient('eip155:1' as never)).toThrow(
32
+ /no full-node endpoint/
33
+ )
34
+ })
35
+
36
+ it('strips trailing slashes from a configured endpoint (no double slash)', async () => {
37
+ fetchMock = mockFetch([{ txID: 'a', raw_data: {} }])
38
+ vi.stubGlobal('fetch', fetchMock)
39
+ const client = new TronGridClient('tron:0x2b6653dc', {
40
+ endpoints: { 'tron:0x2b6653dc': 'https://node.example.com///' }
41
+ })
42
+ await client.createTransaction({ from: 'a', to: 'b', amount: 1 })
43
+ expect(fetchMock.mock.calls[0][0]).toBe(
44
+ 'https://node.example.com/wallet/createtransaction'
45
+ )
46
+ })
47
+
48
+ it('createTransaction posts visible base58 fields and returns the unsigned tx', async () => {
49
+ fetchMock = mockFetch([{ txID: 'abc123', raw_data: {}, visible: true }])
50
+ vi.stubGlobal('fetch', fetchMock)
51
+ const client = new TronGridClient(MAINNET, { apiKey: 'KEY' })
52
+
53
+ const tx = await client.createTransaction({
54
+ from: 'TFrom',
55
+ to: 'TTo',
56
+ amount: 5
57
+ })
58
+
59
+ expect(tx.txID).toBe('abc123')
60
+ const [url, init] = fetchMock.mock.calls[0]
61
+ expect(url).toBe('https://api.trongrid.io/wallet/createtransaction')
62
+ expect(JSON.parse(init.body)).toEqual({
63
+ owner_address: 'TFrom',
64
+ to_address: 'TTo',
65
+ amount: 5,
66
+ visible: true
67
+ })
68
+ expect(init.headers['TRON-PRO-API-KEY']).toBe('KEY')
69
+ })
70
+
71
+ it('createTransaction surfaces a node-side Error field', async () => {
72
+ fetchMock = mockFetch([{ Error: 'bad address' }])
73
+ vi.stubGlobal('fetch', fetchMock)
74
+ const client = new TronGridClient(MAINNET)
75
+ await expect(
76
+ client.createTransaction({ from: 'a', to: 'b', amount: 1 })
77
+ ).rejects.toThrow(/bad address/)
78
+ })
79
+
80
+ it('createTrc20Transfer requires result.result and a transaction', async () => {
81
+ fetchMock = mockFetch([
82
+ {
83
+ result: { result: true },
84
+ transaction: { txID: 'def456', raw_data: {} }
85
+ }
86
+ ])
87
+ vi.stubGlobal('fetch', fetchMock)
88
+ const client = new TronGridClient(MAINNET)
89
+ const tx = await client.createTrc20Transfer({
90
+ from: 'TFrom',
91
+ to: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',
92
+ amount: 1_000_000,
93
+ contractAddress: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'
94
+ })
95
+ expect(tx.txID).toBe('def456')
96
+ const body = JSON.parse(fetchMock.mock.calls[0][1].body)
97
+ expect(body.function_selector).toBe('transfer(address,uint256)')
98
+ expect(body.fee_limit).toBe(100_000_000)
99
+ expect(body.parameter).toHaveLength(128)
100
+ })
101
+
102
+ it('createTrc20Transfer throws a decoded message on a failed estimate', async () => {
103
+ // "REVERT" in hex ASCII.
104
+ fetchMock = mockFetch([
105
+ { result: { result: false, message: '524556455254' } }
106
+ ])
107
+ vi.stubGlobal('fetch', fetchMock)
108
+ const client = new TronGridClient(MAINNET)
109
+ await expect(
110
+ client.createTrc20Transfer({
111
+ from: 'a',
112
+ to: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',
113
+ amount: 1,
114
+ contractAddress: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'
115
+ })
116
+ ).rejects.toThrow(/REVERT/)
117
+ })
118
+
119
+ it('broadcastTransaction returns the txid on result:true', async () => {
120
+ fetchMock = mockFetch([{ result: true, txid: 'tx789' }])
121
+ vi.stubGlobal('fetch', fetchMock)
122
+ const client = new TronGridClient(MAINNET)
123
+ const txid = await client.broadcastTransaction({ txID: 'tx789' })
124
+ expect(txid).toBe('tx789')
125
+ })
126
+
127
+ it('broadcastTransaction throws a decoded message on result:false', async () => {
128
+ fetchMock = mockFetch([
129
+ { result: false, message: '434f4e54524143545f56414c4944415445' }
130
+ ])
131
+ vi.stubGlobal('fetch', fetchMock)
132
+ const client = new TronGridClient(MAINNET)
133
+ await expect(client.broadcastTransaction({ txID: 'x' })).rejects.toThrow(
134
+ /CONTRACT_VALIDATE/
135
+ )
136
+ })
137
+
138
+ it('throws on a non-ok HTTP response', async () => {
139
+ fetchMock.mockResolvedValueOnce({
140
+ ok: false,
141
+ status: 503,
142
+ statusText: 'Service Unavailable',
143
+ json: async () => ({})
144
+ })
145
+ const client = new TronGridClient(MAINNET)
146
+ await expect(
147
+ client.createTransaction({ from: 'a', to: 'b', amount: 1 })
148
+ ).rejects.toThrow(/503/)
149
+ })
150
+ })
151
+
152
+ describe('decodeHexMessage', () => {
153
+ it('decodes even-length hex ASCII', () => {
154
+ expect(decodeHexMessage('524556455254')).toBe('REVERT')
155
+ })
156
+ it('passes through non-hex strings', () => {
157
+ expect(decodeHexMessage('already readable')).toBe('already readable')
158
+ })
159
+ it('does not mangle an all-hex word that decodes to non-printable bytes', () => {
160
+ // "deadbeef" is valid even-length hex but decodes to non-printable bytes.
161
+ expect(decodeHexMessage('deadbeef')).toBe('deadbeef')
162
+ })
163
+ it('passes through odd-length strings unchanged', () => {
164
+ expect(decodeHexMessage('abc')).toBe('abc')
165
+ })
166
+ it('returns empty string for undefined', () => {
167
+ expect(decodeHexMessage(undefined)).toBe('')
168
+ })
169
+ })
@@ -0,0 +1,205 @@
1
+ import type { NetworkId, TronGridConfig } from '@meshconnect/uwc-types'
2
+ import { encodeTrc20TransferParams, TRC20_TRANSFER_SELECTOR } from './abi'
3
+
4
+ /**
5
+ * The minimal shape of an unsigned Tron transaction returned by
6
+ * `/wallet/createtransaction` and `/wallet/triggersmartcontract`.
7
+ *
8
+ * Only `txID` + `raw_data*` are load-bearing for sign/broadcast; the struct
9
+ * carries more fields that pass through opaquely. Typed loosely on purpose so
10
+ * a node-version change to the surrounding shape doesn't break the contract.
11
+ */
12
+ export interface UnsignedTronTransaction {
13
+ txID?: string
14
+ raw_data?: unknown
15
+ raw_data_hex?: string
16
+ visible?: boolean
17
+ [key: string]: unknown
18
+ }
19
+
20
+ /** A transaction with the wallet's signature appended, ready to broadcast. */
21
+ export interface SignedTronTransaction extends UnsignedTronTransaction {
22
+ signature?: string[]
23
+ }
24
+
25
+ /** Broadcast response from `/wallet/broadcasttransaction`. */
26
+ interface BroadcastResponse {
27
+ result?: boolean
28
+ txid?: string
29
+ code?: string
30
+ message?: string
31
+ transaction?: { txID?: string }
32
+ }
33
+
34
+ const DEFAULT_ENDPOINTS: Partial<Record<NetworkId, string>> = {
35
+ 'tron:0x2b6653dc': 'https://api.trongrid.io', // Mainnet
36
+ 'tron:0x94a9059e': 'https://api.shasta.trongrid.io', // Shasta testnet
37
+ 'tron:0xcd8690dc': 'https://nile.trongrid.io' // Nile testnet
38
+ }
39
+
40
+ const DEFAULT_FEE_LIMIT_SUN = 100_000_000 // 100 TRX
41
+
42
+ /**
43
+ * Thin `fetch` wrapper over the three Tron full-node HTTP endpoints this
44
+ * connector needs. No SDK, no `tronweb` — the `/wallet/*` paths are the Tron
45
+ * full-node API standard, identical across TronGrid and any full node.
46
+ */
47
+ export class TronGridClient {
48
+ private readonly baseUrl: string
49
+ private readonly apiKey: string | undefined
50
+ private readonly feeLimitSun: number
51
+
52
+ constructor(networkId: NetworkId, config: TronGridConfig = {}) {
53
+ const url = config.endpoints?.[networkId] ?? DEFAULT_ENDPOINTS[networkId]
54
+ if (!url) {
55
+ throw new Error(
56
+ `tron-connector: no full-node endpoint configured for network "${networkId}". ` +
57
+ `Pass one via TronConnectorConfig.tronGrid.endpoints.`
58
+ )
59
+ }
60
+ // Reject plaintext endpoints: we send the TRON-PRO-API-KEY header and the
61
+ // unsigned tx in the body, so an `http://` host would leak both. Allow
62
+ // loopback http for local dev nodes (e.g. a tron-quickstart container).
63
+ if (
64
+ /^http:\/\//i.test(url) &&
65
+ !/^http:\/\/(localhost|127\.0\.0\.1)/i.test(url)
66
+ ) {
67
+ throw new Error(
68
+ `tron-connector: refusing non-https endpoint "${url}" (would send the API key + tx in cleartext)`
69
+ )
70
+ }
71
+ // Normalise away trailing slashes so path concatenation is predictable.
72
+ // Done with a plain loop (no regex) to avoid any backtracking concern.
73
+ let base = url
74
+ while (base.endsWith('/')) base = base.slice(0, -1)
75
+ this.baseUrl = base
76
+ this.apiKey = config.apiKey
77
+
78
+ const feeLimit = config.feeLimitSun ?? DEFAULT_FEE_LIMIT_SUN
79
+ if (!Number.isInteger(feeLimit) || feeLimit <= 0) {
80
+ throw new Error(
81
+ `tron-connector: feeLimitSun must be a positive integer (SUN), got ${feeLimit}`
82
+ )
83
+ }
84
+ this.feeLimitSun = feeLimit
85
+ }
86
+
87
+ /** Build an unsigned native TRX transfer. Amounts are in SUN. */
88
+ async createTransaction(params: {
89
+ from: string
90
+ to: string
91
+ amount: number
92
+ }): Promise<UnsignedTronTransaction> {
93
+ const tx = await this.post<UnsignedTronTransaction & { Error?: string }>(
94
+ '/wallet/createtransaction',
95
+ {
96
+ owner_address: params.from,
97
+ to_address: params.to,
98
+ amount: params.amount,
99
+ visible: true
100
+ }
101
+ )
102
+ if (tx.Error) {
103
+ throw new Error(`tron-connector: createtransaction failed — ${tx.Error}`)
104
+ }
105
+ if (!tx.txID) {
106
+ throw new Error('tron-connector: createtransaction returned no txID')
107
+ }
108
+ return tx
109
+ }
110
+
111
+ /** Build an unsigned TRC20 `transfer(address,uint256)` transaction. */
112
+ async createTrc20Transfer(params: {
113
+ from: string
114
+ to: string
115
+ amount: number
116
+ contractAddress: string
117
+ }): Promise<UnsignedTronTransaction> {
118
+ const response = await this.post<{
119
+ result?: { result?: boolean; code?: string; message?: string }
120
+ transaction?: UnsignedTronTransaction
121
+ }>('/wallet/triggersmartcontract', {
122
+ owner_address: params.from,
123
+ contract_address: params.contractAddress,
124
+ function_selector: TRC20_TRANSFER_SELECTOR,
125
+ parameter: encodeTrc20TransferParams(params.to, params.amount),
126
+ fee_limit: this.feeLimitSun,
127
+ call_value: 0,
128
+ visible: true
129
+ })
130
+ if (!response.result?.result || !response.transaction?.txID) {
131
+ throw new Error(
132
+ `tron-connector: triggersmartcontract failed — ${
133
+ decodeHexMessage(response.result?.message) ||
134
+ response.result?.code ||
135
+ 'unknown error'
136
+ }`
137
+ )
138
+ }
139
+ return response.transaction
140
+ }
141
+
142
+ /** Broadcast a signed transaction. Returns the on-chain txid. */
143
+ async broadcastTransaction(signed: SignedTronTransaction): Promise<string> {
144
+ const response = await this.post<BroadcastResponse>(
145
+ '/wallet/broadcasttransaction',
146
+ signed
147
+ )
148
+ // The node returns `{ result: false, message: <hex> }` on rejection even
149
+ // when a txid is echoed back — treat anything other than `result: true` as
150
+ // failure.
151
+ if (response.result !== true) {
152
+ throw new Error(
153
+ decodeHexMessage(response.message) ||
154
+ response.code ||
155
+ 'tron-connector: broadcast rejected by node'
156
+ )
157
+ }
158
+ const txid = response.txid ?? response.transaction?.txID ?? signed.txID
159
+ if (!txid) {
160
+ throw new Error('tron-connector: broadcast returned no txid')
161
+ }
162
+ return txid
163
+ }
164
+
165
+ private async post<T>(path: string, body: unknown): Promise<T> {
166
+ const headers: Record<string, string> = {
167
+ 'Content-Type': 'application/json'
168
+ }
169
+ if (this.apiKey) headers['TRON-PRO-API-KEY'] = this.apiKey
170
+
171
+ const response = await fetch(`${this.baseUrl}${path}`, {
172
+ method: 'POST',
173
+ headers,
174
+ body: JSON.stringify(body)
175
+ })
176
+ if (!response.ok) {
177
+ throw new Error(
178
+ `tron-connector: ${path} responded ${response.status} ${response.statusText}`
179
+ )
180
+ }
181
+ return (await response.json()) as T
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Full nodes often return `message` as hex-encoded ASCII
187
+ * (e.g. `"434f4e54524143545f56414c4944415445..."`). Decode when it looks like
188
+ * hex AND the result is printable ASCII; otherwise pass the string through
189
+ * unchanged. The printable check avoids mangling a message that happens to be
190
+ * an all-hex, even-length word (e.g. `"deadbeef"`), which would decode to
191
+ * non-printable bytes.
192
+ */
193
+ export function decodeHexMessage(message: string | undefined): string {
194
+ if (!message) return ''
195
+ if (!/^[0-9a-fA-F]+$/.test(message) || message.length % 2 !== 0) {
196
+ return message
197
+ }
198
+ let decoded = ''
199
+ for (let i = 0; i < message.length; i += 2) {
200
+ decoded += String.fromCharCode(parseInt(message.slice(i, i + 2), 16))
201
+ }
202
+ // Printable ASCII (incl. common whitespace) → it was hex-encoded text.
203
+ // Anything else → the input was more likely a literal string; keep it.
204
+ return /^[\x20-\x7e\t\n\r]*$/.test(decoded) ? decoded : message
205
+ }
@@ -0,0 +1,60 @@
1
+ import type { WalletError } from '@meshconnect/uwc-types'
2
+ import { WalletConnectorError } from '@meshconnect/uwc-types'
3
+
4
+ const REJECTION_PATTERNS = [
5
+ 'user rejected',
6
+ 'user denied',
7
+ 'user cancelled',
8
+ 'user canceled',
9
+ 'rejected by user',
10
+ 'denied by user',
11
+ 'cancelled by user',
12
+ 'canceled by user',
13
+ 'user disapproved',
14
+ 'user declined',
15
+ 'action_rejected',
16
+ 'reject request',
17
+ 'user rejects',
18
+ 'wallet rejected',
19
+ 'confirmation declined'
20
+ ]
21
+
22
+ /**
23
+ * Translate an unknown error (from a wallet `request`/`sign` call or a node
24
+ * response) into a `WalletConnectorError`. User-rejection signals are tagged
25
+ * `type: 'rejected'`; everything else is `type: 'unknown'`.
26
+ *
27
+ * Throws — never returns.
28
+ */
29
+ export function parseError(error: unknown): never {
30
+ if (error instanceof WalletConnectorError) throw error
31
+
32
+ let message = ''
33
+ let isRejected = false
34
+
35
+ if (error && typeof error === 'object') {
36
+ const errObj = error as {
37
+ code?: number | string
38
+ message?: string
39
+ error?: { message?: string }
40
+ }
41
+ // 4001 is the de-facto user-rejected code across injected wallets.
42
+ if (errObj.code === 4001 || errObj.code === 'ACTION_REJECTED') {
43
+ isRejected = true
44
+ }
45
+ message = errObj.message ?? errObj.error?.message ?? String(error)
46
+ } else {
47
+ message = String(error)
48
+ }
49
+
50
+ if (!isRejected) {
51
+ const lower = message.toLowerCase()
52
+ isRejected = REJECTION_PATTERNS.some(pattern => lower.includes(pattern))
53
+ }
54
+
55
+ const walletError: WalletError = {
56
+ type: isRejected ? 'rejected' : 'unknown',
57
+ message
58
+ }
59
+ throw new WalletConnectorError(walletError)
60
+ }