@tomo-inc/chains-service 0.0.2

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 (52) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/README.md +15 -0
  3. package/package.json +38 -0
  4. package/project.json +59 -0
  5. package/src/api/__tests__/config.ts +21 -0
  6. package/src/api/__tests__/token.test.ts +120 -0
  7. package/src/api/__tests__/transaction.test.ts +86 -0
  8. package/src/api/__tests__/user.test.ts +105 -0
  9. package/src/api/__tests__/wallet.test.ts +73 -0
  10. package/src/api/base.ts +52 -0
  11. package/src/api/index.ts +24 -0
  12. package/src/api/network-data.ts +572 -0
  13. package/src/api/network.ts +81 -0
  14. package/src/api/token.ts +182 -0
  15. package/src/api/transaction.ts +59 -0
  16. package/src/api/types/common.ts +35 -0
  17. package/src/api/types/index.ts +13 -0
  18. package/src/api/types/type.ts +283 -0
  19. package/src/api/user.ts +83 -0
  20. package/src/api/utils/index.ts +34 -0
  21. package/src/api/utils/signature.ts +60 -0
  22. package/src/api/wallet.ts +57 -0
  23. package/src/base/network.ts +55 -0
  24. package/src/base/service.ts +33 -0
  25. package/src/base/token.ts +43 -0
  26. package/src/base/transaction.ts +58 -0
  27. package/src/config.ts +21 -0
  28. package/src/dogecoin/base.ts +39 -0
  29. package/src/dogecoin/config.ts +43 -0
  30. package/src/dogecoin/rpc.ts +449 -0
  31. package/src/dogecoin/service.ts +451 -0
  32. package/src/dogecoin/type.ts +29 -0
  33. package/src/dogecoin/utils-doge.ts +105 -0
  34. package/src/dogecoin/utils.ts +601 -0
  35. package/src/evm/rpc.ts +68 -0
  36. package/src/evm/service.ts +403 -0
  37. package/src/evm/utils.ts +92 -0
  38. package/src/index.ts +28 -0
  39. package/src/solana/config.ts +5 -0
  40. package/src/solana/service.ts +312 -0
  41. package/src/solana/types.ts +91 -0
  42. package/src/solana/utils.ts +635 -0
  43. package/src/types/account.ts +58 -0
  44. package/src/types/dapp.ts +7 -0
  45. package/src/types/gas.ts +53 -0
  46. package/src/types/index.ts +81 -0
  47. package/src/types/network.ts +66 -0
  48. package/src/types/tx.ts +181 -0
  49. package/src/types/wallet.ts +49 -0
  50. package/src/wallet.ts +96 -0
  51. package/tsconfig.json +14 -0
  52. package/tsup.config.ts +18 -0
