@@ -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