Skip to content

Commit 29e8525

Browse files
committed
chore: update and simplify tx builder
1 parent bf25e33 commit 29e8525

File tree

6 files changed

+115
-84
lines changed

6 files changed

+115
-84
lines changed

src/constants/params.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const CARDANO_PARAMS = {
2+
COINS_PER_UTXO_WORD: '34482',
3+
MAX_TX_SIZE: 16384,
4+
MAX_VALUE_SIZE: 5000,
5+
} as const;

src/services/blockfrost/index.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,7 @@ export class BlockfrostClient implements BlockchainClient {
2626
const response = await this.client.txSubmit(transaction);
2727
return response;
2828
} catch (err) {
29-
if (err?.data) {
30-
console.log(err?.data);
31-
} else {
32-
console.log(err);
33-
}
29+
console.log(err);
3430
throw Error(ERROR.TRANSACTION_SUBMIT_FAIL);
3531
}
3632
};
@@ -40,11 +36,7 @@ export class BlockfrostClient implements BlockchainClient {
4036
const response = await this.client.addressesUtxosAll(address);
4137
return response;
4238
} catch (err) {
43-
if (err?.data) {
44-
console.log(err?.data);
45-
} else {
46-
console.log(err);
47-
}
39+
console.log(err);
4840
throw Error(ERROR.UTXOS_FETCH_FAIL);
4941
}
5042
};

src/transaction/__tests__/__fixtures__/index.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,13 @@ export const signTransaction = [
5555
txMetadata: {
5656
ticker: [{ source: 'source_name', value: 'fugiat veniam minus' }],
5757
},
58-
result: '83a400818258208911f640d452c3be4ff3d89db63b41ce048c056951286e2e28bbf8a51588ab44000181825839009493315cd92eb5d8c4304e67b7e16ae36d61d34502694657811a2c8e32c728d3861e164cab28cb8f006448139c8f1740ffb8e7aa9e5232dc1a10b2531f021a00029519075820cb798b0bce50604eaf2e0dc89367896b18f0a6ef6b32b57e3c9f83f8ee71e608a1008182582073fea80d424276ad0978d4fe5310e8bc2d485f5f6bb3bf87612989f112ad5a7d5840c40425229749a9434763cf01b492057fd56d7091a6372eaa777a1c9b1ca508c914e6a4ee9c0d40fc10952ed668e9ad65378a28b149de6bd4204bd9f095b0a902a11907b0a1667469636b657281a266736f757263656b736f757263655f6e616d656576616c7565736675676961742076656e69616d206d696e7573',
58+
result: '84a400818258208911f640d452c3be4ff3d89db63b41ce048c056951286e2e28bbf8a51588ab44000181825839009493315cd92eb5d8c4304e67b7e16ae36d61d34502694657811a2c8e32c728d3861e164cab28cb8f006448139c8f1740ffb8e7aa9e5232dc1a10b2531f021a00029519075820cb798b0bce50604eaf2e0dc89367896b18f0a6ef6b32b57e3c9f83f8ee71e608a1008182582073fea80d424276ad0978d4fe5310e8bc2d485f5f6bb3bf87612989f112ad5a7d5840c40425229749a9434763cf01b492057fd56d7091a6372eaa777a1c9b1ca508c914e6a4ee9c0d40fc10952ed668e9ad65378a28b149de6bd4204bd9f095b0a902f5a11907b0a1667469636b657281a266736f757263656b736f757263655f6e616d656576616c7565736675676961742076656e69616d206d696e7573',
5959
},
6060
];
6161

