diff --git a/modules/ecash-agora/src/partial.script.slp.test.ts b/modules/ecash-agora/src/partial.script.slp.test.ts new file mode 100644 --- /dev/null +++ b/modules/ecash-agora/src/partial.script.slp.test.ts @@ -0,0 +1,203 @@ +// Copyright (c) 2024 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import { expect } from 'chai'; +import { + DEFAULT_DUST_LIMIT, + fromHex, + SLP_FUNGIBLE, + SLP_MINT_VAULT, + SLP_NFT1_GROUP, + toHex, +} from 'ecash-lib'; +import { AgoraPartial } from './partial.js'; + +const makerPk = fromHex( + '03000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f', +); +const tokenId = + '707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f'; +const BASE_PARAMS_SLP = { + makerPk, + tokenId, + tokenProtocol: 'SLP' as const, + dustAmount: DEFAULT_DUST_LIMIT, +}; + +describe('AgoraPartial.script SLP', () => { + it('AgoraPartial.script SLP 0,0 1000', () => { + const agoraPartial = new AgoraPartial({ + truncTokens: 1000n, + numTokenTruncBytes: 0, + tokenScaleFactor: 1000000n, + scaledTruncTokensPerTruncSat: 1000000n, + numSatsTruncBytes: 0, + minAcceptedScaledTruncTokens: 1000000n, + ...BASE_PARAMS_SLP, + tokenType: SLP_FUNGIBLE, + scriptLen: 0x7f, + }); + agoraPartial.updateScriptLen(); + expect(agoraPartial.scriptLen).to.equal(207); + expect(toHex(agoraPartial.script().bytecode)).to.equal( + '4c726a04534c500001010453454e4420707172737475767778797a7b7c7d7e7f' + + '808182838485868788898a8b8c8d8e8f080000000000000000000040420f0000' + + '00000040420f000000000040420f000000000003000102030405060708090a0b' + + '0c0d0e0f101112131415161718191a1b1c1d1e1f0800ca9a3b00000000ab7b63' + + '817b6ea269760340420fa269760340420f9700887d94527901377f7578926358' + + '7e780340420f965880bc007e7e68587e52790340420f965880bc007e7e825980' + + 'bc7c7e007e7b033f420f930340420f9658807e041976a914707501517f77a97e' + + '0288ac7e7e6b7d02220258800317a9147e024c7272587d807e7e7e01ab7e5379' + + '01257f7702cf007f5c7f7701207f756b7ea97e01877e7c92647500687b829269' + + '7e6c6c7b7eaa88520144807c7ea86f7bbb7501c17e7c677501517f7768ad0750' + + '41525449414c88044147523087', + ); + }); + + it('AgoraPartial.script SLP 1,1 10000', () => { + const agoraPartial = new AgoraPartial({ + truncTokens: 10000n, + numTokenTruncBytes: 1, + tokenScaleFactor: 2000n, + scaledTruncTokensPerTruncSat: 2000n, + numSatsTruncBytes: 1, + minAcceptedScaledTruncTokens: 2000n, + ...BASE_PARAMS_SLP, + tokenType: SLP_MINT_VAULT, + scriptLen: 0x7f, + }); + agoraPartial.updateScriptLen(); + expect(agoraPartial.scriptLen).to.equal(204); + expect(toHex(agoraPartial.script().bytecode)).to.equal( + '4c726a04534c500001020453454e4420707172737475767778797a7b7c7d7e7f' + + '808182838485868788898a8b8c8d8e8f0800000000000000000101d007000000' + + '000000d007000000000000d00700000000000003000102030405060708090a0b' + + '0c0d0e0f101112131415161718191a1b1c1d1e1f08002d310100000000ab7b63' + + '817b6ea2697602d007a2697602d0079700887d94527901377f75789263587e78' + + '02d007965780bc01007e7e68587e527902d007965780bc01007e7e825980bc7c' + + '7e01007e7b02cf079302d0079657807e041976a914707501517f77a97e0288ac' + + '7e7e6b7d02220258800317a9147e024c7272587d807e7e7e01ab7e537901257f' + + '7702cc007f5c7f7701207f756b7ea97e01877e7c92647500687b8292697e6c6c' + + '7b7eaa88520144807c7ea86f7bbb7501c17e7c677501517f7768ad0750415254' + + '49414c88044147523087', + ); + }); + + it('AgoraPartial.script SLP 2,2 0x30313233', () => { + const agoraPartial = new AgoraPartial({ + truncTokens: 0x30313233n, + numTokenTruncBytes: 2, + tokenScaleFactor: 0x12345678n, + scaledTruncTokensPerTruncSat: 0x98765432n, + numSatsTruncBytes: 2, + minAcceptedScaledTruncTokens: 0x454647n, + ...BASE_PARAMS_SLP, + tokenType: SLP_NFT1_GROUP, + scriptLen: 0x7f, + }); + agoraPartial.updateScriptLen(); + expect(agoraPartial.scriptLen).to.equal(220); + expect(toHex(agoraPartial.script().bytecode)).to.equal( + '4c726a04534c500001810453454e4420707172737475767778797a7b7c7d7e7f' + + '808182838485868788898a8b8c8d8e8f08000000000000000002027856341200' + + '0000003254769800000000474645000000000003000102030405060708090a0b' + + '0c0d0e0f101112131415161718191a1b1c1d1e1f08e8a948e6cc4f6d03ab7b63' + + '817b6ea2697603474645a2697604785634129700887d94527901377f75789263' + + '587e780478563412965680bc0200007e7e68587e52790478563412965680bc02' + + '00007e7e825980bc7c7e0200007e7b053154769800930532547698009656807e' + + '041976a914707501517f77a97e0288ac7e7e6b7d02220258800317a9147e024c' + + '7272587d807e7e7e01ab7e537901257f7702dc007f5c7f7701207f756b7ea97e' + + '01877e7c92647500687b8292697e6c6c7b7eaa88520144807c7ea86f7bbb7501' + + 'c17e7c677501517f7768ad075041525449414c88044147523087', + ); + }); + + it('AgoraPartial.script SLP 3,3 0x7fffffff', () => { + const agoraPartial = new AgoraPartial({ + truncTokens: 0x7fffffffn, + numTokenTruncBytes: 3, + tokenScaleFactor: 0x7fffffffn, + scaledTruncTokensPerTruncSat: 0x7fffffffn, + numSatsTruncBytes: 3, + minAcceptedScaledTruncTokens: 0x7fffffffn, + ...BASE_PARAMS_SLP, + tokenType: SLP_FUNGIBLE, + scriptLen: 0x7f, + }); + agoraPartial.updateScriptLen(); + expect(agoraPartial.scriptLen).to.equal(222); + expect(toHex(agoraPartial.script().bytecode)).to.equal( + '4c726a04534c500001010453454e4420707172737475767778797a7b7c7d7e7f' + + '808182838485868788898a8b8c8d8e8f0800000000000000000303ffffff7f00' + + '000000ffffff7f00000000ffffff7f0000000003000102030405060708090a0b' + + '0c0d0e0f101112131415161718191a1b1c1d1e1f0801000000ffffff3fab7b63' + + '817b6ea2697604ffffff7fa2697604ffffff7f9700887d94527901377f757892' + + '63587e7804ffffff7f965580bc030000007e7e68587e527904ffffff7f965580' + + 'bc030000007e7e825980bc7c7e030000007e7b04feffff7f9304ffffff7f9655' + + '807e041976a914707501517f77a97e0288ac7e7e6b7d02220258800317a9147e' + + '024c7272587d807e7e7e01ab7e537901257f7702de007f5c7f7701207f756b7e' + + 'a97e01877e7c92647500687b8292697e6c6c7b7eaa88520144807c7ea86f7bbb' + + '7501c17e7c677501517f7768ad075041525449414c88044147523087', + ); + }); + + it('AgoraPartial.script SLP 4,4 0x7fff', () => { + const agoraPartial = new AgoraPartial({ + truncTokens: 0x7fffn, + numTokenTruncBytes: 4, + tokenScaleFactor: 0x6fffn, + scaledTruncTokensPerTruncSat: 0x5fffn, + numSatsTruncBytes: 4, + minAcceptedScaledTruncTokens: 0x4fffn, + ...BASE_PARAMS_SLP, + tokenType: SLP_MINT_VAULT, + scriptLen: 0x7f, + }); + agoraPartial.updateScriptLen(); + expect(agoraPartial.scriptLen).to.equal(213); + expect(toHex(agoraPartial.script().bytecode)).to.equal( + '4c726a04534c500001020453454e4420707172737475767778797a7b7c7d7e7f' + + '808182838485868788898a8b8c8d8e8f0800000000000000000404ff6f000000' + + '000000ff5f000000000000ff4f00000000000003000102030405060708090a0b' + + '0c0d0e0f101112131415161718191a1b1c1d1e1f080110ff3700000000ab7b63' + + '817b6ea2697602ff4fa2697602ff6f9700887d94527901377f75789263587e78' + + '02ff6f965480bc04000000007e7e68587e527902ff6f965480bc04000000007e' + + '7e825980bc7c7e04000000007e7b02fe5f9302ff5f9654807e041976a9147075' + + '01517f77a97e0288ac7e7e6b7d02220258800317a9147e024c7272587d807e7e' + + '7e01ab7e537901257f7702d5007f5c7f7701207f756b7ea97e01877e7c926475' + + '00687b8292697e6c6c7b7eaa88520144807c7ea86f7bbb7501c17e7c67750151' + + '7f7768ad075041525449414c88044147523087', + ); + }); + + it('AgoraPartial.script SLP 5,4 0x7fff', () => { + const agoraPartial = new AgoraPartial({ + truncTokens: 0xffffffn, + numTokenTruncBytes: 5, + tokenScaleFactor: 0x8123fffn, + scaledTruncTokensPerTruncSat: 0x4123fffn, + numSatsTruncBytes: 4, + minAcceptedScaledTruncTokens: 0x2123fffn, + ...BASE_PARAMS_SLP, + tokenType: SLP_FUNGIBLE, + scriptLen: 0x7f, + }); + agoraPartial.updateScriptLen(); + expect(agoraPartial.scriptLen).to.equal(233); + expect(toHex(agoraPartial.script().bytecode)).to.equal( + '4c726a04534c500001010453454e4420707172737475767778797a7b7c7d7e7f' + + '808182838485868788898a8b8c8d8e8f0800000000000000000504ff3f120800' + + '000000ff3f120400000000ff3f12020000000003000102030405060708090a0b' + + '0c0d0e0f101112131415161718191a1b1c1d1e1f0801c0edf63f120800ab7b63' + + '817b6ea2697604ff3f1202a2697604ff3f12089700887d94527901377f757892' + + '63587e7804ff3f1208965480537f75bc0500000000007e7e68587e527904ff3f' + + '1208965480537f75bc0500000000007e7e825980bc7c7e04000000007e7b04fe' + + '3f12049304ff3f12049654807e041976a914707501517f77a97e0288ac7e7e6b' + + '7d02220258800317a9147e024c7272587d807e7e7e01ab7e537901257f7702e9' + + '007f5c7f7701207f756b7ea97e01877e7c92647500687b8292697e6c6c7b7eaa' + + '88520144807c7ea86f7bbb7501c17e7c677501517f7768ad075041525449414c' + + '88044147523087', + ); + }); +}); diff --git a/modules/ecash-agora/src/partial.ts b/modules/ecash-agora/src/partial.ts --- a/modules/ecash-agora/src/partial.ts +++ b/modules/ecash-agora/src/partial.ts @@ -2,7 +2,74 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. -import { DEFAULT_DUST_LIMIT } from 'ecash-lib'; +import { + ALL_ANYONECANPAY_BIP143, + ALL_BIP143, + DEFAULT_DUST_LIMIT, + Ecc, + flagSignature, + Op, + OP_0, + OP_0NOTEQUAL, + OP_1, + OP_12, + OP_2, + OP_2DUP, + OP_2OVER, + OP_2SWAP, + OP_3DUP, + OP_8, + OP_9, + OP_ADD, + OP_BIN2NUM, + OP_CAT, + OP_CHECKDATASIGVERIFY, + OP_CHECKSIG, + OP_CHECKSIGVERIFY, + OP_CODESEPARATOR, + OP_DIV, + OP_DROP, + OP_DUP, + OP_ELSE, + OP_ENDIF, + OP_EQUAL, + OP_EQUALVERIFY, + OP_FROMALTSTACK, + OP_GREATERTHANOREQUAL, + OP_HASH160, + OP_HASH256, + OP_IF, + OP_MOD, + OP_NIP, + OP_NOTIF, + OP_NUM2BIN, + OP_OVER, + OP_PICK, + OP_PUSHDATA1, + OP_REVERSEBYTES, + OP_ROT, + OP_SHA256, + OP_SIZE, + OP_SPLIT, + OP_SUB, + OP_SWAP, + OP_TOALTSTACK, + OP_TUCK, + OP_VERIFY, + pushBytesOp, + pushNumberOp, + Script, + sha256d, + Signatory, + slpSend, + strToBytes, + UnsignedTxInput, + Writer, + WriterBytes, + WriterLength, + writeTxOutput, +} from 'ecash-lib'; +import { AGORA_LOKAD_ID } from './consts.js'; /** * "Human viable" parameters for partial Agora offers, can serve as a basis to @@ -367,9 +434,12 @@ } public updateScriptLen() { - // TODO: Actually calculate the script length - // Random placeholder to ensure tests aren't dependent on this - this.scriptLen = Math.floor(Math.random() * 1000); + let measuredLength = this.script().cutOutCodesep(0).bytecode.length; + if (measuredLength >= 0x80) { + this.scriptLen = 0x80; + measuredLength = this.script().cutOutCodesep(0).bytecode.length; + } + this.scriptLen = measuredLength; } /** @@ -444,4 +514,533 @@ const sats = this.askedSats(prepared); return (sats * 1000000000n) / prepared; } + + public adPushdata(): Uint8Array { + const serAdPushdata = (writer: Writer) => { + if (this.tokenProtocol == 'ALP') { + throw new Error('Currently only SLP implemented'); + } + writer.putU8(this.numTokenTruncBytes); + writer.putU8(this.numSatsTruncBytes); + writer.putU64(this.tokenScaleFactor); + writer.putU64(this.scaledTruncTokensPerTruncSat); + writer.putU64(this.minAcceptedScaledTruncTokens); + writer.putBytes(this.makerPk); + }; + const lengthWriter = new WriterLength(); + serAdPushdata(lengthWriter); + const bytesWriter = new WriterBytes(lengthWriter.length); + serAdPushdata(bytesWriter); + return bytesWriter.data; + } + + public covenantConsts(): [Uint8Array, number] { + const adPushdata = this.adPushdata(); + // "Consts" is serialized data with the terms of the offer + the token + // protocol intros. + if (this.tokenProtocol == 'SLP') { + const slpSendIntro = slpSend(this.tokenId, this.tokenType, [ + 0, + ]).bytecode; + const covenantConstsWriter = new WriterBytes( + slpSendIntro.length + adPushdata.length, + ); + covenantConstsWriter.putBytes(slpSendIntro); + covenantConstsWriter.putBytes(adPushdata); + return [covenantConstsWriter.data, slpSendIntro.length]; + } else { + throw new Error('Only SLP implemented'); + } + } + + public script(): Script { + const [covenantConsts, tokenIntroLen] = this.covenantConsts(); + + // Serialize scaled tokens as 8-byte little endian. + // Even though Script currently doesn't support 64-bit integers, + // this allows us to eventually upgrade to 64-bit without changing this + // Script at all. + const scaledTruncTokens8LeWriter = new WriterBytes(8); + scaledTruncTokens8LeWriter.putU64( + this.truncTokens * this.tokenScaleFactor, + ); + const scaledTruncTokens8Le = scaledTruncTokens8LeWriter.data; + + return Script.fromOps([ + // # Push consts + pushBytesOp(covenantConsts), + // # Push offered token amount as scaled trunc tokens, as u64 LE + pushBytesOp(scaledTruncTokens8Le), + // # Use OP_CODESEPERATOR to remove the above two (large) pushops + // # from the sighash preimage (tx size optimization) + OP_CODESEPARATOR, + // OP_ROT(isPurchase, _, _) + OP_ROT, + // OP_IF(isPurchase) + OP_IF, + // scaledTruncTokens = OP_BIN2NUM(scaledTruncTokens8Le) + OP_BIN2NUM, + // OP_ROT(acceptedScaledTruncTokens, _, _) + OP_ROT, + + // # Verify accepted amount doesn't exceed available amount + // OP_2DUP(scaledTruncTokens, acceptedScaledTruncTokens) + OP_2DUP, + // isNotExcessive = OP_GREATERTHANOREQUAL(scaledTruncTokens, + // acceptedScaledTruncTokens) + OP_GREATERTHANOREQUAL, + // OP_VERIFY(isNotExcessive) + OP_VERIFY, + + // # Verify accepted amount is above a required minimum + // OP_DUP(acceptedScaledTruncTokens) + OP_DUP, + // # Ensure minimum accepted amount is not violated + pushNumberOp(this.minAcceptedScaledTruncTokens), + // isEnough = OP_GREATERTHANOREQUAL(acceptedScaledTruncTokens, + // minAcceptedScaledTruncTokens) + OP_GREATERTHANOREQUAL, + // OP_VERIFY(isEnough) + OP_VERIFY, + + // # Verify accepted amount is scaled correctly, must be a + // # multiple of tokenScaleFactor. + // OP_DUP(acceptedScaledTruncTokens) + OP_DUP, + pushNumberOp(this.tokenScaleFactor), + // scaleRemainder = OP_MOD(acceptedScaledTruncTokens, + // tokenScaleFactor) + OP_MOD, + OP_0, + // OP_EQUALVERIFY(scaleRemainder, 0) + OP_EQUALVERIFY, + + // OP_TUCK(_, acceptedScaledTruncTokens); + OP_TUCK, + + // # Calculate tokens left over after purchase + // leftoverScaledTruncTokens = OP_SUB(scaledTruncTokens, + // acceptedScaledTruncTokens) + OP_SUB, + + // # Get token intro from consts + // depthConsts = depth_of(consts) + pushNumberOp(2), + // consts = OP_PICK(depthConsts); + OP_PICK, + + // # Size of the token protocol intro + pushNumberOp(tokenIntroLen), + // tokenIntro, agoraIntro = OP_SPLIT(consts, introSize) + OP_SPLIT, + // OP_DROP(agoraIntro) + OP_DROP, + + // OP_OVER(leftoverScaledTruncTokens, _) + OP_OVER, + + // hasLeftover = OP_0NOTEQUAL(leftoverScaledTruncTokens) + // # (SCRIPT_VERIFY_MINIMALIF is not on eCash, but better be safe) + OP_0NOTEQUAL, + + // Insert (sub)script that builds the OP_RETURN for SLP/ALP + ...this._scriptBuildOpReturn(), + + // # Add trunc padding for sats to un-truncate sats + pushBytesOp(new Uint8Array(this.numSatsTruncBytes)), + + // outputsOpreturnPad = OP_CAT(opreturnOutput, truncPaddingSats) + OP_CAT, + + // OP_ROT(acceptedScaledTruncTokens, _, _) + OP_ROT, + + // # We divide rounding up when we calc sats, so add divisor - 1 + pushNumberOp(this.scaledTruncTokensPerTruncSat - 1n), + OP_ADD, + + // # Price (scaled + truncated) + pushNumberOp(this.scaledTruncTokensPerTruncSat), + + // # Calculate how many (truncated) sats the user has to pay + // requiredTruncSats = OP_DIV(acceptedScaledTruncTokens, + // scaledTruncTokensPerTruncSat) + OP_DIV, + + // # Build the required sats with the correct byte length + // truncLen = 8 - numSatsTruncBytes + pushNumberOp(8 - this.numSatsTruncBytes), + // requiredTruncSatsLe = OP_NUM2BIN(requiredTruncSats, truncLen) + OP_NUM2BIN, + // # Build OP_RETURN output + satoshi amount (8 bytes LE). + // # We already added the padding to un-truncate sats in the + // # previous OP_CAT to the output. + // outputsOpreturnSats = + // OP_CAT(outputsOpreturnPad, requiredTruncSatsLe) + OP_CAT, + + // # Build maker's P2PKH script + // p2pkhIntro = [25, OP_DUP, OP_HASH160, 20] + pushBytesOp(new Uint8Array([25, OP_DUP, OP_HASH160, 20])), + // OP_2OVER(consts, leftoverScaledTruncTokens, _, _); + OP_2OVER, + // OP_DROP(leftoverScaledTruncTokens); + OP_DROP, + // # Slice out pubkey from the consts (always the last 33 bytes) + // pubkeyIdx = consts.length - 33 + pushNumberOp(covenantConsts.length - 33), + // rest, makerPk = OP_SPLIT(consts, pubkeyIdx) + OP_SPLIT, + // OP_NIP(rest, _) + OP_NIP, + // makerPkh = OP_HASH160(makerPk) + OP_HASH160, + // makerP2pkh1 = OP_CAT(p2pkhIntro, makerPkh) + OP_CAT, + // p2pkhOutro = [OP_EQUALVERIFY, OP_CHECKSIG] + pushBytesOp(new Uint8Array([OP_EQUALVERIFY, OP_CHECKSIG])), + // makerScript = OP_CAT(makerP2pkh1, p2pkhOutro) + OP_CAT, + + // # Now we have the first 2 outputs: OP_RETURN + maker P2PKH + // outputsOpreturnMaker = OP_CAT(outputsOpreturnSats, makerScript) + OP_CAT, + // # Move to altstack, we need it when calculating hashOutputs + // OP_TOALTSTACK(outputsOpreturnMaker) + OP_TOALTSTACK, + + // # Build loopback P2SH, will receive the leftover tokens with + // # a Script with the same terms. + // OP_TUCK(_, leftoverScaledTruncTokens); + OP_TUCK, + // P2SH has dust sats + pushNumberOp(this.dustAmount), + OP_8, + // dustAmount8le = OP_NUM2BIN(dustAmount, 8) + OP_NUM2BIN, + // p2shIntro = [23, OP_HASH160, 20] + pushBytesOp(new Uint8Array([23, OP_HASH160, 20])), + // loopbackOutputIntro = OP_CAT(dustAmount8le, p2shIntro); + OP_CAT, + + // # Build the new redeem script; same terms but different + // # scaledTruncTokens8Le. + + // # Build opcode to push consts. Sometimes they get long and we + // # need OP_PUSHDATA1. + // pushConstsOpcode = if consts.length >= OP_PUSHDATA1 { + // [OP_PUSHDATA1, consts.length] + // } else { + // [consts.length] + // } + pushBytesOp( + new Uint8Array( + covenantConsts.length >= OP_PUSHDATA1 + ? [OP_PUSHDATA1, covenantConsts.length] + : [covenantConsts.length], + ), + ), + + // OP_2SWAP(consts, leftoverScaledTruncTokens, _, _) + OP_2SWAP, + OP_8, + // OP_TUCK(_, 8) + OP_TUCK, + // leftoverScaledTruncTokens8le = + // OP_NUM2BIN(leftoverScaledTruncTokens, 8) + OP_NUM2BIN, + // pushLeftoverScaledTruncTokens8le = + // OP_CAT(8, leftoverScaledTruncTokens8le) + OP_CAT, + // constsPushLeftover = + // OP_CAT(consts, pushLeftoverScaledTruncTokens8le) + OP_CAT, + // # The two ops that push consts plus amount + // pushState = OP_CAT(pushConstsOpcode, constsPushLeftover) + OP_CAT, + // opcodesep = [OP_CODESEPARATOR] + pushBytesOp(new Uint8Array([OP_CODESEPARATOR])), + // loopbackScriptIntro = OP_CAT(pushState, opcodesep) + OP_CAT, + // depthPreimage4_10 = depth_of(preimage4_10); + pushNumberOp(3), + // preimage4_10 = OP_PICK(depthPreimage4_10); + OP_PICK, + // scriptCodeIdx = 36 + if scriptLen < 0xfd { 1 } else { 3 } + pushNumberOp(36 + (this.scriptLen < 0xfd ? 1 : 3)), + // outpoint, preimage5_10 = OP_SPLIT(preimage4_10, scriptCodeIdx) + OP_SPLIT, + // OP_NIP(outpoint, __) + OP_NIP, + // # Split out scriptCode + pushNumberOp(this.scriptLen), + // script_code, preimage6_10 = OP_SPLIT(preimage5_10, scriptLen) + OP_SPLIT, + + // # Extract hashOutputs + OP_12, + // (preimage6_7, preimage8_10) = OP_SPLIT(preimage6_10, 12) + OP_SPLIT, + // OP_NIP(preimage6_7, _) + OP_NIP, + // # Split out hashOutputs + pushNumberOp(32), + // actualHashOutputs, preimage9_10 = OP_SPLIT(preimage8_10, 32) + OP_SPLIT, + // OP_DROP(preimage9_10) + OP_DROP, + // # Move to altstack, will be needed later + // OP_TOALTSTACK(actualHashOutputs) + OP_TOALTSTACK, + + // # Build redeemScript of loopback P2SH output + // loopbackScript = OP_CAT(loopbackScriptIntro, scriptCode) + OP_CAT, + // # Calculate script hash for P2SH script + // loopbackScriptHash = OP_HASH160(loopbackScript) + OP_HASH160, + // loopbackOutputIntroSh = + // OP_CAT(loopbackOutputIntro, loopbackScriptHash) + OP_CAT, + // p2shEnd = [OP_EQUAL] + pushBytesOp(new Uint8Array([OP_EQUAL])), + // # Build loopback P2SH output + // loopbackOutput = OP_CAT(loopbackOutputIntroSh, p2shEnd) + OP_CAT, + + // # Check if we have tokens left over and send them back + // # It is cheaper (in bytes) to build the loopback output and then + // # throw it away if needed than to not build it at all. + // OP_SWAP(leftoverScaledTruncTokens, _) + OP_SWAP, + // hasLeftover = OP_0NOTEQUAL(leftoverScaledTruncTokens) + OP_0NOTEQUAL, + // OP_NOTIF(hasLeftover) + OP_NOTIF, + // OP_DROP(loopbackOutput) + OP_DROP, + // loopbackOutput = [] + pushBytesOp(new Uint8Array()), + OP_ENDIF, + + // OP_ROT(buyerOutputs, _, _) + OP_ROT, + + // # Verify user specified output, otherwise total burn on ALP + // buyerOutputs, buyerOutputsSize = OP_SIZE(buyerOutputs) + OP_SIZE, + // isNotEmpty = OP_0NOTEQUAL(buyerOutputsSize) + OP_0NOTEQUAL, + // OP_VERIFY(isNotEmpty) + OP_VERIFY, + + // # Loopback + taker outputs + // outputsLoopbackTaker = OP_CAT(loopbackOutput, buyerOutputs) + OP_CAT, + + // OP_FROMALTSTACK(actualHashOutputs) + OP_FROMALTSTACK, + // OP_FROMALTSTACK(outputsOpreturnMaker) + OP_FROMALTSTACK, + // OP_ROT(outputsLoopbackTaker, _, _) + OP_ROT, + // # Outputs expected by this Script + // expectedOutputs = OP_CAT(outputsOpreturnMaker, + // outputsLoopbackTaker) + OP_CAT, + // expectedHashOutputs = OP_HASH256(expectedOutputs) + OP_HASH256, + // # Verify tx has the expected outputs + // OP_EQUALVERIFY(actualHashOutputs, expectedHashOutputs) + OP_EQUALVERIFY, + + // # Build sighash preimage parts 1 to 3 via OP_NUM2BIN + // txVersion = 2 + OP_2, + // preimage1_3Len = 4 + 32 + 32 + pushNumberOp(4 + 32 + 32), + // preimage1_3 = OP_NUM2BIN(txVersion, preimage1_3Len) + OP_NUM2BIN, + + // # Build full sighash preimage + // OP_SWAP(preimage4_10, preimage1_3) + OP_SWAP, + // preimage = OP_CAT(preimage1_3, preimage4_10) + OP_CAT, + + // # Sighash for this covenant + // preimageSha256 = OP_SHA256(preimage) + OP_SHA256, + + // # Verify our sighash actually matches that of the transaction + // OP_3DUP(covenantPk, covenantSig, preimageSha256) + OP_3DUP, + // OP_ROT(covenantPk, covenantSig, preimageSha256) + OP_ROT, + // OP_CHECKDATASIGVERIFY(covenantSig, preimageSha256, covenantPk) + OP_CHECKDATASIGVERIFY, + // OP_DROP(preimageSha256) + OP_DROP, + // sigHashFlags = [ALL_ANYONECANPAY_BIP143] + pushBytesOp(new Uint8Array([ALL_ANYONECANPAY_BIP143.toInt()])), + // covenantSigFlagged = OP_CAT(covenantSig, sigHashFlags) + OP_CAT, + // covenantSig, pk = OP_SWAP(covenantPk, covenantSigFlagged) + OP_SWAP, + + OP_ELSE, + + // # "Cancel" branch, split out the maker pubkey and verify sig + // # is for the maker pubkey. + // OP_DROP(scaledTruncTokens8le); + OP_DROP, + // pubkeyIdx = consts.length - 33 + pushNumberOp(covenantConsts.length - 33), + // rest, pk = OP_SPLIT(consts, pubkeyIdx) + OP_SPLIT, + // OP_NIP(rest, __) + OP_NIP, + + OP_ENDIF, + + // # SLP and ALP differ at the end of the Script + ...this._scriptOutro(), + ]); + } + + private _scriptBuildOpReturn(): Op[] { + // Script takes in the token amounts and builds the OP_RETURN for the + // corresponding protocol + if (this.tokenProtocol == 'SLP') { + return this._scriptBuildSlpOpReturn(); + } else { + throw new Error('Only SLP implemented'); + } + } + + private _scriptBuildSlpOpReturn(): Op[] { + const scriptSerSlpTruncTokens = () => { + // Serialize the number on the stack using the configured truncation + if (this.numTokenTruncBytes == 5) { + // Edge case where we only have 3 bytes space to serialize the + // number, but if the MSB of the number is set, OP_NUM2BIN will + // serialize using 4 bytes (with the last byte being just 0x00), + // so we always serialize using 4 bytes and then cut the last + // byte (that's always 0x00) off. + return [ + pushNumberOp(4), + OP_NUM2BIN, + pushNumberOp(3), + OP_SPLIT, + OP_DROP, + ]; + } else { + // If we have 4 or more bytes space, we can always serialize + // just using normal OP_NUM2BIN. + return [pushNumberOp(8 - this.numTokenTruncBytes), OP_NUM2BIN]; + } + }; + return [ + // # If there's a leftover, append it to the token amounts + // OP_IF(leftoverScaledTruncTokens) + OP_IF, + // # Size of an SLP amount + OP_8, + // tokenIntro8 = OP_CAT(tokenIntro, 8); + OP_CAT, + // OP_OVER(leftoverScaledTruncTokens, _) + OP_OVER, + // # Scale down the scaled leftover amount + pushNumberOp(this.tokenScaleFactor), + // leftoverTokensTrunc = OP_DIV(leftoverScaledTruncTokens, + // tokenScaleFactor) + OP_DIV, + // # Serialize the leftover trunc tokens (overflow-safe) + ...scriptSerSlpTruncTokens(), + // # SLP uses big-endian, so we have to use OP_REVERSEBYTES + // leftoverTokenTruncBe = OP_REVERSEBYTES(leftoverTokenTruncLe) + OP_REVERSEBYTES, + // # Bytes to un-truncate the leftover tokens + pushBytesOp(new Uint8Array(this.numTokenTruncBytes)), + // # Build the actual 8 byte big-endian leftover + // leftoverToken8be = OP_CAT(leftoverTokenTruncBe, untruncatePad); + OP_CAT, + // # Append the leftover to the token intro + // tokenScript = OP_CAT(tokenIntro8, leftoverToken8be); + OP_CAT, + + OP_ENDIF, + + // # Append accepted token amount going to the taker + // # Size of an SLP amount + OP_8, + // tokenScript = OP_CAT(tokenScript, 8) + OP_CAT, + // # Get the accepted token amount + // depthAcceptedScaledTruncTokens = + // depth_of(acceptedScaledTruncTokens) + pushNumberOp(2), + // acceptedScaledTruncTokens = + // OP_PICK(depthAcceptedScaledTruncTokens) + OP_PICK, + // # Scale down the accepted token amount + pushNumberOp(this.tokenScaleFactor), + // acceptedTokensTrunc = OP_DIV(acceptedScaledTruncTokens, + // tokenScaleFactor) + OP_DIV, + // # Serialize the accepted token amount (overflow-safe) + ...scriptSerSlpTruncTokens(), + // # SLP uses big-endian, so we have to use OP_REVERSEBYTES + // acceptedTokensTruncBe = OP_REVERSEBYTES(acceptedTokensTruncLe); + OP_REVERSEBYTES, + // # Bytes to un-truncate the leftover tokens + pushBytesOp(new Uint8Array(this.numTokenTruncBytes)), + // acceptedTokens8be = OP_CAT(acceptedTokensTruncBe, untruncatePad); + OP_CAT, + + // # Finished SLP token script + // tokenScript = OP_CAT(tokenScript, acceptedTokens8be); + OP_CAT, + + // # Build OP_RETURN script with 0u64 and size prepended + // # tokenScript, tokenScriptSize = OP_SIZE(tokenScript) + OP_SIZE, + + // # Build output value (0u64) + tokenScriptSize. + // # In case the tokenScriptSize > 127, it will be represented as + // # 0xXX00 in Script, but it should be just 0xXX. + // # We could serialize to 2 bytes and then cut one off, but here we + // # use a neat optimization: We serialize to 9 bytes (resulting in + // # 0xXX0000000000000000) and then call OP_REVERSEBYTES, which + // # will result in 0x0000000000000000XX, which is exactly what the + // # first 9 bytes of the OP_RETURN output should look like. + OP_9, + // tokenScriptSize9Le = OP_NUM2BIN(tokenScriptSize, 9) + OP_NUM2BIN, + // opreturnValueSize = OP_REVERSEBYTES(tokenScriptSize9Le); + OP_REVERSEBYTES, + + // OP_SWAP(tokenScript, opreturnValueSize); + OP_SWAP, + // opreturnOutput = OP_CAT(opreturnValueSize, tokenScript); + OP_CAT, + ]; + } + + private _scriptOutro(): Op[] { + if (this.tokenProtocol == 'SLP') { + // Verify the sig, and also ensure the first two push ops of the + // scriptSig are "AGR0" "PARTIAL", which will always have to be the + // first two ops because of the cleanstack rule. + return [ + OP_CHECKSIGVERIFY, + pushBytesOp(strToBytes(AgoraPartial.COVENANT_VARIANT)), + OP_EQUALVERIFY, + pushBytesOp(AGORA_LOKAD_ID), + OP_EQUAL, + ]; + } else { + throw new Error('Only SLP implemented'); + } + } }