|
1 | 1 | import { RustModule } from '../../../../api/ada/lib/cardanoCrypto/rustLoader'; |
2 | 2 | export const normalizeTokenId = (id?: string | null) => (id === '' ? '.' : id); |
3 | 3 |
|
4 | | -type WalletUtxo = { |
5 | | - address: string; // raw address bytes (hex) |
6 | | - output: { |
7 | | - Transaction: { Hash: string }; // 64-hex |
8 | | - UtxoTransactionOutput: { OutputIndex: number }; |
9 | | - tokens?: Array<{ |
10 | | - Token: { |
11 | | - Identifier: string; // '.' for ADA, or `${policyId}.${assetNameHex}` |
12 | | - Metadata: { ticker?: string; policyId?: string; assetName?: string }; |
13 | | - }; |
14 | | - TokenList: { Amount: string }; // decimal string |
15 | | - }>; |
16 | | - inlineDatumCborHex?: string; |
17 | | - datumHashHex?: string; |
18 | | - scriptRefCborHex?: string; |
19 | | - }; |
20 | | -}; |
| 4 | +export const useGetInputs = (walletUtxos: any[]) => { |
| 5 | + const getInputs = async (amounts: { [tokenId: string]: string }) => { |
| 6 | + try { |
| 7 | + const tokenId = Object.keys(amounts)[0]; |
| 8 | + if (tokenId === undefined) { |
| 9 | + throw new Error('No tokenId provided in amounts'); |
| 10 | + } |
| 11 | + const requiredAmount = BigInt(Number(amounts[tokenId])); |
| 12 | + |
| 13 | + const matching = walletUtxos |
| 14 | + .map(utxo => { |
| 15 | + const token = utxo.output.tokens.find(t => { |
| 16 | + return tokenId === '.' ? t.Token.Metadata.ticker === 'ADA' : t.Token.Identifier === tokenId; |
| 17 | + }); |
| 18 | + |
| 19 | + return token |
| 20 | + ? { |
| 21 | + utxo, |
| 22 | + amount: BigInt(token.TokenList.Amount), |
| 23 | + } |
| 24 | + : null; |
| 25 | + }) |
| 26 | + .filter(Boolean) |
| 27 | + .sort((a, b) => (a!.amount > b!.amount ? -1 : 1)) as { |
| 28 | + utxo: any; |
| 29 | + amount: bigint; |
| 30 | + }[]; |
| 31 | + |
| 32 | + const selected: any[] = []; |
| 33 | + let total = BigInt(0); |
| 34 | + |
| 35 | + for (const { utxo, amount } of matching) { |
| 36 | + selected.push(utxo); |
| 37 | + total += amount; |
| 38 | + if (total >= requiredAmount) break; |
| 39 | + } |
21 | 40 |
|
22 | | -const isHex = (s: string) => /^[0-9a-f]*$/i.test(s); |
23 | | -const isHex64 = (s: string) => /^[0-9a-f]{64}$/i.test(s); |
24 | | -const hexToBytes = (hex: string): Uint8Array => { |
25 | | - if (!isHex(hex) || hex.length % 2 !== 0) throw new Error('Invalid hex'); |
26 | | - const out = new Uint8Array(hex.length / 2); |
27 | | - for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.substr(i * 2, 2), 16); |
28 | | - return out; |
29 | | -}; |
30 | | -const bytesToHex = (bytes: Uint8Array): string => { |
31 | | - let hex = ''; |
32 | | - // @ts-ignore |
33 | | - for (let i = 0; i < bytes.length; i++) hex += bytes[i].toString(16).padStart(2, '0'); |
34 | | - return hex; |
35 | | -}; |
36 | | -const looksLikeTUSO = (hex: string) => |
37 | | - typeof hex === 'string' && /^[0-9a-f]+$/i.test(hex) && hex.toLowerCase().startsWith('82825820'); // 82 [input,output] / 82 [hash,index] / 58 20 (32-byte hash) |
38 | | - |
39 | | -export const useGetInputs = (walletUtxos: WalletUtxo[]) => { |
40 | | - // encode one UTxO -> CBOR hex TUSO |
41 | | - const encodeUtxo = (u: WalletUtxo): string => { |
42 | | - const { WalletV4 } = RustModule; |
43 | | - |
44 | | - const input = WalletV4.TransactionInput.new( |
45 | | - WalletV4.TransactionHash.from_hex(u.output.Transaction.Hash), |
46 | | - u.output.UtxoTransactionOutput.OutputIndex |
47 | | - ); |
48 | | - |
49 | | - const addr = WalletV4.Address.from_bytes(hexToBytes(u.address)); |
50 | | - |
51 | | - const value = WalletV4.Value.new(WalletV4.BigNum.from_str('0')); |
52 | | - const ma = WalletV4.MultiAsset.new(); |
53 | | - let lovelaceSet = false; |
54 | | - |
55 | | - for (const t of u.output.tokens ?? []) { |
56 | | - const amt = WalletV4.BigNum.from_str(t.TokenList.Amount); |
57 | | - const isAda = t.Token.Identifier === '.' || (t.Token.Metadata?.ticker ?? '').toUpperCase() === 'ADA'; |
58 | | - |
59 | | - if (isAda) { |
60 | | - value.set_coin(amt); |
61 | | - lovelaceSet = true; |
62 | | - } else { |
63 | | - const policyId = t.Token.Metadata?.policyId; |
64 | | - const assetNameHex = t.Token.Metadata?.assetName; |
65 | | - if (!policyId || !assetNameHex) continue; |
66 | | - const policy = WalletV4.ScriptHash.from_hex(policyId); |
67 | | - const assets = ma.get(policy) ?? WalletV4.Assets.new(); |
68 | | - assets.insert(WalletV4.AssetName.new(hexToBytes(assetNameHex)), amt); |
69 | | - ma.insert(policy, assets); |
| 41 | + if (total < requiredAmount) { |
| 42 | + throw new Error('Not enough balance'); |
70 | 43 | } |
71 | | - } |
72 | 44 |
|
73 | | - if (!lovelaceSet) value.set_coin(WalletV4.BigNum.from_str('0')); |
74 | | - if (ma.len() > 0) value.set_multiasset(ma); |
| 45 | + const inputs = await Promise.all( |
| 46 | + selected.map(async u => { |
| 47 | + const txHash = u.output.Transaction.Hash; |
| 48 | + const index = u.output.UtxoTransactionOutput.OutputIndex; |
75 | 49 |
|
76 | | - const out = WalletV4.TransactionOutput.new(addr, value); |
| 50 | + const receiver = await RustModule.WalletV4.Address.from_bytes(Buffer.from(u.address, 'hex')).to_bech32(); |
77 | 51 |
|
78 | | - if (u.output.inlineDatumCborHex) { |
79 | | - out.set_datum(WalletV4.Datum.new_data(WalletV4.PlutusData.from_bytes(hexToBytes(u.output.inlineDatumCborHex)))); |
80 | | - } else if (u.output.datumHashHex) { |
81 | | - out.set_datum(WalletV4.Datum.new_data_hash(WalletV4.DataHash.from_bytes(hexToBytes(u.output.datumHashHex)))); |
82 | | - } |
83 | | - if (u.output.scriptRefCborHex) { |
84 | | - out.set_script_ref(WalletV4.ScriptRef.from_bytes(hexToBytes(u.output.scriptRefCborHex))); |
85 | | - } |
| 52 | + const input = RustModule.WalletV4.TransactionInput.new(RustModule.WalletV4.TransactionHash.from_hex(txHash), index); |
86 | 53 |
|
87 | | - const tuso = WalletV4.TransactionUnspentOutput.new(input, out); |
88 | | - const hex = bytesToHex(tuso.to_bytes()); |
89 | | - if (!looksLikeTUSO(hex)) throw new Error(`Not a full TransactionUnspentOutput: ${hex.slice(0, 10)}…`); |
90 | | - return hex; |
91 | | - }; |
| 54 | + const value = RustModule.WalletV4.Value.new(RustModule.WalletV4.BigNum.from_str('0')); |
92 | 55 |
|
93 | | - // dedupe + validate by (txHash#index) |
94 | | - const sanitize = (list: WalletUtxo[]) => { |
95 | | - const seen = new Set<string>(); |
96 | | - const out: WalletUtxo[] = []; |
97 | | - for (const u of list) { |
98 | | - const txh = u.output?.Transaction?.Hash; |
99 | | - const idx = u.output?.UtxoTransactionOutput?.OutputIndex; |
100 | | - if (!txh || typeof idx !== 'number' || !isHex64(txh)) continue; |
101 | | - const key = `${txh}#${idx}`; |
102 | | - if (seen.has(key)) continue; |
103 | | - seen.add(key); |
104 | | - out.push(u); |
105 | | - } |
106 | | - // stable order |
107 | | - out.sort( |
108 | | - (a, b) => |
109 | | - a.output.Transaction.Hash.localeCompare(b.output.Transaction.Hash) || |
110 | | - a.output.UtxoTransactionOutput.OutputIndex - b.output.UtxoTransactionOutput.OutputIndex |
111 | | - ); |
112 | | - return out; |
113 | | - }; |
| 56 | + for (const token of u.output.tokens) { |
| 57 | + const amt = RustModule.WalletV4.BigNum.from_str(token.TokenList.Amount); |
| 58 | + |
| 59 | + if (token.Token.Metadata.ticker === 'ADA') { |
| 60 | + value.set_coin(amt); |
| 61 | + } else { |
| 62 | + const policyId = RustModule.WalletV4.ScriptHash.from_hex(token.Token.Metadata.policyId); |
| 63 | + const assetName = RustModule.WalletV4.AssetName.new(Buffer.from(token.Token.Metadata.assetName, 'hex')); |
114 | 64 |
|
115 | | - // returns ALL UTXOs as TUSO CBOR hex |
116 | | - const getInputs = async (): Promise<string[]> => { |
117 | | - const utxos = sanitize(walletUtxos); |
118 | | - return utxos.map(encodeUtxo); |
| 65 | + const multiasset = value.multiasset() || RustModule.WalletV4.MultiAsset.new(); |
| 66 | + const assets = multiasset.get(policyId) || RustModule.WalletV4.Assets.new(); |
| 67 | + assets.insert(assetName, amt); |
| 68 | + multiasset.insert(policyId, assets); |
| 69 | + value.set_multiasset(multiasset); |
| 70 | + } |
| 71 | + } |
| 72 | + |
| 73 | + const output = RustModule.WalletV4.TransactionOutput.new(RustModule.WalletV4.Address.from_bech32(receiver), value); |
| 74 | + |
| 75 | + const utxo = RustModule.WalletV4.TransactionUnspentOutput.new(input, output); |
| 76 | + return Buffer.from(utxo.to_bytes()).toString('hex'); |
| 77 | + }) |
| 78 | + ); |
| 79 | + |
| 80 | + return inputs; |
| 81 | + } catch { |
| 82 | + console.warn('Failed to get inputs'); |
| 83 | + return []; |
| 84 | + } |
119 | 85 | }; |
120 | 86 |
|
121 | 87 | return { getInputs }; |
|
0 commit comments