Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lock to mint #5

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
278 changes: 173 additions & 105 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
]
},
"dependencies": {
"@cat-protocol/cat-sdk": "^1.0.4",
"@cat-protocol/cat-sdk": "^1.0.17",
"@mempool/mempool.js": "^2.3.0",
"bigi": "^1.4.2",
"bitcore-lib-inquisition": "^10.0.30",
Expand Down
237 changes: 237 additions & 0 deletions src/contracts/cat721/lockToMint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import {
CAT20Proto,
CAT20State,
CAT721Proto,
CAT721State,
ChangeInfo,
PreTxStatesInfo,
PrevoutsCtx,
SHPreimage,
STATE_OUTPUT_INDEX,
SigHashUtils,
SpentScriptsCtx,
StateUtils,
TxProof,
TxUtil,
TxoStateHashes,
XrayedTxIdPreimg2,
int32,
} from '@cat-protocol/cat-sdk'
import {
ByteString,
PubKey,
Sig,
SmartContract,
assert,
hash160,
int2ByteString,
len,
method,
prop,
sha256,
toByteString,
} from 'scrypt-ts'

export const MINIMAL_LOCKED_BLOCKS = 17n

export class LockToMint extends SmartContract {
@prop()
cat721Script: ByteString

@prop()
cat20Script: ByteString

@prop()
lockTokenAmount: int32

@prop()
nonce: ByteString

@prop()
lockedBlocks: bigint

constructor(
cat721Script: ByteString,
cat20Script: ByteString,
lockTokenAmount: int32,
nonce: ByteString,
lockedBlocks: bigint
) {
super(...arguments)
this.cat721Script = cat721Script
this.cat20Script = cat20Script
this.lockTokenAmount = lockTokenAmount
this.nonce = nonce
this.lockedBlocks = lockedBlocks
}

@method()
static buildCatTimeLockP2wsh(
pubkey: ByteString,
nonce: ByteString,
lockedBlocks: int32
): ByteString {
// exec in legacy bvm runtime, ecdsa sig
// <pubkey1><pubkey2><nonce><lockedBlocks>76b2516d005579515679567952ae6b6d6d6c77
let pubkey1 = pubkey
let pubkey2 = pubkey
if (len(pubkey) == 32n) {
pubkey1 = toByteString('02') + pubkey
pubkey2 = toByteString('03') + pubkey
}
assert(lockedBlocks >= MINIMAL_LOCKED_BLOCKS)
const lockedBlocksBytes = int2ByteString(lockedBlocks)
return (
toByteString('0020') +
sha256(
toByteString('21') +
pubkey1 +
toByteString('21') +
pubkey2 +
int2ByteString(len(nonce)) +
nonce +
int2ByteString(len(lockedBlocksBytes)) +
lockedBlocksBytes +
toByteString('76b2516d005579515679567952ae6b6d6d6c77')
)
)
}

@method()
public claimNft(
//
curTxoStateHashes: TxoStateHashes,
nftReceiver: CAT721State,
// cat20 tx info
cat20Tx: XrayedTxIdPreimg2,
cat20OutputVal: int32,
cat20OutputIndex: ByteString,
cat20State: CAT20State,
cat20TxStatesInfo: PreTxStatesInfo,
// cat20 owner pubkey and sig
cat20OwnerPubKeyPrefix: ByteString,
cat20OwnerPubkeyX: PubKey,
cat20OwnerPubkeySig: Sig,
//
cat20Change: int32,
// satoshis locked in contract
contractSatoshis: ByteString,
// ctxs
shPreimage: SHPreimage,
prevoutsCtx: PrevoutsCtx,
spentScriptsCtx: SpentScriptsCtx,
changeInfo: ChangeInfo
) {
// Check sighash preimage.
assert(
this.checkSig(
SigHashUtils.checkSHPreimage(shPreimage),
SigHashUtils.Gx
),
'preimage check error'
)
// check ctx
SigHashUtils.checkPrevoutsCtx(
prevoutsCtx,
shPreimage.hashPrevouts,
shPreimage.inputIndex
)
SigHashUtils.checkSpentScriptsCtx(
spentScriptsCtx,
shPreimage.hashSpentScripts
)
// ensure input 0 is nft input
assert(spentScriptsCtx[0] == this.cat721Script)
// ensure input 1 is token input
assert(spentScriptsCtx[1] == this.cat20Script)

// verify cat20 state
const catTx20Txid = TxProof.getTxIdFromPreimg2(cat20Tx)
TxUtil.checkIndex(cat20OutputVal, cat20OutputIndex)
assert(catTx20Txid + cat20OutputIndex == prevoutsCtx.prevouts[1])
// verifyPreStateHash
StateUtils.verifyPreStateHash(
cat20TxStatesInfo,
CAT20Proto.stateHash(cat20State),
cat20Tx.outputScriptList[STATE_OUTPUT_INDEX],
cat20OutputVal
)

// verify pubkey can sig, exec in taproot bvm runtime, schnorr sig
const pubkey = cat20OwnerPubKeyPrefix + cat20OwnerPubkeyX
assert(hash160(pubkey) == cat20State.ownerAddr)
this.checkSig(cat20OwnerPubkeySig, cat20OwnerPubkeyX)

// build catTimeLock p2wsh
const timeLockScript = LockToMint.buildCatTimeLockP2wsh(
pubkey,
this.nonce,
this.lockedBlocks
)

// build outputs
let curStateHashes = toByteString('')
let curStateCnt = 2n

// nft to user
curStateHashes += hash160(CAT721Proto.stateHash(nftReceiver))
const nftOutput = TxUtil.buildOutput(
this.cat721Script,
contractSatoshis
)

// token to lock contract address
curStateHashes += hash160(
CAT20Proto.stateHash({
amount: this.lockTokenAmount,
ownerAddr: hash160(timeLockScript),
})
)
const tokenOutput = TxUtil.buildOutput(
this.cat20Script,
contractSatoshis
)

// if change token amount more than 0, change to user
let tokenChangeOutput = toByteString('')
if (cat20Change > 0n) {
// cat20State
curStateCnt += 1n
curStateHashes += hash160(
CAT20Proto.stateHash({
ownerAddr: cat20State.ownerAddr,
amount: cat20Change,
})
)
tokenChangeOutput = TxUtil.buildOutput(
this.cat20Script,
contractSatoshis
)
}

// time lock output
const catTimeLockOutput = TxUtil.buildOutput(
timeLockScript,
contractSatoshis
)

// change satoshi
const changeOutput = TxUtil.getChangeOutput(changeInfo)

// final build state output
const stateOutput = StateUtils.getCurrentStateOutput(
curStateHashes,
curStateCnt,
curTxoStateHashes
)
const hashOutputs = sha256(
stateOutput +
nftOutput +
tokenOutput +
tokenChangeOutput +
catTimeLockOutput +
changeOutput
)
assert(hashOutputs == shPreimage.hashOutputs, 'hashOutputs mismatch')
}
}
145 changes: 145 additions & 0 deletions src/covenants/cat721/lockToMintCovenant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import {
CAT20State,
CAT721State,
CatPsbt,
ChangeInfo,
Covenant,
InputContext,
Postage,
PreTxStatesInfo,
SubContractCall,
SupportedNetwork,
TapLeafSmartContract,
XrayedTxIdPreimg2,
int32,
pubKeyPrefix,
toXOnly,
} from '@cat-protocol/cat-sdk'
import { ByteString, Sig, int2ByteString } from 'scrypt-ts'
import { LockToMint } from '../../contracts/cat721/lockToMint'

export class LockToMintCovenant extends Covenant {
static readonly LOCKED_ASM_VERSION = '9d317c35a3c058081a43abffb8f2324f'

constructor(
cat721Script: ByteString,
cat20Script: ByteString,
lockTokenAmount: int32,
nonce: ByteString,
lockedBlocks: bigint,
network?: SupportedNetwork
) {
super(
[
{
contract: new LockToMint(
cat721Script,
cat20Script,
BigInt(lockTokenAmount),
nonce,
lockedBlocks
),
},
],
{
lockedAsmVersion: LockToMintCovenant.LOCKED_ASM_VERSION,
network,
}
)
}

serializedState() {
return ''
}

claimNft(
inputIndex: number,
inputCtxs: Map<number, InputContext>,
nftReceiver: CAT721State,
cat20Tx: XrayedTxIdPreimg2,
cat20OutputVal: int32,
cat20OutputIndex: ByteString,
cat20State: CAT20State,
cat20TxStatesInfo: PreTxStatesInfo,
cat20OwnerSig: {
isP2TR: boolean
pubKey: ByteString
},
cat20Change: int32,
serviceFeeInfo: ChangeInfo
): SubContractCall {
return {
method: 'claimNft',
argsBuilder: this.unlockArgsBuilder(
inputIndex,
inputCtxs,
nftReceiver,
cat20Tx,
cat20OutputVal,
cat20OutputIndex,
cat20State,
cat20TxStatesInfo,
cat20OwnerSig,
cat20Change,
serviceFeeInfo
),
}
}

private unlockArgsBuilder(
inputIndex: number,
inputCtxs: Map<number, InputContext>,
nftReceiver: CAT721State,
cat20Tx: XrayedTxIdPreimg2,
cat20OutputVal: int32,
cat20OutputIndex: ByteString,
cat20State: CAT20State,
cat20TxStatesInfo: PreTxStatesInfo,
cat20OwnerSig: {
isP2TR: boolean
pubKey: ByteString
},
cat20Change: int32,
serviceFeeInfo: ChangeInfo
) {
const inputCtx = inputCtxs.get(inputIndex)
if (!inputCtx) {
throw new Error('Input context is not available')
}

return (curPsbt: CatPsbt, tapLeafContract: TapLeafSmartContract) => {
const { shPreimage, prevoutsCtx, spentScriptsCtx } = inputCtx
const args = []
args.push(curPsbt.txState.stateHashList) //curTxoStateHashes
args.push(nftReceiver) //
// cat20 tx info
args.push(cat20Tx) //
args.push(cat20OutputVal) //
args.push(cat20OutputIndex) //
args.push(cat20State) //
args.push(cat20TxStatesInfo) //
// cat20 owner pubkey and sig
args.push(
cat20OwnerSig.isP2TR ? '' : pubKeyPrefix(cat20OwnerSig.pubKey)
)
args.push(toXOnly(cat20OwnerSig.pubKey, cat20OwnerSig.isP2TR))
args.push(() =>
Sig(
curPsbt.getSig(inputIndex, {
publicKey: cat20OwnerSig.pubKey,
disableTweakSigner: cat20OwnerSig.isP2TR ? false : true,
})
)
)
args.push(cat20Change) //
// satoshis locked in contract
args.push(int2ByteString(BigInt(Postage.TOKEN_POSTAGE), 8n)) // nftSatoshiBytes
// ctxs
args.push(shPreimage) // shPreimage
args.push(prevoutsCtx) // prevoutsCtx
args.push(spentScriptsCtx) // spentScriptsCtx
args.push(serviceFeeInfo) // changeInfo
return args
}
}
}
Loading