scrypt-ts
Advanced tools
Comparing version 0.1.3-alpha to 0.1.3-alpha.1
@@ -137,40 +137,2 @@ import { PubKey, Sig, PubKeyHash, SigHashType, OpCodeType, SigHashPreimage, PrivKey } from "./types"; | ||
/** | ||
* Bitcoin script does not provide looping constructs natively for security reasons. sCrypt achieves looping by repeating the loop body maxLoopCount times. For example, the loop | ||
* @example | ||
* ```ts | ||
* loop (5n) (() => { | ||
* x = x * 2; | ||
* }) | ||
* ``` | ||
* is equivalently unrolled to | ||
* @example | ||
* ```ts | ||
* x = x * 2; | ||
* x = x * 2; | ||
* x = x * 2; | ||
* x = x * 2; | ||
* x = x * 2; | ||
* x = x * 2; | ||
* x = x * 2; | ||
* x = x * 2; | ||
* x = x * 2; | ||
* x = x * 2; | ||
* ``` | ||
* If `maxLoopCount` is set too small, the contract may not work correctly. If `maxLoopCount` is set too large, the resulting script is bloated unnecessarily and costs more to execute. There are a number of ways to choose the right `maxLoopCount` judiciously. One way is to simulate the contract off chain and find the number of loops. Another way is to exploit the characteristics of the looping itself. For example, if a loop iterates over each bit of a sha256 hash, `maxLoopCount` is 256. | ||
* ## Induction variable | ||
* Induction variable can be defined when loop index is needed. | ||
* @example | ||
* ```ts | ||
* loop (5n) ((i: number) => { | ||
* // i is the outer loop index | ||
* loop (5n) ((k: number) => { | ||
* // j is the inner loop index | ||
* a += BigInt(i + k); | ||
* }) | ||
* }) | ||
* ``` | ||
* @category loop | ||
*/ | ||
export declare function loop(maxLoopCount: bigint): (fn: (i: number) => void) => void; | ||
/** | ||
* @ignore | ||
@@ -287,5 +249,3 @@ */ | ||
static readonly OP_NOP1: OpCodeType; | ||
static readonly OP_CHECKLOCKTIMEVERIFY: OpCodeType; | ||
static readonly OP_NOP2: OpCodeType; | ||
static readonly OP_CHECKSEQUENCEVERIFY: OpCodeType; | ||
static readonly OP_NOP3: OpCodeType; | ||
@@ -299,4 +259,2 @@ static readonly OP_NOP4: OpCodeType; | ||
static readonly OP_NOP10: OpCodeType; | ||
static readonly OP_SMALLINTEGER: OpCodeType; | ||
static readonly OP_PUBKEYS: OpCodeType; | ||
static readonly OP_PUBKEYHASH: OpCodeType; | ||
@@ -313,3 +271,3 @@ static readonly OP_PUBKEY: OpCodeType; | ||
static toLEUnsigned(n: bigint, l: bigint): string; | ||
static fromLEUnsigned(b: string): bigint; | ||
static fromLEUnsigned(bytes: string): bigint; | ||
static readVarint(buf: string): string; | ||
@@ -342,3 +300,3 @@ static writeVarint(buf: string): string; | ||
static nLocktimeRaw(preimage: SigHashPreimage): string; | ||
static nLocktime(preimage: SigHashPreimage): number; | ||
static nLocktime(preimage: SigHashPreimage): bigint; | ||
static sigHashType(preimage: SigHashPreimage): SigHashType; | ||
@@ -353,5 +311,7 @@ } | ||
static readonly Version: bigint; | ||
buf: string; | ||
pos: bigint; | ||
constructor(buf: string); | ||
eof(): boolean; | ||
readstring(): string; | ||
readBytes(): string; | ||
readBool(): boolean; | ||
@@ -365,3 +325,3 @@ readInt(): bigint; | ||
export declare class VarIntWriter { | ||
static writestring(buf: string): string; | ||
static writeBytes(buf: string): string; | ||
static writeBool(x: boolean): string; | ||
@@ -368,0 +328,0 @@ static writeInt(x: bigint): string; |
"use strict"; | ||
// build-in function | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Constants = exports.Tx = exports.VarIntWriter = exports.VarIntReader = exports.SigHash = exports.Utils = exports.OpCode = exports.asm = exports.loop = exports.assert = exports.flattenSha256 = exports.hash256 = exports.hash160 = exports.sha256 = exports.sha1 = exports.ripemd160 = exports.within = exports.max = exports.min = exports.abs = exports.exit = exports.checkMultiSig = exports.checkSig = exports.reverseBytes = exports.len = exports.num2bin = exports.unpack = exports.pack = void 0; | ||
exports.Constants = exports.Tx = exports.VarIntWriter = exports.VarIntReader = exports.SigHash = exports.Utils = exports.OpCode = exports.asm = exports.assert = exports.flattenSha256 = exports.hash256 = exports.hash160 = exports.sha256 = exports.sha1 = exports.ripemd160 = exports.within = exports.max = exports.min = exports.abs = exports.exit = exports.checkMultiSig = exports.checkSig = exports.reverseBytes = exports.len = exports.num2bin = exports.unpack = exports.pack = void 0; | ||
const scryptlib_1 = require("scryptlib"); | ||
const utils_1 = require("scryptlib/dist/utils"); | ||
const types_1 = require("./types"); | ||
@@ -207,49 +206,2 @@ /** | ||
/** | ||
* Bitcoin script does not provide looping constructs natively for security reasons. sCrypt achieves looping by repeating the loop body maxLoopCount times. For example, the loop | ||
* @example | ||
* ```ts | ||
* loop (5n) (() => { | ||
* x = x * 2; | ||
* }) | ||
* ``` | ||
* is equivalently unrolled to | ||
* @example | ||
* ```ts | ||
* x = x * 2; | ||
* x = x * 2; | ||
* x = x * 2; | ||
* x = x * 2; | ||
* x = x * 2; | ||
* x = x * 2; | ||
* x = x * 2; | ||
* x = x * 2; | ||
* x = x * 2; | ||
* x = x * 2; | ||
* ``` | ||
* If `maxLoopCount` is set too small, the contract may not work correctly. If `maxLoopCount` is set too large, the resulting script is bloated unnecessarily and costs more to execute. There are a number of ways to choose the right `maxLoopCount` judiciously. One way is to simulate the contract off chain and find the number of loops. Another way is to exploit the characteristics of the looping itself. For example, if a loop iterates over each bit of a sha256 hash, `maxLoopCount` is 256. | ||
* ## Induction variable | ||
* Induction variable can be defined when loop index is needed. | ||
* @example | ||
* ```ts | ||
* loop (5n) ((i: number) => { | ||
* // i is the outer loop index | ||
* loop (5n) ((k: number) => { | ||
* // j is the inner loop index | ||
* a += BigInt(i + k); | ||
* }) | ||
* }) | ||
* ``` | ||
* @category loop | ||
*/ | ||
function loop(maxLoopCount) { | ||
let loopFn = (fn) => { | ||
for (let i = 0; i < maxLoopCount; i++) { | ||
fn(i); | ||
} | ||
}; | ||
return loopFn; | ||
} | ||
exports.loop = loop; | ||
; | ||
/** | ||
* @ignore | ||
@@ -268,2 +220,129 @@ */ | ||
exports.OpCode = OpCode; | ||
// push value | ||
OpCode.OP_0 = new types_1.OpCodeType('00'); | ||
OpCode.OP_FALSE = new types_1.OpCodeType('00'); | ||
OpCode.OP_PUSHDATA1 = new types_1.OpCodeType('4c'); | ||
OpCode.OP_PUSHDATA2 = new types_1.OpCodeType('4d'); | ||
OpCode.OP_PUSHDATA4 = new types_1.OpCodeType('4e'); | ||
OpCode.OP_1NEGATE = new types_1.OpCodeType('4f'); | ||
OpCode.OP_RESERVED = new types_1.OpCodeType('50'); | ||
OpCode.OP_1 = new types_1.OpCodeType('51'); | ||
OpCode.OP_TRUE = new types_1.OpCodeType('51'); | ||
OpCode.OP_2 = new types_1.OpCodeType('52'); | ||
OpCode.OP_3 = new types_1.OpCodeType('53'); | ||
OpCode.OP_4 = new types_1.OpCodeType('54'); | ||
OpCode.OP_5 = new types_1.OpCodeType('55'); | ||
OpCode.OP_6 = new types_1.OpCodeType('56'); | ||
OpCode.OP_7 = new types_1.OpCodeType('57'); | ||
OpCode.OP_8 = new types_1.OpCodeType('58'); | ||
OpCode.OP_9 = new types_1.OpCodeType('59'); | ||
OpCode.OP_10 = new types_1.OpCodeType('5a'); | ||
OpCode.OP_11 = new types_1.OpCodeType('5b'); | ||
OpCode.OP_12 = new types_1.OpCodeType('5c'); | ||
OpCode.OP_13 = new types_1.OpCodeType('5d'); | ||
OpCode.OP_14 = new types_1.OpCodeType('5e'); | ||
OpCode.OP_15 = new types_1.OpCodeType('5f'); | ||
OpCode.OP_16 = new types_1.OpCodeType('60'); | ||
// control | ||
OpCode.OP_NOP = new types_1.OpCodeType('61'); | ||
OpCode.OP_VER = new types_1.OpCodeType('62'); | ||
OpCode.OP_IF = new types_1.OpCodeType('63'); | ||
OpCode.OP_NOTIF = new types_1.OpCodeType('64'); | ||
OpCode.OP_VERIF = new types_1.OpCodeType('65'); | ||
OpCode.OP_VERNOTIF = new types_1.OpCodeType('66'); | ||
OpCode.OP_ELSE = new types_1.OpCodeType('67'); | ||
OpCode.OP_ENDIF = new types_1.OpCodeType('68'); | ||
OpCode.OP_VERIFY = new types_1.OpCodeType('69'); | ||
OpCode.OP_RETURN = new types_1.OpCodeType('6a'); | ||
// stack ops | ||
OpCode.OP_TOALTSTACK = new types_1.OpCodeType('6b'); | ||
OpCode.OP_FROMALTSTACK = new types_1.OpCodeType('6c'); | ||
OpCode.OP_2DROP = new types_1.OpCodeType('6d'); | ||
OpCode.OP_2DUP = new types_1.OpCodeType('6e'); | ||
OpCode.OP_3DUP = new types_1.OpCodeType('6f'); | ||
OpCode.OP_2OVER = new types_1.OpCodeType('70'); | ||
OpCode.OP_2ROT = new types_1.OpCodeType('71'); | ||
OpCode.OP_2SWAP = new types_1.OpCodeType('72'); | ||
OpCode.OP_IFDUP = new types_1.OpCodeType('73'); | ||
OpCode.OP_DEPTH = new types_1.OpCodeType('74'); | ||
OpCode.OP_DROP = new types_1.OpCodeType('75'); | ||
OpCode.OP_DUP = new types_1.OpCodeType('76'); | ||
OpCode.OP_NIP = new types_1.OpCodeType('77'); | ||
OpCode.OP_OVER = new types_1.OpCodeType('78'); | ||
OpCode.OP_PICK = new types_1.OpCodeType('79'); | ||
OpCode.OP_ROLL = new types_1.OpCodeType('7a'); | ||
OpCode.OP_ROT = new types_1.OpCodeType('7b'); | ||
OpCode.OP_SWAP = new types_1.OpCodeType('7c'); | ||
OpCode.OP_TUCK = new types_1.OpCodeType('7d'); | ||
// splice ops | ||
OpCode.OP_CAT = new types_1.OpCodeType('7e'); | ||
OpCode.OP_SPLIT = new types_1.OpCodeType('7f'); // after monolith upgrade (May 2018) | ||
OpCode.OP_NUM2BIN = new types_1.OpCodeType('80'); // after monolith upgrade (May 2018) | ||
OpCode.OP_BIN2NUM = new types_1.OpCodeType('81'); // after monolith upgrade (May 2018) | ||
OpCode.OP_SIZE = new types_1.OpCodeType('82'); | ||
// bit logic | ||
OpCode.OP_INVERT = new types_1.OpCodeType('83'); | ||
OpCode.OP_AND = new types_1.OpCodeType('84'); | ||
OpCode.OP_OR = new types_1.OpCodeType('85'); | ||
OpCode.OP_XOR = new types_1.OpCodeType('86'); | ||
OpCode.OP_EQUAL = new types_1.OpCodeType('87'); | ||
OpCode.OP_EQUALVERIFY = new types_1.OpCodeType('88'); | ||
OpCode.OP_RESERVED1 = new types_1.OpCodeType('89'); | ||
OpCode.OP_RESERVED2 = new types_1.OpCodeType('8a'); | ||
// numeric | ||
OpCode.OP_1ADD = new types_1.OpCodeType('8b'); | ||
OpCode.OP_1SUB = new types_1.OpCodeType('8c'); | ||
OpCode.OP_2MUL = new types_1.OpCodeType('8d'); | ||
OpCode.OP_2DIV = new types_1.OpCodeType('8e'); | ||
OpCode.OP_NEGATE = new types_1.OpCodeType('8f'); | ||
OpCode.OP_ABS = new types_1.OpCodeType('90'); | ||
OpCode.OP_NOT = new types_1.OpCodeType('91'); | ||
OpCode.OP_0NOTEQUAL = new types_1.OpCodeType('92'); | ||
OpCode.OP_ADD = new types_1.OpCodeType('93'); | ||
OpCode.OP_SUB = new types_1.OpCodeType('94'); | ||
OpCode.OP_MUL = new types_1.OpCodeType('95'); | ||
OpCode.OP_DIV = new types_1.OpCodeType('96'); | ||
OpCode.OP_MOD = new types_1.OpCodeType('97'); | ||
OpCode.OP_LSHIFT = new types_1.OpCodeType('98'); | ||
OpCode.OP_RSHIFT = new types_1.OpCodeType('99'); | ||
OpCode.OP_BOOLAND = new types_1.OpCodeType('9a'); | ||
OpCode.OP_BOOLOR = new types_1.OpCodeType('9b'); | ||
OpCode.OP_NUMEQUAL = new types_1.OpCodeType('9c'); | ||
OpCode.OP_NUMEQUALVERIFY = new types_1.OpCodeType('9d'); | ||
OpCode.OP_NUMNOTEQUAL = new types_1.OpCodeType('9e'); | ||
OpCode.OP_LESSTHAN = new types_1.OpCodeType('9f'); | ||
OpCode.OP_GREATERTHAN = new types_1.OpCodeType('a0'); | ||
OpCode.OP_LESSTHANOREQUAL = new types_1.OpCodeType('a1'); | ||
OpCode.OP_GREATERTHANOREQUAL = new types_1.OpCodeType('a2'); | ||
OpCode.OP_MIN = new types_1.OpCodeType('a3'); | ||
OpCode.OP_MAX = new types_1.OpCodeType('a4'); | ||
OpCode.OP_WITHIN = new types_1.OpCodeType('a5'); | ||
// crypto | ||
OpCode.OP_RIPEMD160 = new types_1.OpCodeType('a6'); | ||
OpCode.OP_SHA1 = new types_1.OpCodeType('a7'); | ||
OpCode.OP_SHA256 = new types_1.OpCodeType('a8'); | ||
OpCode.OP_HASH160 = new types_1.OpCodeType('a9'); | ||
OpCode.OP_HASH256 = new types_1.OpCodeType('aa'); | ||
OpCode.OP_CODESEPARATOR = new types_1.OpCodeType('ab'); | ||
OpCode.OP_CHECKSIG = new types_1.OpCodeType('ac'); | ||
OpCode.OP_CHECKSIGVERIFY = new types_1.OpCodeType('ad'); | ||
OpCode.OP_CHECKMULTISIG = new types_1.OpCodeType('ae'); | ||
OpCode.OP_CHECKMULTISIGVERIFY = new types_1.OpCodeType('af'); | ||
// expansion | ||
OpCode.OP_NOP1 = new types_1.OpCodeType('b0'); | ||
OpCode.OP_NOP2 = new types_1.OpCodeType('b1'); // previously OP_CHECKLOCKTIMEVERIFY | ||
OpCode.OP_NOP3 = new types_1.OpCodeType('b2'); // OpCode.OP_CHECKSEQUENCEVERIFY; | ||
OpCode.OP_NOP4 = new types_1.OpCodeType('b3'); | ||
OpCode.OP_NOP5 = new types_1.OpCodeType('b4'); | ||
OpCode.OP_NOP6 = new types_1.OpCodeType('b5'); | ||
OpCode.OP_NOP7 = new types_1.OpCodeType('b6'); | ||
OpCode.OP_NOP8 = new types_1.OpCodeType('b7'); | ||
OpCode.OP_NOP9 = new types_1.OpCodeType('b8'); | ||
OpCode.OP_NOP10 = new types_1.OpCodeType('b9'); | ||
// The first static const OpCodeType OP_code value after all defined opcodes | ||
//FIRST_UNDEFINED_OP_VALUE | ||
// template matching params | ||
OpCode.OP_PUBKEYHASH = new types_1.OpCodeType('fd'); | ||
OpCode.OP_PUBKEY = new types_1.OpCodeType('fe'); | ||
OpCode.OP_INVALIDOPCODE = new types_1.OpCodeType('ff'); | ||
/** | ||
@@ -274,5 +353,11 @@ * @category Standard Contracts | ||
// convert signed integer `n` to unsigned integer of `l` string, in little endian | ||
static toLEUnsigned(n, l) { throw new Error('unimplemented'); } | ||
static toLEUnsigned(n, l) { | ||
let m = num2bin(n, l + 1n); | ||
// remove sign byte | ||
return m.slice(0, len(m) - 2); | ||
} | ||
// convert string to unsigned integer, in sign-magnitude little endian | ||
static fromLEUnsigned(b) { throw new Error('unimplemented'); } | ||
static fromLEUnsigned(bytes) { | ||
return unpack(bytes + (0, types_1.b)('00')); | ||
} | ||
/* | ||
@@ -283,20 +368,60 @@ * VarInt (variable integer) is used to encode fields of variable length in a bitcoin transaction | ||
// read a VarInt field from the beginning of 'b' | ||
static readVarint(buf) { throw new Error('unimplemented'); } | ||
static readVarint(buf) { | ||
let l = 0n; | ||
let ret = (0, types_1.b)(''); | ||
let header = buf.slice(0, 2); | ||
if (header == (0, types_1.b)('fd')) { | ||
l = Utils.fromLEUnsigned(buf.slice(2, 6)); | ||
ret = buf.slice(6, 6 + Number(l * 2n)); | ||
} | ||
else if (header == (0, types_1.b)('fe')) { | ||
l = Utils.fromLEUnsigned(buf.slice(2, 10)); | ||
ret = buf.slice(10, 10 + Number(l * 2n)); | ||
} | ||
else if (header == (0, types_1.b)('ff')) { | ||
l = Utils.fromLEUnsigned(buf.slice(2, 18)); | ||
ret = buf.slice(18, 18 + Number(l * 2n)); | ||
} | ||
else { | ||
l = Utils.fromLEUnsigned(buf.slice(0, 2)); | ||
ret = buf.slice(2, 2 + Number(l * 2n)); | ||
} | ||
return ret; | ||
} | ||
// convert 'b' to a VarInt field, including the preceding length | ||
static writeVarint(buf) { throw new Error('unimplemented'); } | ||
static writeVarint(buf) { | ||
let n = len(buf); | ||
let header = (0, types_1.b)(''); | ||
if (n < 0xfd) { | ||
header = Utils.toLEUnsigned(BigInt(n), 1n); | ||
} | ||
else if (n < 0x10000) { | ||
header = (0, types_1.b)('fd') + Utils.toLEUnsigned(BigInt(n), 2n); | ||
} | ||
else if (n < 0x100000000) { | ||
header = (0, types_1.b)('fe') + Utils.toLEUnsigned(BigInt(n), 4n); | ||
} | ||
else if (n < 0x10000000000000000) { | ||
header = (0, types_1.b)('ff') + Utils.toLEUnsigned(BigInt(n), 8n); | ||
} | ||
return header + buf; | ||
} | ||
// build a tx output from its script and satoshi amount | ||
static buildOutput(outputScript, outputSatoshis) { return ""; } | ||
static buildOutput(outputScript, outputSatoshis) { | ||
return num2bin(outputSatoshis, Constants.OutputValueLen) + Utils.writeVarint(outputScript); | ||
} | ||
// build P2PKH script from PubKeyHash | ||
static buildPublicKeyHashScript(pubKeyHash) { | ||
//throw new Error('unimplemented'); | ||
return ""; | ||
return OpCode.OP_DUP.toString() + OpCode.OP_HASH160.toString() + pack(Constants.PubKeyHashLen /* "OP_PUSHDATA0" */) + pubKeyHash.toString() + OpCode.OP_EQUALVERIFY.toString() + OpCode.OP_CHECKSIG.toString(); | ||
} | ||
// build false OPRETURN script from data payload | ||
static buildOpreturnScript(data) { throw new Error('unimplemented'); } | ||
static buildOpreturnScript(data) { | ||
return OpCode.OP_FALSE.toString() + OpCode.OP_RETURN.toString() + Utils.writeVarint(data); | ||
} | ||
} | ||
exports.Utils = Utils; | ||
// number of string to denote output value | ||
Utils.OutputValueLen = 1n; | ||
Utils.OutputValueLen = 8n; | ||
// number of string to denote a public key hash | ||
Utils.PubKeyHashLen = 1n; | ||
Utils.PubKeyHashLen = 20n; | ||
/** | ||
@@ -311,18 +436,50 @@ * @category Standard Contracts | ||
*/ | ||
static nVersion(preimage) { throw new Error('unimplemented'); } | ||
static hashPrevouts(preimage) { throw new Error('unimplemented'); } | ||
static hashSequence(preimage) { throw new Error('unimplemented'); } | ||
static outpoint(preimage) { throw new Error('unimplemented'); } | ||
static nVersion(preimage) { | ||
return preimage.toString().slice(0, 8); | ||
} | ||
static hashPrevouts(preimage) { | ||
return preimage.toString().slice(8, 72); | ||
} | ||
static hashSequence(preimage) { | ||
return preimage.toString().slice(36 * 2, 68 * 2); | ||
} | ||
static outpoint(preimage) { | ||
return preimage.toString().slice(68 * 2, 104 * 2); | ||
} | ||
// scriptCode is just scriptPubKey if there is no CODESEPARATOR in the latter | ||
static scriptCode(preimage) { throw new Error('unimplemented'); } | ||
static valueRaw(preimage) { throw new Error('unimplemented'); } | ||
static value(preimage) { return BigInt(preimage.amount); } | ||
static nSequenceRaw(preimage) { throw new Error('unimplemented'); } | ||
static nSequence(preimage) { throw new Error('unimplemented'); } | ||
static scriptCode(preimage) { | ||
return Utils.readVarint(preimage.toString().slice(104 * 2)); | ||
} | ||
static valueRaw(preimage) { | ||
let l = len(preimage.toString()); | ||
return preimage.toString().slice(l * 2 - 104, l * 2 - 88); | ||
} | ||
static value(preimage) { | ||
return Utils.fromLEUnsigned(SigHash.valueRaw(preimage)); | ||
} | ||
static nSequenceRaw(preimage) { | ||
let l = len(preimage.toString()); | ||
return preimage.toString().slice(l * 2 - 88, l * 2 - 80); | ||
} | ||
static nSequence(preimage) { | ||
return Utils.fromLEUnsigned(SigHash.nSequenceRaw(preimage)); | ||
} | ||
; | ||
static hashOutputs(preimage) { return preimage.hashOutputs; } | ||
static nLocktimeRaw(preimage) { throw new Error('unimplemented'); } | ||
static nLocktime(preimage) { return preimage.nLocktime; } | ||
static hashOutputs(preimage) { | ||
let l = len(preimage.toString()); | ||
return preimage.toString().slice(l * 2 - 80, l * 2 - 16); | ||
} | ||
static nLocktimeRaw(preimage) { | ||
let l = len(preimage.toString()); | ||
return preimage.toString().slice(l * 2 - 80, l * 2 - 16); | ||
} | ||
static nLocktime(preimage) { | ||
return Utils.fromLEUnsigned(SigHash.nLocktimeRaw(preimage)); | ||
} | ||
; | ||
static sigHashType(preimage) { return new types_1.SigHashType(utils_1.DEFAULT_SIGHASH_TYPE); } | ||
static sigHashType(preimage) { | ||
let l = len(preimage.toString()); | ||
let sigHashType = Utils.fromLEUnsigned(preimage.toString().slice(l * 2 - 8, l * 2 - 6)); | ||
return new types_1.SigHashType(Number(sigHashType)); | ||
} | ||
} | ||
@@ -341,10 +498,67 @@ exports.SigHash = SigHash; | ||
constructor(buf) { | ||
this.buf = buf; | ||
this.pos = 0n; | ||
} | ||
eof() { throw new Error('unimplemented'); } | ||
readstring() { throw new Error('unimplemented'); } | ||
readBool() { throw new Error('unimplemented'); } | ||
readInt() { throw new Error('unimplemented'); } | ||
static getStateStart(scriptCode) { throw new Error('unimplemented'); } | ||
eof() { | ||
return this.pos >= len(this.buf); | ||
} | ||
readBytes() { | ||
let l = 0n; | ||
let buf = this.buf; | ||
let ret = (0, types_1.b)(''); | ||
let header = unpack(buf.slice(Number(this.pos * 2n), Number((this.pos + 1n) * 2n))); | ||
this.pos++; | ||
if (header < 0x4cn) { | ||
l = header; | ||
ret = buf.slice(Number(this.pos * 2n), Number((this.pos + l) * 2n)); | ||
} | ||
else if (header == 0x4cn) { | ||
l = Utils.fromLEUnsigned(buf.slice(Number(this.pos * 2n), Number((this.pos + 1n) * 2n))); | ||
this.pos += 1n; | ||
ret = buf.slice(Number(this.pos * 2n), Number((this.pos + l) * 2n)); | ||
} | ||
else if (header == 0x4dn) { | ||
l = Utils.fromLEUnsigned(buf.slice(Number(this.pos * 2n), Number((this.pos + 2n) * 2n))); | ||
this.pos += 2n; | ||
ret = buf.slice(Number(this.pos * 2n), Number((this.pos + l) * 2n)); | ||
} | ||
else if (header == 0x4en) { | ||
l = Utils.fromLEUnsigned(buf.slice(Number(this.pos * 2n), Number((this.pos + 4n) * 2n))); | ||
this.pos += 4n; | ||
ret = buf.slice(Number(this.pos * 2n), Number((this.pos + l) * 2n)); | ||
} | ||
else { | ||
// shall not reach here | ||
assert(false); | ||
} | ||
this.pos += l; | ||
return ret; | ||
} | ||
readBool() { | ||
let buf = this.buf.slice(Number(this.pos * 2n), Number((this.pos + 1n) * 2n)); | ||
this.pos++; | ||
return (0, types_1.b)('00') != buf; | ||
} | ||
readInt() { | ||
return unpack(this.readBytes()); | ||
} | ||
static getStateStart(scriptCode) { | ||
// locking script: code + opreturn + data(state + state_len + version) | ||
let scriptLen = BigInt(len(scriptCode)); | ||
// read state length | ||
let start = scriptLen - VarIntReader.StateLen - VarIntReader.VersionLen; | ||
let end = scriptLen - VarIntReader.VersionLen; | ||
let lb = scriptCode.slice(Number(start), Number(end)); | ||
let stateLen = unpack(lb); | ||
// TODO: check version is as expected | ||
return scriptLen - stateLen - VarIntReader.StateLen - VarIntReader.VersionLen; | ||
} | ||
} | ||
exports.VarIntReader = VarIntReader; | ||
// fixed number of string to denote length of serialized state | ||
VarIntReader.StateLen = 4n; | ||
// fixed number of string to denote version | ||
VarIntReader.VersionLen = 1n; | ||
// version | ||
VarIntReader.Version = 0n; | ||
/** | ||
@@ -355,8 +569,36 @@ * @category Standard Contracts | ||
// return VarInt encoding | ||
static writestring(buf) { throw new Error('unimplemented'); } | ||
static writeBytes(buf) { | ||
let n = BigInt(len(buf)); | ||
let header = (0, types_1.b)(''); | ||
if (n < 0x4c) { | ||
header = Utils.toLEUnsigned(n, 1n); | ||
} | ||
else if (n < 0x100) { | ||
header = (0, types_1.b)('4c') + Utils.toLEUnsigned(n, 1n); | ||
} | ||
else if (n < 0x10000) { | ||
header = (0, types_1.b)('4d') + Utils.toLEUnsigned(n, 2n); | ||
} | ||
else if (n < 0x100000000) { | ||
header = (0, types_1.b)('4e') + Utils.toLEUnsigned(n, 4n); | ||
} | ||
else { | ||
// shall not reach here | ||
assert(false); | ||
} | ||
return header + types_1.b; | ||
} | ||
// uses fixed 1 byte to represent a boolean, plus length | ||
static writeBool(x) { throw new Error('unimplemented'); } | ||
static writeBool(x) { | ||
return x ? (0, types_1.b)('01') : (0, types_1.b)('00'); | ||
} | ||
// bigint is little endian | ||
static writeInt(x) { throw new Error('unimplemented'); } | ||
static serializeState(stateBuf) { throw new Error('unimplemented'); } | ||
static writeInt(x) { | ||
return VarIntWriter.writeBytes(x == 0n ? (0, types_1.b)('00') : pack(x)); | ||
} | ||
static serializeState(stateBuf) { | ||
// locking script: code + opreturn + data(state + state_len + version) | ||
let lenBuf = num2bin(BigInt(len(stateBuf)), VarIntReader.StateLen); | ||
return stateBuf + lenBuf + num2bin(VarIntReader.Version, VarIntReader.VersionLen); | ||
} | ||
} | ||
@@ -398,2 +640,14 @@ exports.VarIntWriter = VarIntWriter; | ||
exports.Constants = Constants; | ||
// number of string to denote input sequence | ||
Constants.InputSeqLen = 4n; | ||
// number of string to denote output value | ||
Constants.OutputValueLen = 8n; | ||
// number of string to denote a public key (compressed) | ||
Constants.PubKeyLen = 33n; | ||
// number of string to denote a public key hash | ||
Constants.PubKeyHashLen = 20n; | ||
// number of string to denote a tx id | ||
Constants.TxIdLen = 32n; | ||
// number of string to denote a outpoint | ||
Constants.OutpointLen = 36n; | ||
//# sourceMappingURL=functions.js.map |
import { PubKey as PubKey_, Sig as Sig_, SigHashPreimage as SigHashPreimage_, Ripemd160 as Ripemd160_, PubKeyHash as PubKeyHash_, Sha256 as Sha256_, Sha1 as Sha1_, SigHashType as SigHashType_, OpCodeType as OpCodeType_, PrivKey as PrivKey_ } from "scryptlib"; | ||
/** | ||
* @ignore | ||
*/ | ||
declare type HexChar = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F'; | ||
/** | ||
* @ignore | ||
*/ | ||
declare type HexByte = `${HexChar}${HexChar}`; | ||
/** | ||
* @ignore | ||
*/ | ||
declare type MatchesPattern<Pattern extends string, Current extends string> = Current extends `` ? string : (Current extends `${Pattern}${infer Rest}` ? MatchesPattern<Pattern, Rest> : "bytes"); | ||
/** | ||
* Represents a hex literal string. | ||
* @param {string} hexStr - should be in format of hex bytes, i.e. `/^([0-9a-fA-F]{2})*$/` | ||
*/ | ||
export declare function b<T extends string & IsHexBytes, IsHexBytes = MatchesPattern<HexByte, T>>(hexStr: T): string; | ||
export declare function b(hexStr: string): string; | ||
/** | ||
@@ -27,3 +15,3 @@ * The auto keyword specifies that the type of the variable, of basic type, declared will be automatically deducted from its initializer. | ||
*/ | ||
declare type Grow<T, A extends Array<T>> = ((x: T, ...xs: A) => void) extends ((...a: infer X) => void) ? X : never; | ||
declare type Grow<T, A extends Array<T>> = ((_arrElem: T, ...xs: A) => void) extends ((...a: infer X) => void) ? X : never; | ||
/** | ||
@@ -49,3 +37,3 @@ * @ignore | ||
*/ | ||
export declare type FixedArray<T, N extends number> = GrowToSize<T, [], N>; | ||
export declare type FixedArray<T, LEN extends number> = GrowToSize<T, [], LEN>; | ||
/** | ||
@@ -56,2 +44,3 @@ * a public key type. | ||
export declare class PubKey extends PubKey_ { | ||
toString(format?: 'hex'): string; | ||
} | ||
@@ -63,2 +52,3 @@ /** | ||
export declare class Sig extends Sig_ { | ||
toString(format?: 'hex'): string; | ||
} | ||
@@ -70,2 +60,3 @@ /** | ||
export declare class SigHashPreimage extends SigHashPreimage_ { | ||
toString(format?: 'hex'): string; | ||
} | ||
@@ -77,2 +68,3 @@ /** | ||
export declare class Ripemd160 extends Ripemd160_ { | ||
toString(format?: 'hex'): string; | ||
} | ||
@@ -84,2 +76,3 @@ /** | ||
export declare class PubKeyHash extends PubKeyHash_ { | ||
toString(format?: 'hex'): string; | ||
} | ||
@@ -91,2 +84,3 @@ /** | ||
export declare class Sha256 extends Sha256_ { | ||
toString(format?: 'hex'): string; | ||
} | ||
@@ -98,2 +92,3 @@ /** | ||
export declare class Sha1 extends Sha1_ { | ||
toString(format?: 'hex'): string; | ||
} | ||
@@ -105,2 +100,3 @@ /** | ||
export declare class SigHashType extends SigHashType_ { | ||
toString(format?: 'hex'): string; | ||
} | ||
@@ -112,2 +108,3 @@ /** | ||
export declare class OpCodeType extends OpCodeType_ { | ||
toString(format?: 'hex'): string; | ||
} | ||
@@ -119,3 +116,4 @@ /** | ||
export declare class PrivKey extends PrivKey_ { | ||
toString(format?: 'hex'): string; | ||
} | ||
export {}; |
@@ -19,2 +19,5 @@ "use strict"; | ||
class PubKey extends scryptlib_1.PubKey { | ||
toString(format = 'hex') { | ||
return this.serialize(); | ||
} | ||
} | ||
@@ -27,2 +30,5 @@ exports.PubKey = PubKey; | ||
class Sig extends scryptlib_1.Sig { | ||
toString(format = 'hex') { | ||
return this.serialize(); | ||
} | ||
} | ||
@@ -35,2 +41,5 @@ exports.Sig = Sig; | ||
class SigHashPreimage extends scryptlib_1.SigHashPreimage { | ||
toString(format = 'hex') { | ||
return this.serialize(); | ||
} | ||
} | ||
@@ -43,2 +52,5 @@ exports.SigHashPreimage = SigHashPreimage; | ||
class Ripemd160 extends scryptlib_1.Ripemd160 { | ||
toString(format = 'hex') { | ||
return this.serialize(); | ||
} | ||
} | ||
@@ -51,2 +63,5 @@ exports.Ripemd160 = Ripemd160; | ||
class PubKeyHash extends scryptlib_1.PubKeyHash { | ||
toString(format = 'hex') { | ||
return this.serialize(); | ||
} | ||
} | ||
@@ -59,2 +74,5 @@ exports.PubKeyHash = PubKeyHash; | ||
class Sha256 extends scryptlib_1.Sha256 { | ||
toString(format = 'hex') { | ||
return this.serialize(); | ||
} | ||
} | ||
@@ -67,2 +85,5 @@ exports.Sha256 = Sha256; | ||
class Sha1 extends scryptlib_1.Sha1 { | ||
toString(format = 'hex') { | ||
return this.serialize(); | ||
} | ||
} | ||
@@ -75,2 +96,5 @@ exports.Sha1 = Sha1; | ||
class SigHashType extends scryptlib_1.SigHashType { | ||
toString(format = 'hex') { | ||
return this.serialize(); | ||
} | ||
} | ||
@@ -83,2 +107,5 @@ exports.SigHashType = SigHashType; | ||
class OpCodeType extends scryptlib_1.OpCodeType { | ||
toString(format = 'hex') { | ||
return this.serialize(); | ||
} | ||
} | ||
@@ -91,4 +118,7 @@ exports.OpCodeType = OpCodeType; | ||
class PrivKey extends scryptlib_1.PrivKey { | ||
toString(format = 'hex') { | ||
return this.serialize(); | ||
} | ||
} | ||
exports.PrivKey = PrivKey; | ||
//# sourceMappingURL=types.js.map |
/// <reference types="bsv" /> | ||
import "reflect-metadata"; | ||
import { AbstractContract, VerifyResult, bsv } from "scryptlib"; | ||
import { Script } from "scryptlib/dist/abi"; | ||
import { SigHashPreimage, SigHashType } from "./builtins/types"; | ||
import { AbstractContract, VerifyResult, bsv, ContractDescription } from "scryptlib"; | ||
import { SigHashPreimage, SigHashType, Sig, PubKey } from "./builtins/types"; | ||
/** | ||
@@ -31,2 +30,3 @@ * The input point where the smart contract is spent by the transaction | ||
export declare class SmartContract { | ||
flags: number; | ||
lockingTo?: TxOutputRef; | ||
@@ -37,7 +37,8 @@ unlockingFrom?: TxInputRef; | ||
static compile(): Promise<void>; | ||
static loadDesc(desc: ContractDescription): Promise<void>; | ||
private static _getScryptFile; | ||
constructor(...args: any[]); | ||
verify(entryMethodInvoking: (self: typeof this) => void): VerifyResult; | ||
getUnlockingScript(callPub: (self: typeof this) => void): Script; | ||
get lockingScript(): Script; | ||
getUnlockingScript(callPub: (self: typeof this) => void): bsv.Script; | ||
get lockingScript(): bsv.Script; | ||
clone(): this; | ||
@@ -47,2 +48,5 @@ markAsGenesis(): void; | ||
protected getStateScript(): string; | ||
protected checkSig(signature: Sig, publickey: PubKey): boolean; | ||
checkPubkeyEncoding(publickey: PubKey): boolean; | ||
checkSignatureEncoding(signature: Sig): boolean; | ||
protected updateState(preimage: SigHashPreimage, balance: bigint): boolean; | ||
@@ -49,0 +53,0 @@ private delegateCall; |
@@ -48,2 +48,4 @@ "use strict"; | ||
const lodash_clonedeep_1 = __importDefault(require("lodash.clonedeep")); | ||
const md5 = require("md5"); | ||
const utils_1 = require("scryptlib/dist/utils"); | ||
/** | ||
@@ -61,2 +63,3 @@ * The main contract class. To write a contract, extend this class as such: | ||
constructor(...args) { | ||
this.flags = utils_1.DEFAULT_FLAGS; | ||
if (!SmartContract.DelegateClazz) { | ||
@@ -86,2 +89,11 @@ throw new Error(`'${this.constructor.name}.compile' should be called before initilizing any instance!`); | ||
} | ||
static async loadDesc(desc) { | ||
let filePath = this._getScryptFile(); | ||
const sourceContent = fs.readFileSync(filePath, 'utf8'); | ||
const md5Hash = md5(sourceContent); | ||
if (this.name !== desc.contract || desc.md5 !== md5Hash) { | ||
throw new Error(`Contract description file cannot match contract \`${this.name}\`!`); | ||
} | ||
SmartContract.DelegateClazz = (0, scryptlib_1.buildContractClass)(desc); | ||
} | ||
static _getScryptFile() { | ||
@@ -160,2 +172,60 @@ let scryptFile = Reflect.getMetadata("scrypt:file", this); | ||
} | ||
checkSig(signature, publickey) { | ||
if (!this.checkSignatureEncoding(signature) || !this.checkPubkeyEncoding(publickey)) { | ||
return false; | ||
} | ||
let fSuccess = false; | ||
try { | ||
const sig = scryptlib_1.bsv.crypto.Signature.fromTxFormat(Buffer.from(signature.toString(), 'hex')); | ||
const pubkey = scryptlib_1.bsv.PublicKey.fromBuffer(Buffer.from(publickey.toString(), 'hex'), false); | ||
const tx = this.txContext.tx; | ||
const inputIndex = this.txContext.inputIndex || 0; | ||
const inputSatoshis = this.txContext.inputSatoshis || 100000; | ||
fSuccess = tx.verifySignature(sig, pubkey, inputIndex, this.lockingScript, inputSatoshis, this.flags); | ||
} | ||
catch (e) { | ||
// invalid sig or pubkey | ||
fSuccess = false; | ||
} | ||
return fSuccess; | ||
} | ||
checkPubkeyEncoding(publickey) { | ||
if ((this.flags & scryptlib_1.bsv.Script.Interpreter.SCRIPT_VERIFY_STRICTENC) !== 0 && !scryptlib_1.bsv.PublicKey.isValid(publickey.toString())) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
checkSignatureEncoding(signature) { | ||
var buf = Buffer.from(signature.toString(), 'hex'); | ||
var sig; | ||
// Empty signature. Not strictly DER encoded, but allowed to provide a | ||
// compact way to provide an invalid signature for use with CHECK(MULTI)SIG | ||
if (buf.length === 0) { | ||
return true; | ||
} | ||
if ((this.flags & (scryptlib_1.bsv.Script.Interpreter.SCRIPT_VERIFY_DERSIG | scryptlib_1.bsv.Script.Interpreter.SCRIPT_VERIFY_LOW_S | scryptlib_1.bsv.Script.Interpreter.SCRIPT_VERIFY_STRICTENC)) !== 0 && !scryptlib_1.bsv.crypto.Signature.isTxDER(buf)) { | ||
return false; | ||
} | ||
else if ((this.flags & scryptlib_1.bsv.Script.Interpreter.SCRIPT_VERIFY_LOW_S) !== 0) { | ||
sig = scryptlib_1.bsv.crypto.Signature.fromTxFormat(buf); | ||
if (!sig.hasLowS()) { | ||
return false; | ||
} | ||
} | ||
else if ((this.flags & scryptlib_1.bsv.Script.Interpreter.SCRIPT_VERIFY_STRICTENC) !== 0) { | ||
sig = scryptlib_1.bsv.crypto.Signature.fromTxFormat(buf); | ||
if (!sig.hasDefinedHashtype()) { | ||
return false; | ||
} | ||
if (!(this.flags & scryptlib_1.bsv.Script.Interpreter.SCRIPT_ENABLE_SIGHASH_FORKID) && | ||
(sig.nhashtype & scryptlib_1.bsv.crypto.Signature.SIGHASH_FORKID)) { | ||
return false; | ||
} | ||
if ((this.flags & scryptlib_1.bsv.Script.Interpreter.SCRIPT_ENABLE_SIGHASH_FORKID) && | ||
!(sig.nhashtype & scryptlib_1.bsv.crypto.Signature.SIGHASH_FORKID)) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
updateState(preimage, balance) { | ||
@@ -198,2 +268,8 @@ // require(Tx.checkPreimageSigHashType(preimage, SigHash.ALL | SigHash.FORKID)); | ||
__metadata("design:type", Function), | ||
__metadata("design:paramtypes", [types_1.Sig, types_1.PubKey]), | ||
__metadata("design:returntype", Boolean) | ||
], SmartContract.prototype, "checkSig", null); | ||
__decorate([ | ||
decorators_1.method, | ||
__metadata("design:type", Function), | ||
__metadata("design:paramtypes", [types_1.SigHashPreimage, BigInt]), | ||
@@ -200,0 +276,0 @@ __metadata("design:returntype", Boolean) |
@@ -5,2 +5,2 @@ import * as ts from 'typescript'; | ||
*/ | ||
export default function transformProgram(program: ts.Program, host: ts.CompilerHost | undefined, pluginOptions: ts.PluginConfig, { ts: tsInstance }: ts.ProgramTransformerExtras): ts.Program; | ||
export default function (program: ts.Program, pluginOptions: any): (ctx: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile; |
@@ -33,6 +33,6 @@ "use strict"; | ||
*/ | ||
function transformProgram(program, host, pluginOptions, { ts: tsInstance }) { | ||
const compilerOptions = program.getCompilerOptions(); | ||
const compilerHost = getPatchedHost(host, tsInstance, compilerOptions); | ||
const rootFileNames = program.getRootFileNames().map(path.normalize); | ||
function default_1(program, pluginOptions) { | ||
if (pluginOptions.debug) { | ||
console.log('transformer loaded with options:', pluginOptions, '\n'); | ||
} | ||
let tsconfigDir = process.env['TS_NODE_PROJECT'] ? path.dirname(process.env['TS_NODE_PROJECT']) : '.'; | ||
@@ -53,85 +53,53 @@ tsconfigDir = path.isAbsolute(tsconfigDir) ? tsconfigDir : path.join(program.getCurrentDirectory(), tsconfigDir); | ||
let indexer = new idexer_1.Indexer({ tsconfigDir, scryptOutDir }); | ||
let checker = program.getTypeChecker(); | ||
const transformedSource = tsInstance.transform(program.getSourceFiles(), [ | ||
transformFile.bind(tsInstance, checker, tsRootDir, scryptOutDir, indexer) | ||
], compilerOptions).transformed; | ||
/* Render modified files and create new SourceFiles for them to use in host's cache */ | ||
const { printFile } = tsInstance.createPrinter(); | ||
for (const sourceFile of transformedSource) { | ||
const { fileName, languageVersion } = sourceFile; | ||
const updatedSourceFile = tsInstance.createSourceFile(fileName, printFile(sourceFile), languageVersion); | ||
compilerHost.fileCache.set(fileName, updatedSourceFile); | ||
} | ||
/* Re-create Program instance */ | ||
return tsInstance.createProgram(rootFileNames, compilerOptions, compilerHost, program); | ||
} | ||
exports.default = transformProgram; | ||
function getPatchedHost(maybeHost, tsInstance, compilerOptions) { | ||
const fileCache = new Map(); | ||
const compilerHost = maybeHost ?? tsInstance.createCompilerHost(compilerOptions, true); | ||
const cacheableVersion = (originalFunc) => { | ||
return function (fileName) { | ||
fileName = path.normalize(fileName); | ||
if (fileCache.has(fileName)) | ||
return fileCache.get(fileName); | ||
const sourceFile = originalFunc.apply(void 0, Array.from(arguments)); | ||
fileCache.set(fileName, sourceFile); | ||
return sourceFile; | ||
}; | ||
}; | ||
return Object.assign(compilerHost, { | ||
getSourceFile: cacheableVersion(compilerHost.getSourceFile), | ||
getSourceFileByPath: cacheableVersion(compilerHost.getSourceFileByPath), | ||
fileCache | ||
}); | ||
} | ||
function transformFile(checker, tsRootDir, scryptOutDir, indexer, ctx) { | ||
const tsInstance = this; | ||
return (sourceFile) => { | ||
// skip declaration files of *.d.ts | ||
if (sourceFile.fileName.endsWith('.d.ts')) { | ||
return sourceFile; | ||
} | ||
// console.log('scanning...', sourceFile.fileName); | ||
// ts source file to root dir relative path which will be keeped in scrypt output structure. | ||
const root2srcRelativePath = path.relative(tsRootDir, sourceFile.fileName); | ||
// the relative path from the scrypt output root directory to the file | ||
const scryptFile = root2srcRelativePath.replace(/\.ts$/, '.scrypt'); | ||
let transpiler = new transpiler_1.Transpiler(sourceFile, checker, scryptOutDir, scryptFile, indexer); | ||
if (!transpiler.isTransformable()) { | ||
return sourceFile; | ||
} | ||
transpiler.transform(); | ||
let metaDataDefs = []; | ||
function visitor(node) { | ||
if (transpiler.contractDefs.includes(node)) { | ||
const className = node.name?.escapedText.toString(); | ||
if (className) { | ||
// `Reflect.defineMetadata("scrypt:file", $relative_path_to_scrypt, $className)` | ||
const defineScryptPath = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('Reflect'), "defineMetadata"), [], [ | ||
ts.factory.createStringLiteral("scrypt:file"), | ||
ts.factory.createStringLiteral(scryptFile), | ||
ts.factory.createIdentifier(className) | ||
]); | ||
const defineSrcPath = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('Reflect'), "defineMetadata"), [], [ | ||
ts.factory.createStringLiteral("__filename"), | ||
ts.factory.createIdentifier("__filename"), | ||
ts.factory.createIdentifier(className) | ||
]); | ||
// const printer = ts.createPrinter(); | ||
// const tsCode = printer.printNode(ts.EmitHint.Expression, defineMetaCall, sourceFile) | ||
// console.log('inserted code: ', tsCode) | ||
metaDataDefs.push(defineScryptPath, defineSrcPath); | ||
return (ctx) => { | ||
return (sourceFile) => { | ||
// ts source file to root dir relative path which will be keeped in scrypt output structure. | ||
const root2srcRelativePath = path.relative(tsRootDir, sourceFile.fileName); | ||
// the relative path from the scrypt output root directory to the file | ||
const scryptFile = root2srcRelativePath.replace(/\.ts$/, '.scrypt'); | ||
let transpiler = new transpiler_1.Transpiler(sourceFile, program.getTypeChecker(), scryptOutDir, scryptFile, indexer); | ||
if (!transpiler.isTransformable()) { | ||
return sourceFile; | ||
} | ||
transpiler.transform(); | ||
let metaDataDefs = []; | ||
function visitor(node) { | ||
if (transpiler.contractDefs.includes(node)) { | ||
const className = node.name?.escapedText.toString(); | ||
if (className) { | ||
// `Reflect.defineMetadata("scrypt:file", $relative_path_to_scrypt, $className)` | ||
const defineScryptPath = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('Reflect'), "defineMetadata"), [], [ | ||
ts.factory.createStringLiteral("scrypt:file"), | ||
ts.factory.createStringLiteral(scryptFile), | ||
ts.factory.createIdentifier(className) | ||
]); | ||
const defineSrcPath = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('Reflect'), "defineMetadata"), [], [ | ||
ts.factory.createStringLiteral("__filename"), | ||
ts.factory.createIdentifier("__filename"), | ||
ts.factory.createIdentifier(className) | ||
]); | ||
// const printer = ts.createPrinter(); | ||
// const tsCode = printer.printNode(ts.EmitHint.Expression, defineMetaCall, sourceFile) | ||
// console.log('inserted code: ', tsCode) | ||
metaDataDefs.push(defineScryptPath, defineSrcPath); | ||
} | ||
return node; | ||
} | ||
return node; | ||
return ts.visitEachChild(node, visitor, ctx); | ||
} | ||
return tsInstance.visitEachChild(node, visitor, ctx); | ||
} | ||
tsInstance.visitEachChild(sourceFile, visitor, ctx); | ||
return ts.factory.updateSourceFile(sourceFile, | ||
// appends meta data definitions to the end of source file. | ||
sourceFile.statements | ||
.concat(ts.factory.createNodeArray(metaDataDefs.map(e => ts.factory.createExpressionStatement(e)))), sourceFile.isDeclarationFile, sourceFile.referencedFiles, sourceFile.typeReferenceDirectives, sourceFile.hasNoDefaultLib, sourceFile.libReferenceDirectives); | ||
ts.visitEachChild(sourceFile, visitor, ctx); | ||
const statements = []; | ||
sourceFile.statements.forEach(s => { | ||
statements.push(s); | ||
if (s.kind === ts.SyntaxKind.ClassDeclaration) { | ||
metaDataDefs.forEach(e => statements.push(ts.factory.createExpressionStatement(e))); | ||
} | ||
}); | ||
return ts.factory.updateSourceFile(sourceFile, | ||
// appends meta data definitions to the end of source file. | ||
statements, sourceFile.isDeclarationFile, sourceFile.referencedFiles, sourceFile.typeReferenceDirectives, sourceFile.hasNoDefaultLib, sourceFile.libReferenceDirectives); | ||
}; | ||
}; | ||
} | ||
exports.default = default_1; | ||
//# sourceMappingURL=transformer.js.map |
@@ -8,3 +8,3 @@ import * as ts from 'typescript'; | ||
sourceMap: number[][]; | ||
constructor(prefixTabs?: number, currentCol?: number, codeLines?: string, sourceMap?: never[]); | ||
constructor(prefixTabs?: number, currentCol?: number, codeLines?: string, sourceMap?: any[]); | ||
copy(): EmittedLine; | ||
@@ -64,4 +64,4 @@ findByCol(col: number): number[]; | ||
private transformExpression; | ||
private transformArrayType; | ||
private transformTypeNode; | ||
private transformEnclosingTypeNode; | ||
private transformType; | ||
private transformModifiers; | ||
@@ -68,0 +68,0 @@ private transformImports; |
@@ -196,4 +196,4 @@ "use strict"; | ||
coordinates: { | ||
line: sourcemap[2], | ||
character: sourcemap[3] | ||
line: sourcemap ? sourcemap[2] : -1, | ||
character: sourcemap ? sourcemap[3] : -1, | ||
} | ||
@@ -289,3 +289,3 @@ })); | ||
transformPropertySignature(node, toSection) { | ||
return this.transformTypeNode(node.type, toSection, this.getCoordinates(node.type.getStart())) | ||
return this.transformEnclosingTypeNode(node.type, toSection) | ||
.append(` ${node.name.getText()}`, this.getCoordinates(node.name.getStart())) | ||
@@ -312,3 +312,3 @@ .append(';'); | ||
.appendWith(this, toSec => { | ||
return this.transformTypeNode(node.type, toSec, this.getCoordinates(node.type?.getStart())); | ||
return this.transformEnclosingTypeNode(node.type, toSec); | ||
}) | ||
@@ -386,3 +386,3 @@ .append(` ${node.name.getText()}`, this.getCoordinates(node.name.getStart())); | ||
.appendWith(this, toSec => { | ||
return this.transformTypeNode(node.type, toSec, this.getCoordinates(node.type.getStart())); | ||
return this.transformEnclosingTypeNode(node.type, toSec); | ||
}) | ||
@@ -401,3 +401,3 @@ .append(' '); | ||
.appendWith(this, toSec => { | ||
return this.transformTypeNode(node.type, toSec, this.getCoordinates(node.type.getStart())); | ||
return this.transformEnclosingTypeNode(node.type, toSec); | ||
}) | ||
@@ -427,6 +427,3 @@ .append(' ') | ||
const s = node; | ||
if (s.expression.kind === ts.SyntaxKind.CallExpression && /^loop(\s*)\([\w.\s]*\)/.test(s.expression.getText())) { | ||
return this.transformExpression(s.expression, toSection); | ||
} | ||
else if (s.expression.kind === ts.SyntaxKind.CallExpression && /^super(\s*)\((.+?)\)/.test(s.expression.getText())) { | ||
if (s.expression.kind === ts.SyntaxKind.CallExpression && /^super(\s*)\(.*\)/.test(s.expression.getText())) { | ||
return toSection; | ||
@@ -442,15 +439,12 @@ } | ||
if (stmt.declarationList.declarations.length > 1) { | ||
throw new TranspileError(`untransfomable statement kind ${node.kind}`, this.getLocation(node.pos)); | ||
throw new TranspileError(`untransfomable statement: ${node.getText()}`, this.getLocation(node.pos)); | ||
} | ||
const d = stmt.declarationList.declarations[0]; | ||
if (!d.initializer) { | ||
throw new TranspileError(`untransfomable statement kind ${node.kind}`, this.getLocation(node.getStart())); | ||
throw new TranspileError(`untransfomable statement: ${node.getText()}`, this.getLocation(node.getStart())); | ||
} | ||
if (!d.type) { | ||
throw new TranspileError(`untransfomable statement kind ${node.kind}`, this.getLocation(node.getStart())); | ||
} | ||
return toSection | ||
.appendWith(this, toSec => { | ||
return this.transformTypeNode(d.type, toSec, this.getCoordinates(d.type.getStart())); | ||
}) | ||
// use `d.type` as type context node if it exists, | ||
// otherwise use `d.initializer` to provide type context so we can leverage the type inference. | ||
const typeCtxNode = d.type || d.initializer; | ||
return this.transformEnclosingTypeNode(typeCtxNode, toSection) | ||
.append(` ${d.name.getText()}`, this.getCoordinates(d.name.getStart())) | ||
@@ -491,4 +485,77 @@ .append(" = ") | ||
} | ||
case (ts.SyntaxKind.ForStatement): { | ||
const s = node; | ||
let inductionVar = undefined; | ||
if (s.initializer?.kind === ts.SyntaxKind.VariableDeclarationList) { | ||
let ivDeclare = s.initializer.declarations[0]; | ||
// initializer expr must match `let $i = 0;` | ||
if (ivDeclare.initializer?.getText() === "0") { | ||
inductionVar = ivDeclare.name; | ||
} | ||
} | ||
if (!inductionVar) { | ||
throw new TranspileError(`for statement in @method should have induction variable declaration as: 'for(let $i = 0; ...; ...)'`, this.getLocation(s.pos)); | ||
} | ||
let loopCount = undefined; | ||
if (s.condition?.kind === ts.SyntaxKind.BinaryExpression) { | ||
let cond = s.condition; | ||
// condition expr must match `$i < $constNum;` | ||
if (cond.left.getText() === inductionVar.getText() | ||
&& cond.operatorToken.kind === ts.SyntaxKind.LessThanToken) { | ||
const condRight = cond.right; | ||
if (condRight.kind === ts.SyntaxKind.NumericLiteral) { | ||
// when `$constNum` is a number literal | ||
loopCount = condRight; | ||
} | ||
else { | ||
// when `$constNum` might refer to a CTC | ||
const symbol = this._checker.getSymbolAtLocation(condRight); | ||
// for const variable like `const N = xxx;` | ||
if (symbol.valueDeclaration?.kind === ts.SyntaxKind.VariableDeclaration | ||
&& ts.getCombinedNodeFlags(symbol.valueDeclaration) === ts.NodeFlags.Const) { | ||
const valueDec = symbol.valueDeclaration; | ||
const initValue = valueDec.initializer; | ||
if (initValue?.kind === ts.SyntaxKind.NumericLiteral) { | ||
loopCount = initValue; | ||
} | ||
} | ||
// for readonly property like `readonly N = xxx;` | ||
if (symbol.valueDeclaration?.kind === ts.SyntaxKind.PropertyDeclaration | ||
&& ts.getModifiers(symbol.valueDeclaration) | ||
.map(m => m.kind).includes(ts.SyntaxKind.ReadonlyKeyword)) { | ||
const valueDec = symbol.valueDeclaration; | ||
const initValue = valueDec.initializer; | ||
if (initValue?.kind === ts.SyntaxKind.NumericLiteral) { | ||
loopCount = initValue; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
if (!loopCount) { | ||
throw new TranspileError(`for statement in @method should have condition expression as: 'for(...; $i < $constNum; ...)'`, this.getLocation(s.pos)); | ||
} | ||
let postIncIV = false; | ||
// incrementor expr must match `$i++` | ||
if (s.incrementor?.kind === ts.SyntaxKind.PostfixUnaryExpression) { | ||
let inc = s.incrementor; | ||
if (inc.operator === ts.SyntaxKind.PlusPlusToken | ||
&& inc.operand.getText() === inductionVar.getText()) { | ||
postIncIV = true; | ||
} | ||
} | ||
if (!postIncIV) { | ||
throw new TranspileError(`for statement in @method should have incrementor expression as: 'for(...; ...; $i++)'`, this.getLocation(s.pos)); | ||
} | ||
return toSection | ||
.append('loop (') | ||
.append(loopCount.getText(), this.getCoordinates(loopCount.pos)) | ||
.append(') : ') | ||
.append(`${inductionVar.getText()} `, this.getCoordinates(inductionVar.pos)) | ||
.appendWith(this, toSec => { | ||
return this.transformStatement(s.statement, toSec); | ||
}); | ||
} | ||
default: { | ||
throw new TranspileError(`untransfomable statement kind ${node.kind}`, this.getLocation(node.pos)); | ||
throw new TranspileError(`untransfomable statement ${node.getText()}`, this.getLocation(node.pos)); | ||
} | ||
@@ -572,3 +639,2 @@ } | ||
let e = node; | ||
let isLoop = e.expression.kind === ts.SyntaxKind.CallExpression && e.expression.expression.getText() === 'loop'; | ||
let isSlice = e.expression.kind === ts.SyntaxKind.PropertyAccessExpression && e.expression.name.getText() === 'slice'; | ||
@@ -579,3 +645,3 @@ let isToString = e.expression.kind === ts.SyntaxKind.PropertyAccessExpression && e.expression.name.getText() === 'toString'; | ||
let isBytesLiteral = e.expression.kind === ts.SyntaxKind.Identifier && e.expression.getText() === 'b'; | ||
const isStillCallExpr = !isLoop && !isSerialize && !isToString && !isBytesLiteral && !isNumberConvert; | ||
const isStillCallExpr = !isSerialize && !isToString && !isBytesLiteral && !isNumberConvert; | ||
// transform the callee's identifier | ||
@@ -587,6 +653,2 @@ toSection.appendWith(this, toSec => { | ||
} | ||
else if (e.expression.getText() === "loop") { | ||
toSec.append("loop", srcLoc); | ||
return toSec; | ||
} | ||
else if (isNumberConvert) { | ||
@@ -683,9 +745,9 @@ return toSec; | ||
} | ||
else if (name === "ALL_ANYONECANPAY_FORKID") { | ||
else if (name === "ANYONECANPAY_ALL_FORKID") { | ||
toSection.append("SigHash.ALL | SigHash.ANYONECANPAY | SigHash.FORKID", this.getCoordinates(e.name.getStart())); | ||
} | ||
else if (name === "NONE_ANYONECANPAY_ORKID") { | ||
else if (name === "ANYONECANPAY_NONE_FORKID") { | ||
toSection.append("SigHash.NONE | SigHash.ANYONECANPAY | SigHash.FORKID", this.getCoordinates(e.name.getStart())); | ||
} | ||
else if (name === "SINGLE_ANYONECANPAY_FORKID") { | ||
else if (name === "ANYONECANPAY_SINGLE_FORKID") { | ||
toSection.append("SigHash.SINGLE | SigHash.ANYONECANPAY | SigHash.FORKID", this.getCoordinates(e.name.getStart())); | ||
@@ -803,9 +865,2 @@ } | ||
} | ||
case (ts.SyntaxKind.TaggedTemplateExpression): { | ||
const e = node; | ||
if (e.tag.getText() == "b") { | ||
toSection.append(`b'${e.template.getText().replaceAll("`", "")}'`, this.getCoordinates(node.getStart())); | ||
} | ||
break; | ||
} | ||
case (ts.SyntaxKind.ParenthesizedExpression): { | ||
@@ -844,152 +899,112 @@ const e = node; | ||
} | ||
transformArrayType(tnode) { | ||
const scryptType = (0, utils_1.getBuildInType)(tnode.getText()); | ||
if (scryptType) { | ||
return [scryptType, []]; | ||
} | ||
else { | ||
let typeReferenceNode = tnode; | ||
let typeName = typeReferenceNode.typeName.getText(); | ||
let type = this._checker.getTypeFromTypeNode(tnode); | ||
let typeSymbol = type.getSymbol(); | ||
if (typeSymbol) { | ||
let symbolFile = this.findDeclarationFile(typeSymbol); | ||
if (symbolFile === this._srcFile) { | ||
this._localTypeSymbols.set(typeName, typeSymbol); | ||
// transform the type enclosed in the node. | ||
transformEnclosingTypeNode(node, toSection) { | ||
const coordinates = this.getCoordinates(node.pos); | ||
const type = this._checker.getTypeAtLocation(node); | ||
return this.transformType(type, node.getText(), coordinates, toSection); | ||
} | ||
// `typeStrCtx` is the type's literal name or the text of the node which encloses the type. | ||
transformType(type, typeStrCtx, coordinates, toSection) { | ||
switch (type.flags) { | ||
case (ts.TypeFlags.Union + ts.TypeFlags.Boolean): // This is the real internal type of `boolean`, it's a union of `true` and `false` | ||
case ts.TypeFlags.BooleanLiteral: | ||
case ts.TypeFlags.Boolean: { | ||
return toSection.append("bool", coordinates); | ||
} | ||
case ts.TypeFlags.StringLiteral: | ||
case ts.TypeFlags.String: { | ||
return toSection.append("bytes", coordinates); | ||
} | ||
case ts.TypeFlags.NumberLiteral: | ||
case ts.TypeFlags.Number: | ||
case ts.TypeFlags.BigIntLiteral: | ||
case ts.TypeFlags.BigInt: { | ||
return toSection.append("int", coordinates); | ||
} | ||
case ts.TypeFlags.Object: { | ||
const typeString = type.symbol === undefined | ||
? this._checker.typeToString(type, undefined, ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope) | ||
: (type.aliasSymbol === undefined ? type.symbol.escapedName : type.aliasSymbol.escapedName).toString(); | ||
// for inferred array type, like `x = [1, 2]` | ||
if (typeString === "Array") { | ||
throw new TranspileError(`untransformable \`Array\` type, please used \`FixedArray\` type instead.`, { fileName: this._srcFile.fileName, coordinates }); | ||
} | ||
else { | ||
this._importedTypeSymbols.set(typeName, typeSymbol); | ||
// for declared object literal type, like `x : {prop: number}` | ||
if (typeString === "__type") { | ||
throw new TranspileError(`untransformable literal object type for: '${typeStrCtx}'`, { fileName: this._srcFile.fileName, coordinates }); | ||
} | ||
} | ||
switch (type.flags) { | ||
case ts.TypeFlags.String: { | ||
return ["bytes", []]; | ||
// for inferred object literal type, like `x = {prop: 1}` | ||
if (typeString === "__object") { | ||
throw new TranspileError(`untransformable literal object type for: '${typeStrCtx}'`, { fileName: this._srcFile.fileName, coordinates }); | ||
} | ||
case ts.TypeFlags.Number: { | ||
return ["int", []]; | ||
// for bigint & number wrapper | ||
if (typeString === "BigInt" || typeString === "Number") { | ||
return toSection.append('int', coordinates); | ||
} | ||
case ts.TypeFlags.BigInt: { | ||
return ["int", []]; | ||
// for string wrapper | ||
if (typeString === "String") { | ||
return toSection.append('bytes', coordinates); | ||
} | ||
case ts.TypeFlags.Object: { | ||
if (typeName === "FixedArray") { | ||
const args = typeReferenceNode.typeArguments || []; | ||
let tn = ""; | ||
let dimensionals = []; | ||
args.forEach((arg) => { | ||
if (arg.kind === ts.SyntaxKind.BigIntKeyword) { | ||
tn = "int"; | ||
} | ||
else if (arg.kind === ts.SyntaxKind.BooleanKeyword) { | ||
tn = "bool"; | ||
} | ||
else if (arg.kind === ts.SyntaxKind.TypeReference) { | ||
const [t, d] = this.transformArrayType(arg); | ||
tn = t; | ||
dimensionals = d; | ||
} | ||
else if (arg.kind === ts.SyntaxKind.LiteralType) { | ||
dimensionals.push(arg.getText()); | ||
} | ||
}); | ||
return [tn, dimensionals]; | ||
} | ||
else { | ||
return [typeName, []]; | ||
} | ||
// for boolean wrapper | ||
if (typeString === "Boolean") { | ||
return toSection.append('bool', coordinates); | ||
} | ||
default: { | ||
throw new TranspileError(`untransfor111mable type ${type.flags}`, { fileName: this._srcFile.fileName, coordinates: this.getCoordinates(tnode.pos) }); | ||
if (typeString.match(/\[_arrElem: .+\]/)) { | ||
// built-in type `FixedArray` goes here. | ||
const getBaseElemType = (typeRef) => { | ||
if (typeRef.typeArguments?.length > 0) { | ||
let innerTypeRef = typeRef.typeArguments[0]; | ||
return getBaseElemType(innerTypeRef); | ||
} | ||
else { | ||
return typeRef; | ||
} | ||
}; | ||
const baseElemType = getBaseElemType(type); | ||
// touchedTypeSymbol = baseElemType.symbol; | ||
// `typeString` may looks like: `[_arrElem: [_arrElem: t], ...]` | ||
const baseTypeReg = /_arrElem: ([^\[,\]]+)/; | ||
// transform the `typeString` to an array object like: [[0, 0], ...], so we can get its lengths for each nested level | ||
const arrStructure = JSON.parse(typeString | ||
.replaceAll(new RegExp(baseTypeReg, 'g'), "0") | ||
.replaceAll('_arrElem:', '')); | ||
const getNestedArrayLens = (arr) => { | ||
let arrLen = arr.length; | ||
if (arr[0] instanceof Array) { | ||
return [arrLen].concat(getNestedArrayLens(arr[0])); | ||
} | ||
else { | ||
return [arrLen]; | ||
} | ||
}; | ||
const arrLens = getNestedArrayLens(arrStructure); | ||
this.transformType(baseElemType, typeStrCtx, coordinates, toSection) | ||
.append(`${arrLens.map(i => '[' + i + ']').join('')}`, coordinates); | ||
} | ||
} | ||
} | ||
} | ||
transformTypeNode(tnode, toSection, coordinates) { | ||
const scryptType = (0, utils_1.getBuildInType)(tnode.getText()); | ||
if (scryptType) { | ||
toSection.append(scryptType, coordinates); | ||
} | ||
else { | ||
let typeReferenceNode = tnode; | ||
let typeName = typeReferenceNode.typeName.getText(); | ||
let type = this._checker.getTypeFromTypeNode(tnode); | ||
let typeSymbol = type.getSymbol(); | ||
if (typeSymbol) { | ||
let symbolFile = this.findDeclarationFile(typeSymbol); | ||
if (symbolFile === this._srcFile) { | ||
this._localTypeSymbols.set(typeName, typeSymbol); | ||
} | ||
else { | ||
this._importedTypeSymbols.set(typeName, typeSymbol); | ||
} | ||
} | ||
switch (type.flags) { | ||
case ts.TypeFlags.String: { | ||
toSection.append('bytes', coordinates); | ||
break; | ||
} | ||
case ts.TypeFlags.Number: { | ||
toSection.append('int', coordinates); | ||
break; | ||
} | ||
case ts.TypeFlags.BigInt: { | ||
toSection.append('int', coordinates); | ||
} | ||
case ts.TypeFlags.Object: { | ||
if (typeName === "FixedArray") { | ||
const [t, dimensionals] = this.transformArrayType(tnode); | ||
toSection.append(`${t}${dimensionals.reverse().map(d => `[${d}]`).join('')}`, coordinates); | ||
// all user defined or std types go here. | ||
toSection.append(typeString === "PubKeyHash" ? "Ripemd160" : typeString, coordinates); | ||
if (type.symbol) { | ||
// record type visting if its symbol exits, for the purpose of `struct` & `import` generations. | ||
let symbolFile = this.findDeclarationFile(type.symbol); | ||
if (symbolFile === this._srcFile) { | ||
this._localTypeSymbols.set(typeString, type.symbol); | ||
} | ||
else { | ||
this._importedTypeSymbols.set(typeString, type.symbol); | ||
} | ||
} | ||
else { | ||
toSection.append(typeName, coordinates); | ||
} | ||
break; | ||
} | ||
case ts.TypeFlags.Any: { | ||
toSection.append('auto', coordinates); | ||
break; | ||
} | ||
default: { | ||
throw new TranspileError(`untransformable type ${type.flags}`, { fileName: this._srcFile.fileName, coordinates }); | ||
} | ||
break; | ||
} | ||
case ts.TypeFlags.Any: { | ||
toSection.append('auto', coordinates); | ||
break; | ||
} | ||
default: { | ||
throw new TranspileError(`untransformable type in '${typeStrCtx}', flag: ${type.flags}`, { fileName: this._srcFile.fileName, coordinates }); | ||
} | ||
} | ||
return toSection; | ||
} | ||
// private transformType(type: ts.Type, toSection: EmittedSection, coordinates?: ts.LineAndCharacter) : EmittedSection { | ||
// switch (type.flags) { | ||
// case ts.TypeFlags.String: { | ||
// toSection.append('bytes', coordinates); | ||
// break; | ||
// } | ||
// case ts.TypeFlags.Number: { | ||
// toSection.append('int', coordinates); | ||
// break; | ||
// } | ||
// case ts.TypeFlags.BigInt: { | ||
// toSection.append('int', coordinates); | ||
// } | ||
// case ts.TypeFlags.Object: { | ||
// if (typeName === "FixedArray") { | ||
// const [t, dimensionals] = this.transformArrayType(tnode); | ||
// toSection.append(`${t}${dimensionals.reverse().map(d => `[${d}]`).join('') | ||
// }`, coordinates); | ||
// } else { | ||
// toSection.append(typeName, coordinates); | ||
// } | ||
// break; | ||
// } | ||
// case ts.TypeFlags.Any: { | ||
// toSection.append('auto', coordinates); | ||
// break; | ||
// } | ||
// default: { | ||
// throw new TranspileError( | ||
// `untransformable type ${type.flags}`, | ||
// { fileName: this._srcFile.fileName, coordinates } | ||
// ); | ||
// } | ||
// } | ||
// return toSection; | ||
// } | ||
transformModifiers(node, toSection) { | ||
@@ -1036,3 +1051,3 @@ let modifiers = ts.getModifiers(node); | ||
let moduleUsed = false; | ||
moduleSymbol.exports.forEach((e) => { | ||
(moduleSymbol?.exports || []).forEach((e) => { | ||
// mark modules as used if their exported symbols were found used in contracts. | ||
@@ -1039,0 +1054,0 @@ moduleUsed ||= usedSymbols.includes(e); |
{ | ||
"name": "scrypt-ts", | ||
"version": "0.1.3-alpha", | ||
"version": "0.1.3-alpha.1", | ||
"description": "A toolset for building sCrypt smart contract applications on Bitcoin SV network written in typescript.", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
292
README.md
# scrypt-ts | ||
`scrypt-ts` is a Typescript framework to write smart contracts on Bitcoin SV. | ||
`scrypt-ts` is a Typescript framework to write smart contracts on Bitcoin SV. | ||
# Installation | ||
Use this command to install `scrypt-ts` to your project: | ||
`npm install -S scrypt-ts` | ||
# Setup | ||
## 1. Update `tsconfig.json` | ||
`scrypt-ts` depends on [ts-patch](https://github.com/nonara/ts-patch) to provide a custom plugin support for typescript. So first we need to add `scrypt-ts` plugin and enable decorators in `tsconfig.json` file like: | ||
```json | ||
{ | ||
"compilerOptions": { | ||
... | ||
"experimentalDecorators": true, | ||
"plugins": [ | ||
{ | ||
"transform": "scrypt-ts/dist/transformer", // Required | ||
"transformProgram": true, // Required | ||
"outDir": "./scrypt", // Optional, define the auto-generated `.scrypt` files folder | ||
"debug": false // Optional, enable/disable debug log in console. | ||
} | ||
] | ||
} | ||
} | ||
``` | ||
**Note**: Currently there is an issue with typescript version `4.9.x`, so make sure to lock typescript version to `4.8.4`: | ||
## 2. Download sCrypt compiler | ||
`scrypt-ts` also depends on the native sCrypt compiler which could be downloaded with command: | ||
``` | ||
curl -Ls https://scrypt.io/setup | sh -s -- | ||
``` | ||
That's all, you're ready to go! | ||
# Usage | ||
## Write Contract | ||
A contract can be written as a class that extends the `SmartContract` base, a simple example could be like this: | ||
```ts | ||
import { SmartContract, method, prop, assert } from "scrypt-ts"; | ||
class Demo extends SmartContract { | ||
@prop() | ||
x: bigint; | ||
constructor(x: bigint) { | ||
super(x); | ||
this.x = x; | ||
} | ||
@method | ||
public unlock(x: bigint) { | ||
assert(this.add(this.x, 1n) === x); | ||
} | ||
@method | ||
add(x0: bigint, x1:bigint) : bigint { | ||
return x0 + x1; | ||
} | ||
} | ||
``` | ||
### Decorator: `@prop(state=false)` | ||
Use this decorator on class properties to mark them as contract properties, which means the values would be stored on chain within [tx](https://wiki.bitcoinsv.io/index.php/Bitcoin_Transactions). | ||
This decorator can take a boolean parameter, which indicates whether it can be updated later. If it's `true`, the property is so called a `stateful` property and its value stored on chain can be updated between contract calls; otherwise, its value can not be changed since the contract deploy. | ||
### Decorator: `@method` | ||
Use this decorator on class methods to mark them as contract methods. The logic implemented in these methods would be serialized into [tx](https://wiki.bitcoinsv.io/index.php/Bitcoin_Transactions) and be executed on chain. | ||
The class methods decorated by `@method` have some special requirements / restrains that should be followed: | ||
* Within these methods, only functions provided as built-ins from `scrypt-ts` or methods also decorated by `@method` can be called; Similarly, only the properties decorated by `@prop` can be visited. | ||
* With `public` modifier, a method is marked as an entry method that could be called outside the contract class, especially during a tx building process. The main purpose of these methods is to validate / verify / check assertions for its input parameters according to its `@prop` decorated properties. The return value must be `void`. | ||
* Without a `public` modifier, a method is kind of an inner function usually be called within the contract class. It can return any valid types described later. | ||
### Types | ||
The types can be used in `@prop` and `@method` are restricted to these kinds: | ||
* Basic types: `boolean` / `string` / `bigint`; | ||
*Note*: the type `number` is not allowed in `@prop` because it may cause precision issues, and it's recommended to be used only in a few cases. | ||
* Types composed by the basic types at the end level, for example like: | ||
```ts | ||
type ST = { | ||
x: bigint; | ||
} | ||
interface ST1 { | ||
x: ST; | ||
y: string; | ||
} | ||
``` | ||
* Array types **must** be the built-in version of `FixedArray`, which has a compile time constant declared as its length, for example like: | ||
```ts | ||
let aaa: FixedArray<bigint, 3> = [1n, 3n, 3n]; | ||
// 2d array | ||
let abb: FixedArray<FixedArray<bigint, 2>, 3> = [[1n, 3n], [1n, 3n], [1n, 3n]]; | ||
``` | ||
* Other `SmartContract` subclasses provided as libraries. | ||
### Statements | ||
There are also some other restraints / rules on the statemets that could be used within the `@method`s besides the previously mentioned. | ||
#### `for` statement | ||
Because of the underlaying limitation of `loop` implemetion on Bitcoin script, one can only use a compile time const number as the loop iterations. | ||
So currently if you want to build a loop inside `@method`s, there is only one restricted version of `for` statement that could be used. It's looks like: | ||
```ts | ||
for(let $i = 0; $i < $constNum; $i++) { | ||
... | ||
} | ||
``` | ||
Note that the initial value `0` and the `<` operator and the post unary operator `++` are all unchangeable. | ||
* `$i` can be whatever you named the induction variable; | ||
* `$constNum` should be an expression of a CTC numberic value of the followings: | ||
A number literal like: | ||
```ts | ||
for(let i = 0; i < 5; i++ ) ... | ||
``` | ||
Or a `const` variable name like: | ||
```ts | ||
const N = 3; | ||
for(let i = 0; i < N; i++ ) ... | ||
``` | ||
Or a `readonly` property name like: | ||
```ts | ||
class X { | ||
static readonly N = 3; | ||
} | ||
for(let i = 0; i < X.N; i++ ) ... | ||
``` | ||
#### `console.log` statement | ||
As descirbed before, all the javascript/typescript built-in functions / global variables are also not allowed to be used in `@method`s, but there are few exceptions. | ||
One exceptional statement is `console.log`, which can be used to output logs for debugging purpose. | ||
## Build | ||
Just run `npx tsc`, or `npm run build` if you have script as below declared in `package.json`: | ||
```json | ||
{ | ||
"scripts": { | ||
"build": "tsc" | ||
} | ||
} | ||
``` | ||
The `tsc` compiling process may output diagnostic informations in console about the contract class, update the source code if needed. | ||
## Test | ||
You could write tests using tools like `mocha`, for example: | ||
```js | ||
describe('Test SmartContract `Demo`', () => { | ||
before(async () => { | ||
await Demo.compile(); | ||
}) | ||
it('should pass the public method unit test successfully.', async () => { | ||
let demo = new Demo(1n); | ||
let result = demo.verify(() => demo.unlock(2n)); | ||
expect(result.success, result.error).to.eq(true); | ||
expect(() => { | ||
demo.unlock(3n); | ||
}).to.throw(/Execution failed/) | ||
}) | ||
}) | ||
``` | ||
## Deploy and Call | ||
Generally speaking, if you want to deploy or call the contract to BSV network, it takes three steps: | ||
### 1. Build contract instance: | ||
Giving proper parameters to get an up-to-date contract instance. | ||
### 2. Build tx: | ||
Build a tx corresponding to your business logic, especially to set the tx's proper input & output script with contract instance. | ||
For example, to get the locking script, use code like: | ||
```js | ||
instance.lockingScript; | ||
``` | ||
To get the unlocking script for a certain `entryMethod`, use code like: | ||
```js | ||
instance.getUnlockingScript(() => { | ||
intance.entryMethod(...); | ||
}) | ||
``` | ||
### 3. Send tx: | ||
The final step is to sign and send the tx to the network. | ||
Here is an example code to deploy & call a `Demo` contract. | ||
```js | ||
await Demo.compile(); | ||
// build contract instance | ||
const demo = new Demo(2n); | ||
const balance = 1000; | ||
// build contract deploy tx | ||
const utxos = await fetchUtxos(); | ||
const unsignedDeployTx = | ||
new bsv.Transaction() | ||
.from(utxos) | ||
.addOutput(new bsv.Transaction.Output({ | ||
// get the locking script for `demo` instance | ||
script: demo.lockingScript, | ||
satoshis: balance, | ||
})); | ||
// send contract deploy tx | ||
const deployTx = await signAndSend(unsignedDeployTx); | ||
console.log('contract deployed: ', deployTx.id) | ||
// build contract call tx | ||
const unsignedCallTx = | ||
new bsv.Transaction() | ||
.addInput(new bsv.Transaction.Input({ | ||
prevTxId: deployTx.id, | ||
outputIndex: outputIdx, | ||
script: demo.getUnlockingScript(() => { | ||
// call public method to get the unlocking script for `demo` instance. | ||
demo.unlock(3n); | ||
}), | ||
output: deployTx.outputs[outputIdx] | ||
})) | ||
.addOutput( | ||
new bsv.Transaction.Output({ | ||
script: bsv.Script.buildPublicKeyHashOut(publicKey.toAddress()), | ||
satoshis: balance / 2 | ||
}) | ||
); | ||
// send contract call tx | ||
const callTx = await signAndSend(unsignedCallTx); | ||
console.log('contract called: ', callTx.id) | ||
``` | ||
# Documentation | ||
`scrypt-ts` documentation is available [here](). |
143747
21
3157
293