Skip to content

Commit 0d618e6

Browse files
committed
Merge master commit 639a077 into rel/latest
2 parents 4c13f55 + 639a077 commit 0d618e6

File tree

394 files changed

+6140
-2840
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

394 files changed

+6140
-2840
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ We specifically limit our support to these versions of Node, not because this pa
6060

6161
As each Node LTS version reaches its end-of-life we will exclude that version from the node engines property of our package's package.json file. Removing a Node version is considered a breaking change and will entail the publishing of a new major version of this package. We will not accept any requests to support an end-of-life version of Node, and any pull requests or issues regarding support for an end-of-life version of Node will be closed. We will accept code that allows this package to run on newer, non-LTS, versions of Node. Furthermore, we will attempt to ensure our own changes work on the latest version of Node. To help in that commitment, our continuous integration setup runs the full test suite on the latest release of the following versions of node:
6262

63-
- `18`
6463
- `20`
64+
- `22`
6565

6666
JavaScript package managers should allow you to install this package with any version of Node, with, at most, a warning if your version of Node does not fall within the range specified by our node engines property. If you encounter issues installing this package on a supported version of Node, please report the issue to us.
6767

examples/ts/ada/adaSplitUtxos.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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+
}

modules/abstract-lightning/src/lightning/parseWithdrawPsbt.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,15 @@ export function verifyChangeAddress(
110110
}).address;
111111
break;
112112
case 86: // P2TR (Taproot)
113-
derivedAddress = utxolib.payments.p2tr({
114-
pubkey: derivedPubkey,
115-
network,
116-
}).address;
113+
// P2TR requires x-only pubkey (32 bytes)
114+
const xOnlyPubkey = derivedPubkey.length === 33 ? derivedPubkey.subarray(1, 33) : derivedPubkey;
115+
derivedAddress = utxolib.payments.p2tr(
116+
{
117+
pubkey: xOnlyPubkey,
118+
network,
119+
},
120+
{ eccLib: utxolib.ecc }
121+
).address;
117122
break;
118123
default:
119124
throw new Error(`Unsupported purpose: ${purpose}`);
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import * as utxolib from '@bitgo/utxo-lib';
2+
3+
export interface PsbtCreationOptions {
4+
network: utxolib.Network;
5+
inputValue?: number;
6+
outputValue?: number;
7+
outputAddress?: string;
8+
changeValue?: number;
9+
changeDerivationPath?: string;
10+
changePurpose?: 49 | 84 | 86;
11+
includeChangeOutput?: boolean;
12+
masterKey?: utxolib.BIP32Interface; // Optional master key for deriving addresses
13+
}
14+
15+
export interface PsbtCreationResult {
16+
psbt: utxolib.Psbt;
17+
masterKey: utxolib.BIP32Interface;
18+
changeDerivationPath: string;
19+
changePurpose: number;
20+
}
21+
22+
/**
23+
* Creates a PSBT for testing purposes with customizable options.
24+
* This helper function generates a PSBT with a fake input and configurable outputs.
25+
*
26+
* @param options - Configuration options for PSBT creation
27+
* @returns A constructed PSBT instance and the master key used
28+
*/
29+
export function createTestPsbt(options: PsbtCreationOptions): PsbtCreationResult {
30+
const {
31+
network,
32+
inputValue = 500000,
33+
outputValue = 100000,
34+
outputAddress,
35+
changeValue,
36+
changeDerivationPath = "m/84'/0'/0'/1/6",
37+
changePurpose = 84,
38+
includeChangeOutput = true,
39+
masterKey,
40+
} = options;
41+
const fixedSeed = Buffer.from('0101010101010101010101010101010101010101010101010101010101010101', 'hex');
42+
const accountMasterKey = masterKey || utxolib.bip32.fromSeed(fixedSeed, network);
43+
44+
const inputPrivateKey = Buffer.from('0202020202020202020202020202020202020202020202020202020202020202', 'hex');
45+
const inputKeyPair = utxolib.ECPair.fromPrivateKey(inputPrivateKey, { network });
46+
const p2wpkhInput = utxolib.payments.p2wpkh({
47+
pubkey: Buffer.from(inputKeyPair.publicKey),
48+
network,
49+
});
50+
51+
// Create a new PSBT instance
52+
const psbt = new utxolib.Psbt({ network });
53+
54+
// Add a fake input to the PSBT
55+
const fakeTxId = 'ca6852598b48230ac870814b935b0d982d3968eb00a1d97332dceb6cd9b8505e';
56+
const fakeVout = 1;
57+
58+
psbt.addInput({
59+
hash: fakeTxId,
60+
index: fakeVout,
61+
witnessUtxo: {
62+
script: p2wpkhInput.output!,
63+
value: BigInt(inputValue),
64+
},
65+
bip32Derivation: [
66+
{
67+
masterFingerprint: Buffer.alloc(4, 0),
68+
path: "m/84'/0'/0'/0/0",
69+
pubkey: Buffer.from(inputKeyPair.publicKey),
70+
},
71+
],
72+
});
73+
74+
// Add recipient output
75+
let recipientAddress: string;
76+
if (outputAddress) {
77+
recipientAddress = outputAddress;
78+
} else {
79+
const recipientPrivateKey = Buffer.from('0303030303030303030303030303030303030303030303030303030303030303', 'hex');
80+
const recipientKeyPair = utxolib.ECPair.fromPrivateKey(recipientPrivateKey, { network });
81+
// P2TR requires x-only pubkey (32 bytes, without the prefix byte)
82+
const xOnlyPubkey =
83+
recipientKeyPair.publicKey.length === 33
84+
? recipientKeyPair.publicKey.subarray(1, 33)
85+
: recipientKeyPair.publicKey;
86+
const recipientP2tr = utxolib.payments.p2tr(
87+
{
88+
pubkey: xOnlyPubkey,
89+
network,
90+
},
91+
{ eccLib: utxolib.ecc }
92+
);
93+
recipientAddress = recipientP2tr.address!;
94+
}
95+
96+
psbt.addOutput({
97+
address: recipientAddress,
98+
value: BigInt(outputValue),
99+
});
100+
101+
// Add change output if requested
102+
if (includeChangeOutput) {
103+
const calculatedChangeValue = changeValue !== undefined ? changeValue : inputValue - outputValue - 10000; // 10k sats fee
104+
105+
// Parse the derivation path to get the change and address indices
106+
// Expected format: m/purpose'/coin_type'/account'/change/address_index
107+
const pathSegments = changeDerivationPath.split('/');
108+
const changeIndex = Number(pathSegments[pathSegments.length - 2]);
109+
const addressIndex = Number(pathSegments[pathSegments.length - 1]);
110+
111+
// Derive the change key from the master key
112+
const changeNode = accountMasterKey.derive(changeIndex).derive(addressIndex);
113+
const changePubkey = changeNode.publicKey;
114+
115+
let changeAddress: string;
116+
let changePayment;
117+
118+
// Create change address based on purpose
119+
switch (changePurpose) {
120+
case 49: // P2SH-P2WPKH
121+
changePayment = utxolib.payments.p2sh({
122+
redeem: utxolib.payments.p2wpkh({
123+
pubkey: changePubkey,
124+
network,
125+
}),
126+
network,
127+
});
128+
changeAddress = changePayment.address!;
129+
break;
130+
case 84: // P2WPKH
131+
changePayment = utxolib.payments.p2wpkh({
132+
pubkey: changePubkey,
133+
network,
134+
});
135+
changeAddress = changePayment.address!;
136+
break;
137+
case 86: // P2TR
138+
const xOnlyChangePubkey = changePubkey.length === 33 ? changePubkey.subarray(1, 33) : changePubkey;
139+
changePayment = utxolib.payments.p2tr(
140+
{
141+
pubkey: xOnlyChangePubkey,
142+
network,
143+
},
144+
{ eccLib: utxolib.ecc }
145+
);
146+
changeAddress = changePayment.address!;
147+
break;
148+
default:
149+
throw new Error(`Unsupported purpose: ${changePurpose}`);
150+
}
151+
152+
psbt.addOutput({
153+
address: changeAddress,
154+
value: BigInt(calculatedChangeValue),
155+
});
156+
157+
// Add bip32Derivation to the change output
158+
psbt.updateOutput(1, {
159+
bip32Derivation: [
160+
{
161+
masterFingerprint: Buffer.alloc(4, 0),
162+
path: changeDerivationPath,
163+
pubkey: changePubkey,
164+
},
165+
],
166+
});
167+
}
168+
169+
return {
170+
psbt,
171+
masterKey: accountMasterKey,
172+
changeDerivationPath,
173+
changePurpose,
174+
};
175+
}

0 commit comments

Comments
 (0)