@multiplatform.one/web3 8.3.0 → 8.4.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 (171) hide show
  1. package/README.md +33 -2
  2. package/dist/cjs/clients/tron.cjs +176 -0
  3. package/dist/cjs/clients/tron.native.js +318 -0
  4. package/dist/cjs/clients/tron.native.js.map +1 -0
  5. package/dist/cjs/clients/useTronClient.cjs +50 -0
  6. package/dist/cjs/clients/useTronClient.native.js +63 -0
  7. package/dist/cjs/clients/useTronClient.native.js.map +1 -0
  8. package/dist/cjs/connectkit/adapters/TronAdapter.cjs +139 -0
  9. package/dist/cjs/connectkit/adapters/TronAdapter.native.js +217 -0
  10. package/dist/cjs/connectkit/adapters/TronAdapter.native.js.map +1 -0
  11. package/dist/cjs/connectkit/connectors/createWalletMessage.cjs +3 -3
  12. package/dist/cjs/connectkit/connectors/createWalletMessage.native.js +3 -3
  13. package/dist/cjs/connectkit/connectors/createWalletMessage.native.js.map +1 -1
  14. package/dist/cjs/connectkit/connectors/ensureWalletConnectors.cjs +2 -1
  15. package/dist/cjs/connectkit/connectors/ensureWalletConnectors.native.js +18 -35
  16. package/dist/cjs/connectkit/connectors/ensureWalletConnectors.native.js.map +1 -1
  17. package/dist/cjs/connectkit/connectors/syntheticChains.cjs +10 -4
  18. package/dist/cjs/connectkit/connectors/syntheticChains.native.js +10 -4
  19. package/dist/cjs/connectkit/connectors/syntheticChains.native.js.map +1 -1
  20. package/dist/cjs/connectkit/defaultConfig.cjs +2 -1
  21. package/dist/cjs/connectkit/defaultConfig.native.js +2 -1
  22. package/dist/cjs/connectkit/defaultConfig.native.js.map +1 -1
  23. package/dist/cjs/connectkit/wallets/walletConfigs.cjs +12 -0
  24. package/dist/cjs/connectkit/wallets/walletConfigs.native.js +12 -0
  25. package/dist/cjs/connectkit/wallets/walletConfigs.native.js.map +1 -1
  26. package/dist/cjs/families/host.native.js.map +1 -1
  27. package/dist/cjs/families/tron.cjs +92 -0
  28. package/dist/cjs/families/tron.native.js +120 -0
  29. package/dist/cjs/families/tron.native.js.map +1 -0
  30. package/dist/cjs/providers/WalletProvider.cjs +1 -1
  31. package/dist/cjs/tron.cjs +46 -0
  32. package/dist/cjs/tron.native.js +49 -0
  33. package/dist/cjs/tron.native.js.map +1 -0
  34. package/dist/cjs/wagmi/host.cjs +9 -4
  35. package/dist/cjs/wagmi/host.native.js +6 -3
  36. package/dist/cjs/wagmi/host.native.js.map +1 -1
  37. package/dist/esm/clients/tron.mjs +133 -0
  38. package/dist/esm/clients/tron.mjs.map +1 -0
  39. package/dist/esm/clients/tron.native.js +272 -0
  40. package/dist/esm/clients/tron.native.js.map +1 -0
  41. package/dist/esm/clients/useTronClient.mjs +25 -0
  42. package/dist/esm/clients/useTronClient.mjs.map +1 -0
  43. package/dist/esm/clients/useTronClient.native.js +35 -0
  44. package/dist/esm/clients/useTronClient.native.js.map +1 -0
  45. package/dist/esm/connectkit/adapters/TronAdapter.mjs +111 -0
  46. package/dist/esm/connectkit/adapters/TronAdapter.mjs.map +1 -0
  47. package/dist/esm/connectkit/adapters/TronAdapter.native.js +186 -0
  48. package/dist/esm/connectkit/adapters/TronAdapter.native.js.map +1 -0
  49. package/dist/esm/connectkit/connectors/createWalletMessage.mjs +3 -3
  50. package/dist/esm/connectkit/connectors/createWalletMessage.mjs.map +1 -1
  51. package/dist/esm/connectkit/connectors/createWalletMessage.native.js +3 -3
  52. package/dist/esm/connectkit/connectors/createWalletMessage.native.js.map +1 -1
  53. package/dist/esm/connectkit/connectors/ensureWalletConnectors.mjs +2 -1
  54. package/dist/esm/connectkit/connectors/ensureWalletConnectors.mjs.map +1 -1
  55. package/dist/esm/connectkit/connectors/ensureWalletConnectors.native.js +18 -35
  56. package/dist/esm/connectkit/connectors/ensureWalletConnectors.native.js.map +1 -1
  57. package/dist/esm/connectkit/connectors/syntheticChains.mjs +8 -4
  58. package/dist/esm/connectkit/connectors/syntheticChains.mjs.map +1 -1
  59. package/dist/esm/connectkit/connectors/syntheticChains.native.js +8 -4
  60. package/dist/esm/connectkit/connectors/syntheticChains.native.js.map +1 -1
  61. package/dist/esm/connectkit/defaultConfig.mjs +2 -1
  62. package/dist/esm/connectkit/defaultConfig.mjs.map +1 -1
  63. package/dist/esm/connectkit/defaultConfig.native.js +2 -1
  64. package/dist/esm/connectkit/defaultConfig.native.js.map +1 -1
  65. package/dist/esm/connectkit/wallets/walletConfigs.mjs +12 -0
  66. package/dist/esm/connectkit/wallets/walletConfigs.mjs.map +1 -1
  67. package/dist/esm/connectkit/wallets/walletConfigs.native.js +12 -0
  68. package/dist/esm/connectkit/wallets/walletConfigs.native.js.map +1 -1
  69. package/dist/esm/families/host.mjs.map +1 -1
  70. package/dist/esm/families/host.native.js.map +1 -1
  71. package/dist/esm/families/tron.mjs +65 -0
  72. package/dist/esm/families/tron.mjs.map +1 -0
  73. package/dist/esm/families/tron.native.js +90 -0
  74. package/dist/esm/families/tron.native.js.map +1 -0
  75. package/dist/esm/providers/WalletProvider.mjs +1 -1
  76. package/dist/esm/providers/WalletProvider.mjs.map +1 -1
  77. package/dist/esm/tron.mjs +6 -0
  78. package/dist/esm/tron.mjs.map +1 -0
  79. package/dist/esm/tron.native.js +6 -0
  80. package/dist/esm/tron.native.js.map +1 -0
  81. package/dist/esm/wagmi/host.mjs +9 -4
  82. package/dist/esm/wagmi/host.mjs.map +1 -1
  83. package/dist/esm/wagmi/host.native.js +6 -3
  84. package/dist/esm/wagmi/host.native.js.map +1 -1
  85. package/dist/jsx/clients/tron.mjs +133 -0
  86. package/dist/jsx/clients/tron.mjs.map +1 -0
  87. package/dist/jsx/clients/tron.native.js +318 -0
  88. package/dist/jsx/clients/tron.native.js.map +1 -0
  89. package/dist/jsx/clients/useTronClient.mjs +25 -0
  90. package/dist/jsx/clients/useTronClient.mjs.map +1 -0
  91. package/dist/jsx/clients/useTronClient.native.js +63 -0
  92. package/dist/jsx/clients/useTronClient.native.js.map +1 -0
  93. package/dist/jsx/connectkit/adapters/TronAdapter.mjs +111 -0
  94. package/dist/jsx/connectkit/adapters/TronAdapter.mjs.map +1 -0
  95. package/dist/jsx/connectkit/adapters/TronAdapter.native.js +217 -0
  96. package/dist/jsx/connectkit/adapters/TronAdapter.native.js.map +1 -0
  97. package/dist/jsx/connectkit/connectors/createWalletMessage.mjs +3 -3
  98. package/dist/jsx/connectkit/connectors/createWalletMessage.mjs.map +1 -1
  99. package/dist/jsx/connectkit/connectors/createWalletMessage.native.js +3 -3
  100. package/dist/jsx/connectkit/connectors/createWalletMessage.native.js.map +1 -1
  101. package/dist/jsx/connectkit/connectors/ensureWalletConnectors.mjs +2 -1
  102. package/dist/jsx/connectkit/connectors/ensureWalletConnectors.mjs.map +1 -1
  103. package/dist/jsx/connectkit/connectors/ensureWalletConnectors.native.js +18 -35
  104. package/dist/jsx/connectkit/connectors/ensureWalletConnectors.native.js.map +1 -1
  105. package/dist/jsx/connectkit/connectors/syntheticChains.mjs +8 -4
  106. package/dist/jsx/connectkit/connectors/syntheticChains.mjs.map +1 -1
  107. package/dist/jsx/connectkit/connectors/syntheticChains.native.js +10 -4
  108. package/dist/jsx/connectkit/connectors/syntheticChains.native.js.map +1 -1
  109. package/dist/jsx/connectkit/defaultConfig.mjs +2 -1
  110. package/dist/jsx/connectkit/defaultConfig.mjs.map +1 -1
  111. package/dist/jsx/connectkit/defaultConfig.native.js +2 -1
  112. package/dist/jsx/connectkit/defaultConfig.native.js.map +1 -1
  113. package/dist/jsx/connectkit/wallets/walletConfigs.mjs +12 -0
  114. package/dist/jsx/connectkit/wallets/walletConfigs.mjs.map +1 -1
  115. package/dist/jsx/connectkit/wallets/walletConfigs.native.js +12 -0
  116. package/dist/jsx/connectkit/wallets/walletConfigs.native.js.map +1 -1
  117. package/dist/jsx/families/host.mjs.map +1 -1
  118. package/dist/jsx/families/host.native.js.map +1 -1
  119. package/dist/jsx/families/tron.mjs +65 -0
  120. package/dist/jsx/families/tron.mjs.map +1 -0
  121. package/dist/jsx/families/tron.native.js +120 -0
  122. package/dist/jsx/families/tron.native.js.map +1 -0
  123. package/dist/jsx/providers/WalletProvider.mjs +1 -1
  124. package/dist/jsx/providers/WalletProvider.mjs.map +1 -1
  125. package/dist/jsx/tron.mjs +6 -0
  126. package/dist/jsx/tron.mjs.map +1 -0
  127. package/dist/jsx/tron.native.js +49 -0
  128. package/dist/jsx/tron.native.js.map +1 -0
  129. package/dist/jsx/wagmi/host.mjs +9 -4
  130. package/dist/jsx/wagmi/host.mjs.map +1 -1
  131. package/dist/jsx/wagmi/host.native.js +6 -3
  132. package/dist/jsx/wagmi/host.native.js.map +1 -1
  133. package/dist/keycloak/login.iife.js +380 -380
  134. package/package.json +13 -7
  135. package/src/clients/tron.spec.ts +118 -0
  136. package/src/clients/tron.ts +186 -0
  137. package/src/clients/useTronClient.ts +26 -0
  138. package/src/connectkit/adapters/TronAdapter.spec.ts +155 -0
  139. package/src/connectkit/adapters/TronAdapter.ts +168 -0
  140. package/src/connectkit/adapters/discover.ts +1 -1
  141. package/src/connectkit/connectors/createWalletMessage.ts +3 -3
  142. package/src/connectkit/connectors/ensureWalletConnectors.ts +2 -1
  143. package/src/connectkit/connectors/syntheticChains.ts +8 -2
  144. package/src/connectkit/defaultConfig.ts +5 -2
  145. package/src/connectkit/wallets/walletConfigs.tsx +8 -0
  146. package/src/families/host.ts +9 -2
  147. package/src/families/tron.spec.ts +118 -0
  148. package/src/families/tron.ts +63 -0
  149. package/src/providers/WalletProvider.tsx +1 -1
  150. package/src/tron.ts +26 -0
  151. package/src/wagmi/host.tsx +6 -2
  152. package/types/clients/tron.d.ts +65 -0
  153. package/types/clients/tron.d.ts.map +1 -0
  154. package/types/clients/useTronClient.d.ts +8 -0
  155. package/types/clients/useTronClient.d.ts.map +1 -0
  156. package/types/connectkit/adapters/TronAdapter.d.ts +35 -0
  157. package/types/connectkit/adapters/TronAdapter.d.ts.map +1 -0
  158. package/types/connectkit/adapters/discover.d.ts +1 -1
  159. package/types/connectkit/adapters/discover.d.ts.map +1 -1
  160. package/types/connectkit/connectors/ensureWalletConnectors.d.ts.map +1 -1
  161. package/types/connectkit/connectors/syntheticChains.d.ts +4 -0
  162. package/types/connectkit/connectors/syntheticChains.d.ts.map +1 -1
  163. package/types/connectkit/defaultConfig.d.ts.map +1 -1
  164. package/types/connectkit/wallets/walletConfigs.d.ts.map +1 -1
  165. package/types/families/host.d.ts +9 -2
  166. package/types/families/host.d.ts.map +1 -1
  167. package/types/families/tron.d.ts +15 -0
  168. package/types/families/tron.d.ts.map +1 -0
  169. package/types/tron.d.ts +5 -0
  170. package/types/tron.d.ts.map +1 -0
  171. package/types/wagmi/host.d.ts.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplatform.one/web3",