@@ -0,0 +1,312 @@
1
+ import { ChainTypes, SupportedChainTypes, TomoApiDomains } from "@tomo-inc/wallet-utils";
2
+
3
+ import { BaseService } from "../base/service";
4
+
5
+ import { BaseConfig, MSG_PREFIX } from "./config";
6
+ import { createLegacyTx, createTokenLegacyTransaction2, txToHex } from "./utils";
7
+
8
+ import { IaccountInfo, QueryGasParams, QueryGasResponse, TomoAppInfo, TransactionParams } from "../types";
9
+ import { QueryRentParams, SolanaRentInfo } from "./types";
10
+
11
+ import { getAssociatedTokenAddress } from "@solana/spl-token";
12
+ import { Connection, PublicKey, VersionedTransaction, sendAndConfirmRawTransaction } from "@solana/web3.js";
13
+ import { BigNumber } from "bignumber.js";
14
+ import { formatUnits, parseUnits, toHex } from "viem";
15
+ import * as base from "../dogecoin/base";
16
+
17
+ export class SolanaService extends BaseService {
18
+ private static instance: SolanaService;
19
+ public chainType: ChainTypes | "";
20
+ public rpcUrl: string;
21
+ private connection: Connection;
22
+
23
+ public constructor(chainType: ChainTypes, accountInfo: IaccountInfo, tomoAppInfo: TomoAppInfo) {
24
+ super(tomoAppInfo, accountInfo);
25
+ this.chainType = chainType;
26
+ const config: any = {
27
+ commitment: "confirmed",
28
+ disableRetryOnRateLimit: false,
29
+ confirmTransactionInitialTimeout: 120000,
30
+ fetch: (url: string, options: any) => {
31
+ return fetch(url, { ...options });
32
+ },
33
+ };
34
+ const domain = TomoApiDomains[tomoAppInfo.tomoStage];
35
+ const rpcUrl = `${domain}/rpc/v1/solana`;
36
+
37
+ this.rpcUrl = rpcUrl;
38
+ this.connection = new Connection(rpcUrl, config);
39
+ }
40
+
41
+ public static getInstance(chainType: ChainTypes, accountInfo: any, tomoAppInfo: TomoAppInfo) {
42
+ if (!SolanaService.instance) {
43
+ SolanaService.instance = new SolanaService(chainType, accountInfo, tomoAppInfo);
44
+ }
45
+ return SolanaService.instance;
46
+ }
47
+
48
+ public async getCurrentWalletAccount() {
49
+ return await this.accountInfo.getCurrent();
50
+ }
51
+
52
+ public async getAccount(): Promise<{ publicKey: string; address: string }> {
53
+ const { currentAddress, publicKey }: any = await this.accountInfo.getIds();
54
+ return { publicKey: publicKey || currentAddress, address: currentAddress };
55
+ }
56
+
57
+ public async signMessage(params: any): Promise<{ message: string; signature: string; publicKey: string }> {
58
+ const { currentAddress, publicKey }: any = await this.accountInfo.getIds();
59
+
60
+ const { message } = params;
61
+ const signature = await this.accountInfo.signMessage(message);
62
+
63
+ return {
64
+ message: MSG_PREFIX + message,
65
+ signature,
66
+ publicKey: publicKey || currentAddress,
67
+ };
68
+ }
69
+
70
+ public async signIn(
71
+ params: any,
72
+ ): Promise<{ address: string; publicKey: string; signature: string; signedMessage: string }> {
73
+ const { currentAddress, publicKey }: any = await this.accountInfo.getIds();
74
+
75
+ const { signature = "" } = await this.signMessage(params);
76
+
77
+ return {
78
+ address: currentAddress,
79
+ publicKey: publicKey || currentAddress,
80
+ signature,
81
+ signedMessage: MSG_PREFIX + params.message,
82
+ };
83
+ }
84
+
85
+ public async request({ method, params }: { method: string; params: any }): Promise<any> {
86
+ if (!this[method as keyof SolanaService]) {
87
+ throw new Error(`${method} in request is not supported`);
88
+ }
89
+ return this[method as keyof SolanaService](params);
90
+ }
91
+
92
+ public async _queryGasInfo(txData: TransactionParams): Promise<QueryGasResponse> {
93
+ const { tokenAddress, chainId, amount = 0, decimals = 9 } = txData;
94
+ const value = tokenAddress ? "0x1" : toHex(parseUnits(amount.toString(), decimals));
95
+ let callData = "0x";
96
+
97
+ if (tokenAddress) {
98
+ const transaction = await createTokenLegacyTransaction2(txData as any, this.connection);
99
+ callData = "0x" + txToHex(transaction);
100
+ } else {
101
+ const transaction = await createLegacyTx(txData as any, this.connection);
102
+ callData = "0x" + txToHex(transaction);
103
+ }
104
+
105
+ const gasLimitParam: any = {
106
+ from: txData.from,
107
+ to: txData.to,
108
+ value,
109
+ };
110
+ const queryGasParams: QueryGasParams = {
111
+ chainIndex: Number(chainId),
112
+ callData,
113
+ gasLimitParam,
114
+ addressList: [txData.from],
115
+ };
116
+ const {
117
+ data: gasInfo,
118
+ success,
119
+ message,
120
+ }: any = await this.transactions.queryGasInfo({
121
+ chainType: this.chainType,
122
+ params: queryGasParams,
123
+ });
124
+ if (!success) {
125
+ console.error("queryGasInfo solana", txData, message, gasInfo);
126
+
127
+ const { gasFee } = BaseConfig;
128
+ return {
129
+ success: true,
130
+ gasFee: gasFee.toString(),
131
+ };
132
+ }
133
+
134
+ const nativeChainId = SupportedChainTypes[this.chainType as ChainTypes].chainId as string;
135
+ const { nativeCurrency } = await this.networks.getNetworkByChainId(nativeChainId);
136
+
137
+ const { baseFee = 1, gasLimit = 0 } = gasInfo;
138
+
139
+ const baseFeeBN = new BigNumber(baseFee).times(2);
140
+ const gasLimitBN = new BigNumber(gasLimit);
141
+ const calcFee = (feeLevel: string) => {
142
+ const priorityFee = gasLimitBN
143
+ .times(gasInfo[feeLevel] || 0)
144
+ .dividedBy(1000000)
145
+ .integerValue(BigNumber.ROUND_DOWN); // only integer
146
+
147
+ const gasFee = baseFeeBN.plus(priorityFee);
148
+ return formatUnits(BigInt(gasFee.toNumber()), nativeCurrency?.decimals || 9);
149
+ };
150
+
151
+ return {
152
+ success: true,
153
+ fees: {
154
+ low: calcFee("priorityFeeLow"),
155
+ medium: calcFee("priorityFeeMedium"),
156
+ high: calcFee("priorityFeeHigh"),
157
+ },
158
+ gasFee: calcFee("priorityFeeHigh"),
159
+ };
160
+ }
161
+
162
+ public async sendSignedTx(signedTx: string): Promise<string> {
163
+ let rawTx: Buffer;
164
+ try {
165
+ const hexBuffer = Buffer.from(signedTx, "hex");
166
+ VersionedTransaction.deserialize(hexBuffer);
167
+ rawTx = hexBuffer;
168
+ } catch (error) {
169
+ const base58Buffer = Buffer.from(base.toBase58(signedTx as any));
170
+ VersionedTransaction.deserialize(base58Buffer);
171
+ rawTx = base58Buffer;
172
+ }
173
+
174
+ const transaction = VersionedTransaction.deserialize(rawTx);
175
+ if (!transaction.signatures || transaction.signatures.length === 0) {
176
+ throw new Error("lack of sign");
177
+ }
178
+
179
+ const confirmationStrategy: any = {
180
+ commitment: "confirmed",
181
+ preflightCommitment: "processed",
182
+ skipPreflight: false,
183
+ maxRetries: 5,
184
+ };
185
+ try {
186
+ // First simulate the transaction
187
+ const simulation = await this.connection.simulateTransaction(transaction);
188
+
189
+ if (simulation.value.err) {
190
+ const logs = simulation.value.logs || [];
191
+ const message = logs[logs.length - 1] || simulation.value.err || "Unknown error occurred";
192
+ console.error("send err logs:", logs, message);
193
+
194
+ // Generic simulation error
195
+ throw new Error(`Transaction failed: ${message}`);
196
+ }
197
+
198
+ const signature = await sendAndConfirmRawTransaction(this.connection, rawTx, confirmationStrategy);
199
+ return signature;
200
+ } catch (error: any) {
201
+ if (error.message.includes("TransactionExpiredTimeoutError")) {
202
+ const status = await this.connection.getSignatureStatus(transaction.signatures[0].toString());
203
+ if (status.value?.confirmationStatus === "confirmed") {
204
+ return transaction.signatures[0].toString();
205
+ }
206
+ }
207
+ throw error;
208
+ }
209
+ }
210
+
211
+ public async signTransaction({ rawTransaction }: { rawTransaction: string }): Promise<string> {
212
+ const params = {
213
+ rawTransaction,
214
+ walletId: -1,
215
+ rpcUrl: this.rpcUrl,
216
+ };
217
+ const signature = await this.accountInfo.signTransaction(JSON.stringify(params));
218
+
219
+ return signature;
220
+ }
221
+
222
+ public async signAndSendTransaction({ rawTransaction }: { rawTransaction: string }): Promise<string> {
223
+ const signedTx = await this.signTransaction({ rawTransaction });
224
+ if (!signedTx) {
225
+ return "";
226
+ }
227
+
228
+ const signature = await this.sendSignedTx(signedTx);
229
+ return signature;
230
+ }
231
+
232
+ public async signAllTransactions({ rawTransactions }: { rawTransactions: string[] }) {
233
+ throw new Error("no support");
234
+ }
235
+
236
+ public async signAndSendAllTransactions({ rawTransactions }: { rawTransactions: string[] }) {
237
+ throw new Error("no support");
238
+ }
239
+
240
+ public async queryRent(params: QueryRentParams): Promise<SolanaRentInfo> {
241
+ // Predefined constants for Solana rent calculations
242
+ const MIN_ACCOUNT_ACTIVATION_SOL = "0.0009"; // Minimum SOL required to activate a new account
243
+ const SPL_TOKEN_ACCOUNT_CREATION_FEE = "0.001"; // Fee to create SPL token account
244
+
245
+ try {
246
+ const { toAddress, tokenAddress } = params;
247
+
248
+ let minTransferAmount = "0";
249
+ let createSplTokenFee: string | null = null;
250
+
251
+ if (tokenAddress) {
252
+ // SPL Token transfer
253
+ // Check if recipient has token account
254
+ const tokenAccountExists = await this.checkTokenAccount(toAddress, tokenAddress);
255
+
256
+ if (!tokenAccountExists) {
257
+ createSplTokenFee = SPL_TOKEN_ACCOUNT_CREATION_FEE; // SPL Token account creation fee
258
+ }
259
+ } else {
260
+ // SOL transfer
261
+ // Check if recipient account exists
262
+ const accountExists = await this.checkAccountExists(toAddress);
263
+
264
+ if (!accountExists) {
265
+ // If recipient account doesn't exist, minimum SOL required to activate account
266
+ minTransferAmount = MIN_ACCOUNT_ACTIVATION_SOL;
267
+ }
268
+ }
269
+
270
+ const rentInfo: SolanaRentInfo = {
271
+ minTransferAmount,
272
+ createSplTokenFee,
273
+ };
274
+
275
+ return rentInfo;
276
+ } catch (error: any) {
277
+ console.error("queryRent error:", error);
278
+
279
+ // Return default values to ensure transaction is not blocked
280
+ const defaultRentInfo: SolanaRentInfo = {
281
+ minTransferAmount: "0",
282
+ createSplTokenFee: null,
283
+ };
284
+
285
+ return defaultRentInfo;
286
+ }
287
+ }
288
+
289
+ private async checkAccountExists(address: string): Promise<boolean> {
290
+ try {
291
+ const accountInfo = await this.connection.getAccountInfo(new PublicKey(address));
292
+ return accountInfo !== null;
293
+ } catch (error) {
294
+ console.warn("Failed to check account exists:", error);
295
+ return false; // Conservative estimate, assume account doesn't exist
296
+ }
297
+ }
298
+
299
+ private async checkTokenAccount(ownerAddress: string, tokenMint: string): Promise<boolean> {
300
+ try {
301
+ const owner = new PublicKey(ownerAddress);
302
+ const mint = new PublicKey(tokenMint);
303
+
304
+ const associatedTokenAddress = await getAssociatedTokenAddress(mint, owner);
305
+ const accountInfo = await this.connection.getAccountInfo(associatedTokenAddress);
306
+ return accountInfo !== null;
307
+ } catch (error) {
308
+ console.warn("Failed to check token account:", error);
309
+ return false; // Conservative estimate, assume account doesn't exist
310
+ }
311
+ }
312
+ }
@@ -0,0 +1,91 @@
1
+ import { Cluster } from "@solana/web3.js";
2
+ import { SubmitParamsType } from "../types";
3
+
4
+ // solana standard data define.
5
+
6
+ export type PhantomAPIs = {
7
+ // connect
8
+ connect: ({ onlyIfTrusted }: any) => Promise<object>;
9
+ disconnect: () => Promise<boolean>;
10
+ getAccount: () => Promise<object>;
11
+ getBalance: () => Promise<object>;
12
+ on: (type: "accountChanged" | "balanceChanged", callback: (data: any) => void) => void;
13
+ signedMessage: ({ message }: any) => Promise<string>;
14
+ };
15
+
16
+ export type SignInInput = {
17
+ domain?: string;
18
+ statement?: string;
19
+ version?: string;
20
+ nonce?: string;
21
+ network?: Cluster;
22
+ issuedAt?: string;
23
+ resources?: `https://${string}`[];
24
+ };
25
+
26
+ export interface SolanaChainInfo {
27
+ id: Cluster;
28
+ name: string;
29
+ rpcUrl: string;
30
+ explorer: string;
31
+ symbol: string;
32
+ decimals: number;
33
+ }
34
+
35
+ export interface IntentTxData {
36
+ from?: string;
37
+ to?: string;
38
+ amount?: number;
39
+ tokenAddress?: string;
40
+ decimals?: number;
41
+ priorityFee?: number;
42
+ network?: Cluster;
43
+ type?: TransactionType;
44
+ submitType?: SubmitParamsType;
45
+ chainId?: string;
46
+ }
47
+
48
+ export type TransactionType = "transfer" | "tokenTransfer" | "mplTransfer";
49
+ export type SolSignParam = {
50
+ type: TransactionType;
51
+ payer: string;
52
+ blockHash: string;
53
+ from: string;
54
+ to: string;
55
+ amount?: number | string;
56
+ mint?: string;
57
+ createAssociatedAddress?: boolean;
58
+ version?: number;
59
+ tokenStandard?: number;
60
+ token2022?: boolean;
61
+ decimal?: number;
62
+ computeUnitLimit?: number;
63
+ computeUnitPrice?: number;
64
+ needPriorityFee?: boolean;
65
+ };
66
+
67
+ export type TokenInfo = {
68
+ address: string;
69
+ icon: string; //`htpps://${string}`;
70
+ name: string;
71
+ symbol: string;
72
+ decimals?: number;
73
+ };
74
+
75
+ export interface SolanaRentInfo {
76
+ minTransferAmount: string;
77
+ createSplTokenFee: string | null;
78
+ }
79
+
80
+ export interface QueryRentParams {
81
+ toAddress: string;
82
+ tokenAddress?: string;
83
+ }
84
+
85
+ export interface QueryRentResult {
86
+ success: boolean;
87
+ method: string;
88
+ data: {
89
+ result: SolanaRentInfo;
90
+ };
91
+ }