@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,601 @@
|
|
|
1
|
+
import { cache } from "@tomo-inc/wallet-utils";
|
|
2
|
+
import { BigNumber } from "bignumber.js";
|
|
3
|
+
import { DogecoinUtils } from "./utils-doge";
|
|
4
|
+
|
|
5
|
+
import * as API from "./rpc";
|
|
6
|
+
|
|
7
|
+
import { ChainTypes, SupportedChainTypes } from "@tomo-inc/wallet-utils";
|
|
8
|
+
import { DECIMALS, RPC_URL, network } from "./config";
|
|
9
|
+
import { DogeSpendableUtxos } from "./type";
|
|
10
|
+
|
|
11
|
+
import * as base from "./base";
|
|
12
|
+
|
|
13
|
+
import { Psbt } from "bitcoinjs-lib";
|
|
14
|
+
|
|
15
|
+
const KOINU_PER_DOGE = new BigNumber(DECIMALS); // 10^8
|
|
16
|
+
|
|
17
|
+
export async function waitOnly(ms: number) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
setTimeout(() => {
|
|
20
|
+
resolve(ms);
|
|
21
|
+
}, ms);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function toSatoshi(bitcion: number | string): number {
|
|
26
|
+
try {
|
|
27
|
+
const amount = new BigNumber(bitcion);
|
|
28
|
+
if (amount.isNaN() || amount.isNegative()) {
|
|
29
|
+
throw new Error("Invalid amount");
|
|
30
|
+
}
|
|
31
|
+
return amount.times(KOINU_PER_DOGE).integerValue(BigNumber.ROUND_DOWN).toNumber();
|
|
32
|
+
} catch (error: any) {
|
|
33
|
+
throw new Error(`toSatoshi failed: ${error.message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function toBitcoin(satoshis: number | string): number {
|
|
38
|
+
try {
|
|
39
|
+
const amount = new BigNumber(satoshis);
|
|
40
|
+
if (amount.isNaN() || amount.isNegative()) {
|
|
41
|
+
throw new Error("Invalid Koinu amount");
|
|
42
|
+
}
|
|
43
|
+
return amount.dividedBy(KOINU_PER_DOGE).toNumber();
|
|
44
|
+
} catch (error: any) {
|
|
45
|
+
throw new Error(`toBitcoin failed: ${error.message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const isDogeCoin = (token: any = {}, network: any = {}) => {
|
|
50
|
+
return token?.address === "" && (network?.chainId === "3" || network?.chainId === "221122420");
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const isDogeOSDevnet = (network: any = {}) => {
|
|
54
|
+
return network?.chainId === "221122420";
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const isDogeMainnet = (network: any = {}) => {
|
|
58
|
+
return network?.chainId === "3";
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const isHasDoge = (balances: any) => {
|
|
62
|
+
return !!balances?.[300];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const isDogeChain = (network: any = {}) => {
|
|
66
|
+
const { chainType, chainIndex } = network;
|
|
67
|
+
return chainType === ChainTypes.DOGE && chainIndex === SupportedChainTypes[ChainTypes.DOGE].chainIndex;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export function hexToBase64(hex: string): string {
|
|
71
|
+
try {
|
|
72
|
+
return base?.toBase64(base?.fromHex(hex));
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error("PSBT hex to base64 failed:", error);
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function base64ToHex(base64: string): string {
|
|
80
|
+
try {
|
|
81
|
+
return base?.toHex(base?.fromBase64(base64));
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error("PSBT base64 to hex failed:", error);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const Drc20LargeNumber = (n: number) => {
|
|
89
|
+
const num = n || 0;
|
|
90
|
+
|
|
91
|
+
if (num >= 1000 && num < 1000000) {
|
|
92
|
+
return num.toLocaleString();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (num >= 1000000) {
|
|
96
|
+
return formatCompactNumber(num, 1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return num.toString();
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
//from mydoge
|
|
103
|
+
export const formatCompactNumber = (num = 0, decimals = 1) => {
|
|
104
|
+
const suffixes = ["", "k", "m", "billion", "trillion"];
|
|
105
|
+
const absNum = Math.abs(num);
|
|
106
|
+
|
|
107
|
+
if (absNum < 1000) return num.toFixed(decimals);
|
|
108
|
+
|
|
109
|
+
const exp = Math.min(Math.floor(Math.log10(absNum) / 3), suffixes.length - 1);
|
|
110
|
+
const shortened = num / 1000 ** exp;
|
|
111
|
+
|
|
112
|
+
return `${shortened.toLocaleString(undefined, {
|
|
113
|
+
minimumFractionDigits: 0,
|
|
114
|
+
maximumFractionDigits: decimals,
|
|
115
|
+
})} ${suffixes[exp]}`;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
//from mydoge
|
|
119
|
+
export function formatSatoshisAsDoge(value: number, maxDecimals?: number): string {
|
|
120
|
+
if (value >= 1) {
|
|
121
|
+
const newValue = toBitcoin(Math.floor(value));
|
|
122
|
+
return formatDoge(newValue, maxDecimals);
|
|
123
|
+
} else {
|
|
124
|
+
return formatDoge(value / 1e8, 10);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
//from mydoge
|
|
129
|
+
export function formatDoge(value: number | string, maxDecimals?: number, useGrouping?: any): string {
|
|
130
|
+
let newValue = value;
|
|
131
|
+
const opts: any = {};
|
|
132
|
+
if (maxDecimals !== undefined) {
|
|
133
|
+
opts.maximumFractionDigits = maxDecimals;
|
|
134
|
+
}
|
|
135
|
+
if (useGrouping !== undefined) {
|
|
136
|
+
opts.useGrouping = useGrouping;
|
|
137
|
+
}
|
|
138
|
+
// show 4.20 instead of 4.2
|
|
139
|
+
if (newValue === 4.2) {
|
|
140
|
+
opts.minimumFractionDigits = 2;
|
|
141
|
+
}
|
|
142
|
+
newValue = newValue.toLocaleString(undefined, opts);
|
|
143
|
+
return newValue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function addUsedUtxos(newUsedUtxos: object) {
|
|
147
|
+
const usedUtxos = cache.get("UsedUtxos") || {};
|
|
148
|
+
for (const txid in newUsedUtxos) {
|
|
149
|
+
usedUtxos[txid] = 1;
|
|
150
|
+
}
|
|
151
|
+
cache.set("UsedUtxos", usedUtxos, false);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function getUsedUtxos() {
|
|
155
|
+
return cache.get("UsedUtxos") || {};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function createInscriptionPsbt({
|
|
159
|
+
inscriptionId,
|
|
160
|
+
senderAddress,
|
|
161
|
+
recipientAddress,
|
|
162
|
+
utxos,
|
|
163
|
+
fee,
|
|
164
|
+
amount,
|
|
165
|
+
location,
|
|
166
|
+
}: any) {
|
|
167
|
+
const inscription = await API.getTxDetail(inscriptionId);
|
|
168
|
+
|
|
169
|
+
let vout = inscription.vout[0]?.n;
|
|
170
|
+
if (location) {
|
|
171
|
+
const locations = location.split(":");
|
|
172
|
+
vout = Number(locations[1] || vout);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const inputs = [
|
|
176
|
+
{
|
|
177
|
+
txId: inscriptionId,
|
|
178
|
+
vOut: vout,
|
|
179
|
+
amount: toSatoshi(amount),
|
|
180
|
+
nonWitnessUtxo: inscription.hex,
|
|
181
|
+
address: senderAddress,
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
const sendCost = toSatoshi(fee);
|
|
186
|
+
|
|
187
|
+
const usedUtxos = getUsedUtxos();
|
|
188
|
+
const sortedUtxos = utxos.sort((a: any, b: any) => b.outputValue - a.outputValue);
|
|
189
|
+
|
|
190
|
+
// First pass: collect sufficient UTXOs to cover the fee
|
|
191
|
+
const selectedUtxos = [];
|
|
192
|
+
let accumulatedAmount = 0;
|
|
193
|
+
|
|
194
|
+
for (const utxo of sortedUtxos) {
|
|
195
|
+
const { txid } = utxo;
|
|
196
|
+
if (accumulatedAmount >= sendCost || usedUtxos[txid]) continue;
|
|
197
|
+
|
|
198
|
+
selectedUtxos.push(utxo);
|
|
199
|
+
accumulatedAmount += Number(utxo.outputValue);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Second pass: concurrently fetch UTXO details
|
|
203
|
+
const utxoDetailPromises = selectedUtxos.map(async (utxo) => {
|
|
204
|
+
try {
|
|
205
|
+
const utxoDetail = await API.getTxDetail(utxo.txid);
|
|
206
|
+
return { utxo, utxoDetail };
|
|
207
|
+
} catch (error) {
|
|
208
|
+
return { utxo, utxoDetail: null };
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const utxoResults = await Promise.all(utxoDetailPromises);
|
|
213
|
+
|
|
214
|
+
// Third pass: construct inputs from successfully fetched details
|
|
215
|
+
const usingUtxos: any = {};
|
|
216
|
+
let addedAmount = 0;
|
|
217
|
+
|
|
218
|
+
for (const { utxo, utxoDetail } of utxoResults) {
|
|
219
|
+
if (addedAmount >= sendCost) break;
|
|
220
|
+
|
|
221
|
+
const { txid, vout, outputValue, address = senderAddress } = utxo;
|
|
222
|
+
|
|
223
|
+
if (utxoDetail?.hex && !usedUtxos[txid] && !usingUtxos[txid]) {
|
|
224
|
+
inputs.push({
|
|
225
|
+
txId: txid,
|
|
226
|
+
vOut: vout,
|
|
227
|
+
amount: Number(outputValue),
|
|
228
|
+
nonWitnessUtxo: utxoDetail.hex,
|
|
229
|
+
address,
|
|
230
|
+
});
|
|
231
|
+
usingUtxos[txid] = 1;
|
|
232
|
+
addedAmount += Number(outputValue);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (addedAmount < sendCost) {
|
|
237
|
+
throw new Error("not enough funds to cover amount and fee");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const params: any = {
|
|
241
|
+
address: senderAddress,
|
|
242
|
+
inputs,
|
|
243
|
+
outputs: [
|
|
244
|
+
{
|
|
245
|
+
address: recipientAddress,
|
|
246
|
+
amount: toSatoshi(amount),
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
address: senderAddress,
|
|
250
|
+
amount: addedAmount - sendCost,
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const psbtBase64 = DogecoinUtils.buildPsbtToBase64(params);
|
|
256
|
+
return { psbtBase64, usingUtxos };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function createPsbt({
|
|
260
|
+
from,
|
|
261
|
+
to,
|
|
262
|
+
amount,
|
|
263
|
+
fee,
|
|
264
|
+
spendableUtxos,
|
|
265
|
+
}: {
|
|
266
|
+
from: string;
|
|
267
|
+
to: string;
|
|
268
|
+
amount: number;
|
|
269
|
+
fee: number;
|
|
270
|
+
spendableUtxos: DogeSpendableUtxos[];
|
|
271
|
+
}) {
|
|
272
|
+
let utxos = spendableUtxos;
|
|
273
|
+
if (!utxos || utxos.length === 0) {
|
|
274
|
+
utxos = await API.getSpendableUtxos(from);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const sendAmount = toSatoshi(amount);
|
|
278
|
+
const sendCost = toSatoshi(fee);
|
|
279
|
+
const totalNeeded = sendAmount + sendCost;
|
|
280
|
+
|
|
281
|
+
const sortedUtxos = utxos.sort((a, b) => b.outputValue - a.outputValue);
|
|
282
|
+
|
|
283
|
+
// First pass: collect sufficient UTXOs to cover the amount + fee
|
|
284
|
+
const selectedUtxos = [];
|
|
285
|
+
let accumulatedAmount = 0;
|
|
286
|
+
|
|
287
|
+
for (const utxo of sortedUtxos) {
|
|
288
|
+
if (accumulatedAmount >= totalNeeded) break;
|
|
289
|
+
|
|
290
|
+
selectedUtxos.push(utxo);
|
|
291
|
+
accumulatedAmount += Number(utxo.outputValue);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (accumulatedAmount < totalNeeded) {
|
|
295
|
+
throw new Error("not enough funds to cover amount and fee");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Second pass: concurrently fetch UTXO details
|
|
299
|
+
const utxoDetailPromises = selectedUtxos.map(async (utxo) => {
|
|
300
|
+
try {
|
|
301
|
+
const utxoDetail = await API.getTxDetail(utxo.txid);
|
|
302
|
+
return { utxo, utxoDetail };
|
|
303
|
+
} catch (error) {
|
|
304
|
+
return { utxo, utxoDetail: null };
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const utxoResults = await Promise.all(utxoDetailPromises);
|
|
309
|
+
|
|
310
|
+
// Third pass: construct inputs from successfully fetched details
|
|
311
|
+
const inputs = [];
|
|
312
|
+
const usingUtxos: any = {};
|
|
313
|
+
let addedAmount = 0;
|
|
314
|
+
|
|
315
|
+
for (const { utxo, utxoDetail } of utxoResults) {
|
|
316
|
+
if (addedAmount >= totalNeeded) break;
|
|
317
|
+
|
|
318
|
+
const { txid, vout, outputValue, address = from }: any = utxo;
|
|
319
|
+
|
|
320
|
+
if (utxoDetail?.hex && !usingUtxos[txid]) {
|
|
321
|
+
inputs.push({
|
|
322
|
+
txId: txid,
|
|
323
|
+
vOut: vout,
|
|
324
|
+
amount: Number(outputValue),
|
|
325
|
+
nonWitnessUtxo: utxoDetail.hex,
|
|
326
|
+
address,
|
|
327
|
+
});
|
|
328
|
+
usingUtxos[txid] = 1;
|
|
329
|
+
addedAmount += Number(outputValue);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (addedAmount < totalNeeded) {
|
|
334
|
+
throw new Error("not enough funds to cover amount and fee");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const outputs = [
|
|
338
|
+
{
|
|
339
|
+
address: to,
|
|
340
|
+
amount: sendAmount,
|
|
341
|
+
},
|
|
342
|
+
];
|
|
343
|
+
|
|
344
|
+
const changeAmount = addedAmount - sendAmount - sendCost;
|
|
345
|
+
if (changeAmount > 0) {
|
|
346
|
+
outputs.push({
|
|
347
|
+
address: from,
|
|
348
|
+
amount: changeAmount,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const params = {
|
|
353
|
+
address: from,
|
|
354
|
+
inputs,
|
|
355
|
+
outputs,
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const psbtBase64 = DogecoinUtils.buildPsbtToBase64(params as any);
|
|
359
|
+
return { psbtBase64, usingUtxos };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export async function createTxData(txInfo: any, senderAddress: string) {
|
|
363
|
+
let txData = {
|
|
364
|
+
...txInfo,
|
|
365
|
+
...{
|
|
366
|
+
sender: senderAddress,
|
|
367
|
+
feeRate: 1,
|
|
368
|
+
amountMismatch: false,
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
const { feePerKB }: any = await API.estimateSmartFee({ senderAddress });
|
|
374
|
+
const { balance = 0 } = await API.getBalance(senderAddress);
|
|
375
|
+
let amountMismatch = false;
|
|
376
|
+
const balanceBN = new BigNumber(balance);
|
|
377
|
+
const amountBN = new BigNumber(Number(txInfo.amount)).times(DECIMALS);
|
|
378
|
+
if (balanceBN < amountBN.plus(feePerKB)) {
|
|
379
|
+
amountMismatch = true;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
txData = {
|
|
383
|
+
...txInfo,
|
|
384
|
+
...{
|
|
385
|
+
sender: senderAddress,
|
|
386
|
+
feeRate: feePerKB,
|
|
387
|
+
amountMismatch,
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
} catch (err) {
|
|
391
|
+
console.error(err);
|
|
392
|
+
}
|
|
393
|
+
return txData;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function createSocialTxData(data: any, account: any) {
|
|
397
|
+
const senderAddress = data.senderAddress || data.sender || data.from || data.fromAddress;
|
|
398
|
+
const fee = data.fee || data.feeRate || 0;
|
|
399
|
+
return {
|
|
400
|
+
from: senderAddress,
|
|
401
|
+
to: data.to,
|
|
402
|
+
amount: Number(data.amount),
|
|
403
|
+
fee: toSatoshi(fee),
|
|
404
|
+
// accountId: account?.id,
|
|
405
|
+
walletId: -1,
|
|
406
|
+
rpcUrl: data.rpcUrl || RPC_URL,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export interface DogeTxData {
|
|
411
|
+
amountMismatch?: boolean;
|
|
412
|
+
amount: number;
|
|
413
|
+
fee: number;
|
|
414
|
+
to: string;
|
|
415
|
+
from: string;
|
|
416
|
+
senderAddress?: string;
|
|
417
|
+
sender?: string;
|
|
418
|
+
fromAddress?: string;
|
|
419
|
+
spendableUtxos?: DogeSpendableUtxos[];
|
|
420
|
+
}
|
|
421
|
+
export async function createBtcTxData(txData: DogeTxData) {
|
|
422
|
+
const transferAmount = toSatoshi(txData.amount);
|
|
423
|
+
const feeAmount = toSatoshi(txData.fee);
|
|
424
|
+
const senderAddress = txData.senderAddress || txData.sender || txData.from || txData.fromAddress;
|
|
425
|
+
|
|
426
|
+
const usedUtxos = getUsedUtxos();
|
|
427
|
+
const usingUtxos: any = {};
|
|
428
|
+
|
|
429
|
+
let utxos = txData.spendableUtxos;
|
|
430
|
+
if (!utxos || utxos.length === 0) {
|
|
431
|
+
throw new Error("No spendable UTXOs available");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
utxos = utxos.sort((a, b) => b.outputValue - a.outputValue);
|
|
435
|
+
|
|
436
|
+
const requiredAmount = transferAmount + feeAmount;
|
|
437
|
+
let total = 0;
|
|
438
|
+
const inputs = [];
|
|
439
|
+
|
|
440
|
+
// select enough UTXOs
|
|
441
|
+
for (const utxo of utxos) {
|
|
442
|
+
if (total >= requiredAmount) break;
|
|
443
|
+
|
|
444
|
+
if (!usedUtxos[utxo.txid]) {
|
|
445
|
+
inputs.push({
|
|
446
|
+
txId: utxo.txid,
|
|
447
|
+
vOut: utxo.vout,
|
|
448
|
+
amount: Number(utxo.outputValue),
|
|
449
|
+
});
|
|
450
|
+
usingUtxos[utxo.txid] = 1;
|
|
451
|
+
total += Number(utxo.outputValue);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// check balance
|
|
456
|
+
if (total < requiredAmount) {
|
|
457
|
+
throw new Error("Insufficient funds");
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const outputs = [
|
|
461
|
+
{
|
|
462
|
+
address: txData.to,
|
|
463
|
+
amount: transferAmount,
|
|
464
|
+
},
|
|
465
|
+
];
|
|
466
|
+
|
|
467
|
+
const txParams = {
|
|
468
|
+
inputs,
|
|
469
|
+
outputs,
|
|
470
|
+
address: senderAddress, // charge
|
|
471
|
+
feePerB: 100000,
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
return { params: txParams, usingUtxos };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export function createDrc20Data(utxos: any[], senderAddress: string, inscription: string) {
|
|
478
|
+
if (!utxos || utxos.length === 0) {
|
|
479
|
+
throw new Error("UTXOs are required");
|
|
480
|
+
}
|
|
481
|
+
if (!senderAddress) {
|
|
482
|
+
throw new Error("Sender address is required");
|
|
483
|
+
}
|
|
484
|
+
if (!inscription) {
|
|
485
|
+
throw new Error("Inscription data is required");
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const commitTxPrevOutputList = [];
|
|
489
|
+
const usedUtxos = getUsedUtxos();
|
|
490
|
+
const usingUtxos: any = {};
|
|
491
|
+
|
|
492
|
+
const txFeeDefault = 30000000; //0.3 DOGE
|
|
493
|
+
let addedAmount = 0;
|
|
494
|
+
utxos = utxos.sort((a, b) => b.outputValue - a.outputValue);
|
|
495
|
+
for (const utxo of utxos) {
|
|
496
|
+
const { txid, vout, outputValue, address = senderAddress } = utxo;
|
|
497
|
+
if (addedAmount < txFeeDefault && !usedUtxos[txid]) {
|
|
498
|
+
commitTxPrevOutputList.push({
|
|
499
|
+
txId: txid,
|
|
500
|
+
vOut: vout,
|
|
501
|
+
amount: Number(outputValue),
|
|
502
|
+
address,
|
|
503
|
+
});
|
|
504
|
+
usingUtxos[txid] = 1;
|
|
505
|
+
addedAmount += Number(outputValue);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (addedAmount < txFeeDefault) {
|
|
510
|
+
throw new Error("insufficient balance.");
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// const inscriptionHex = base.toHex(Buffer.from(inscription));
|
|
514
|
+
const inscriptionData = {
|
|
515
|
+
contentType: "text/plain;charset=utf8",
|
|
516
|
+
body: inscription,
|
|
517
|
+
revealAddr: senderAddress,
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const request = {
|
|
521
|
+
type: 1,
|
|
522
|
+
commitTxPrevOutputList,
|
|
523
|
+
commitFeeRate: 10000,
|
|
524
|
+
revealFeeRate: 9000,
|
|
525
|
+
revealOutValue: 0,
|
|
526
|
+
inscriptionData,
|
|
527
|
+
changeAddress: senderAddress,
|
|
528
|
+
};
|
|
529
|
+
return { params: request, usingUtxos };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function decodePsbt(psbt: string, type = "hex") {
|
|
533
|
+
try {
|
|
534
|
+
if (type === "base64") {
|
|
535
|
+
// Convert from base64 to hex
|
|
536
|
+
const psbtHex = base.toHex(base.fromBase64(psbt));
|
|
537
|
+
return Psbt.fromHex(psbtHex, {
|
|
538
|
+
network,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return Psbt.fromHex(psbt, {
|
|
543
|
+
network,
|
|
544
|
+
});
|
|
545
|
+
} catch (error) {
|
|
546
|
+
console.error("Failed to decode PSBT:", error);
|
|
547
|
+
throw error;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export class TransactionParser {
|
|
552
|
+
public rawTx: string;
|
|
553
|
+
|
|
554
|
+
constructor(rawTx: string) {
|
|
555
|
+
this.rawTx = rawTx;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
hexToText(hex: string) {
|
|
559
|
+
let str = "";
|
|
560
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
561
|
+
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
|
|
562
|
+
}
|
|
563
|
+
return str;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
extractOPReturnData() {
|
|
567
|
+
const ordIndex = this.rawTx.indexOf("6f7264"); // 'ord' in hex
|
|
568
|
+
if (ordIndex === -1) return null;
|
|
569
|
+
|
|
570
|
+
const dataHex = this.rawTx.substring(ordIndex);
|
|
571
|
+
const dataText = this.hexToText(dataHex);
|
|
572
|
+
|
|
573
|
+
// Extract JSON data
|
|
574
|
+
const jsonMatch = dataText.match(/\{.*?\}/);
|
|
575
|
+
return jsonMatch ? JSON.parse(jsonMatch[0]) : null;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
parseScript() {
|
|
579
|
+
return {
|
|
580
|
+
version: this.rawTx.substring(0, 8),
|
|
581
|
+
inputCount: parseInt(this.rawTx.substring(8, 10)),
|
|
582
|
+
inputs: this.parseInputs(),
|
|
583
|
+
opReturnData: this.extractOPReturnData(),
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
parseInputs() {
|
|
588
|
+
const inputs = [];
|
|
589
|
+
const position = 10; // After version and input count
|
|
590
|
+
|
|
591
|
+
// Parse first input
|
|
592
|
+
const firstInput = {
|
|
593
|
+
txid: this.rawTx.substring(position, position + 64),
|
|
594
|
+
vout: this.rawTx.substring(position + 64, position + 72),
|
|
595
|
+
scriptData: this.extractOPReturnData(),
|
|
596
|
+
};
|
|
597
|
+
inputs.push(firstInput);
|
|
598
|
+
|
|
599
|
+
return inputs;
|
|
600
|
+
}
|
|
601
|
+
}
|
package/src/evm/rpc.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createPublicClient, http, parseUnits } from "viem";
|
|
2
|
+
import { getContract, erc20Abi, encodeFunctionData, decodeFunctionData } from "viem";
|
|
3
|
+
|
|
4
|
+
export function getRPCClient(network: any) {
|
|
5
|
+
const { chainId, name, rpcUrls, nativeCurrencyDecimals, nativeCurrencyName, nativeCurrencySymbol } = network;
|
|
6
|
+
|
|
7
|
+
const myCustomChain = {
|
|
8
|
+
id: Number(chainId) || chainId,
|
|
9
|
+
name,
|
|
10
|
+
nativeCurrency: {
|
|
11
|
+
name: nativeCurrencyName,
|
|
12
|
+
symbol: nativeCurrencySymbol,
|
|
13
|
+
decimals: nativeCurrencyDecimals,
|
|
14
|
+
},
|
|
15
|
+
rpcUrls: {
|
|
16
|
+
default: {
|
|
17
|
+
http: rpcUrls,
|
|
18
|
+
webSocket: [],
|
|
19
|
+
},
|
|
20
|
+
public: {
|
|
21
|
+
http: rpcUrls,
|
|
22
|
+
webSocket: [],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
blockExplorers: {
|
|
26
|
+
default: {
|
|
27
|
+
name: "Explorer",
|
|
28
|
+
url: rpcUrls[0],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const rpcClient = createPublicClient({
|
|
34
|
+
chain: myCustomChain,
|
|
35
|
+
pollingInterval: 10_000,
|
|
36
|
+
cacheTime: 10_000,
|
|
37
|
+
transport: http(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return { rpcClient };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getERC20Contract(client: any, address: `0x${string}`) {
|
|
44
|
+
return getContract({
|
|
45
|
+
address,
|
|
46
|
+
abi: erc20Abi,
|
|
47
|
+
client,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function decodeErc20Func(data: `0x${string}`) {
|
|
52
|
+
return decodeFunctionData({ data, abi: erc20Abi });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createErc20TxData(params: any, token: any) {
|
|
56
|
+
const { decimals, address: tokenAddress } = token;
|
|
57
|
+
const value = parseUnits(params?.amount.toString(), decimals);
|
|
58
|
+
const callData = encodeFunctionData({
|
|
59
|
+
abi: erc20Abi,
|
|
60
|
+
functionName: "transfer",
|
|
61
|
+
args: [params.to, value],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
...params,
|
|
66
|
+
...{ amount: 0, data: callData, to: tokenAddress },
|
|
67
|
+
};
|
|
68
|
+
}
|