11import { randomUtxo , randomToken , randomNFT } from '../../src/utils.js' ;
22import {
33 Contract , SignatureTemplate , ElectrumNetworkProvider , MockNetworkProvider ,
4+ TransactionBuilder ,
5+ NetworkProvider ,
46} from '../../src/index.js' ;
57import {
68 alicePkh ,
@@ -14,9 +16,10 @@ import artifact from '../fixture/p2pkh.artifact.js';
1416// TODO: Replace this with unlockers
1517describe ( 'P2PKH-tokens' , ( ) => {
1618 let p2pkhInstance : Contract < typeof artifact > ;
19+ let provider : NetworkProvider ;
1720
1821 beforeAll ( ( ) => {
19- const provider = process . env . TESTS_USE_CHIPNET
22+ provider = process . env . TESTS_USE_CHIPNET
2023 ? new ElectrumNetworkProvider ( Network . CHIPNET )
2124 : new MockNetworkProvider ( ) ;
2225
@@ -61,15 +64,19 @@ describe('P2PKH-tokens', () => {
6164 throw new Error ( 'No token UTXO found with fungible tokens' ) ;
6265 }
6366
67+ const fullBchBalance = nonTokenUtxos . reduce ( ( total , utxo ) => total + utxo . satoshis , 0n ) + tokenUtxo . satoshis ;
68+
6469 const to = p2pkhInstance . tokenAddress ;
6570 const amount = 1000n ;
6671 const { token } = tokenUtxo ;
67-
68- const tx = await p2pkhInstance . functions
69- . spend ( alicePub , new SignatureTemplate ( alicePriv ) )
70- . from ( nonTokenUtxos )
71- . from ( tokenUtxo )
72- . to ( to , amount , token )
72+ const fee = 1000n ;
73+ const changeAmount = fullBchBalance - fee - amount ;
74+
75+ const tx = await new TransactionBuilder ( { provider } )
76+ . addInputs ( nonTokenUtxos , p2pkhInstance . unlock . spend ( alicePub , new SignatureTemplate ( alicePriv ) ) )
77+ . addInput ( tokenUtxo , p2pkhInstance . unlock . spend ( alicePub , new SignatureTemplate ( alicePriv ) ) )
78+ . addOutput ( { to, amount, token } )
79+ . addOutput ( { to, amount : changeAmount } )
7380 . send ( ) ;
7481
7582 const txOutputs = getTxOutputs ( tx ) ;
@@ -87,24 +94,21 @@ describe('P2PKH-tokens', () => {
8794
8895 const to = p2pkhInstance . tokenAddress ;
8996 const amount = 1000n ;
97+ const fee = 1000n ;
98+ const fullBchBalance = nftUtxo1 . satoshis + nftUtxo2 . satoshis + nonTokenUtxos . reduce (
99+ ( total , utxo ) => total + utxo . satoshis , 0n ,
100+ ) ;
101+ const changeAmount = fullBchBalance - fee - amount ;
90102
91- // We ran into a bug with the order of the properties, so we re-order the properties here to test that it works
92- const reorderedToken1 = {
93- nft : {
94- commitment : nftUtxo1 . token ! . nft ! . commitment ,
95- capability : nftUtxo1 . token ! . nft ! . capability ,
96- } ,
97- category : nftUtxo1 . token ! . category ,
98- amount : 0n ,
99- } ;
103+ const unlocker = p2pkhInstance . unlock . spend ( alicePub , new SignatureTemplate ( alicePriv ) ) ;
100104
101- const tx = await p2pkhInstance . functions
102- . spend ( alicePub , new SignatureTemplate ( alicePriv ) )
103- . from ( nonTokenUtxos )
104- . from ( nftUtxo1 )
105- . from ( nftUtxo2 )
106- . to ( to , amount , reorderedToken1 )
107- . to ( to , amount , nftUtxo2 . token )
105+ const tx = await new TransactionBuilder ( { provider } )
106+ . addInputs ( nonTokenUtxos , unlocker )
107+ . addInput ( nftUtxo1 , unlocker )
108+ . addInput ( nftUtxo2 , unlocker )
109+ . addOutput ( { to , amount , token : nftUtxo1 . token } )
110+ . addOutput ( { to, amount, token : nftUtxo2 . token } )
111+ . addOutput ( { to, amount : changeAmount } )
108112 . send ( ) ;
109113
110114 const txOutputs = getTxOutputs ( tx ) ;
@@ -113,59 +117,23 @@ describe('P2PKH-tokens', () => {
113117 ) ;
114118 } ) ;
115119
116- it ( 'can automatically select UTXOs for fungible tokens' , async ( ) => {
117- const contractUtxos = await p2pkhInstance . getUtxos ( ) ;
118- const tokenUtxo = contractUtxos . find ( isFungibleTokenUtxo ) ;
119-
120- if ( ! tokenUtxo ) {
121- throw new Error ( 'No token UTXO found with fungible tokens' ) ;
122- }
123-
120+ it ( 'can create new token category (NFT and fungible token)' , async ( ) => {
121+ const fee = 1000n ;
124122 const to = p2pkhInstance . tokenAddress ;
125- const amount = 1000n ;
126- const { token } = tokenUtxo ;
127-
128- const tx = await p2pkhInstance . functions
129- . spend ( alicePub , new SignatureTemplate ( alicePriv ) )
130- . to ( to , amount , token )
131- . send ( ) ;
132-
133- const txOutputs = getTxOutputs ( tx ) ;
134- expect ( txOutputs ) . toEqual ( expect . arrayContaining ( [ { to, amount, token } ] ) ) ;
135- } ) ;
136-
137- it ( 'adds automatic change output for fungible tokens' , async ( ) => {
138- const contractUtxos = await p2pkhInstance . getUtxos ( ) ;
139- const tokenUtxo = contractUtxos . find ( isFungibleTokenUtxo ) ;
140- const nonTokenUtxos = contractUtxos . filter ( isNonTokenUtxo ) ;
141123
142- if ( ! tokenUtxo ) {
143- throw new Error ( 'No token UTXO found with fungible tokens' ) ;
144- }
145-
146- const to = p2pkhInstance . tokenAddress ;
147- const amount = 1000n ;
148- const { token } = tokenUtxo ;
124+ // As a prerequisite to creating a new token category, we need a vout0 UTXO, so we create one here
125+ const nonTokenUtxosBeforeGenesis = ( await p2pkhInstance . getUtxos ( ) ) . filter ( isNonTokenUtxo ) ;
126+ const preGenesisAmount = 10_000n ;
127+ const fullBchBalance = nonTokenUtxosBeforeGenesis . reduce ( ( total , utxo ) => total + utxo . satoshis , 0n ) ;
128+ const preGenesisChangeAmount = fullBchBalance - fee - preGenesisAmount ;
149129
150- const tx = await p2pkhInstance . functions
151- . spend ( alicePub , new SignatureTemplate ( alicePriv ) )
152- . from ( nonTokenUtxos )
153- . from ( tokenUtxo )
154- . to ( to , amount )
130+ await new TransactionBuilder ( { provider } )
131+ . addInputs ( nonTokenUtxosBeforeGenesis , p2pkhInstance . unlock . spend ( alicePub , new SignatureTemplate ( alicePriv ) ) )
132+ . addOutput ( { to, amount : preGenesisAmount } )
133+ . addOutput ( { to, amount : preGenesisChangeAmount } )
155134 . send ( ) ;
156135
157- const txOutputs = getTxOutputs ( tx ) ;
158- expect ( txOutputs ) . toEqual ( expect . arrayContaining ( [ { to, amount, token } ] ) ) ;
159- } ) ;
160-
161- it ( 'can create new token categories' , async ( ) => {
162- const to = p2pkhInstance . tokenAddress ;
163-
164- // Send a transaction to be used as the genesis UTXO
165- await p2pkhInstance . functions
166- . spend ( alicePub , new SignatureTemplate ( alicePriv ) )
167- . to ( to , 10_000n )
168- . send ( ) ;
136+ //////////////////////////////////////////////////////////////////////////////////////////////////
169137
170138 const contractUtxos = await p2pkhInstance . getUtxos ( ) ;
171139 const [ genesisUtxo ] = contractUtxos . filter ( ( utxo ) => utxo . vout === 0 && utxo . satoshis > 2000 ) ;
@@ -175,6 +143,7 @@ describe('P2PKH-tokens', () => {
175143 }
176144
177145 const amount = 1000n ;
146+ const changeAmount = genesisUtxo . satoshis - fee - amount ;
178147 const token : TokenDetails = {
179148 amount : 1000n ,
180149 category : genesisUtxo . txid ,
@@ -184,72 +153,16 @@ describe('P2PKH-tokens', () => {
184153 } ,
185154 } ;
186155
187- const tx = await p2pkhInstance . functions
188- . spend ( alicePub , new SignatureTemplate ( alicePriv ) )
189- . from ( genesisUtxo )
190- . to ( to , amount , token )
191- . send ( ) ;
192-
193- const txOutputs = getTxOutputs ( tx ) ;
194- expect ( txOutputs ) . toEqual ( expect . arrayContaining ( [ { to, amount, token } ] ) ) ;
195- } ) ;
196-
197- it ( 'adds automatic change output for NFTs' , async ( ) => {
198- const contractUtxos = await p2pkhInstance . getUtxos ( ) ;
199- const nftUtxo = contractUtxos . find ( isNftUtxo ) ;
200- const nonTokenUtxos = contractUtxos . filter ( isNonTokenUtxo ) ;
201-
202- if ( ! nftUtxo ) {
203- throw new Error ( 'No token UTXO found with an NFT' ) ;
204- }
205-
206- const to = p2pkhInstance . tokenAddress ;
207- const amount = 1000n ;
208- const { token } = nftUtxo ;
209-
210- const tx = await p2pkhInstance . functions
211- . spend ( alicePub , new SignatureTemplate ( alicePriv ) )
212- . from ( nonTokenUtxos )
213- . from ( nftUtxo )
214- . to ( to , amount )
215- . send ( ) ;
216-
217- const txOutputs = getTxOutputs ( tx ) ;
218- expect ( txOutputs ) . toEqual ( expect . arrayContaining ( [ { to, amount, token } ] ) ) ;
219- } ) ;
220-
221- it ( 'can disable automatic change output for fungible tokens' , async ( ) => {
222- const contractUtxos = await p2pkhInstance . getUtxos ( ) ;
223- const tokenUtxo = contractUtxos . find ( isFungibleTokenUtxo ) ;
224- const nonTokenUtxos = contractUtxos . filter ( isNonTokenUtxo ) ;
225-
226- if ( ! tokenUtxo ) {
227- throw new Error ( 'No token UTXO found with fungible tokens' ) ;
228- }
229-
230- const to = p2pkhInstance . tokenAddress ;
231- const amount = 1000n ;
232- const token = { ...tokenUtxo . token ! , amount : tokenUtxo . token ! . amount - 1n } ;
233-
234- const tx = await p2pkhInstance . functions
235- . spend ( alicePub , new SignatureTemplate ( alicePriv ) )
236- . from ( nonTokenUtxos )
237- . from ( tokenUtxo )
238- . to ( to , amount , token )
239- . withoutTokenChange ( )
156+ const tx = await new TransactionBuilder ( { provider } )
157+ . addInput ( genesisUtxo , p2pkhInstance . unlock . spend ( alicePub , new SignatureTemplate ( alicePriv ) ) )
158+ . addOutput ( { to, amount, token } )
159+ . addOutput ( { to, amount : changeAmount } )
240160 . send ( ) ;
241161
242162 const txOutputs = getTxOutputs ( tx ) ;
243163 expect ( txOutputs ) . toEqual ( expect . arrayContaining ( [ { to, amount, token } ] ) ) ;
244-
245- // Check that the change output is not present
246- txOutputs . forEach ( ( output ) => {
247- expect ( output . token ?. amount ) . not . toEqual ( 1n ) ;
248- } ) ;
249164 } ) ;
250165
251- it . todo ( 'can disable automatic change output for NFTs' ) ;
252-
253166 it ( 'should throw an error when trying to send more tokens than the contract has' , async ( ) => {
254167 const contractUtxos = await p2pkhInstance . getUtxos ( ) ;
255168 const tokenUtxo = contractUtxos . find ( isFungibleTokenUtxo ) ;
@@ -262,15 +175,22 @@ describe('P2PKH-tokens', () => {
262175 const to = p2pkhInstance . tokenAddress ;
263176 const amount = 1000n ;
264177 const token = { ...tokenUtxo . token ! , amount : tokenUtxo . token ! . amount + 1n } ;
178+ const fee = 1000n ;
179+ const fullBchBalance = nonTokenUtxos . reduce ( ( total , utxo ) => total + utxo . satoshis , 0n ) + tokenUtxo . satoshis ;
180+ const changeAmount = fullBchBalance - fee - amount ;
181+
182+ const unlocker = p2pkhInstance . unlock . spend ( alicePub , new SignatureTemplate ( alicePriv ) ) ;
265183
266- const txPromise = p2pkhInstance . functions
267- . spend ( alicePub , new SignatureTemplate ( alicePriv ) )
268- . from ( nonTokenUtxos )
269- . from ( tokenUtxo )
270- . to ( to , amount , token )
184+ const txPromise = new TransactionBuilder ( { provider } )
185+ . addInputs ( nonTokenUtxos , unlocker )
186+ . addInput ( tokenUtxo , unlocker )
187+ . addOutput ( { to , amount , token } )
188+ . addOutput ( { to, amount : changeAmount } )
271189 . send ( ) ;
272190
273- await expect ( txPromise ) . rejects . toThrow ( / I n s u f f i c i e n t f u n d s f o r t o k e n / ) ;
191+ await expect ( txPromise ) . rejects . toThrow (
192+ / t h e s u m o f f u n g i b l e t o k e n s i n t h e t r a n s a c t i o n o u t p u t s e x c e e d t h a t o f t h e t r a n s a c t i o n i n p u t s f o r a c a t e g o r y / ,
193+ ) ;
274194 } ) ;
275195
276196 it ( 'should throw an error when trying to send a token the contract doesn\'t have' , async ( ) => {
@@ -285,15 +205,21 @@ describe('P2PKH-tokens', () => {
285205 const to = p2pkhInstance . tokenAddress ;
286206 const amount = 1000n ;
287207 const token = { ...tokenUtxo . token ! , category : '0000000000000000000000000000000000000000000000000000000000000000' } ;
288-
289- const txPromise = p2pkhInstance . functions
290- . spend ( alicePub , new SignatureTemplate ( alicePriv ) )
291- . from ( nonTokenUtxos )
292- . from ( tokenUtxo )
293- . to ( to , amount , token )
208+ const fee = 1000n ;
209+ const fullBchBalance = nonTokenUtxos . reduce ( ( total , utxo ) => total + utxo . satoshis , 0n ) + tokenUtxo . satoshis ;
210+ const changeAmount = fullBchBalance - fee - amount ;
211+
212+ const unlocker = p2pkhInstance . unlock . spend ( alicePub , new SignatureTemplate ( alicePriv ) ) ;
213+ const txPromise = new TransactionBuilder ( { provider } )
214+ . addInputs ( nonTokenUtxos , unlocker )
215+ . addInput ( tokenUtxo , unlocker )
216+ . addOutput ( { to, amount, token } )
217+ . addOutput ( { to, amount : changeAmount } )
294218 . send ( ) ;
295219
296- await expect ( txPromise ) . rejects . toThrow ( / I n s u f f i c i e n t f u n d s f o r t o k e n / ) ;
220+ await expect ( txPromise ) . rejects . toThrow (
221+ / t h e t r a n s a c t i o n c r e a t e s n e w f u n g i b l e t o k e n s f o r a c a t e g o r y w i t h o u t a m a t c h i n g g e n e s i s i n p u t / ,
222+ ) ;
297223 } ) ;
298224
299225 it ( 'should throw an error when trying to send an NFT the contract doesn\'t have' , async ( ) => {
@@ -308,20 +234,25 @@ describe('P2PKH-tokens', () => {
308234 const to = p2pkhInstance . tokenAddress ;
309235 const amount = 1000n ;
310236 const token = { ...nftUtxo . token ! , category : '0000000000000000000000000000000000000000000000000000000000000000' } ;
311-
312- const txPromise = p2pkhInstance . functions
313- . spend ( alicePub , new SignatureTemplate ( alicePriv ) )
314- . from ( nonTokenUtxos )
315- . from ( nftUtxo )
316- . to ( to , amount , token )
237+ const fee = 1000n ;
238+ const fullBchBalance = nonTokenUtxos . reduce ( ( total , utxo ) => total + utxo . satoshis , 0n ) + nftUtxo . satoshis ;
239+ const changeAmount = fullBchBalance - fee - amount ;
240+
241+ const unlocker = p2pkhInstance . unlock . spend ( alicePub , new SignatureTemplate ( alicePriv ) ) ;
242+ const txPromise = new TransactionBuilder ( { provider } )
243+ . addInputs ( nonTokenUtxos , unlocker )
244+ . addInput ( nftUtxo , unlocker )
245+ . addOutput ( { to, amount, token } )
246+ . addOutput ( { to, amount : changeAmount } )
317247 . send ( ) ;
318248
319- await expect ( txPromise ) . rejects . toThrow ( / N F T o u t p u t w i t h t o k e n c a t e g o r y .* d o e s n o t h a v e c o r r e s p o n d i n g i n p u t / ) ;
249+ await expect ( txPromise ) . rejects . toThrow (
250+ / t h e t r a n s a c t i o n c r e a t e s a n i m m u t a b l e t o k e n f o r a c a t e g o r y w i t h o u t a m a t c h i n g m i n t i n g t o k e n / ,
251+ ) ;
320252 } ) ;
321253
322- it . todo ( 'can mint new NFTs if the NFT has minting capabilities' ) ;
323- it . todo ( 'can change the NFT commitment if the NFT has mutable capabilities' ) ;
324- // TODO: Add more edge case tests for NFTs (minting, mutable, change outputs with multiple kinds of NFTs)
254+ it . todo ( 'cannot burn fungible tokens when allowImplicitFungibleTokenBurn is false (default)' ) ;
255+ it . todo ( 'can burn fungible tokens when allowImplicitFungibleTokenBurn is true' ) ;
325256 } ) ;
326257} ) ;
327258
0 commit comments