From 9f9588406acb6d85f78817f5a62b5a3f23a1c2cc Mon Sep 17 00:00:00 2001 From: msinkec Date: Thu, 21 Dec 2023 11:49:06 +0100 Subject: [PATCH] Fix bsv-20 sell limit order and add test. --- src/contracts/bsv20SellLimitOrder.ts | 83 ++++++++++++++++++---- tests/bsv20Auction.test.ts | 18 ++--- tests/bsv20SellLimitOrder.test.ts | 102 +++++++++++++++++++++++++++ 3 files changed, 180 insertions(+), 23 deletions(-) create mode 100644 tests/bsv20SellLimitOrder.test.ts diff --git a/src/contracts/bsv20SellLimitOrder.ts b/src/contracts/bsv20SellLimitOrder.ts index 97093841..9f4714bb 100644 --- a/src/contracts/bsv20SellLimitOrder.ts +++ b/src/contracts/bsv20SellLimitOrder.ts @@ -1,8 +1,7 @@ -import { BSV20V2 } from 'scrypt-ord' +import { BSV20V2, Ordinal } from 'scrypt-ord' import { ByteString, PubKey, - Addr, Sig, Utils, hash256, @@ -10,11 +9,13 @@ import { prop, pubKey2Addr, assert, - len, toByteString, - slice, - int2ByteString, hash160, + MethodCallOptions, + ContractTransaction, + bsv, + Addr, + StatefulNext, } from 'scrypt-ts' /** @@ -56,7 +57,7 @@ export class BSV20SellLimitOrder extends BSV20V2 { } @method() - public buy(amount: bigint) { + public buy(amount: bigint, buyer: Addr) { // Check token amount doesn't exceed total. assert( this.tokenAmtSold + amount < this.tokenAmt, @@ -71,20 +72,16 @@ export class BSV20SellLimitOrder extends BSV20V2 { const tokensRemaining = this.tokenAmt - this.tokenAmtSold let outputs = toByteString('') if (tokensRemaining > 0n) { - outputs = this.buildStateOutput(1n) + outputs = this.buildStateOutputFT(tokensRemaining) } // Ensure the sold tokens are being payed out to the buyer. - outputs += BSV20V2.buildTransferOutput( - pubKey2Addr(this.seller), - this.id, - amount - ) + outputs += BSV20V2.buildTransferOutput(buyer, this.id, amount) - // Ensure the next output is paying the to the Bitcoin to the seller. + // Ensure the next output is paying the Bitcoin to the seller. const satsForSeller = this.pricePerUnit * amount outputs += Utils.buildPublicKeyHashOutput( - hash160(this.seller), + pubKey2Addr(this.seller), satsForSeller ) @@ -99,4 +96,62 @@ export class BSV20SellLimitOrder extends BSV20V2 { public cancel(buyerSig: Sig) { assert(this.checkSig(buyerSig, this.seller)) } + + static async buyTxBuilder( + current: BSV20SellLimitOrder, + options: MethodCallOptions, + amount: bigint, + buyer: Addr + ): Promise { + const defaultAddress = await current.signer.getDefaultAddress() + + const next = current.next() + next.tokenAmtSold += amount + const tokensRemaining = next.tokenAmt - next.tokenAmtSold + + next.setAmt(tokensRemaining) + + const tx = new bsv.Transaction().addInput(current.buildContractInput()) + + if (tokensRemaining > 0n) { + const stateOut = new bsv.Transaction.Output({ + script: next.lockingScript, + satoshis: 1, + }) + tx.addOutput(stateOut) + } + const buyerOut = BSV20SellLimitOrder.buildTransferOutput( + buyer, + next.id, + amount + ) + tx.addOutput( + bsv.Transaction.Output.fromBufferReader( + new bsv.encoding.BufferReader(Buffer.from(buyerOut, 'hex')) + ) + ) + + const satsForSeller = next.pricePerUnit * amount + const paymentOut = new bsv.Transaction.Output({ + script: bsv.Script.fromHex( + Utils.buildPublicKeyHashScript(pubKey2Addr(next.seller)) + ), + satoshis: Number(satsForSeller), + }) + tx.addOutput(paymentOut) + + tx.change(options.changeAddress || defaultAddress) + + return { + tx, + atInputIndex: 0, + nexts: [ + { + instance: next, + balance: 1, + atOutputIndex: 0, + } as StatefulNext, + ], + } + } } diff --git a/tests/bsv20Auction.test.ts b/tests/bsv20Auction.test.ts index 19cfbe8f..240dde0d 100644 --- a/tests/bsv20Auction.test.ts +++ b/tests/bsv20Auction.test.ts @@ -54,7 +54,7 @@ async function main() { const ordinalUTXO = bsv20p2pkh.utxo - console.log('Mock BSV-20 ordinal deployed:', ordinalUTXO.txId) + console.log('Mock BSV-20 tokens deployed:', ordinalUTXO.txId) await sleep(3) @@ -91,7 +91,7 @@ async function main() { const nextInstance = currentInstance.next() nextInstance.bidder = newHighestBidder - const contractTx = await currentInstance.methods.bid( + const callRes = await currentInstance.methods.bid( newHighestBidder, bid, { @@ -103,7 +103,7 @@ async function main() { } as MethodCallOptions ) - console.log('Bid Tx:', contractTx.tx.id) + console.log('Bid Tx:', callRes.tx.id) balance += Number(bid) currentInstance = nextInstance @@ -182,7 +182,7 @@ async function main() { } ) - let contractTx = await currentInstance.methods.close( + let callRes = await currentInstance.methods.close( (sigResps) => findSig(sigResps, publicKeyAuctioneer), { pubKeyOrAddrToSign: publicKeyAuctioneer, @@ -196,7 +196,7 @@ async function main() { // If we would like to broadcast, here we need to sign ordinal UTXO input. const ordinalSig = signTx( - contractTx.tx, + callRes.tx, privateKeyAuctioneer, bsv.Script.fromHex(ordinalUTXO.script), ordinalUTXO.satoshis, @@ -204,7 +204,7 @@ async function main() { bsv.crypto.Signature.ANYONECANPAY_SINGLE ) - contractTx.tx.inputs[0].setScript( + callRes.tx.inputs[0].setScript( bsv.Script.fromASM( `${ordinalSig} ${publicKeyAuctioneer.toByteString()}` ) @@ -218,14 +218,14 @@ async function main() { options: MethodCallOptions ) => { return Promise.resolve({ - tx: contractTx.tx, + tx: callRes.tx, atInputIndex: 1, nexts: [], }) } ) - contractTx = await currentInstance.methods.close( + callRes = await currentInstance.methods.close( (sigResps) => findSig(sigResps, publicKeyAuctioneer), { pubKeyOrAddrToSign: publicKeyAuctioneer, @@ -235,7 +235,7 @@ async function main() { } as MethodCallOptions ) - console.log('Close Tx: ', contractTx.tx.id) + console.log('Close Tx: ', callRes.tx.id) } describe('Test SmartContract `BSV20Auction`', () => { diff --git a/tests/bsv20SellLimitOrder.test.ts b/tests/bsv20SellLimitOrder.test.ts new file mode 100644 index 00000000..de97c1b0 --- /dev/null +++ b/tests/bsv20SellLimitOrder.test.ts @@ -0,0 +1,102 @@ +import { getDefaultSigner, sleep } from './utils/helper' +import { Addr, bsv, findSig, PubKey, toByteString, UTXO } from 'scrypt-ts' +import { myAddress, myPrivateKey, myPublicKey } from './utils/privateKey' +import { BSV20SellLimitOrder } from '../src/contracts/bsv20SellLimitOrder' +import { BSV20V2P2PKH, OrdiMethodCallOptions } from 'scrypt-ord' + +async function main() { + BSV20SellLimitOrder.loadArtifact() + + const privateKeySeller = myPrivateKey + const publicKeySeller = myPublicKey + + ///// Mint some tokens to a P2PKH first. ///// + const max = 1000000000000000n // Whole token amount. + const dec = 8n // Decimal precision. + const sym = toByteString('TEST', true) + const pricePerUnit = 10n + + const bsv20p2pkh = new BSV20V2P2PKH( + toByteString(''), + sym, + max, + dec, + Addr(publicKeySeller.toAddress().toByteString()) + ) + await bsv20p2pkh.connect(getDefaultSigner(privateKeySeller)) + const tokenId = await bsv20p2pkh.deployToken() + const ordinalUTXO = bsv20p2pkh.utxo + + console.log('Mock BSV-20 tokens deployed:', ordinalUTXO.txId) + + ///// Transfer tokens to a sell order instance. ///// + const transferAmt = 100000n + const instance = new BSV20SellLimitOrder( + toByteString(tokenId, true), + sym, + max, + dec, + transferAmt, + PubKey(publicKeySeller.toByteString()), + pricePerUnit + ) + await instance.connect(getDefaultSigner(privateKeySeller)) + + const { tx: transferTx } = await bsv20p2pkh.methods.unlock( + (sigResps) => findSig(sigResps, publicKeySeller), + PubKey(publicKeySeller.toByteString()), + { + transfer: { + instance, + amt: transferAmt, + }, + pubKeyOrAddrToSign: publicKeySeller, + } as OrdiMethodCallOptions + ) + + console.log('BSV-20 sell order deployed: ', transferTx.id) + + ///// Perform buying. ///// + let currentInstance = instance + for (let i = 0; i < 3; i++) { + const amount = 100n + const buyer = myAddress + + currentInstance.bindTxBuilder('buy', BSV20SellLimitOrder.buyTxBuilder) + const callRes = await currentInstance.methods.buy( + amount, + Addr(buyer.toByteString()) + ) + + console.log('Buy Tx:', callRes.tx.id) + + currentInstance = callRes.nexts[0].instance + } + + ////// Cancel sell limit order and transfer remaining tokens back to seller. ///// + const recipient = new BSV20V2P2PKH( + toByteString(tokenId, true), + sym, + max, + dec, + Addr(publicKeySeller.toAddress().toByteString()) + ) + const contractTx = await currentInstance.methods.cancel( + (sigResps) => findSig(sigResps, publicKeySeller), + { + transfer: { + instance: recipient, + amt: currentInstance.tokenAmt - currentInstance.tokenAmtSold, + }, + pubKeyOrAddrToSign: publicKeySeller, + } as OrdiMethodCallOptions + ) + + console.log('Cancel Tx: ', contractTx.tx.id) +} + +describe('Test SmartContract `BSV20SellLimitOrder`', () => { + it('should succeed', async () => { + await main() + }) +})