@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,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
|
+
}
|