3
- "version": "8.3.0",
3
+ "version": "8.4.0",
4
4
  "description": "Web3 wallet integration for multiplatform.one ecosystem with ConnectKit and wagmi",
5
5
  "keywords": [
6
6
  "connectkit",
@@ -105,6 +105,12 @@
105
105
  "import": "./dist/esm/solana.mjs",
106
106
  "require": "./dist/cjs/solana.cjs"
107
107
  },
108
+ "./tron": {
109
+ "source": "./src/tron.ts",
110
+ "types": "./types/tron.d.ts",
111
+ "import": "./dist/esm/tron.mjs",
112
+ "require": "./dist/cjs/tron.cjs"
113
+ },
108
114
  "./phantom": {
109
115
  "source": "./src/phantom/index.ts",
110
116
  "types": "./types/phantom/index.d.ts",
@@ -120,10 +126,10 @@
120
126
  "@aave/account": "^0.2.1",
121
127
  "@coinbase/wallet-sdk": "^4.4.0",
122
128
  "@metamask/connect-evm": "^2.1.1",
123
- "@multiplatform.one/connectkit": "^8.3.0",
124
- "@multiplatform.one/gpg": "^8.3.0",
125
- "@multiplatform.one/platform": "^8.3.0",
126
- "@multiplatform.one/theme": "^8.3.0",
129
+ "@multiplatform.one/connectkit": "^8.4.0",
130
+ "@multiplatform.one/gpg": "^8.4.0",
131
+ "@multiplatform.one/platform": "^8.4.0",
132
+ "@multiplatform.one/theme": "^8.4.0",
127
133
  "@noble/ciphers": "^1.3.0",
128
134
  "@noble/curves": "^1.9.7",
129
135
  "@phosphor-icons/react": "^2.1.10",
@@ -147,8 +153,8 @@
147
153
  "wagmi": "^3.7.1"
148
154
  },
