@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.
- package/CHANGELOG.md +3 -0
- package/README.md +15 -0
- package/package.json +38 -0
- package/project.json +59 -0
- package/src/api/__tests__/config.ts +21 -0
- package/src/api/__tests__/token.test.ts +120 -0
- package/src/api/__tests__/transaction.test.ts +86 -0
- package/src/api/__tests__/user.test.ts +105 -0
- package/src/api/__tests__/wallet.test.ts +73 -0
- package/src/api/base.ts +52 -0
- package/src/api/index.ts +24 -0
- package/src/api/network-data.ts +572 -0
- package/src/api/network.ts +81 -0
- package/src/api/token.ts +182 -0
- package/src/api/transaction.ts +59 -0
- package/src/api/types/common.ts +35 -0
- package/src/api/types/index.ts +13 -0
- package/src/api/types/type.ts +283 -0
- package/src/api/user.ts +83 -0
- package/src/api/utils/index.ts +34 -0
- package/src/api/utils/signature.ts +60 -0
- package/src/api/wallet.ts +57 -0
- package/src/base/network.ts +55 -0
- package/src/base/service.ts +33 -0
- package/src/base/token.ts +43 -0
- package/src/base/transaction.ts +58 -0
- package/src/config.ts +21 -0
- package/src/dogecoin/base.ts +39 -0
- package/src/dogecoin/config.ts +43 -0
- package/src/dogecoin/rpc.ts +449 -0
- package/src/dogecoin/service.ts +451 -0
- package/src/dogecoin/type.ts +29 -0
- package/src/dogecoin/utils-doge.ts +105 -0
- package/src/dogecoin/utils.ts +601 -0
- package/src/evm/rpc.ts +68 -0
- package/src/evm/service.ts +403 -0
- package/src/evm/utils.ts +92 -0
- package/src/index.ts +28 -0
- package/src/solana/config.ts +5 -0
- package/src/solana/service.ts +312 -0
- package/src/solana/types.ts +91 -0
- package/src/solana/utils.ts +635 -0
- package/src/types/account.ts +58 -0
- package/src/types/dapp.ts +7 -0
- package/src/types/gas.ts +53 -0
- package/src/types/index.ts +81 -0
- package/src/types/network.ts +66 -0
- package/src/types/tx.ts +181 -0
- package/src/types/wallet.ts +49 -0
- package/src/wallet.ts +96 -0
- package/tsconfig.json +14 -0
- 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
|
+
}
|