Skip to content

Building and reading scripts

An SVScript is a script, which is to say a list of chunks, each of which is either an opcode or a data push. ScriptBuilder assembles one for us, and the SVScript constructors parse one from hex, ASM or the library’s own string format. The language itself is described in the guide’s Bitcoin Script page.

Opcodes are constants on OpCodes. addData pushes bytes, and chooses the push opcode from their length.

final pubkeyHash = Uint8List.fromList(HEX.decode('94837d2d5d6106aa97db38957dcc294181ee91e9'));
final script = ScriptBuilder()
.opCode(OpCodes.OP_DUP)
.opCode(OpCodes.OP_HASH160)
.addData(pubkeyHash) // picks the smallest push opcode for the length
.opCode(OpCodes.OP_EQUALVERIFY)
.opCode(OpCodes.OP_CHECKSIG)
.build();
script.toHex(); // 76a91494837d2d5d6106aa97db38957dcc294181ee91e988ac
script.toString(type: 'asm'); // OP_DUP OP_HASH160 94837d2d5d6106aa97db38957dcc294181ee91e9 OP_EQUALVERIFY OP_CHECKSIG
script.toString(); // OP_DUP OP_HASH160 20 0x94837d2d5d6106aa97db38957dcc294181ee91e9 OP_EQUALVERIFY OP_CHECKSIG

toString(type: 'asm') gives the ASM that most tools use. toString() gives the library’s own format, which writes each push as <length> 0x<hex>. This is the format that SVScript.fromString reads, and the one that UTXO maps use.

final fromHex = SVScript.fromHex('76a91494837d2d5d6106aa97db38957dcc294181ee91e988ac');
final fromAsm = SVScript.fromASM(
'OP_DUP OP_HASH160 94837d2d5d6106aa97db38957dcc294181ee91e9 OP_EQUALVERIFY OP_CHECKSIG');
// The library's own format writes each push as "<length> 0x<hex>".
final fromString = SVScript.fromString(
'OP_DUP OP_HASH160 20 0x94837d2d5d6106aa97db38957dcc294181ee91e9 OP_EQUALVERIFY OP_CHECKSIG');

number encodes an integer the way script arithmetic expects it: OP_0 to OP_16 and OP_1NEGATE for small values, and a minimal little-endian push otherwise.

final script = ScriptBuilder()
.number(5) // OP_5
.number(1000) // a minimal little-endian push: e803
.number(-1) // OP_1NEGATE
.opTrue() // OP_1
.opFalse() // OP_0
.build();
script.toString(type: 'asm'); // OP_5 e803 -1 OP_1 0
final big = ScriptBuilder().addData(Uint8List(300)).build();
big.chunks[0].opcodenum == OpCodes.OP_PUSHDATA2; // true: 300 bytes needs a 2-byte length

chunks lists the parts of a script. A chunk is a data push when buf holds bytes. Otherwise opcodenum is the opcode, and OpCodes.getOpCodeName gives us its name.

// script: 0 OP_RETURN 68656c6c6f
final pushes = <String>[];
for (final ScriptChunk chunk in script.chunks) {
if (chunk.buf != null && chunk.buf!.isNotEmpty) {
pushes.add(utf8.decode(chunk.buf!)); // a data push
} else {
OpCodes.getOpCodeName(chunk.opcodenum); // an opcode: OP_0, OP_RETURN
}
}
// pushes: [hello]

ScriptBuilder.fromScript copies an existing script’s chunks, and the …AtIndex methods and insertData insert at a given position. The original script is left unchanged.

// Let's prefix a P2PKH script with data, the shape P2PKHDataLockBuilder produces.
final withData = ScriptBuilder.fromScript(p2pkh)
.insertData(0, Uint8List.fromList(utf8.encode('note')))
.opCodeAtIndex(1, OpCodes.OP_DROP)
.build();

ScriptPattern has static checks for the standard script types. For more detail, including the values inside a script, we use script templates.

ScriptPattern.isP2PKH(p2pkh); // true
ScriptPattern.isP2SH(p2pkh); // false
ScriptPattern.isOpReturn(SVScript.fromHex('006a0568656c6c6f')); // true
p2pkh.isPushOnly(); // false

Please note that SVScript.isScriptHashOut() is misnamed in 3.1.2. It tests for P2PKH. Use ScriptPattern.isP2SH for P2SH.