diff --git a/package-lock.json b/package-lock.json index e7893c23..28e8ed48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "scrypt-ts": "^1.2.2", + "scrypt-ts": "^1.3.0", "scrypt-ts-lib": "^0.1.20" }, "devDependencies": { @@ -4015,9 +4015,9 @@ ] }, "node_modules/scrypt-ts": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/scrypt-ts/-/scrypt-ts-1.2.2.tgz", - "integrity": "sha512-7GQF8JZvkYH7dXck1aKlCKuOy0cWVEEkPdGnQ75A6w6PLUTN0iOAZsKxMEkjM4qbV4NhKgJuQZJj15GSmUeaVg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scrypt-ts/-/scrypt-ts-1.3.0.tgz", + "integrity": "sha512-fPh8Dsy6lfG9nb9mfyqKRQsU9jEE/FBsH/LGp+N5psups8t8LeeAlRuJF9lICwLVnvDQmN+jWtSqNf7u/pTBHA==", "dependencies": { "deep-equal": "^2.2.0", "fast-diff": "^1.2.0", @@ -7645,9 +7645,9 @@ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, "scrypt-ts": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/scrypt-ts/-/scrypt-ts-1.2.2.tgz", - "integrity": "sha512-7GQF8JZvkYH7dXck1aKlCKuOy0cWVEEkPdGnQ75A6w6PLUTN0iOAZsKxMEkjM4qbV4NhKgJuQZJj15GSmUeaVg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scrypt-ts/-/scrypt-ts-1.3.0.tgz", + "integrity": "sha512-fPh8Dsy6lfG9nb9mfyqKRQsU9jEE/FBsH/LGp+N5psups8t8LeeAlRuJF9lICwLVnvDQmN+jWtSqNf7u/pTBHA==", "requires": { "deep-equal": "^2.2.0", "fast-diff": "^1.2.0", diff --git a/package.json b/package.json index 9860eea7..72e75cec 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,11 @@ "main": "index.js", "scripts": { "precompile": "rimraf scrypt.index.json && rimraf artifacts", - "compile": "npx scrypt-cli@latest compile --asm --tsconfig='tsconfig.json'", + "compile": "npx scrypt-cli@latest compile", "pretest": "npm run compile", "test": "mocha -i -f 'Heavy'", "test-all": "mocha --no-timeouts", - "testnet": "mocha --config=.mocharc-testnet.json", + "testnet": "mocha --no-timeouts --config=.mocharc-testnet.json", "lint": "eslint . --ext .js,.ts --fix && prettier --write --ignore-unknown \"**/*\"", "lint-check": "eslint . --ext .js,.ts && prettier --check --ignore-unknown \"**/*\"", "prepare": "husky install", @@ -26,7 +26,7 @@ "author": "", "license": "MIT", "dependencies": { - "scrypt-ts": "^1.2.2", + "scrypt-ts": "^1.3.0", "scrypt-ts-lib": "^0.1.20" }, "devDependencies": { diff --git a/src/contracts/cointoss.ts b/src/contracts/cointoss.ts index 08c4a551..45067c46 100644 --- a/src/contracts/cointoss.ts +++ b/src/contracts/cointoss.ts @@ -1,62 +1,58 @@ -import {assert} from 'console' -import {prop, -SmartContract, -PubKey, -sha256, -Sha256, -method, -Sig, -hash256, -ByteString} from 'scrypt-ts' +import { + prop, + SmartContract, + PubKey, + Sha256, + method, + Sig, + hash256, + ByteString, + assert, +} from 'scrypt-ts' -export class CoinToss extends SmartContract{ +export class CoinToss extends SmartContract { + @prop() + alice: PubKey + @prop() + bob: PubKey + @prop() + aliceHash: Sha256 + @prop() + bobHash: Sha256 + @prop() + N: ByteString -@prop() -alice : PubKey -@prop() -bob : PubKey -@prop() -aliceHash : Sha256 -@prop() -bobHash : Sha256 -@prop() -N : ByteString + constructor( + alice: PubKey, + bob: PubKey, + aliceHash: Sha256, + bobHash: Sha256, + N: ByteString + ) { + super(...arguments) -constructor(alice : PubKey, - bob : PubKey, - aliceHash : Sha256, - bobHash : Sha256, - N : ByteString){ - super(...arguments) - - this.alice = alice - this.bob = bob - this.aliceHash = aliceHash - this.bobHash = bobHash - this.N = N - + this.alice = alice + this.bob = bob + this.aliceHash = aliceHash + this.bobHash = bobHash + this.N = N } - + @method() - public toss(aliceNonce : ByteString, bobNonce : ByteString, sig : Sig){ - - assert(hash256(aliceNonce) == this.aliceHash, 'hash mismatch') - - assert(hash256(bobNonce) == this.bobHash, 'hash mismatch') - - // if head bob wins else alice wins - if(aliceNonce == bobNonce || aliceNonce == this.N){ - let winner : PubKey = this.bob - // if winner take all the money - assert(this.checkSig(sig, winner)) - } else{ - let winner : PubKey = this.alice - // winner take all the money - assert(this.checkSig(sig, winner)) - } - - assert(true) - - } + public toss(aliceNonce: ByteString, bobNonce: ByteString, sig: Sig) { + assert(hash256(aliceNonce) == this.aliceHash, 'hash mismatch') -} \ No newline at end of file + assert(hash256(bobNonce) == this.bobHash, 'hash mismatch') + + // if head bob wins else alice wins + if (aliceNonce == bobNonce || aliceNonce == this.N) { + const winner: PubKey = this.bob + // if winner take all the money + assert(this.checkSig(sig, winner)) + } else { + const winner: PubKey = this.alice + // winner take all the money + assert(this.checkSig(sig, winner)) + } + } +} diff --git a/src/contracts/multiPartyHashPuzzle.ts b/src/contracts/multiPartyHashPuzzle.ts index f0d4a7d1..79b0a00c 100644 --- a/src/contracts/multiPartyHashPuzzle.ts +++ b/src/contracts/multiPartyHashPuzzle.ts @@ -35,6 +35,5 @@ export class MultiPartyHashPuzzle extends SmartContract { for (let i = 0; i < MultiPartyHashPuzzle.N; i++) { assert(sha256(preimages[i]) == this.hashes[i], 'hash mismatch') } - assert(true) } } diff --git a/src/contracts/ordinalAuction.ts b/src/contracts/ordinalAuction.ts new file mode 100644 index 00000000..199c0b0f --- /dev/null +++ b/src/contracts/ordinalAuction.ts @@ -0,0 +1,185 @@ +import { + assert, + MethodCallOptions, + ContractTransaction, + ByteString, + hash256, + method, + prop, + PubKey, + Sig, + SmartContract, + Utils, + bsv, + hash160, + slice, + StatefulNext, + TxOutputRef, +} from 'scrypt-ts' + +import Transaction = bsv.Transaction +import Address = bsv.Address +import Script = bsv.Script + +export class OrdinalAuction extends SmartContract { + static readonly LOCKTIME_BLOCK_HEIGHT_MARKER = 500000000 + static readonly UINT_MAX = 0xffffffffn + + @prop() + ordnialPrevout: ByteString + + // The bidder's public key. + @prop(true) + bidder: PubKey + + // The auctioneer's public key. + @prop() + readonly auctioneer: PubKey + + // Deadline of the auction. Can be block height or timestamp. + @prop() + readonly auctionDeadline: bigint + + constructor( + ordinalPrevout: ByteString, + auctioneer: PubKey, + auctionDeadline: bigint + ) { + super(...arguments) + this.ordnialPrevout = ordinalPrevout + this.bidder = auctioneer + this.auctioneer = auctioneer + this.auctionDeadline = auctionDeadline + } + + // Call this public method to bid with a higher offer. + @method() + public bid(bidder: PubKey, bid: bigint) { + const highestBid: bigint = this.ctx.utxo.value + assert( + bid > highestBid, + 'the auction bid is lower than the current highest bid' + ) + + // Change the public key of the highest bidder. + const highestBidder: PubKey = this.bidder + this.bidder = bidder + + // Auction continues with a higher bidder. + const auctionOutput: ByteString = this.buildStateOutput(bid) + + // Refund previous highest bidder. + const refundOutput: ByteString = Utils.buildPublicKeyHashOutput( + hash160(highestBidder), + highestBid + ) + let outputs: ByteString = auctionOutput + refundOutput + + // Add change output. + outputs += this.buildChangeOutput() + + assert( + hash256(outputs) == this.ctx.hashOutputs, + 'hashOutputs check failed' + ) + } + + // Close the auction if deadline is reached. + @method() + public close(sigAuctioneer: Sig) { + // Check if using block height. + if ( + this.auctionDeadline < OrdinalAuction.LOCKTIME_BLOCK_HEIGHT_MARKER + ) { + // Enforce nLocktime field to also use block height. + assert( + this.ctx.locktime < OrdinalAuction.LOCKTIME_BLOCK_HEIGHT_MARKER + ) + } + assert( + this.ctx.sequence < OrdinalAuction.UINT_MAX, + 'input sequence should less than UINT_MAX' + ) + assert( + this.ctx.locktime >= this.auctionDeadline, + 'auction is not over yet' + ) + + // Check signature of the auctioneer. + assert( + this.checkSig(sigAuctioneer, this.auctioneer), + 'signature check failed' + ) + + // Check the passed prevouts byte string is correct. + assert( + hash256(this.prevouts) == this.ctx.hashPrevouts, + 'hashPrevouts mismatch' + ) + + // Ensure the first input in spending the auctioned ordinal UTXO. + assert( + slice(this.prevouts, 0n, 36n) == this.ordnialPrevout, + 'first input is not spending specified ordinal UTXO' + ) + + // Ensure the ordinal is being payed out to the winning bidder. + let outputs = Utils.buildPublicKeyHashOutput(hash160(this.bidder), 1n) + + // Ensure the second output is paying the bid to the auctioneer. + outputs += Utils.buildPublicKeyHashOutput( + hash160(this.auctioneer), + this.ctx.utxo.value + ) + + // Add change output. + outputs += this.buildChangeOutput() + + // Check outputs. + assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs mismatch') + } + + // User defined transaction builder for calling function `bid` + static bidTxBuilder( + current: OrdinalAuction, + options: MethodCallOptions, + bidder: PubKey, + bid: bigint + ): Promise { + const next = options.next as StatefulNext + + const unsignedTx: Transaction = new Transaction() + // add contract input + .addInput(current.buildContractInput(options.fromUTXO)) + // build next instance output + .addOutput( + new Transaction.Output({ + script: next.instance.lockingScript, + satoshis: Number(bid), + }) + ) + // build refund output + .addOutput( + new Transaction.Output({ + script: Script.fromHex( + Utils.buildPublicKeyHashScript(hash160(current.bidder)) + ), + satoshis: current.balance, + }) + ) + // build change output + .change(options.changeAddress) + + return Promise.resolve({ + tx: unsignedTx, + atInputIndex: 0, + nexts: [ + { + instance: next.instance, + atOutputIndex: 0, + balance: next.balance, + }, + ], + }) + } +} diff --git a/src/contracts/perceptron2.ts b/src/contracts/perceptron2.ts index 7b77795b..824e55db 100644 --- a/src/contracts/perceptron2.ts +++ b/src/contracts/perceptron2.ts @@ -72,8 +72,6 @@ export class Perceptron2 extends SmartContract { // Prediction must match actual label. assert(this.outputs[i] == prediction, 'Wrong prediction.') } - - assert(true) } // Binary step function. diff --git a/tests/local/ordinalAuction.test.ts b/tests/local/ordinalAuction.test.ts new file mode 100644 index 00000000..abe28bab --- /dev/null +++ b/tests/local/ordinalAuction.test.ts @@ -0,0 +1,223 @@ +import { OrdinalAuction } from '../../src/contracts/ordinalAuction' +import { + bsv, + ByteString, + findSig, + hash160, + int2ByteString, + MethodCallOptions, + PubKey, + reverseByteString, + Sig, + toByteString, + toHex, + Utils, + UTXO, +} from 'scrypt-ts' +import { expect } from 'chai' +import { getDummySigner, getDummyUTXO, randomPrivateKey } from '../utils/helper' +import { randomBytes } from 'crypto' + +describe('Test SmartContract `OrdinalAuction`', () => { + const [privateKeyAuctioneer, publicKeyAuctioneer, , addressAuctioneer] = + randomPrivateKey() + + const bidderPrivateKeys: bsv.PrivateKey[] = [] + const bidderPublicKeys: bsv.PublicKey[] = [] + const bidderAddresses: bsv.Address[] = [] + for (let i = 0; i < 3; i++) { + const [privateKeyBidder, publicKeyBidder, , addressBidder] = + randomPrivateKey() + bidderPrivateKeys.push(privateKeyBidder) + bidderPublicKeys.push(publicKeyBidder) + bidderAddresses.push(addressBidder) + } + + const auctionDeadline = Math.round(new Date('2020-01-03').valueOf() / 1000) + + const ordinalUTXO: UTXO = { + txId: randomBytes(32).toString('hex'), + outputIndex: 0, + script: Utils.buildPublicKeyHashScript( + hash160(publicKeyAuctioneer.toHex()) + ), + satoshis: 1, + } + + const ordinalPrevout: ByteString = + reverseByteString(toByteString(ordinalUTXO.txId), 32n) + + int2ByteString(BigInt(ordinalUTXO.outputIndex), 4n) + + let auction: OrdinalAuction + + before(async () => { + await OrdinalAuction.compile() + + auction = new OrdinalAuction( + ordinalPrevout, + PubKey(toHex(publicKeyAuctioneer)), + BigInt(auctionDeadline) + ) + + auction.bindTxBuilder('bid', OrdinalAuction.bidTxBuilder) + + await auction.connect(getDummySigner(privateKeyAuctioneer)) + }) + + it('should pass whole auction', async () => { + let balance = 1 + let currentInstance = auction + + // Perform bidding. + for (let i = 0; i < 3; i++) { + const newHighestBidder = PubKey(toHex(bidderPublicKeys[i])) + const bid = BigInt(balance + 100) + + const nextInstance = currentInstance.next() + nextInstance.bidder = newHighestBidder + + const contractTx = await currentInstance.methods.bid( + newHighestBidder, + bid, + { + fromUTXO: getDummyUTXO(balance), + changeAddress: bidderAddresses[i], + next: { + instance: nextInstance, + balance: Number(bid), + }, + } as MethodCallOptions + ) + + const result = contractTx.tx.verifyScript(contractTx.atInputIndex) + expect(result.success, result.error).to.eq(true) + + balance += Number(bid) + currentInstance = nextInstance + } + + const fromUTXO = getDummyUTXO(balance) + + // Close the auction. + currentInstance.bindTxBuilder( + 'close', + async ( + current: OrdinalAuction, + options: MethodCallOptions, + sigAuctioneer: Sig, + prevouts: ByteString + ) => { + const unsignedTx: bsv.Transaction = new bsv.Transaction() + + // add input that unlocks ordinal UTXO + unsignedTx + .addInput( + new bsv.Transaction.Input({ + prevTxId: ordinalUTXO.txId, + outputIndex: ordinalUTXO.outputIndex, + script: bsv.Script.fromHex('00'.repeat(34)), + }), + bsv.Script.fromHex(ordinalUTXO.script), + ordinalUTXO.satoshis + ) + .addInput(current.buildContractInput(options.fromUTXO)) + + // build ordinal destination output + unsignedTx + .addOutput( + new bsv.Transaction.Output({ + script: bsv.Script.fromHex( + Utils.buildPublicKeyHashScript( + hash160(current.bidder) + ) + ), + satoshis: 1, + }) + ) + // build auctioneer payment output + .addOutput( + new bsv.Transaction.Output({ + script: bsv.Script.fromHex( + Utils.buildPublicKeyHashScript( + hash160(current.auctioneer) + ) + ), + satoshis: current.utxo.satoshis, + }) + ) + + if (options.changeAddress) { + unsignedTx.change(options.changeAddress) + } + + if (options.sequence !== undefined) { + unsignedTx.inputs[1].sequenceNumber = options.sequence + } + + if (options.lockTime !== undefined) { + unsignedTx.nLockTime = options.lockTime + } + + return Promise.resolve({ + tx: unsignedTx, + atInputIndex: 1, + nexts: [], + }) + } + ) + + let contractTx = await currentInstance.methods.close( + (sigResps) => findSig(sigResps, publicKeyAuctioneer), + { + fromUTXO, + pubKeyOrAddrToSign: publicKeyAuctioneer, + changeAddress: addressAuctioneer, + lockTime: auctionDeadline + 1, + sequence: 0, + exec: false, // Do not execute the contract yet, only get the created calling transaction. + } as MethodCallOptions + ) + + currentInstance.bindTxBuilder( + 'close', + async ( + current: OrdinalAuction, + options: MethodCallOptions, + sigAuctioneer: Sig, + prevouts: ByteString + ) => { + return Promise.resolve({ + tx: contractTx.tx, + atInputIndex: 1, + nexts: [], + }) + } + ) + + // Assemble prevouts byte string. + let prevouts = toByteString('') + contractTx.tx.inputs.forEach((input) => { + prevouts += reverseByteString( + toByteString(input.prevTxId.toString('hex')), + 32n + ) + prevouts += int2ByteString(BigInt(input.outputIndex), 4n) + }) + + contractTx = await currentInstance.methods.close( + (sigResps) => findSig(sigResps, publicKeyAuctioneer), + { + fromUTXO, + pubKeyOrAddrToSign: publicKeyAuctioneer, + changeAddress: addressAuctioneer, + lockTime: auctionDeadline + 1, + sequence: 0, + } as MethodCallOptions + ) + + const result = contractTx.tx.verifyScript(contractTx.atInputIndex) + expect(result.success, result.error).to.eq(true) + + // If we would like to broadcast, here we need to sign ordinal UTXO input. + }) +}) diff --git a/tests/testnet/ordinalAuction.ts b/tests/testnet/ordinalAuction.ts new file mode 100644 index 00000000..b4a761e0 --- /dev/null +++ b/tests/testnet/ordinalAuction.ts @@ -0,0 +1,286 @@ +import { getDefaultSigner, randomPrivateKey } from '../utils/helper' +import { + bsv, + ByteString, + findSig, + hash160, + int2ByteString, + method, + MethodCallOptions, + PubKey, + PubKeyHash, + reverseByteString, + Sig, + SmartContract, + toByteString, + toHex, + Utils, + UTXO, +} from 'scrypt-ts' +import { myPrivateKey, myPublicKey } from '../utils/privateKey' +import { OrdinalAuction } from '../../src/contracts/ordinalAuction' +import { signTx } from 'scryptlib' + +async function deployOrdinal(dest: PubKeyHash, msg: string): Promise { + const signer = getDefaultSigner() + await signer.provider?.connect() + + const address = await signer.getDefaultAddress() + + // TODO: pick only as many utxos as needed + const utxos = await signer.listUnspent(address) + + // Add msg as text/plain inscription. + const msgBuff = Buffer.from(msg, 'utf8') + const msgHex = msgBuff.toString('hex') + const inscription = bsv.Script.fromASM( + `OP_FALSE OP_IF 6f7264 OP_TRUE 746578742f706c61696e OP_FALSE ${msgHex} OP_ENDIF` + ) + + const unsignedTx = new bsv.Transaction() + .from(utxos) + .addOutput( + new bsv.Transaction.Output({ + script: bsv.Script.fromHex( + Utils.buildPublicKeyHashScript(dest) + ).add(inscription), + satoshis: 1, + }) + ) + .change(address) + + const resp = await signer.signAndsendTransaction(unsignedTx, { address }) + + return { + txId: resp.id, + outputIndex: 0, + script: resp.outputs[0].script.toHex(), + satoshis: resp.outputs[0].satoshis, + } +} + +async function main() { + await OrdinalAuction.compile() + + const privateKeyAuctioneer = myPrivateKey + const publicKeyAuctioneer = myPublicKey + const addressAuctioneer = publicKeyAuctioneer.toAddress() + + const bidderPrivateKeys: bsv.PrivateKey[] = [] + const bidderPublicKeys: bsv.PublicKey[] = [] + const bidderAddresses: bsv.Address[] = [] + for (let i = 0; i < 3; i++) { + const [privateKeyBidder, publicKeyBidder, , addressBidder] = + randomPrivateKey() + bidderPrivateKeys.push(privateKeyBidder) + bidderPublicKeys.push(publicKeyBidder) + bidderAddresses.push(addressBidder) + } + + const auctionDeadline = Math.round(new Date('2020-01-03').valueOf() / 1000) + + const ordinalUTXO = await deployOrdinal( + hash160(publicKeyAuctioneer.toHex()), + 'Hello, sCrypt!' + ) + console.log('Ordinal deployed:', ordinalUTXO.txId) + + const ordinalPrevout: ByteString = + reverseByteString(toByteString(ordinalUTXO.txId), 32n) + + int2ByteString(BigInt(ordinalUTXO.outputIndex), 4n) + + const auction = new OrdinalAuction( + ordinalPrevout, + PubKey(toHex(publicKeyAuctioneer)), + BigInt(auctionDeadline) + ) + + auction.bindTxBuilder('bid', OrdinalAuction.bidTxBuilder) + + await auction.connect(getDefaultSigner(privateKeyAuctioneer)) + + // contract deployment + const minBid = 1 + const deployTx = await auction.deploy(minBid) + console.log('Auction contract deployed: ', deployTx.id) + + let balance = minBid + let currentInstance = auction + + // Perform bidding. + for (let i = 0; i < 3; i++) { + const newHighestBidder = PubKey(toHex(bidderPublicKeys[i])) + const bid = BigInt(balance + 1) + + const nextInstance = currentInstance.next() + nextInstance.bidder = newHighestBidder + + const contractTx = await currentInstance.methods.bid( + newHighestBidder, + bid, + { + changeAddress: bidderAddresses[i], + next: { + instance: nextInstance, + balance: Number(bid), + }, + } as MethodCallOptions + ) + + console.log('Bid Tx:', contractTx.tx.id) + + balance += Number(bid) + currentInstance = nextInstance + } + + // Close the auction + currentInstance.bindTxBuilder( + 'close', + async ( + current: OrdinalAuction, + options: MethodCallOptions, + sigAuctioneer: Sig, + prevouts: ByteString + ) => { + const unsignedTx: bsv.Transaction = new bsv.Transaction() + + // add input that unlocks ordinal UTXO + unsignedTx + .addInput( + new bsv.Transaction.Input({ + prevTxId: ordinalUTXO.txId, + outputIndex: ordinalUTXO.outputIndex, + script: bsv.Script.fromHex('00'.repeat(34)), + }), + bsv.Script.fromHex(ordinalUTXO.script), + ordinalUTXO.satoshis + ) + .addInput(current.buildContractInput(options.fromUTXO)) + + //// Add all fee inputs here as well. + //.from(feeUTXO) + + // build ordinal destination output + unsignedTx + .addOutput( + new bsv.Transaction.Output({ + script: bsv.Script.fromHex( + Utils.buildPublicKeyHashScript( + hash160(current.bidder) + ) + ), + satoshis: 1, + }) + ) + // build auctioneer payment output + .addOutput( + new bsv.Transaction.Output({ + script: bsv.Script.fromHex( + Utils.buildPublicKeyHashScript( + hash160(current.auctioneer) + ) + ), + satoshis: current.utxo.satoshis, + }) + ) + + if (options.changeAddress) { + unsignedTx.change(options.changeAddress) + } + + if (options.sequence !== undefined) { + unsignedTx.inputs[1].sequenceNumber = options.sequence + } + + if (options.lockTime !== undefined) { + unsignedTx.nLockTime = options.lockTime + } + + return Promise.resolve({ + tx: unsignedTx, + atInputIndex: 1, + nexts: [], + }) + } + ) + + let contractTx = await currentInstance.methods.close( + (sigResps) => findSig(sigResps, publicKeyAuctioneer), + toByteString('00'.repeat(254)), // Fill with some dummy data to compensate for the fee after we pass the real data. + { + pubKeyOrAddrToSign: publicKeyAuctioneer, + changeAddress: addressAuctioneer, + lockTime: auctionDeadline + 1, + sequence: 0, + partiallySigned: true, + exec: false, // Do not execute the contract yet, only get the created calling transaction. + } as MethodCallOptions + ) + + // If we would like to broadcast, here we need to sign ordinal UTXO input. + const ordinalSig = signTx( + contractTx.tx, + privateKeyAuctioneer, + bsv.Script.fromHex(ordinalUTXO.script), + ordinalUTXO.satoshis, + 0 + ) + contractTx.tx.inputs[0] = new bsv.Transaction.Input({ + prevTxId: ordinalUTXO.txId, + outputIndex: ordinalUTXO.outputIndex, + script: bsv.Script.fromASM( + `${ordinalSig} ${publicKeyAuctioneer.toHex()}` + ), + }) + contractTx.tx.inputs[0].output = new bsv.Transaction.Output({ + script: bsv.Script.fromHex(ordinalUTXO.script), + satoshis: ordinalUTXO.satoshis, + }) + + // Bind tx builder, that just simply re-uses the tx we created above. + currentInstance.bindTxBuilder( + 'close', + async ( + current: OrdinalAuction, + options: MethodCallOptions, + sigAuctioneer: Sig, + prevouts: ByteString + ) => { + return Promise.resolve({ + tx: contractTx.tx, + atInputIndex: 1, + nexts: [], + }) + } + ) + + // Assemble prevouts byte string. + let prevouts = toByteString('') + contractTx.tx.inputs.forEach((input) => { + prevouts += reverseByteString( + toByteString(input.prevTxId.toString('hex')), + 32n + ) + prevouts += int2ByteString(BigInt(input.outputIndex), 4n) + }) + + contractTx = await currentInstance.methods.close( + (sigResps) => findSig(sigResps, publicKeyAuctioneer), + prevouts, + { + pubKeyOrAddrToSign: publicKeyAuctioneer, + changeAddress: addressAuctioneer, + lockTime: auctionDeadline + 1, + sequence: 0, + autoPayFee: false, + } as MethodCallOptions + ) + + console.log('Close Tx: ', contractTx.tx.id) +} + +describe('Test SmartContract `OrdinalAuction` on testnet', () => { + it('should succeed', async () => { + await main() + }) +}) diff --git a/tests/testnet/p2pkh-anyonecanpay.ts b/tests/testnet/p2pkh-anyonecanpay.ts index 01024027..6e4c930a 100644 --- a/tests/testnet/p2pkh-anyonecanpay.ts +++ b/tests/testnet/p2pkh-anyonecanpay.ts @@ -1,6 +1,6 @@ import { P2PKH } from '../../src/contracts/p2pkh' -import { getDefaultSigner } from '../utils/helper' -import { myPublicKey, myPublicKeyHash } from '../utils/privateKey' +import { getDefaultSigner, sleep } from '../utils/helper' +import { myPrivateKey, myPublicKey, myPublicKeyHash } from '../utils/privateKey' import { findSig, @@ -10,6 +10,8 @@ import { toHex, bsv, ContractTransaction, + DefaultProvider, + TestWallet, } from 'scrypt-ts' async function main() { @@ -17,12 +19,23 @@ async function main() { const p2pkh = new P2PKH(PubKeyHash(toHex(myPublicKeyHash))) // Signer who unlocks / signs P2PKH UTXO. - const mainSigner = getDefaultSigner() + const mainSigner = new TestWallet( + myPrivateKey, + new DefaultProvider({ + network: bsv.Networks.testnet, + }) + ) // Signer who pays fee. // For simplicity here, we just again use the same default signer, but it // could be any other signer. - const feeSigner = getDefaultSigner() + const feeSigner = new TestWallet(myPrivateKey) + + await feeSigner.connect( + new DefaultProvider({ + network: bsv.Networks.testnet, + }) + ) // Connect the signer. await p2pkh.connect(mainSigner) @@ -86,6 +99,7 @@ async function main() { // Get UTXOs for for the signer, who will pay the fee. const address = await feeSigner.getDefaultAddress() + await sleep(3) const utxos = await feeSigner.listUnspent(address) // Spend retrieved UTXOs to pay the transaction fee. Any change will @@ -101,8 +115,117 @@ async function main() { console.log('P2PKH contract called: ', callTx.id) } +async function main2() { + const p2pkh = new P2PKH(PubKeyHash(toHex(myPublicKeyHash))) + + // Signer who unlocks / signs P2PKH UTXO. + const mainSigner = new TestWallet( + myPrivateKey, + new DefaultProvider({ + network: bsv.Networks.testnet, + }) + ) + + // Signer who pays fee. + // For simplicity here, we just again use the same default signer, but it + // could be any other signer. + const feeSigner = new TestWallet(myPrivateKey) + + await feeSigner.connect( + new DefaultProvider({ + network: bsv.Networks.testnet, + }) + ) + + // Connect the signer. + await p2pkh.connect(mainSigner) + + // Deploy the P2PKH contract. + const deployTx = await p2pkh.deploy(1) + console.log('P2PKH contract deployed: ', deployTx.id) + + // Bind custom call tx builder. It adds a single input, which will call + // our deployed smart contracts "unlock" method. Additionally, it adds an + // unspendable OP_RETURN output. + p2pkh.bindTxBuilder( + 'unlock', + async ( + current: P2PKH, + options: MethodCallOptions + ): Promise => { + const tx = new bsv.Transaction() + + // Get UTXOs for for the signer, who will pay the fee. + const address = await feeSigner.getDefaultAddress() + + await sleep(3) + const utxos = await feeSigner.listUnspent(address) + + // Spend retrieved UTXOs to pay the transaction fee. Any change will + // be returned to the fee signers address. + tx.from(utxos) + + tx.addInput(current.buildContractInput()).addOutput( + new bsv.Transaction.Output({ + script: bsv.Script.fromASM('OP_FALSE OP_RETURN 0101'), + satoshis: 0, + }) + ) + + return { + tx: tx, + /** The input index of previous contract UTXO to spend in the method calling tx */ + atInputIndex: utxos.length, + nexts: [], + } + } + ) + + // Construct the call tx locally (notice the "pratiallySigned" flag). + // Use the ANYONECANPAY_SINGLE sighash flag to sign the first input. + const sigHashType = bsv.crypto.Signature.ANYONECANPAY_SINGLE + const { tx: callTx } = await p2pkh.methods.unlock( + // pass signature, the first parameter, to `unlock` + // after the signer signs the transaction, the signatures are returned in `SignatureResponse[]` + // you need to find the signature or signatures you want in the return through the public key or address + // here we use `myPublicKey` to find the signature because we signed the transaction with `myPrivateKey` before + (sigResps) => findSig(sigResps, myPublicKey, sigHashType), + // pass public key, the second parameter, to `unlock` + PubKey(toHex(myPublicKey)), + // method call options + { + // tell the signer to use the private key corresponding to `myPublicKey` to sign this transaction + // that is using `myPrivateKey` to sign the transaction + pubKeyOrAddrToSign: { + pubKeyOrAddr: myPublicKey, + sigHashType: sigHashType, + }, + // this flag will make the call tx not broadcast, but only be created locally + partiallySigned: true, + // don't auto-add any fee inputs + autoPayFee: false, + } as MethodCallOptions + ) + + const address = await feeSigner.getDefaultAddress() + callTx.change(address) + + // Finally, sign the newly added inputs and broadcast the modified transaction. + // Notice, that if the main singer wouldn't use the ANYONECANPAY_SINGLE sighash flag, + // Then the call to the "unlock" method (first input) wouldn't successfully evaluate anymore. + + await feeSigner.signAndsendTransaction(callTx, { address }) + + console.log('P2PKH contract called: ', callTx.id) +} + describe('Test SmartContract `P2PKH` with ANYONECANPAY_SINGLE on testnet', () => { it('should succeed', async () => { + await P2PKH.compile() + // contract at first inputIndex await main() + await sleep(5) + // contract at third inputIndex + await main2() }) })