diff --git a/src/contracts/bsv20Option.ts b/src/contracts/bsv20CallOption.ts similarity index 98% rename from src/contracts/bsv20Option.ts rename to src/contracts/bsv20CallOption.ts index 3cb6249b..a8212b61 100644 --- a/src/contracts/bsv20Option.ts +++ b/src/contracts/bsv20CallOption.ts @@ -11,7 +11,7 @@ import { Utils, } from 'scrypt-ts' -export class Bsv20Option extends BSV20V2 { +export class Bsv20CallOption extends BSV20V2 { @prop() grantor: PubKey diff --git a/src/contracts/bsv20PutOption.ts b/src/contracts/bsv20PutOption.ts new file mode 100644 index 00000000..842d48e7 --- /dev/null +++ b/src/contracts/bsv20PutOption.ts @@ -0,0 +1,163 @@ +import { BSV20V2 } from 'scrypt-ord' +import { + assert, + ByteString, + byteString2Int, + hash256, + method, + prop, + PubKey, + pubKey2Addr, + Sig, + slice, + Utils, +} from 'scrypt-ts' +import { RabinPubKey, RabinSig, RabinVerifier } from 'scrypt-ts-lib' + +export class Bsv20PutOption extends BSV20V2 { + @prop() + grantor: PubKey + + @prop(true) + grantee: PubKey + + @prop() + tokenAmt: bigint + + @prop() + strikePrice: bigint + + @prop() + expirationTime: bigint + + @prop(true) + forSale: boolean + + @prop(true) + premium: bigint + + @prop() + oraclePubKey: RabinPubKey + + constructor( + id: ByteString, + sym: ByteString, + max: bigint, + dec: bigint, + grantor: PubKey, + grantee: PubKey, + tokenAmt: bigint, + strikePrice: bigint, + expirationTime: bigint, + forSale: boolean, + premium: bigint, + oraclePubKey: RabinPubKey + ) { + super(id, sym, max, dec) + this.init(...arguments) + + this.grantor = grantor + this.grantee = grantee + this.tokenAmt = tokenAmt + this.strikePrice = strikePrice + this.expirationTime = expirationTime + this.forSale = forSale + this.premium = premium + this.oraclePubKey = oraclePubKey + } + + @method() + public exercise( + sigGrantee: Sig, + oracleSig: RabinSig, + oracleMsg: ByteString + ) { + // Check oracle signature. + assert( + RabinVerifier.verifySig(oracleMsg, oracleSig, this.oraclePubKey), + 'oracle sig verify failed' + ) + + // Check that we're unlocking the UTXO specified in the oracles message. + assert( + slice(this.prevouts, 0n, 36n) == slice(oracleMsg, 0n, 36n), + 'first input is not spending specified ordinal UTXO' + ) + + // Get token amount held by the UTXO from oracle message. + const utxoTokenAmt = byteString2Int(slice(oracleMsg, 36n, 44n)) + + // Check token amount is correct. + assert(utxoTokenAmt == this.tokenAmt, 'invalid token amount') + + // Check grantee sig. + assert(this.checkSig(sigGrantee, this.grantee), 'invalid sig grantee') + + // Ensure grantor gets payed tokens. + let outputs = BSV20V2.buildTransferOutput( + pubKey2Addr(this.grantor), + this.id, + this.tokenAmt + ) + + // Ensure grantee gets payed satoshis. + const satAmt = this.strikePrice * this.tokenAmt + outputs += Utils.buildAddressOutput(pubKey2Addr(this.grantee), satAmt) + + outputs += this.buildChangeOutput() + + // Enforce outputs in call tx. + assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs mismatch') + } + + @method() + public buy(newGrantee: PubKey) { + // Check if option is up for sale. + assert(this.forSale, 'option is not up for sale') + + // Set new grantee. + const prevGrantee = this.grantee + this.grantee = newGrantee + + // Toggle for sale flag. + this.forSale = false + + // Propagate contract. + let outputs = this.buildStateOutput(this.ctx.utxo.value) + + // Make sure premium is payed to previous grantee / holder. + outputs += Utils.buildAddressOutput( + pubKey2Addr(prevGrantee), + this.premium + ) + + outputs += this.buildChangeOutput() + assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs mismatch') + } + + @method() + public listForSale(sigGrantee: Sig, premium: bigint) { + // Check grantee sig. + assert(this.checkSig(sigGrantee, this.grantee), 'invalid sig grantee') + + // Store premium value in property. + this.premium = premium + + // Toggle for sale flag. + this.forSale = true + + // Propagate contract. + let outputs = this.buildStateOutput(this.ctx.utxo.value) + outputs += this.buildChangeOutput() + assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs mismatch') + } + + @method() + public expiry(sigGrantor: Sig) { + // Check grantor sig. + assert(this.checkSig(sigGrantor, this.grantor), 'invalid sig grantor') + + // Check if expired. + assert(this.timeLock(this.expirationTime), 'option has not yet expired') + } +} diff --git a/src/contracts/bsv20SellLimitOrder.ts b/src/contracts/bsv20SellLimitOrder.ts index a847d941..a9a64551 100644 --- a/src/contracts/bsv20SellLimitOrder.ts +++ b/src/contracts/bsv20SellLimitOrder.ts @@ -56,7 +56,7 @@ export class BSV20SellLimitOrder extends BSV20V2 { } @method() - public buy(amount: bigint, buyerAddr: Addr) { + public buy(amount: bigint) { // Check token amount doesn't exceed total. assert( this.tokenAmtSold + amount < this.tokenAmt,