|
| 1 | +/** |
| 2 | + * ADA UTXO Split Script |
| 3 | + * Splits UTXOs into 2 ADA outputs (last output may be 1-3 ADA) |
| 4 | + * Example split transaction: https://preprod.cardanoscan.io/transaction/f7094b57e3729c6fc2908f63be1d8e6ab91af587ae8e8112faea5c74f2e57155?tab=utxo |
| 5 | + */ |
| 6 | + |
| 7 | +import { coins } from '@bitgo/statics'; |
| 8 | +import { TransactionBuilderFactory, Transaction } from '@bitgo/sdk-coin-ada'; |
| 9 | +import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs'; |
| 10 | +import axios from 'axios'; |
| 11 | + |
| 12 | +const CONFIG = { |
| 13 | + privateKey: '', |
| 14 | + address: '', |
| 15 | + denominationAda: 2, |
| 16 | +}; |
| 17 | + |
| 18 | +const KOIOS_API = 'https://preprod.koios.rest/api/v1'; |
| 19 | +const ADA = 1_000_000; |
| 20 | + |
| 21 | +async function splitUtxos() { |
| 22 | + // Step 1: Select unspents |
| 23 | + const { utxos, total } = await selectUnspents(); |
| 24 | + if (!utxos.length) throw new Error('No UTXOs found'); |
| 25 | + |
| 26 | + const numOutputs = Math.floor(Number(total) / ADA / CONFIG.denominationAda); |
| 27 | + if (numOutputs < 2) throw new Error('Insufficient funds for split'); |
| 28 | + |
| 29 | + // Step 2: Generate transaction |
| 30 | + const unsignedTx = await generateTransaction(utxos, total); |
| 31 | + |
| 32 | + // Step 3: Sign transaction |
| 33 | + const signedTx = signTransaction(unsignedTx); |
| 34 | + |
| 35 | + // Step 4: Submit transaction |
| 36 | + await submitTransaction(signedTx); |
| 37 | +} |
| 38 | + |
| 39 | +splitUtxos() |
| 40 | + .then(() => process.exit(0)) |
| 41 | + .catch((e: Error) => { |
| 42 | + console.error('Error:', e.message); |
| 43 | + process.exit(1); |
| 44 | + }); |
| 45 | + |
| 46 | +interface UTXO { |
| 47 | + tx_hash: string; |
| 48 | + tx_index: number; |
| 49 | + value: string; |
| 50 | +} |
| 51 | + |
| 52 | +/** |
| 53 | + * Step 1: Select unspents |
| 54 | + * Fetches all UTXOs for the address and calculates total balance |
| 55 | + */ |
| 56 | +async function selectUnspents(): Promise<{ utxos: UTXO[]; total: bigint }> { |
| 57 | + const response = await axios.post( |
| 58 | + `${KOIOS_API}/address_info`, |
| 59 | + { _addresses: [CONFIG.address] }, |
| 60 | + { headers: { 'Content-Type': 'application/json' }, timeout: 30000 } |
| 61 | + ); |
| 62 | + |
| 63 | + const utxos: UTXO[] = response.data?.[0]?.utxo_set || []; |
| 64 | + const total = utxos.reduce((sum, u) => sum + BigInt(u.value), BigInt(0)); |
| 65 | + |
| 66 | + console.log(`Step 1: Found ${utxos.length} UTXOs, total ${Number(total) / ADA} ADA`); |
| 67 | + return { utxos, total }; |
| 68 | +} |
| 69 | + |
| 70 | +/** |
| 71 | + * Step 2: Generate transaction with outputs |
| 72 | + * Creates (N-1) outputs of exact denomination, last output handled by changeAddress |
| 73 | + */ |
| 74 | +async function generateTransaction(utxos: UTXO[], total: bigint): Promise<Transaction> { |
| 75 | + const denom = BigInt(CONFIG.denominationAda * ADA); |
| 76 | + const numOutputs = Math.floor(Number(total) / ADA / CONFIG.denominationAda); |
| 77 | + |
| 78 | + const factory = new TransactionBuilderFactory(coins.get('tada')); |
| 79 | + const txBuilder = factory.getTransferBuilder(); |
| 80 | + |
| 81 | + // Add inputs |
| 82 | + utxos.forEach((u) => txBuilder.input({ transaction_id: u.tx_hash, transaction_index: u.tx_index })); |
| 83 | + |
| 84 | + // Add (N-1) outputs of exact denomination |
| 85 | + for (let i = 0; i < numOutputs - 1; i++) { |
| 86 | + txBuilder.output({ address: CONFIG.address, amount: denom.toString() }); |
| 87 | + } |
| 88 | + |
| 89 | + // Last output handled by changeAddress (will be ~1-3 ADA) |
| 90 | + txBuilder.changeAddress(CONFIG.address, total.toString()); |
| 91 | + |
| 92 | + const tip = await axios.get(`${KOIOS_API}/tip`, { timeout: 30000 }); |
| 93 | + txBuilder.ttl(tip.data[0].abs_slot + 7200); |
| 94 | + |
| 95 | + const tx = (await txBuilder.build()) as Transaction; |
| 96 | + console.log(`Step 2: Built tx with ${numOutputs} outputs, fee ${Number(tx.getFee) / ADA} ADA`); |
| 97 | + return tx; |
| 98 | +} |
| 99 | + |
| 100 | +/** |
| 101 | + * Step 3: Sign transaction |
| 102 | + * Signs the transaction with the private key |
| 103 | + */ |
| 104 | +function signTransaction(tx: Transaction): Transaction { |
| 105 | + const priv = CardanoWasm.PrivateKey.from_bech32(CONFIG.privateKey); |
| 106 | + const hash = CardanoWasm.hash_transaction(tx.transaction.body()); |
| 107 | + |
| 108 | + const witnessSet = CardanoWasm.TransactionWitnessSet.new(); |
| 109 | + const vkeyWitnesses = CardanoWasm.Vkeywitnesses.new(); |
| 110 | + vkeyWitnesses.add(CardanoWasm.make_vkey_witness(hash, priv)); |
| 111 | + witnessSet.set_vkeys(vkeyWitnesses); |
| 112 | + |
| 113 | + tx.transaction = CardanoWasm.Transaction.new(tx.transaction.body(), witnessSet, tx.transaction.auxiliary_data()); |
| 114 | + |
| 115 | + console.log(`Step 3: Signed tx ${tx.toJson().id}`); |
| 116 | + return tx; |
| 117 | +} |
| 118 | + |
| 119 | +/** |
| 120 | + * Step 4: Submit transaction |
| 121 | + * Broadcasts the signed transaction to the network |
| 122 | + */ |
| 123 | +async function submitTransaction(tx: Transaction): Promise<void> { |
| 124 | + const signedTxHex = tx.toBroadcastFormat(); |
| 125 | + const bytes = Uint8Array.from(Buffer.from(signedTxHex, 'hex')); |
| 126 | + |
| 127 | + await axios.post(`${KOIOS_API}/submittx`, bytes, { |
| 128 | + headers: { 'Content-Type': 'application/cbor' }, |
| 129 | + timeout: 30000, |
| 130 | + }); |
| 131 | + |
| 132 | + console.log(`Step 4: https://preprod.cardanoscan.io/transaction/${tx.toJson().id}`); |
| 133 | +} |
0 commit comments