6262
export const composeTransaction = [
6363
{
64-
description: 'composeTransaction',
64+
description: 'composeTransaction 1',
6565
address:
6666
'addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp',
6767
utxos: [
@@ -83,16 +83,16 @@ export const composeTransaction = [
8383
},
8484
],
8585
result: {
86-
outputAmount: '1824379',
87-
totalFeesAmount: '175621',
86+
outputAmount: '1824335',
87+
totalFeesAmount: '175665',
8888
usedUtxos: [
8989
{
90-
amount: [{ quantity: '1000000', unit: 'lovelace' }],
91-
block: 'd42e18d2980f62474379c2b40d00e78825a00e6a788978e20ab14170531f1703',
92-
output_index: 0,
9390
tx_hash:
9491
'c2d3af74aed2ff310890c2b54fce15ac42127959036ebc8261154fb4c0c9e0a1',
9592
tx_index: 0,
93+
output_index: 0,
94+
amount: [{ unit: 'lovelace', quantity: '1000000' }],
95+
block: 'd42e18d2980f62474379c2b40d00e78825a00e6a788978e20ab14170531f1703',
9696
},
9797
{
9898
amount: [{ quantity: '1000000', unit: 'lovelace' }],
@@ -106,7 +106,7 @@ export const composeTransaction = [
106106
},
107107
},
108108
{
109-
description: 'composeTransaction',
109+
description: 'composeTransaction 2',
110110
address:
111111
'addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp',
112112
utxos: [
@@ -120,8 +120,8 @@ export const composeTransaction = [
120120
},
121121
],
122122
result: {
123-
outputAmount: '9825963',
124-
totalFeesAmount: '174037',
123+
outputAmount: '9825919',
124+
totalFeesAmount: '174081',
125125
usedUtxos: [
126126
{
127127
tx_hash:

src/transaction/__tests__/composeTransaction.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@ describe('utils - composeTransaction', () => {
1010
const tx = utils.composeTransaction(f.address, metadata, f.utxos);
1111

1212
// output amount
13-
expect(tx.txBody.outputs().get(0).amount().coin().to_str()).toBe(
14-
f.result.outputAmount,
15-
);
13+
if (tx.txBody.outputs().len() > 0) {
14+
expect(
15+
tx.txBody.outputs().get(0).amount().coin().to_str(),
16+
).toBe(f.result.outputAmount);
17+
} else {
18+
expect(f.result.outputAmount).toBe('0');
19+
}
1620
expect(tx.info.outputAmount.to_str()).toBe(f.result.outputAmount);
1721

1822
// fee amount

src/transaction/composeTransaction.ts

Lines changed: 78 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs';
2-
import { sortUtxos } from '../utils/transaction';
2+
import { bigNumFromStr, getAssetAmount, sortUtxos } from '../utils/transaction';
33
import { UTXO } from '../types';
4+
import { CARDANO_PARAMS } from '../constants/params';
45

56
export const composeTransaction = (
67
address: string,
@@ -20,46 +21,49 @@ export const composeTransaction = (
2021
if (!utxos || utxos.length === 0) {
2122
throw 'No UTXOs to include in the transaction.';
2223
}
23-
24-
const minUtxoValue = CardanoWasm.BigNum.from_str('1000000');
2524
const txBuilder = CardanoWasm.TransactionBuilder.new(
26-
CardanoWasm.LinearFee.new(
27-
CardanoWasm.BigNum.from_str('44'),
28-
CardanoWasm.BigNum.from_str('155381'),
29-
),
30-
minUtxoValue,
31-
// pool deposit
32-
CardanoWasm.BigNum.from_str('500000000'),
33-
// key deposit
34-
CardanoWasm.BigNum.from_str('2000000'),
25+
CardanoWasm.TransactionBuilderConfigBuilder.new()
26+
.fee_algo(
27+
CardanoWasm.LinearFee.new(
28+
bigNumFromStr('44'),
29+
bigNumFromStr('155381'),
30+
),
31+
)
32+
.pool_deposit(bigNumFromStr('500000000'))
33+
.key_deposit(bigNumFromStr('2000000'))
34+
.coins_per_utxo_word(
35+
bigNumFromStr(CARDANO_PARAMS.COINS_PER_UTXO_WORD),
36+
)
37+
.max_value_size(CARDANO_PARAMS.MAX_VALUE_SIZE)
38+
.max_tx_size(CARDANO_PARAMS.MAX_TX_SIZE)
39+
.build(),
3540
);
3641

3742
const outputAddr = CardanoWasm.Address.from_bech32(address);
3843

39-
let utxosTotalAmount = CardanoWasm.BigNum.from_str('0');
40-
let totalFeesAmount = CardanoWasm.BigNum.from_str('0');
41-
42-
// set metadata
44+
// Set metadata
4345
const txMetadata = CardanoWasm.AuxiliaryData.from_bytes(
4446
metadatum.to_bytes(),
4547
);
4648
txBuilder.set_auxiliary_data(txMetadata);
47-
totalFeesAmount = txBuilder.min_fee();
4849

49-
const testOutput = CardanoWasm.TransactionOutput.new(
50+
// Dummy output for calculating needed fee for a change output
51+
const dummyOutput = CardanoWasm.TransactionOutput.new(
5052
outputAddr,
51-
CardanoWasm.Value.new(minUtxoValue),
53+
CardanoWasm.Value.new(bigNumFromStr('1000000')),
5254
);
53-
const outputFee = txBuilder.fee_for_output(testOutput);
54-
totalFeesAmount = totalFeesAmount.checked_add(outputFee);
55+
const dummyOutputFee = txBuilder.fee_for_output(dummyOutput);
5556

57+
// add inputs
58+
const usedUtxos = [];
5659
const lovelaceUtxos = utxos.filter(
5760
u => !u.amount.find(a => a.unit !== 'lovelace'),
5861
);
59-
const sortedUtxos = sortUtxos(lovelaceUtxos);
60-
const usedUtxos = [];
61-
for (const utxo of sortedUtxos) {
62-
const amount = utxo.amount.find(a => a.unit === 'lovelace')?.quantity;
62+
const sorted = sortUtxos(lovelaceUtxos);
63+
let utxosTotalAmount = bigNumFromStr('0');
64+
const cUtxo = CardanoWasm.TransactionUnspentOutputs.new();
65+
for (const utxo of sorted) {
66+
const amount = getAssetAmount(utxo);
6367
if (!amount) continue;
6468

6569
const input = CardanoWasm.TransactionInput.new(
@@ -68,47 +72,72 @@ export const composeTransaction = (
6872
),
6973
utxo.output_index,
7074
);
71-
72-
const inputValue = CardanoWasm.Value.new(
73-
CardanoWasm.BigNum.from_str(amount.toString()),
75+
const inputValue = CardanoWasm.Value.new(bigNumFromStr(amount));
76+
const singleUtxo = CardanoWasm.TransactionUnspentOutput.new(
77+
input,
78+
CardanoWasm.TransactionOutput.new(outputAddr, inputValue),
7479
);
75-
76-
const inputFee = txBuilder.fee_for_input(outputAddr, input, inputValue);
80+
cUtxo.add(singleUtxo);
7781
txBuilder.add_input(outputAddr, input, inputValue);
78-
79-
totalFeesAmount = totalFeesAmount.checked_add(inputFee);
80-
utxosTotalAmount = utxosTotalAmount.checked_add(
81-
CardanoWasm.BigNum.from_str(amount.toString()),
82-
);
82+
utxosTotalAmount = utxosTotalAmount.checked_add(bigNumFromStr(amount));
8383
usedUtxos.push(utxo);
84-
84+
const fee = txBuilder.min_fee();
8585
if (
8686
utxosTotalAmount.compare(
87-
totalFeesAmount.checked_add(minUtxoValue),
87+
bigNumFromStr('1000000')
88+
.checked_add(fee)
89+
.checked_add(dummyOutputFee),
8890
) >= 0
8991
) {
90-
// we have enough utxos to cover fee + minUtxoOutput
92+
// enough utxo to cover change output
9193
break;
9294
}
9395
}
9496

95-
const outputAmount = utxosTotalAmount.checked_sub(totalFeesAmount);
96-
// add output to the tx
97-
txBuilder.add_output(
98-
CardanoWasm.TransactionOutput.new(
99-
outputAddr,
100-
CardanoWasm.Value.new(outputAmount),
101-
),
102-
);
103-
104-
txBuilder.set_fee(totalFeesAmount);
97+
// Coin selection and change output
98+
// Would be nice to use coinselection from CSL, but if there is an input with 1 ADA it will burn it all for fee
99+
// instead of adding another input and returning change output
100+
// txBuilder.add_inputs_from(
101+
// cUtxo,
102+
// CardanoWasm.CoinSelectionStrategyCIP2.LargestFirst,
103+
// );
104+
txBuilder.add_change_if_needed(outputAddr);
105105

106-
// once the transaction is ready, we build it to get the tx body without witnesses
106+
// Build tx
107107
const txBody = txBuilder.build();
108108
const txId = Buffer.from(
109109
CardanoWasm.hash_transaction(txBody).to_bytes(),
110110
).toString('hex');
111111

112+
// Derive few unnecessary fields that are at least used in tests
113+
const totalFeesAmount = txBody.fee();
114+
let outputAmount = bigNumFromStr('0');
115+
116+
// Fill usedUtxos from txBody.inputs()
117+
// const usedUtxos = [];
118+
// let utxosTotalAmount = bigNumFromStr('0');
119+
// for (let i = 0; i < txBody.inputs().len(); i++) {
120+
// const utxo = txBody.inputs().get(i);
121+
// const originalUtxo = utxos.find(
122+
// u =>
123+
// u.tx_hash ===
124+
// Buffer.from(utxo.transaction_id().to_bytes()).toString(
125+
// 'hex',
126+
// ) && u.output_index === utxo.index(),
127+
// );
128+
// if (originalUtxo) {
129+
// usedUtxos.push(originalUtxo);
130+
// utxosTotalAmount = utxosTotalAmount.checked_add(
131+
// bigNumFromStr(getAssetAmount(originalUtxo)),
132+
// );
133+
// }
134+
// }
135+
136+
for (let i = 0; i < txBody.outputs().len(); i++) {
137+
const output = txBody.outputs().get(i);
138+
outputAmount = outputAmount.checked_add(output.amount().coin());
139+
}
140+
112141
return {
113142
txId,
114143
txBody,

src/utils/transaction.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
11
import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs';
22
import { UTXO } from '../types';
33

4+
export const bigNumFromStr = (num: string): CardanoWasm.BigNum =>
5+
CardanoWasm.BigNum.from_str(num);
6+
7+
export const getAssetAmount = (
8+
obj: Pick<UTXO, 'amount'>,
9+
asset = 'lovelace',
10+
): string => obj.amount.find(a => a.unit === asset)?.quantity ?? '0';
11+
412
export const sortUtxos = (utxos: UTXO[]): UTXO[] => {
13+
// smallest first
514
return utxos.sort((a, b) => {
6-
const amountA = CardanoWasm.BigNum.from_str(
7-
a.amount.find(a => a.unit === 'lovelace')?.quantity ?? '0',
8-
);
9-
const amountB = CardanoWasm.BigNum.from_str(
10-
b.amount.find(a => a.unit === 'lovelace')?.quantity ?? '0',
11-
);
15+
const amountA = bigNumFromStr(getAssetAmount(a));
16+
const amountB = bigNumFromStr(getAssetAmount(b));
1217
return amountA.compare(amountB);
1318
});
1419
};
1520

1621
export const getRemainingBalance = (utxos: UTXO[]): string => {
17-
let balance = CardanoWasm.BigNum.from_str('0');
22+
let balance = bigNumFromStr('0');
1823
utxos.forEach(u => {
19-
balance = balance.checked_add(
20-
CardanoWasm.BigNum.from_str(
21-
u.amount.find(a => a.unit === 'lovelace')?.quantity ?? '0',
22-
),
23-
);
24+
balance = balance.checked_add(bigNumFromStr(getAssetAmount(u)));
2425
});
2526
return balance.to_str();
2627
};

0 commit comments

Comments
 (0)