Skip to content

Commit e507c4e

Browse files
chore(ada): ada sponsor txn example script
ada sponsor transaction example script using ada SDK Ticket: WIN-8061
1 parent 8789bdb commit e507c4e

File tree

1 file changed

+307
-0
lines changed

1 file changed

+307
-0
lines changed
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
/**
2+
* ADA Sponsorship Transaction Script
3+
*
4+
* Builds and broadcasts a sponsored ADA transaction where:
5+
* - Sender sends funds to recipient
6+
* - Sponsor pays the transaction fee
7+
* - Both sender and sponsor sign the transaction
8+
* Example sponsor transaction: https://preprod.cardanoscan.io/transaction/2197f936e53414a21e4967b9530f8d40b644ed31d07364cca8ce4f424a3fb061?tab=utxo
9+
*/
10+
11+
import { coins } from '@bitgo/statics';
12+
import { TransactionBuilderFactory, Transaction } from '@bitgo/sdk-coin-ada';
13+
import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs';
14+
import axios from 'axios';
15+
16+
const DEFAULT_CONFIG = {
17+
senderPrivateKey:
18+
'ed25519e_sk15qjyp5gkcfdz79awg4dfg2krscpcp5m5rsqmd53qnqtgnxn0jfpuln32hw6kudcvvedkpwepm2dm95pp9u25shju3n7872fkmly7k8sgpqj4h',
19+
senderAddress:
20+
'addr_test1qr3nwg8cpm9w3ga2v0peens94zey7js9zdu4h738gthck5ulny0aw0lpkn0nh6j7fzy0ddhfhe2968hvgcxflzr6a2fsg442nk',
21+
sponsorPrivateKey:
22+
'ed25519e_sk1ez29dxmsn8cwlxpk6ztef604ms2mzvmyjtsw85erxylesehk64g4dn73wqepnh7qzjzu0qqr9qppvgj3swv8hx808dn2dfrayzwj2uch5zac7',
23+
sponsorAddress:
24+
'addr_test1qp9jnqmkkkgwen0cqm4d7cstesw9um0wuduxwdfj3g7jtdsywn92ak44jje4h7q2gl3zkkhwks05z482ck86fmr4seas5ev9mc',
25+
recipientAddress:
26+
'addr_test1qzmdme7mn38nck3335zl6xavtmqxxr0x67jdffq38ms2tn4kmhnah8z083drrrg9l5d6chkqvvx7d4ay6jjpz0hq5h8qf4j042',
27+
amountToSend: '1000000',
28+
fee: '200000',
29+
minUtxoValue: '1000000',
30+
};
31+
32+
const KOIOS_API = 'https://preprod.koios.rest/api/v1';
33+
34+
// ============================================================================
35+
// Main: Build Sponsorship Transaction
36+
// ============================================================================
37+
38+
/**
39+
* Build and sign a sponsorship transaction
40+
*
41+
* Transaction structure:
42+
* - Inputs: [sender UTXOs] + [sponsor UTXOs]
43+
* - Outputs: [recipient] + [sponsor change] + [sender change]
44+
*/
45+
async function buildSponsorshipTransaction() {
46+
// Step 1: Select unspents
47+
const unspents = await selectUnspents();
48+
49+
// Step 2: Generate transaction with outputs
50+
const unsignedTx = await generateTransaction(unspents);
51+
52+
// Step 3: Sign transaction
53+
const signedTx = signTransaction(unsignedTx);
54+
55+
const txData = signedTx.toJson();
56+
const signedTxHex = signedTx.toBroadcastFormat();
57+
58+
console.log(`Transaction ID: ${txData.id}`);
59+
console.log(`Fee: ${signedTx.getFee} lovelace`);
60+
61+
// Step 4: Submit transaction
62+
try {
63+
const submittedTxHash = await submitTransaction(signedTxHex);
64+
console.log(`Submitted: https://preprod.cardanoscan.io/transaction/${submittedTxHash}`);
65+
} catch (error: unknown) {
66+
const axiosError = error as { response?: { data?: unknown }; message?: string };
67+
const errMsg = axiosError.response?.data ? JSON.stringify(axiosError.response.data) : axiosError.message;
68+
console.error(`Submission failed: ${errMsg}`);
69+
console.log(`Signed tx hex: ${signedTxHex}`);
70+
}
71+
72+
return { txId: txData.id, signedTxHex, fee: signedTx.getFee };
73+
}
74+
75+
// ============================================================================
76+
// Entry Point
77+
// ============================================================================
78+
79+
buildSponsorshipTransaction()
80+
.then(() => process.exit(0))
81+
.catch((error: Error) => {
82+
console.error('Error:', error.message);
83+
process.exit(1);
84+
});
85+
86+
// ============================================================================
87+
// Step 1: Select Unspents
88+
// ============================================================================
89+
90+
/**
91+
* Select UTXOs from sender and sponsor addresses
92+
* - Sender UTXOs cover the transfer amount
93+
* - Sponsor UTXOs cover the fee
94+
*/
95+
async function selectUnspents(): Promise<SelectedUnspents> {
96+
const [senderInfo, sponsorInfo] = await Promise.all([
97+
getAddressInfo(DEFAULT_CONFIG.senderAddress),
98+
getAddressInfo(DEFAULT_CONFIG.sponsorAddress),
99+
]);
100+
101+
if (senderInfo.utxo_set.length === 0) throw new Error('Sender has no UTXOs');
102+
if (sponsorInfo.utxo_set.length === 0) throw new Error('Sponsor has no UTXOs');
103+
104+
const amountToSend = BigInt(DEFAULT_CONFIG.amountToSend);
105+
const fee = BigInt(DEFAULT_CONFIG.fee);
106+
const minUtxoValue = BigInt(DEFAULT_CONFIG.minUtxoValue);
107+
108+
// Select sender UTXOs to cover amount + change
109+
let senderInputTotal = BigInt(0);
110+
const senderInputs: UTXO[] = [];
111+
for (const utxo of senderInfo.utxo_set) {
112+
senderInputs.push(utxo);
113+
senderInputTotal += BigInt(utxo.value);
114+
if (senderInputTotal >= amountToSend + minUtxoValue) break;
115+
}
116+
if (senderInputTotal < amountToSend) {
117+
throw new Error(`Insufficient sender funds. Have: ${senderInputTotal}, Need: ${amountToSend}`);
118+
}
119+
120+
// Select sponsor UTXOs to cover fee + change
121+
let sponsorInputTotal = BigInt(0);
122+
const sponsorInputs: UTXO[] = [];
123+
for (const utxo of sponsorInfo.utxo_set) {
124+
sponsorInputs.push(utxo);
125+
sponsorInputTotal += BigInt(utxo.value);
126+
if (sponsorInputTotal >= fee + minUtxoValue) break;
127+
}
128+
if (sponsorInputTotal < fee) {
129+
throw new Error(`Insufficient sponsor funds. Have: ${sponsorInputTotal}, Need: ${fee}`);
130+
}
131+
132+
return { senderInputs, senderInputTotal, sponsorInputs, sponsorInputTotal };
133+
}
134+
135+
// ============================================================================
136+
// Step 2: Generate and Set Outputs
137+
// ============================================================================
138+
139+
/**
140+
* Build unsigned transaction with inputs and outputs
141+
* Outputs: [recipient] + [sponsor change] + [sender change]
142+
*/
143+
async function generateTransaction(unspents: SelectedUnspents): Promise<Transaction> {
144+
const factory = new TransactionBuilderFactory(coins.get('tada'));
145+
const txBuilder = factory.getTransferBuilder();
146+
147+
const currentSlot = await getTip();
148+
const ttl = currentSlot + 7200;
149+
150+
const amountToSend = BigInt(DEFAULT_CONFIG.amountToSend);
151+
const fee = BigInt(DEFAULT_CONFIG.fee);
152+
const minUtxoValue = BigInt(DEFAULT_CONFIG.minUtxoValue);
153+
154+
// Add sender inputs
155+
for (const utxo of unspents.senderInputs) {
156+
txBuilder.input({ transaction_id: utxo.tx_hash, transaction_index: utxo.tx_index });
157+
}
158+
159+
// Add sponsor inputs
160+
for (const utxo of unspents.sponsorInputs) {
161+
txBuilder.input({ transaction_id: utxo.tx_hash, transaction_index: utxo.tx_index });
162+
}
163+
164+
// Output 1: Recipient receives the transfer amount
165+
txBuilder.output({ address: DEFAULT_CONFIG.recipientAddress, amount: amountToSend.toString() });
166+
167+
// Output 2: Sponsor change (sponsor input - fee)
168+
const sponsorChange = unspents.sponsorInputTotal - fee;
169+
if (sponsorChange >= minUtxoValue) {
170+
txBuilder.output({ address: DEFAULT_CONFIG.sponsorAddress, amount: sponsorChange.toString() });
171+
}
172+
173+
// Output 3: Sender change (handled by changeAddress)
174+
const totalInputBalance = unspents.senderInputTotal + unspents.sponsorInputTotal;
175+
txBuilder.changeAddress(DEFAULT_CONFIG.senderAddress, totalInputBalance.toString());
176+
177+
// Set TTL and fee
178+
txBuilder.ttl(ttl);
179+
txBuilder.fee(fee.toString());
180+
181+
return (await txBuilder.build()) as Transaction;
182+
}
183+
184+
// ============================================================================
185+
// Step 3: Sign Transaction
186+
// ============================================================================
187+
188+
/**
189+
* Sign transaction with both sender and sponsor keys
190+
*/
191+
function signTransaction(unsignedTx: Transaction): Transaction {
192+
const senderPrivKey = CardanoWasm.PrivateKey.from_bech32(DEFAULT_CONFIG.senderPrivateKey);
193+
const sponsorPrivKey = CardanoWasm.PrivateKey.from_bech32(DEFAULT_CONFIG.sponsorPrivateKey);
194+
195+
const txHash = CardanoWasm.hash_transaction(unsignedTx.transaction.body());
196+
197+
// Create witnesses for both parties
198+
const senderWitness = CardanoWasm.make_vkey_witness(txHash, senderPrivKey);
199+
const sponsorWitness = CardanoWasm.make_vkey_witness(txHash, sponsorPrivKey);
200+
201+
// Build witness set
202+
const witnessSet = CardanoWasm.TransactionWitnessSet.new();
203+
const vkeyWitnesses = CardanoWasm.Vkeywitnesses.new();
204+
vkeyWitnesses.add(senderWitness);
205+
vkeyWitnesses.add(sponsorWitness);
206+
witnessSet.set_vkeys(vkeyWitnesses);
207+
208+
// Create signed transaction
209+
const signedCardanoTx = CardanoWasm.Transaction.new(
210+
unsignedTx.transaction.body(),
211+
witnessSet,
212+
unsignedTx.transaction.auxiliary_data()
213+
);
214+
215+
unsignedTx.transaction = signedCardanoTx;
216+
return unsignedTx;
217+
}
218+
219+
// ============================================================================
220+
// Step 4: Submit Transaction
221+
// ============================================================================
222+
223+
/**
224+
* Submit signed transaction to the blockchain
225+
*/
226+
async function submitTransaction(signedTxHex: string): Promise<string> {
227+
const bytes = Uint8Array.from(Buffer.from(signedTxHex, 'hex'));
228+
const response = await axios.post(`${KOIOS_API}/submittx`, bytes, {
229+
headers: { 'Content-Type': 'application/cbor' },
230+
timeout: 30000,
231+
});
232+
return response.data;
233+
}
234+
235+
// ============================================================================
236+
// Helper Functions
237+
// ============================================================================
238+
239+
/**
240+
* Fetch UTXOs for an address from Koios API
241+
*/
242+
async function getAddressInfo(address: string): Promise<AddressInfo> {
243+
try {
244+
const response = await axios.post(
245+
`${KOIOS_API}/address_info`,
246+
{ _addresses: [address] },
247+
{ headers: { 'Content-Type': 'application/json' }, timeout: 30000 }
248+
);
249+
250+
if (!response.data || response.data.length === 0) {
251+
return { balance: '0', utxo_set: [] };
252+
}
253+
254+
const data = response.data[0];
255+
return {
256+
balance: data.balance || '0',
257+
utxo_set: (data.utxo_set || []).map((utxo: { tx_hash: string; tx_index: number; value: string }) => ({
258+
tx_hash: utxo.tx_hash,
259+
tx_index: utxo.tx_index,
260+
value: utxo.value,
261+
})),
262+
};
263+
} catch (error: unknown) {
264+
const axiosError = error as { response?: { status?: number }; message?: string };
265+
if (axiosError.response?.status === 400) {
266+
return { balance: '0', utxo_set: [] };
267+
}
268+
throw new Error(`Failed to fetch address info: ${axiosError.message}`);
269+
}
270+
}
271+
272+
/**
273+
* Get current blockchain tip for TTL calculation
274+
*/
275+
async function getTip(): Promise<number> {
276+
const response = await axios.get(`${KOIOS_API}/tip`, {
277+
headers: { 'Content-Type': 'application/json' },
278+
timeout: 30000,
279+
});
280+
if (!response.data || response.data.length === 0) {
281+
throw new Error('Failed to get blockchain tip');
282+
}
283+
return response.data[0].abs_slot;
284+
}
285+
286+
// ============================================================================
287+
// Types
288+
// ============================================================================
289+
290+
interface UTXO {
291+
tx_hash: string;
292+
tx_index: number;
293+
value: string;
294+
}
295+
296+
interface AddressInfo {
297+
balance: string;
298+
utxo_set: UTXO[];
299+
}
300+
301+
interface SelectedUnspents {
302+
senderInputs: UTXO[];
303+
senderInputTotal: bigint;
304+
sponsorInputs: UTXO[];
305+
sponsorInputTotal: bigint;
306+
}
307+

0 commit comments

Comments
 (0)