Skip to content

Commit f7f461f

Browse files
authored
feat: support networks tokens and nft in builder (#31)
1 parent af95c6d commit f7f461f

3 files changed

Lines changed: 365 additions & 124 deletions

File tree

packages/sdk/src/mintlayer-connect-sdk.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4190,6 +4190,8 @@ class Signer {
41904190
}
41914191
}
41924192

4193+
export { Transaction } from './transaction';
4194+
41934195
export {
41944196
Client,
41954197
Signer,

packages/sdk/src/transaction.ts

Lines changed: 159 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -65,23 +65,31 @@ export class Transaction {
6565
private utxos: Utxo[]
6666
private transactionId: string
6767
private hexRepresentation: string
68-
private binRepresentation: Uint8Array
68+
private binRepresentation: { inputs: Uint8Array[]; outputs: Uint8Array[]; transactionsize: number } | null
6969
private jsonRepresentation: TransactionJSONRepresentation
7070
private currentBlockHeight: number
7171
private client: Client
7272
private network: 'mainnet' | 'testnet'
7373
private changeAddress: string
7474

75-
constructor({ client }: { client?: Client }) {
75+
constructor({
76+
client,
77+
network,
78+
currentBlockHeight,
79+
}: {
80+
client?: Client;
81+
network?: 'mainnet' | 'testnet';
82+
currentBlockHeight?: number | string | bigint;
83+
} = {}) {
7684
this.inputs = [];
7785
this.outputs = [];
7886
this.utxos = [];
7987
this.transactionId = '';
8088
this.hexRepresentation = '';
81-
this.binRepresentation = new Uint8Array([]);
82-
this.currentBlockHeight = 0;
89+
this.binRepresentation = null;
90+
this.currentBlockHeight = currentBlockHeight !== undefined ? Number(currentBlockHeight) : 0;
8391
this.jsonRepresentation = {};
84-
this.network = 'testnet';
92+
this.network = network ?? 'testnet';
8593
this.fee = BigInt(0);
8694
this.changeAddress = '';
8795

@@ -95,6 +103,16 @@ export class Transaction {
95103
return this;
96104
}
97105

106+
setNetwork(network: 'mainnet' | 'testnet') {
107+
this.network = network;
108+
return this;
109+
}
110+
111+
setCurrentBlockHeight(height: number | string | bigint) {
112+
this.currentBlockHeight = Number(height);
113+
return this;
114+
}
115+
98116
addInput(input: Input) {
99117
this.inputs.push(input);
100118
return this;
@@ -122,128 +140,168 @@ export class Transaction {
122140
return this.transactionId;
123141
}
124142

143+
// Signer-compatibility getters (match the shape used by Signer.sign())
144+
get JSONRepresentation(): TransactionJSONRepresentation {
145+
return this.jsonRepresentation;
146+
}
147+
get BINRepresentation() {
148+
return this.binRepresentation;
149+
}
150+
get HEXRepresentation_unsigned(): string {
151+
return this.hexRepresentation;
152+
}
153+
get transaction_id(): string {
154+
return this.transactionId;
155+
}
156+
125157
build() {
126-
if(!this.client && !this.utxos.length) {
158+
if (!this.client && !this.utxos.length) {
127159
throw new Error('Client or UTXOs are required to build transaction');
128160
}
129-
if(!this.client && !this.changeAddress) {
161+
if (!this.client && !this.changeAddress) {
130162
throw new Error('Client or Change Address are required to build transaction');
131163
}
132164

133-
const input_amount_coin_req = this.outputs.reduce((acc, item) => {
134-
if (item.value.type === 'Coin') {
135-
return acc + BigInt(item.value.amount.atoms);
165+
const declaredOutputs: Output[] = [...this.outputs];
166+
167+
// Sum coin and per-token requirements from user-declared outputs.
168+
let input_amount_coin_req = 0n;
169+
const token_reqs = new Map<string, bigint>();
170+
for (const out of declaredOutputs) {
171+
const val = (out as any)?.value;
172+
if (!val) continue;
173+
if (val.type === 'Coin') {
174+
input_amount_coin_req += BigInt(val.amount.atoms);
175+
} else if (val.type === 'TokenV1') {
176+
token_reqs.set(
177+
val.token_id,
178+
(token_reqs.get(val.token_id) ?? 0n) + BigInt(val.amount.atoms),
179+
);
136180
}
137-
return acc;
138-
}, 0n);
181+
}
139182

140-
let preciseFee = BigInt(0);
141-
let previousFee = BigInt(-1);
142-
const MAX_ATTEMPTS = 10;
143-
let attempts = 0;
183+
const networkId = this.network === 'mainnet' ? 0 : 1;
144184

145-
while (attempts < MAX_ATTEMPTS) {
146-
attempts++;
185+
let preciseFee = 0n;
186+
let previousFee = -1n;
187+
const MAX_ATTEMPTS = 10;
147188

189+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
148190
const totalFee = preciseFee;
149-
const input_amount_coin_req_w_fee = input_amount_coin_req + totalFee;
150-
151-
this.inputs = this.selectUTXOs(this.utxos, input_amount_coin_req_w_fee, null);
152-
153-
const totalInputValueCoin = this.inputs.reduce((acc, item) => acc + BigInt(item.utxo!.value.amount.decimal), 0n);
191+
const coin_req_w_fee = input_amount_coin_req + totalFee;
192+
193+
const coinInputs = this.selectUTXOs(this.utxos, coin_req_w_fee, null);
194+
const totalCoinIn = coinInputs.reduce(
195+
(acc, item) => acc + BigInt(item.utxo!.value.amount.atoms),
196+
0n,
197+
);
198+
if (totalCoinIn < coin_req_w_fee) {
199+
throw new Error('Not enough coin UTXOs');
200+
}
154201

155-
const changeAmountCoin = totalInputValueCoin - input_amount_coin_req_w_fee;
202+
const tokenInputsAll: UtxoInput[] = [];
203+
const tokenChanges: Array<{ token_id: string; amount: bigint }> = [];
204+
for (const [token_id, req] of token_reqs.entries()) {
205+
const tInputs = this.selectUTXOs(this.utxos, req, token_id);
206+
const totalIn = tInputs.reduce(
207+
(acc, item) => acc + BigInt(item.utxo!.value.amount.atoms),
208+
0n,
209+
);
210+
if (totalIn < req) {
211+
throw new Error(`Not enough token UTXOs for ${token_id}`);
212+
}
213+
tokenInputsAll.push(...tInputs);
214+
if (totalIn > req) {
215+
tokenChanges.push({ token_id, amount: totalIn - req });
216+
}
217+
}
156218

157-
if (changeAmountCoin > 0n) {
158-
this.outputs.push({
219+
const finalOutputs: Output[] = [...declaredOutputs];
220+
const changeCoin = totalCoinIn - coin_req_w_fee;
221+
if (changeCoin > 0n) {
222+
finalOutputs.push({
159223
type: 'Transfer',
160224
value: {
161225
type: 'Coin',
162226
amount: {
163-
atoms: changeAmountCoin.toString(),
164-
decimal: atomsToDecimal(changeAmountCoin.toString(), 11).toString(),
227+
atoms: changeCoin.toString(),
228+
decimal: atomsToDecimal(changeCoin.toString(), 11).toString(),
229+
},
230+
},
231+
destination: this.changeAddress,
232+
});
233+
}
234+
for (const c of tokenChanges) {
235+
finalOutputs.push({
236+
type: 'Transfer',
237+
value: {
238+
type: 'TokenV1',
239+
token_id: c.token_id,
240+
amount: {
241+
atoms: c.amount.toString(),
242+
decimal: c.amount.toString(),
165243
},
166244
},
167245
destination: this.changeAddress,
168246
});
169247
}
170248

249+
const finalInputs: Input[] = [...coinInputs, ...tokenInputsAll];
250+
171251
const JSONRepresentation: TransactionJSONRepresentation = {
172-
inputs: this.inputs,
173-
outputs: this.outputs,
252+
inputs: finalInputs,
253+
outputs: finalOutputs,
174254
fee: {
175255
atoms: totalFee.toString(),
176256
decimal: atomsToDecimal(totalFee.toString(), 11).toString(),
177257
},
178-
id: 'to_be_filled_in'
258+
id: 'to_be_filled_in',
179259
};
180260

181-
const BINRepresentation = this.getTransactionBINrepresentation(JSONRepresentation, this.network === 'mainnet' ? 0 : 1);
261+
const BINRepresentation = this.getTransactionBINrepresentation(JSONRepresentation, networkId);
182262

183-
const transaction_size_in_bytes = BigInt(Math.ceil(BINRepresentation.transactionsize));
184-
const fee_amount_per_kb = BigInt('100000000000'); // TODO: Get the current feerate from the network
185-
const nextPreciseFee = (fee_amount_per_kb * transaction_size_in_bytes + BigInt(999)) / BigInt(1000);
263+
const tx_size = BigInt(Math.ceil(BINRepresentation.transactionsize));
264+
const fee_per_kb = 100_000_000_000n; // TODO: fetch live feerate
265+
const nextPreciseFee = (fee_per_kb * tx_size + 999n) / 1000n;
186266

187267
if (nextPreciseFee === preciseFee || nextPreciseFee === previousFee) {
188268
const transaction = encode_transaction(
189269
mergeUint8Arrays(BINRepresentation.inputs),
190270
mergeUint8Arrays(BINRepresentation.outputs),
191271
BigInt(0),
192272
);
193-
194273
const transaction_id = get_transaction_id(transaction, true);
195-
this.transactionId = transaction_id;
196274

197-
// if (finalOutputs.some((output) => output.type === 'IssueNft')) {
198-
// const token_id = get_token_id(
199-
// mergeUint8Arrays(BINRepresentation.inputs),
200-
// this.network === 'mainnet' ? Network.Mainnet : Network.Testnet,
201-
// );
202-
// const index = finalOutputs.findIndex((output) => output.type === 'IssueNft');
203-
// const output = finalOutputs[index] as IssueNftOutput;
204-
// finalOutputs[index] = {
205-
// ...output,
206-
// token_id,
207-
// };
208-
// }
209-
//
210-
// const HEXRepresentation_unsigned = transaction.reduce(
211-
// (acc, byte) => acc + byte.toString(16).padStart(2, '0'),
212-
// '',
213-
// );
214-
//
215-
// return {
216-
// JSONRepresentation: {
217-
// ...JSONRepresentation,
218-
// id: transaction_id,
219-
// },
220-
// BINRepresentation,
221-
// HEXRepresentation_unsigned,
222-
// transaction_id,
223-
// };
275+
this.fee = totalFee;
276+
this.transactionId = transaction_id;
277+
this.binRepresentation = BINRepresentation;
278+
this.hexRepresentation = transaction.reduce(
279+
(acc, byte) => acc + byte.toString(16).padStart(2, '0'),
280+
'',
281+
);
282+
this.jsonRepresentation = { ...JSONRepresentation, id: transaction_id };
283+
return this;
224284
}
225285

226286
previousFee = preciseFee;
227287
preciseFee = nextPreciseFee;
228-
this.fee = preciseFee;
229288
}
230289

231-
return this;
290+
throw new Error('Failed to build transaction after maximum attempts');
232291
}
233292

234293
hex() {
235294
return this.hexRepresentation;
236295
}
237296

238-
json() {
297+
json(): TransactionJSONRepresentation {
298+
return this.jsonRepresentation;
299+
}
300+
301+
getFee() {
239302
return {
240-
inputs: this.inputs,
241-
outputs: this.outputs,
242-
fee: {
243-
atoms: this.fee.toString(),
244-
decimal: atomsToDecimal(this.fee.toString(), 11).toString(),
245-
},
246-
id: this.transactionId,
303+
atoms: this.fee.toString(),
304+
decimal: atomsToDecimal(this.fee.toString(), 11).toString(),
247305
};
248306
}
249307

@@ -492,7 +550,10 @@ export class Transaction {
492550

493551
const { destination: address, token_id } = output;
494552

495-
const chainTip = '200000'; // TODO unhardcode
553+
if (!this.currentBlockHeight) {
554+
throw new Error('currentBlockHeight is required for IssueNft — call setCurrentBlockHeight(...)');
555+
}
556+
const chainTip = this.currentBlockHeight;
496557

497558
return encode_output_issue_nft(
498559
token_id as string,
@@ -512,7 +573,10 @@ export class Transaction {
512573
if (output.type === 'IssueFungibleToken') {
513574
const { authority, is_freezable, metadata_uri, number_of_decimals, token_ticker, total_supply } = output;
514575

515-
const chainTip = '200000'; // TODO: unhardcode height
576+
if (!this.currentBlockHeight) {
577+
throw new Error('currentBlockHeight is required for IssueFungibleToken — call setCurrentBlockHeight(...)');
578+
}
579+
const chainTip = this.currentBlockHeight;
516580

517581
const is_token_freezable = is_freezable ? FreezableToken.Yes : FreezableToken.No;
518582

@@ -651,6 +715,25 @@ export class Transaction {
651715
}
652716
}
653717

718+
transferToken(destination: string, amount: string, token_id: string): Output {
719+
return {
720+
type: 'Transfer',
721+
destination,
722+
value: {
723+
type: 'TokenV1',
724+
token_id,
725+
amount: {
726+
atoms: amount,
727+
decimal: amount,
728+
},
729+
},
730+
};
731+
}
732+
733+
transferNft(destination: string, token_id: string): Output {
734+
return this.transferToken(destination, '1', token_id);
735+
}
736+
654737
// actions
655738
stakingWithdraw() {
656739
return {

0 commit comments

Comments
 (0)