149
155
  "devDependencies": {
150
- "@multiplatform.one/config": "8.3.0",
151
- "@multiplatform.one/test-utils": "8.3.0",
156
+ "@multiplatform.one/config": "8.4.0",
157
+ "@multiplatform.one/test-utils": "8.4.0",
152
158
  "@tamagui/build": "2.7.6",
153
159
  "@tanstack/react-query": "^5.104.3",
154
160
  "@testing-library/jest-dom": "^7.0.1",
@@ -0,0 +1,118 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import {
3
+ isTronAddress,
4
+ readTrc20Balance,
5
+ toTronBase58,
6
+ toTronHex,
7
+ tronClient,
8
+ TronGridError,
9
+ } from "./tron";
10
+
11
+ const usdt = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
12
+ const usdtHex = "41a614f803b6fd780986a42c78ec9c7f77e6ded13c";
13
+ const holder = "TKHuVq1oKVruCGLvqVexFs6dawKv6fQgFs";
14
+
15
+ function answering(body: unknown, status = 200) {
16
+ return vi.fn(async (_url: string | URL | Request, _init?: RequestInit) => {
17
+ return new Response(JSON.stringify(body), { status });
18
+ });
19
+ }
20
+
21
+ describe("Tron addresses", () => {
22
+ it("converts between base58, 41 hex and bare 20-byte hex", () => {
23
+ expect(toTronHex(usdt)).toBe(usdtHex);
24
+ expect(toTronBase58(usdtHex)).toBe(usdt);
25
+ expect(toTronBase58(`0x${usdtHex.slice(2)}`)).toBe(usdt);
26
+ expect(toTronBase58("0x52842189638234da1c683e278d4a07d7ea76fcbb")).toBe(
27
+ "THVWhQra6ziciHXtxzdzUKt2fzCPEvLM4F",
28
+ );
29
+ });
30
+
31
+ it("refuses a bad checksum, a wrong version byte and a wrong length", () => {
32
+ expect(() => toTronHex(`${usdt.slice(0, -1)}u`)).toThrow(/checksum|not a Tron/);
33
+ expect(isTronAddress("1BoatSLRHtKNngkdXEeobR76b53LETtpyT")).toBe(false);
34
+ expect(isTronAddress("0x1234")).toBe(false);
35
+ expect(isTronAddress("not an address")).toBe(false);
36
+ expect(isTronAddress(usdt)).toBe(true);
37
+ });
38
+ });
39
+
40
+ describe("readTrc20Balance", () => {
41
+ it("posts balanceOf(owner) to TronGrid and reads the word back", async () => {
42
+ const fetch = answering({
43
+ result: { result: true },
44
+ constant_result: [`${"0".repeat(56)}0001e240`],
45
+ });
46
+ const balance = await readTrc20Balance({ token: usdt, owner: holder, network: "nile", fetch });
47
+ expect(balance).toBe(123456n);
48
+ const [url, init] = fetch.mock.calls[0];
49
+ expect(url).toBe("https://nile.trongrid.io/wallet/triggerconstantcontract");
50
+ expect(init?.method).toBe("POST");
51
+ const body = JSON.parse(String(init?.body));
52
+ expect(body).toEqual({
53
+ owner_address: holder,
54
+ contract_address: usdt,
55
+ function_selector: "balanceOf(address)",
56
+ parameter: `${"0".repeat(24)}${toTronHex(holder).slice(2)}`,
57
+ visible: true,
58
+ });
59
+ expect(init?.headers).not.toHaveProperty("TRON-PRO-API-KEY");
60
+ });
61
+
62
+ it("defaults to mainnet, takes an API key and an API URL of the app's own", async () => {
63
+ const fetch = answering({ result: { result: true }, constant_result: [""] });
64
+ await expect(
65
+ readTrc20Balance({ token: usdt, owner: holder, fetch, apiKey: "k" }),
66
+ ).resolves.toBe(0n);
67
+ expect(fetch.mock.calls[0][0]).toBe("https://api.trongrid.io/wallet/triggerconstantcontract");
68
+ expect(fetch.mock.calls[0][1]?.headers).toHaveProperty("TRON-PRO-API-KEY", "k");
69
+ await readTrc20Balance({ token: usdt, owner: holder, fetch, apiUrl: "https://node.example/" });
70
+ expect(fetch.mock.calls[1][0]).toBe("https://node.example/wallet/triggerconstantcontract");
71
+ });
72
+
73
+ it("names TronGrid's reason when the call fails", async () => {
74
+ const reason = Buffer.from("REVERT opcode executed").toString("hex");
75
+ const fetch = answering({ result: { code: "CONTRACT_EXE_ERROR", message: reason } });
76
+ await expect(readTrc20Balance({ token: usdt, owner: holder, fetch })).rejects.toThrow(
77
+ "TronGrid balanceOf failed: REVERT opcode executed",
78
+ );
79
+ });
80
+
81
+ it("rejects on an HTTP error with its status", async () => {
82
+ const fetch = answering({}, 429);
83
+ const failure = readTrc20Balance({ token: usdt, owner: holder, fetch });
84
+ await expect(failure).rejects.toBeInstanceOf(TronGridError);
85
+ await expect(failure).rejects.toMatchObject({ status: 429 });
86
+ });
87
+
88
+ it("refuses a malformed owner before any request", async () => {
89
+ const fetch = answering({});
90
+ await expect(readTrc20Balance({ token: usdt, owner: "nope", fetch })).rejects.toThrow(
91
+ /not a Tron address/,
92
+ );
93
+ expect(fetch).not.toHaveBeenCalled();
94
+ });
95
+ });
96
+
97
+ describe("tronClient", () => {
98
+ it("signs through the adapter and reads the connected account's balance on its network", async () => {
99
+ const signMessage = vi.fn(async () => "0xsig");
100
+ const fetch = answering({ result: { result: true }, constant_result: ["0a"] });
101
+ const client = tronClient({ address: holder, signMessage }, { network: "nile", fetch });
102
+ expect(client).toMatchObject({
103
+ family: "tron",
104
+ address: holder,
105
+ chainId: "tron",
106
+ network: "nile",
107
+ capabilities: ["signMessage"],
108
+ });
109
+ await expect(client.signMessage("hi")).resolves.toBe("0xsig");
110
+ await expect(client.trc20Balance(usdt)).resolves.toBe(10n);
111
+ expect(JSON.parse(String(fetch.mock.calls[0][1]?.body)).owner_address).toBe(holder);
112
+ expect(fetch.mock.calls[0][0]).toContain("nile.trongrid.io");
113
+ });
114
+
115
+ it("needs a connected adapter", () => {
116
+ expect(() => tronClient({ address: null, signMessage: vi.fn() })).toThrow(/connect a Tron/);
117
+ });
118
+ });
@@ -0,0 +1,186 @@
1
+ import bs58 from "bs58";
2
+ import { bytesToHex, hexToBytes, hexToString, sha256 } from "viem";
3
+ import type { WalletAdapter } from "../connectkit/adapters/types";
4
+ import type { ChainClient, ClientCapability } from "./types";
5
+
6
+ /** The Tron networks TronGrid serves. */
7
+ export type TronNetwork = "mainnet" | "nile" | "shasta";
8
+
9
+ /** TronGrid's HTTP API per network. */
10
+ export const tronGridUrls: Readonly<Record<TronNetwork, string>> = Object.freeze({
11
+ mainnet: "https://api.trongrid.io",
12
+ nile: "https://nile.trongrid.io",
13
+ shasta: "https://api.shasta.trongrid.io",
14
+ });
15
+
16
+ const tronPrefix = 0x41;
17
+
18
+ const checksumOf = (payload: Uint8Array) => sha256(sha256(payload, "bytes"), "bytes").slice(0, 4);
19
+
20
+ /**
21
+ * A Tron address as its 21 bytes, version byte 0x41 first. Takes base58
22
+ * (`T…`), 41-prefixed hex, or a bare 20-byte EVM-style hex. Throws on a bad
23
+ * checksum or length.
24
+ */
25
+ export function tronAddressBytes(address: string): Uint8Array {
26
+ const value = address.trim();
27
+ if (/^(0x)?[0-9a-fA-F]+$/.test(value)) {
28
+ const bytes = hexToBytes(value.startsWith("0x") ? (value as `0x${string}`) : `0x${value}`);
29
+ if (bytes.length === 20) return Uint8Array.of(tronPrefix, ...bytes);
30
+ if (bytes.length === 21 && bytes[0] === tronPrefix) return bytes;
31
+ throw new Error(`not a Tron address: ${address}`);
32
+ }
33
+ let decoded: Uint8Array;
34
+ try {
35
+ decoded = bs58.decode(value);
36
+ } catch {
37
+ throw new Error(`not a Tron address: ${address}`);
38
+ }
39
+ if (decoded.length !== 25 || decoded[0] !== tronPrefix) {
40
+ throw new Error(`not a Tron address: ${address}`);
41
+ }
42
+ const payload = decoded.slice(0, 21);
43
+ const checksum = checksumOf(payload);
44
+ if (!checksum.every((byte, index) => byte === decoded[21 + index])) {
45
+ throw new Error(`Tron address checksum mismatch: ${address}`);
46
+ }
47
+ return payload;
48
+ }
49
+
50
+ /** The base58 form TronLink shows (`T…`), from any form tronAddressBytes takes. */
51
+ export function toTronBase58(address: string): string {
52
+ const payload = tronAddressBytes(address);
53
+ return bs58.encode(Uint8Array.of(...payload, ...checksumOf(payload)));
54
+ }
55
+
56
+ /** The 41-prefixed hex form TronGrid takes when `visible` is false. */
57
+ export function toTronHex(address: string): string {
58
+ return bytesToHex(tronAddressBytes(address)).slice(2);
59
+ }
60
+
61
+ /** Whether `address` is a well-formed Tron address in any form tronAddressBytes takes. */
62
+ export function isTronAddress(address: string): boolean {
63
+ try {
64
+ tronAddressBytes(address);
65
+ return true;
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ /** Where a TronGrid read goes: a network, or an API URL of the app's own. */
72
+ export interface TronGridOptions {
73
+ /** Defaults to `mainnet`. Ignored when `apiUrl` is given. */
74
+ network?: TronNetwork;
75
+ /** A TronGrid-compatible full node, in place of the network's TronGrid URL. */
76
+ apiUrl?: string;
77
+ /** Sent as `TRON-PRO-API-KEY`. TronGrid serves keyless reads at a lower rate. */
78
+ apiKey?: string;
79
+ fetch?: typeof fetch;
80
+ signal?: AbortSignal;
81
+ }
82
+
83
+ export interface Trc20BalanceParameters extends TronGridOptions {
84
+ /** The TRC-20 contract. */
85
+ token: string;
86
+ /** The account whose balance is read. */
87
+ owner: string;
88
+ }
89
+
90
+ /** A TronGrid call that answered without a result. */
91
+ export class TronGridError extends Error {
92
+ readonly status?: number;
93
+ constructor(message: string, status?: number) {
94
+ super(message);
95
+ this.name = "TronGridError";
96
+ this.status = status;
97
+ }
98
+ }
99
+
100
+ function decodeMessage(message: string | undefined): string | undefined {
101
+ if (!message) return undefined;
102
+ if (!/^[0-9a-fA-F]+$/.test(message) || message.length % 2) return message;
103
+ try {
104
+ return hexToString(`0x${message}`);
105
+ } catch {
106
+ return message;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * A TRC-20 `balanceOf(owner)` in the token's smallest unit, read through
112
+ * TronGrid's `triggerconstantcontract`. No wallet and no tronweb: one POST.
113
+ */
114
+ export async function readTrc20Balance(parameters: Trc20BalanceParameters): Promise<bigint> {
115
+ const { token, owner, network = "mainnet", apiUrl, apiKey, signal } = parameters;
116
+ const request = parameters.fetch ?? globalThis.fetch;
117
+ const ownerBytes = tronAddressBytes(owner);
118
+ const base = (apiUrl ?? tronGridUrls[network]).replace(/\/+$/, "");
119
+ const response = await request(`${base}/wallet/triggerconstantcontract`, {
120
+ method: "POST",
121
+ headers: {
122
+ accept: "application/json",
123
+ "content-type": "application/json",
124
+ ...(apiKey ? { "TRON-PRO-API-KEY": apiKey } : {}),
125
+ },
126
+ body: JSON.stringify({
127
+ owner_address: toTronBase58(owner),
128
+ contract_address: toTronBase58(token),
129
+ function_selector: "balanceOf(address)",
130
+ parameter: bytesToHex(ownerBytes.slice(1)).slice(2).padStart(64, "0"),
131
+ visible: true,
132
+ }),
133
+ ...(signal ? { signal } : {}),
134
+ });
135
+ if (!response.ok) {
136
+ throw new TronGridError(`TronGrid answered ${response.status} for balanceOf`, response.status);
137
+ }
138
+ const body = (await response.json()) as {
139
+ result?: { result?: boolean; code?: string; message?: string };
140
+ constant_result?: string[];
141
+ Error?: string;
142
+ };
143
+ const word = body.constant_result?.[0];
144
+ if (!body.result?.result || word === undefined) {
145
+ const reason = decodeMessage(body.result?.message) ?? body.Error ?? body.result?.code;
146
+ throw new TronGridError(`TronGrid balanceOf failed${reason ? `: ${reason}` : ""}`);
147
+ }
148
+ return word === "" ? 0n : BigInt(`0x${word}`);
149
+ }
150
+
151
+ type MessageAdapter = Pick<WalletAdapter, "address" | "signMessage">;
152
+
153
+ export interface TronClientOptions extends Omit<TronGridOptions, "signal"> {}
154
+
155
+ /**
156
+ * The connected Tron wallet: message signing through TronLink and TRC-20
157
+ * reads through TronGrid on the wallet's network.
158
+ */
159
+ export interface TronClient extends ChainClient<"tron"> {
160
+ readonly address: string;
161
+ readonly chainId: "tron";
162
+ /** The network TronLink's node is on, which `trc20Balance` reads. */
163
+ readonly network: TronNetwork;
164
+ /** TronLink's `signMessageV2`, 0x hex. */
165
+ signMessage(message: string): Promise<string>;
166
+ /** A TRC-20 balance in the token's smallest unit, for the connected account unless `owner` is given. */
167
+ trc20Balance(token: string, owner?: string, signal?: AbortSignal): Promise<bigint>;
168
+ }
169
+
170
+ const capabilities: readonly ClientCapability[] = ["signMessage"];
171
+
172
+ export function tronClient(adapter: MessageAdapter, options: TronClientOptions = {}): TronClient {
173
+ if (!adapter.address) throw new Error("tronClient: connect a Tron wallet first");
174
+ const address = adapter.address;
175
+ const network = options.network ?? "mainnet";
176
+ return {
177
+ family: "tron",
178
+ address,
179
+ chainId: "tron",
180
+ network,
181
+ capabilities,
182
+ signMessage: (message) => adapter.signMessage(message),
183
+ trc20Balance: (token, owner = address, signal) =>
184
+ readTrc20Balance({ ...options, network, token, owner, ...(signal ? { signal } : {}) }),
185
+ };
186
+ }
@@ -0,0 +1,26 @@
1
+ import { useMemo } from "react";
2
+ import { useAccount } from "wagmi";
3
+ import { connectorAdapter } from "./adapter";
4
+ import { useOptionalConnection } from "../core/react";
5
+ import { TronAdapter } from "../connectkit/adapters/TronAdapter";
6
+ import { tronClient, type TronClient } from "./tron";
7
+
8
+ /**
9
+ * The connected TronLink wallet: the connection store's client when one is
10
+ * mounted, else wagmi's shim connector's. Undefined while no Tron wallet
11
+ * holds the connection.
12
+ */
13
+ export function useTronClient(): TronClient | undefined {
14
+ const connection = useOptionalConnection();
15
+ const { connector, address } = useAccount();
16
+ return useMemo(() => {
17
+ if (connection) {
18
+ return connection.family === "tron"
19
+ ? ((connection.client as TronClient | null) ?? undefined)
20
+ : undefined;
21
+ }
22
+ const adapter = connectorAdapter(connector, "tron");
23
+ if (!adapter?.address) return undefined;
24
+ return tronClient(adapter, adapter instanceof TronAdapter ? { network: adapter.network } : {});
25
+ }, [connection, connector, address]);
26
+ }
@@ -0,0 +1,155 @@
1
+ // @vitest-environment happy-dom
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+ import { discoverTronWallets, hasTronWallet, TronAdapter, tronNetworkOf } from "./TronAdapter";
4
+
5
+ const address = "TKHuVq1oKVruCGLvqVexFs6dawKv6fQgFs";
6
+ const globals: string[] = [];
7
+ const install = (name: string, value: unknown) => {
8
+ globals.push(name);
9
+ (window as unknown as Record<string, unknown>)[name] = value;
10
+ };
11
+
12
+ afterEach(() => {
13
+ for (const name of globals.splice(0)) Reflect.deleteProperty(window, name);
14
+ });
15
+
16
+ function tronWeb(host = "https://api.trongrid.io") {
17
+ return {
18
+ ready: true,
19
+ defaultAddress: { base58: false as string | false },
20
+ fullNode: { host },
21
+ trx: { signMessageV2: vi.fn(async (message: string) => `0xsigned:${message}`) },
22
+ };
23
+ }
24
+
25
+ function tronLink(answer: unknown = { code: 200, message: "ok" }, host?: string) {
26
+ const web = tronWeb(host);
27
+ const link = {
28
+ ready: true,
29
+ tronWeb: web,
30
+ request: vi.fn(async () => {
31
+ if (answer && typeof answer === "object" && (answer as { code?: number }).code === 200) {
32
+ web.defaultAddress.base58 = address;
33
+ }
34
+ return answer;
35
+ }),
36
+ };
37
+ install("tronLink", link);
38
+ install("tronWeb", web);
39
+ return { link, web };
40
+ }
41
+
42
+ describe("discoverTronWallets", () => {
43
+ it("lists TronLink only when it is injected", () => {
44
+ expect(hasTronWallet()).toBe(false);
45
+ expect(discoverTronWallets()).toEqual([]);
46
+ tronLink();
47
+ const [entry] = discoverTronWallets();
48
+ expect(entry).toMatchObject({
49
+ id: "wallet:tron:tronlink",
50
+ name: "TronLink",
51
+ ecosystem: "tron",
52
+ });
53
+ expect(entry.adapter).toBeInstanceOf(TronAdapter);
54
+ });
55
+ });
56
+
57
+ describe("TronAdapter", () => {
58
+ it("connects through tron_requestAccounts and takes tronWeb's base58 account", async () => {
59
+ const { link } = tronLink();
60
+ const adapter = new TronAdapter();
61
+ await adapter.connect();
62
+ expect(link.request).toHaveBeenCalledWith({ method: "tron_requestAccounts" });
63
+ expect(adapter.address).toBe(address);
64
+ expect(adapter.isConnected).toBe(true);
65
+ expect(adapter.networkId).toBe("tron:mainnet");
66
+ });
67
+
68
+ it("reports the network TronLink's node is on", () => {
69
+ tronLink(undefined, "https://nile.trongrid.io");
70
+ expect(new TronAdapter().network).toBe("nile");
71
+ expect(tronNetworkOf("https://api.shasta.trongrid.io")).toBe("shasta");
72
+ expect(tronNetworkOf(undefined)).toBe("mainnet");
73
+ });
74
+
75
+ it("rejects when TronLink is locked, refuses, or is missing", async () => {
76
+ tronLink("");
77
+ await expect(new TronAdapter().connect()).rejects.toThrow(/locked/);
78
+ for (const name of globals.splice(0)) Reflect.deleteProperty(window, name);
79
+ tronLink({ code: 4001, message: "User rejected" });
80
+ await expect(new TronAdapter().connect()).rejects.toThrow("User rejected");
81
+ for (const name of globals.splice(0)) Reflect.deleteProperty(window, name);
82
+ await expect(new TronAdapter().connect()).rejects.toThrow(/Install TronLink/);
83
+ });
84
+
85
+ it("connects through the TIP-1193 provider when TronLink exposes window.tron", async () => {
86
+ const web = tronWeb();
87
+ const listeners = new Set<(accounts: string[]) => void>();
88
+ const provider = {
89
+ tronWeb: web,
90
+ request: vi.fn(async () => {
91
+ web.defaultAddress.base58 = address;
92
+ return [address];
93
+ }),
94
+ on: (_event: string, listener: (accounts: string[]) => void) => listeners.add(listener),
95
+ removeListener: (_event: string, listener: (accounts: string[]) => void) =>
96
+ listeners.delete(listener),
97
+ };
98
+ install("tron", provider);
99
+ const adapter = new TronAdapter();
100
+ await adapter.connect();
101
+ expect(provider.request).toHaveBeenCalledWith({ method: "eth_requestAccounts" });
102
+ const changed = vi.fn();
103
+ const unsubscribe = adapter.subscribe(changed);
104
+ web.defaultAddress.base58 = false;
105
+ for (const listener of listeners) listener([]);
106
+ expect(changed).toHaveBeenCalledTimes(1);
107
+ expect(adapter.address).toBeNull();
108
+ unsubscribe();
109
+ expect(listeners.size).toBe(0);
110
+ });
111
+
112
+ it("restores the account TronLink already shares, and says false when it shares none", async () => {
113
+ const { web } = tronLink();
114
+ const adapter = new TronAdapter();
115
+ await expect(adapter.restore()).resolves.toBe(false);
116
+ web.defaultAddress.base58 = address;
117
+ await expect(adapter.restore()).resolves.toBe(true);
118
+ expect(adapter.address).toBe(address);
119
+ for (const name of globals.splice(0)) Reflect.deleteProperty(window, name);
120
+ await expect(new TronAdapter().restore()).rejects.toThrow(/not.*yet|yet/);
121
+ });
122
+
123
+ it("follows TronLink's account messages", async () => {
124
+ const { web } = tronLink();
125
+ const adapter = new TronAdapter();
126
+ await adapter.connect();
127
+ const changed = vi.fn();
128
+ const unsubscribe = adapter.subscribe(changed);
129
+ web.defaultAddress.base58 = "TJCo98saj6WND61g1uuKwJ9GMWMT9WkJFo";
130
+ window.dispatchEvent(
131
+ new MessageEvent("message", { data: { message: { action: "accountsChanged" } } }),
132
+ );
133
+ expect(changed).toHaveBeenCalledTimes(1);
134
+ expect(adapter.address).toBe("TJCo98saj6WND61g1uuKwJ9GMWMT9WkJFo");
135
+ window.dispatchEvent(new MessageEvent("message", { data: { message: { action: "other" } } }));
136
+ expect(changed).toHaveBeenCalledTimes(1);
137
+ unsubscribe();
138
+ window.dispatchEvent(
139
+ new MessageEvent("message", { data: { message: { action: "disconnect" } } }),
140
+ );
141
+ expect(changed).toHaveBeenCalledTimes(1);
142
+ });
143
+
144
+ it("signs with signMessageV2 once connected, and builds no sign-in message", async () => {
145
+ const { web } = tronLink();
146
+ const adapter = new TronAdapter();
147
+ await expect(adapter.signMessage("hi")).rejects.toThrow(/connect a Tron wallet/);
148
+ await adapter.connect();
149
+ await expect(adapter.signMessage("hi")).resolves.toBe("0xsigned:hi");
150
+ expect(web.trx.signMessageV2).toHaveBeenCalledWith("hi");
151
+ await expect(adapter.buildMessage({ nonce: "n" })).rejects.toThrow(/not supported yet/);
152
+ await adapter.disconnect();
153
+ expect(adapter.address).toBeNull();
154
+ });
155
+ });