@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,449 @@
1
+ import axios from "axios";
2
+
3
+ import { TRANSACTION_PAGE_SIZE, BLOCK_CONFIRMATIONS, FEE_RATE_KB } from "./config";
4
+ import { RPC_URL, MYDOGE_BASE_URL, RPC_TIMEOUT, TX_SIZE } from "./config";
5
+ import { TransactionParser, toSatoshi, toBitcoin } from "./utils";
6
+ import { cache } from "@tomo-inc/wallet-utils";
7
+ import { DogeSpendableUtxos, DunesTransactionParams } from "./type";
8
+
9
+ export const mydoge = axios.create({
10
+ baseURL: MYDOGE_BASE_URL,
11
+ });
12
+
13
+ const api = axios.create({
14
+ baseURL: RPC_URL,
15
+ timeout: RPC_TIMEOUT,
16
+ });
17
+
18
+ export async function getBalance(address?: string) {
19
+ if (!address) {
20
+ return {
21
+ address: "",
22
+ balance: 0,
23
+ };
24
+ }
25
+ const path = `/address/${address}?page=1&pageSize=10`;
26
+ const api = `${MYDOGE_BASE_URL}/wallet/info?route=` + encodeURIComponent(path);
27
+
28
+ const res: any = await mydoge.get(api);
29
+ return res.data || {};
30
+ }
31
+
32
+ export type Drc20Transaction = {
33
+ blockHash: string;
34
+ blockHeight: number;
35
+ blockTime: number;
36
+ confirmations: number;
37
+ fees: string;
38
+ hex: string;
39
+ size: number;
40
+ txid: string;
41
+ value: string;
42
+ valueIn: string;
43
+ version: number;
44
+ vin: {
45
+ addresses: string[];
46
+ hex: string;
47
+ isAddress: boolean;
48
+ n: number;
49
+ sequence: number;
50
+ txid: string;
51
+ value: string;
52
+ }[];
53
+ vout: {
54
+ addresses: string[];
55
+ value: string;
56
+ n: number;
57
+ hex: string;
58
+ isAddress: boolean;
59
+ }[];
60
+ tick?: string;
61
+ tickAmount?: number;
62
+ drc20Detail?: Drc20Detail;
63
+ };
64
+ export async function getTxDetail(txId: string): Promise<Drc20Transaction> {
65
+ if (!txId) {
66
+ return {} as Drc20Transaction;
67
+ }
68
+ const path = `/tx/${txId}`;
69
+ const api = `${MYDOGE_BASE_URL}/wallet/info?route=` + encodeURIComponent(path);
70
+
71
+ const res = await mydoge.get(api);
72
+
73
+ return res.data || {};
74
+ }
75
+
76
+ export async function getAccountDrc20List(address: string, config?: any) {
77
+ const api = `/drc20/${address}`;
78
+ const res: any = await mydoge.get(api);
79
+ return res.data || {};
80
+ }
81
+
82
+ const Drc20DetailsCacheKey = "Drc20Details";
83
+ const Drc20DetailsCache = cache.get(Drc20DetailsCacheKey) || {};
84
+ function isCacheAvailable(time: number) {
85
+ const cacheTime = 20 * 60 * 1000; // 30 minutes
86
+ return Date.now() - time <= cacheTime;
87
+ }
88
+ export type Drc20Detail = {
89
+ changePercent: string;
90
+ currentSupply: number;
91
+ floorPrice: number;
92
+ holders: number;
93
+ lastPrice: number;
94
+ maxSupply: number;
95
+ pic: string;
96
+ sales: number;
97
+ tick: string;
98
+ twentyFourHourVolume: string;
99
+ volume: string;
100
+ };
101
+ export async function getDrc20Detail(ticker: string): Promise<Drc20Detail> {
102
+ const drc20Detail = Drc20DetailsCache?.[ticker];
103
+ if (drc20Detail?.cacheTime && isCacheAvailable(drc20Detail?.cacheTime)) {
104
+ return drc20Detail;
105
+ }
106
+
107
+ const api = `/drc20/data/${ticker}`;
108
+ const res: any = await mydoge.get(api);
109
+ const detail = res.data || {};
110
+ if (detail.pic) {
111
+ Drc20DetailsCache[ticker] = { ...detail, cacheTime: Date.now() };
112
+ cache.set(Drc20DetailsCacheKey, Drc20DetailsCache, false);
113
+ }
114
+ return detail;
115
+ }
116
+
117
+ export async function getDRC20Inscriptions(address: string, ticker: string) {
118
+ const api = `${MYDOGE_BASE_URL}/inscriptions/${address}?filter=drc20&ticker=${encodeURIComponent(ticker)}`;
119
+ const query = (await mydoge.get(api)).data;
120
+ return query;
121
+ }
122
+
123
+ export async function getDRC20Balance(address: string, ticker: string) {
124
+ const api = `${MYDOGE_BASE_URL}/drc20/${address}?ticker=${ticker}`;
125
+ const res = (await mydoge.get(api))?.data || {};
126
+ return res;
127
+ }
128
+
129
+ async function getUtxos(
130
+ address: string,
131
+ cursor: number,
132
+ result: any,
133
+ filter: string,
134
+ tx: { txid: string; vout: number } | null = null,
135
+ ): Promise<DogeSpendableUtxos[] | undefined> {
136
+ const query = (
137
+ await mydoge.get(`${MYDOGE_BASE_URL}/utxos/${address}?filter=${filter}${cursor ? `&cursor=${cursor}` : ""}`)
138
+ ).data;
139
+
140
+ let { utxos } = query;
141
+
142
+ if (tx) {
143
+ utxos = utxos.filter((utxo: any) => utxo.txid === tx?.txid && utxo.vout === tx?.vout);
144
+ }
145
+
146
+ result.push(
147
+ ...utxos.map((i: any) => ({
148
+ txid: i.txid,
149
+ vout: i.vout,
150
+ outputValue: i.satoshis,
151
+ script: i.script_pubkey,
152
+ ...(filter === "inscriptions" && { inscriptions: i.inscriptions }),
153
+ })),
154
+ );
155
+
156
+ if (result.length && tx) {
157
+ return;
158
+ }
159
+
160
+ result = result.sort((a: any, b: any) => toBitcoin(b.outputValue) - toBitcoin(a.outputValue));
161
+
162
+ if (query.next_cursor) {
163
+ return getUtxos(address, query.next_cursor, result, filter, tx);
164
+ }
165
+ }
166
+
167
+ export async function getInscriptionsUtxos(address: string) {
168
+ const inscriptions: any[] = [];
169
+ await getUtxos(address, 0, inscriptions, "inscriptions");
170
+
171
+ return inscriptions;
172
+ }
173
+
174
+ export async function getSpendableUtxos(address: string) {
175
+ const utxos: any[] = [];
176
+ await getUtxos(address, 0, utxos, "spendable");
177
+
178
+ return utxos;
179
+ }
180
+
181
+ export async function getUnSpentUtxos(address: string) {
182
+ try {
183
+ const result = await api.get(`/address/${address}/?unspent=true&limit=${0}`);
184
+ const unSpentUtxo = result?.data;
185
+ if (unSpentUtxo?.length) return unSpentUtxo.filter((utxo: any) => !utxo.spentTxid);
186
+ return [];
187
+ } catch (e) {
188
+ return [];
189
+ }
190
+ }
191
+
192
+ export async function getInscriptionsUtxo(address: string, tx: any) {
193
+ const inscriptions: any[] = [];
194
+ await getUtxos(address, 0, inscriptions, "inscriptions", tx);
195
+
196
+ return inscriptions[0];
197
+ }
198
+
199
+ export async function sendTransaction({ signed, senderAddress }: any) {
200
+ // const signed = signRawTx(
201
+ // data.rawTx,
202
+ // decryptedWallet.children[selectedAddressIndex],
203
+ // );
204
+
205
+ const jsonrpcReq = {
206
+ jsonrpc: "2.0",
207
+ id: `${senderAddress}_send_${Date.now()}`,
208
+ method: "sendrawtransaction",
209
+ params: [signed],
210
+ };
211
+ const jsonrpcRes = (await mydoge.post("/wallet/rpc", jsonrpcReq)).data;
212
+ return jsonrpcRes;
213
+ }
214
+
215
+ //nft
216
+ export async function getNFTs(address?: string): Promise<{ list: any[]; total: number }> {
217
+ if (!address) {
218
+ throw new Error("address is required");
219
+ }
220
+
221
+ const res: any = await mydoge.get(`/inscriptions/${address}`);
222
+ const { list = [], total = 0 } = res?.data || {};
223
+ return { list, total };
224
+ }
225
+
226
+ export async function createNFTTransaction(params: {
227
+ senderAddress: string;
228
+ recipientAddress: string;
229
+ location: string;
230
+ inscriptionId: string;
231
+ }) {
232
+ const { senderAddress, recipientAddress, location, inscriptionId } = params;
233
+ try {
234
+ const res: any = await mydoge.post("/tx/prepare/inscription", {
235
+ sender: senderAddress,
236
+ recipient: recipientAddress,
237
+ location: location,
238
+ inscriptionId: inscriptionId,
239
+ });
240
+ const { rawTx, fee, amount } = res?.data || {};
241
+
242
+ return {
243
+ rawTx,
244
+ fee,
245
+ amount,
246
+ };
247
+ } catch (err) {
248
+ console.error("createNFTTransaction", err);
249
+ return {};
250
+ }
251
+ }
252
+
253
+ //dunes
254
+ export async function getDunesBalance(address: string, ticker?: string) {
255
+ const api = `/dunes/${address}${ticker ? `?ticker=${ticker}` : ""}`;
256
+ const res = (await mydoge.get(api))?.data || {};
257
+ return res;
258
+ }
259
+
260
+ const DuneDetailsCacheKey = "DuneDetails";
261
+ const DuneDetailsCache = cache.get(DuneDetailsCacheKey) || {};
262
+ export async function getDuneDetail(ticker: string): Promise<Drc20Detail> {
263
+ const duneDetail = DuneDetailsCache?.[ticker];
264
+ if (duneDetail?.cacheTime && isCacheAvailable(duneDetail?.cacheTime)) {
265
+ return duneDetail;
266
+ }
267
+ try {
268
+ const api = `/dunes/data/${ticker}`;
269
+ const res: any = await mydoge.get(api);
270
+ const detail = res.data || {};
271
+ if (detail.pic) {
272
+ DuneDetailsCache[ticker] = { ...detail, cacheTime: Date.now() };
273
+ cache.set(DuneDetailsCacheKey, DuneDetailsCache, false);
274
+ }
275
+ return detail;
276
+ } catch (err) {
277
+ throw new Error("dune detail not found");
278
+ }
279
+ }
280
+
281
+ export async function createDunesTransaction(params: DunesTransactionParams) {
282
+ try {
283
+ const { senderAddress = "", recipientAddress = "", amount = 0, ticker = "" } = params;
284
+ const { balances = [] } = await getDunesBalance(senderAddress, ticker);
285
+ const { duneId } = balances[0] || {};
286
+ if (!duneId) {
287
+ throw new Error("duneId not found");
288
+ }
289
+ const response = await mydoge.post("/tx/prepare/dune", {
290
+ sender: senderAddress,
291
+ recipient: recipientAddress,
292
+ amount,
293
+ duneId,
294
+ });
295
+
296
+ const { rawTx, fee, inputs } = response.data;
297
+
298
+ return {
299
+ rawTx,
300
+ fee,
301
+ inputs, // usingUtxos
302
+ };
303
+ } catch (err) {
304
+ console.error("createDunesTransaction", err);
305
+ return {};
306
+ }
307
+ }
308
+
309
+ export async function estimateSmartFee({ senderAddress }: { senderAddress: string }) {
310
+ const smartfeeReq = {
311
+ jsonrpc: "2.0",
312
+ id: `${senderAddress}_estimatesmartfee_${Date.now()}`,
313
+ method: "estimatesmartfee",
314
+ params: [BLOCK_CONFIRMATIONS], // confirm within x blocks
315
+ };
316
+ const feeData = (await mydoge.post("/wallet/rpc", smartfeeReq)).data;
317
+ const feeRate = feeData?.result?.feerate || FEE_RATE_KB;
318
+ const feePerKB = toSatoshi(feeRate * 2);
319
+
320
+ return { feePerKB };
321
+ }
322
+
323
+ // Build a raw transaction and determine fee
324
+ export async function onCreateTransaction({ data, sendResponse }: { data: any; sendResponse: any }) {
325
+ const amountSatoshi = toSatoshi(data.dogeAmount);
326
+ const amount = toBitcoin(amountSatoshi);
327
+
328
+ try {
329
+ const response = await mydoge.post("/v3/tx/prepare", {
330
+ sender: data.senderAddress,
331
+ recipient: data.recipientAddress,
332
+ amount,
333
+ });
334
+ const { rawTx, fee, amount: resultAmount } = response.data;
335
+ let amountMismatch = false;
336
+
337
+ if (resultAmount < amount - fee) {
338
+ amountMismatch = true;
339
+ }
340
+
341
+ sendResponse?.({
342
+ rawTx,
343
+ fee,
344
+ amount: resultAmount,
345
+ amountMismatch,
346
+ });
347
+ } catch (err) {
348
+ sendResponse?.(false);
349
+ }
350
+ }
351
+
352
+ export type Drc20TransactionResponse = {
353
+ transactions: Drc20Transaction[];
354
+ txIds: string[];
355
+ totalPages: number;
356
+ page: number;
357
+ };
358
+
359
+ export async function getTransactions(
360
+ address: string,
361
+ config?: {
362
+ pageSize: number;
363
+ pageNumber: number;
364
+ },
365
+ ): Promise<Drc20TransactionResponse> {
366
+ const { pageSize = 10, pageNumber = 1 } = config || {};
367
+ const size = Math.min(pageSize, TRANSACTION_PAGE_SIZE);
368
+ // Get txids
369
+ let txIds = [];
370
+ let totalPages;
371
+ let page;
372
+
373
+ try {
374
+ const response = (
375
+ await mydoge.get("/wallet/info", {
376
+ params: {
377
+ route: `/address/${address}?page=${pageNumber}&pageSize=${size}`,
378
+ },
379
+ })
380
+ ).data;
381
+
382
+ txIds = response.txids;
383
+ totalPages = response.totalPages;
384
+ page = response.page;
385
+
386
+ const res = await Promise.all(
387
+ txIds?.map(async (txId: string) => {
388
+ const detail = await getTxDetail(txId);
389
+ const tx = new TransactionParser(detail.hex);
390
+ const parsedData: any = tx.parseScript();
391
+ return {
392
+ ...detail,
393
+ tick: parsedData?.opReturnData?.tick,
394
+ tickAmount: parsedData?.opReturnData?.amt,
395
+ };
396
+ }) || [],
397
+ );
398
+
399
+ const filterTickList = [...new Set(res.map((item) => item.tick).filter((i) => i))];
400
+ const tickList = [];
401
+ for (const item of filterTickList) {
402
+ try {
403
+ const detail = await getDrc20Detail(item);
404
+ tickList.push(detail);
405
+ } catch (err) {
406
+ console.error(`Failed to fetch detail for ${item}`, err);
407
+ tickList.push({ tick: item });
408
+ }
409
+ }
410
+ // const tickList = await throttledMap(
411
+ // filterTickList,
412
+ // async (item) => {
413
+ // try {
414
+ // return await getDrc20Detail(item);
415
+ // } catch (err) {
416
+ // console.error(`Failed to fetch detail for ${item}`, err);
417
+ // return { tick: item };
418
+ // }
419
+ // },
420
+ // 1,
421
+ // );
422
+ const tickMap = tickList.reduce((acc, cur) => {
423
+ acc[cur.tick] = cur;
424
+ return acc;
425
+ }, {} as any);
426
+ res.forEach((item) => {
427
+ item.drc20Detail = tickMap[item.tick];
428
+ });
429
+
430
+ return { transactions: res || [], txIds, totalPages, page };
431
+ } catch (err) {
432
+ throw new Error("getTransactions failed");
433
+ }
434
+ }
435
+
436
+ export const getDogeFeeByBlock = async (address?: string, n = 22) => {
437
+ const res = await api.get(`/fee/${n}`);
438
+ return (res.data?.feerate || 0.01) * TX_SIZE;
439
+ };
440
+
441
+ export const sendDogeTx = async (rawTx: string) => {
442
+ try {
443
+ const res = await api.post("/tx/send", { rawTx });
444
+ return res.data;
445
+ } catch (e: any) {
446
+ if (typeof e?.response?.data === "string") return Promise.reject(e?.response?.data);
447
+ else return Promise.reject(e.message);
448
+ }
449
+ };