feat: improve error handling in Stacks message parsing

This commit is contained in:
Matthew Little
2020-02-19 18:13:27 -05:00
parent 7ddb70d376
commit a9bfbb5431
14 changed files with 405 additions and 159 deletions

View File

@@ -3,10 +3,15 @@ module.exports = {
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
'eslint-plugin-tsdoc',
],
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
project: [
'./tsconfig.json',
],
ecmaVersion: 2019,
sourceType: 'module',
},
extends: [
'eslint:recommended',
@@ -15,8 +20,14 @@ module.exports = {
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:prettier/recommended',
],
ignorePatterns: ["lib/*"],
ignorePatterns: [
'lib/*',
],
rules: {
"@typescript-eslint/no-use-before-define": ["error", "nofunc"]
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/no-use-before-define': ['error', 'nofunc'],
'@typescript-eslint/no-floating-promises': 'error',
'no-warning-comments': 'warn',
'tsdoc/syntax': 'error',
}
};

4
.vscode/launch.json vendored
View File

@@ -14,7 +14,9 @@
],
"args": [
"${workspaceFolder}/src/index.ts"
]
],
"outputCapture": "std",
"internalConsoleOptions": "openOnSessionStart"
}
]
}

5
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"search.exclude": {
"./lib/**": true
},
}

75
package-lock.json generated
View File

@@ -30,6 +30,44 @@
"integrity": "sha512-A+wfdVwD1Sxd/D3PPJI67Evo7q2fp15hCOFLB5jmzcS1MdN8BQdFm6j51Sti8xLN4qHmuYkicbFBUluGx2h63g==",
"dev": true
},
"@microsoft/tsdoc": {
"version": "0.12.16",
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.12.16.tgz",
"integrity": "sha512-SX8JNEVy6U5+56aQnQB8A2XK+WSF//b0kBa6KqxE48pcccqVuIu1ePAR/EWd1cQB6zWv26QIY5uv/++qm+Z3Mw==",
"dev": true
},
"@microsoft/tsdoc-config": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.13.0.tgz",
"integrity": "sha512-ga2nChnT2K4y9zqQ0/8xf3TgFz+zbgxLXVRsIsbBhgx2gi0Ez4VQ2CKjElu5kF0uzLz6fLemkkVzUyi/ptekvw==",
"dev": true,
"requires": {
"@microsoft/tsdoc": "0.12.16",
"ajv": "~6.10.2",
"jju": "~1.4.0",
"resolve": "~1.12.0"
},
"dependencies": {
"ajv": {
"version": "6.10.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz",
"integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==",
"dev": true,
"requires": {
"fast-deep-equal": "^2.0.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
},
"fast-deep-equal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
"dev": true
}
}
},
"@types/eslint-visitor-keys": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
@@ -43,9 +81,9 @@
"dev": true
},
"@types/node": {
"version": "12.12.27",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.27.tgz",
"integrity": "sha512-odQFl/+B9idbdS0e8IxDl2ia/LP8KZLXhV3BUeI98TrZp0uoIzQPhGd+5EtzHmT0SMOIaPd7jfz6pOHLWTtl7A=="
"version": "13.7.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.4.tgz",
"integrity": "sha512-oVeL12C6gQS/GAExndigSaLxTrKpQPxewx9bOcwfvJiJge4rr7wNaph4J+ns5hrmIV2as5qxqN8YKthn9qh0jw=="
},
"@typescript-eslint/eslint-plugin": {
"version": "2.20.0",
@@ -384,6 +422,16 @@
"prettier-linter-helpers": "^1.0.0"
}
},
"eslint-plugin-tsdoc": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.2.1.tgz",
"integrity": "sha512-PPobACY4MUmk1r7oQZwQ53GcUvkmlGhkEoBc+JWRcHHXrJ7Ny4OGWUoha9lzLJCyKOP5C8AYjVAr2rxahsXWZg==",
"dev": true,
"requires": {
"@microsoft/tsdoc": "0.12.16",
"@microsoft/tsdoc-config": "0.13.0"
}
},
"eslint-scope": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz",
@@ -683,6 +731,12 @@
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
"dev": true
},
"jju": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz",
"integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=",
"dev": true
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -845,6 +899,12 @@
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
"dev": true
},
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
},
"prelude-ls": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
@@ -884,6 +944,15 @@
"integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==",
"dev": true
},
"resolve": {
"version": "1.12.3",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.3.tgz",
"integrity": "sha512-hF6+hAPlxjqHWrw4p1rF3Wztbgxd4AjA5VlUzY5zcTb4J8D3JK4/1RjU48pHz2PJWzGVsLB1VWZkvJzhK2CCOA==",
"dev": true,
"requires": {
"path-parse": "^1.0.6"
}
},
"resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",

View File

@@ -6,8 +6,8 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx -f visualstudio",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx -f visualstudio --fix"
"lint": "eslint . --ext .js,.jsx,.ts,.tsx -f codeframe",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx -f codeframe --fix"
},
"repository": {
"type": "git",
@@ -20,8 +20,11 @@
},
"homepage": "https://github.com/blockstack/blockstack-core-sidecar#readme",
"prettier": "@blockstack/prettier-config",
"engines": {
"node": ">=13"
},
"dependencies": {
"@types/node": "^12",
"@types/node": "^13.7.4",
"big-integer": "^1.6.48",
"smart-buffer": "^4.1.0",
"ts-node": "^8.6.2",
@@ -34,6 +37,7 @@
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-tsdoc": "^0.2.1",
"prettier": "1.19.1"
}
}

View File

@@ -1,5 +1,6 @@
import { Readable } from 'stream';
import { SmartBuffer } from 'smart-buffer';
import { isEnum } from './helpers';
class BufferReader extends SmartBuffer {
readBigUIntLE(length: number): bigint {
@@ -8,6 +9,7 @@ class BufferReader extends SmartBuffer {
const num = BigInt(`0x${hex}`);
return num;
}
readBigUIntBE(length: number): bigint {
const buffer = this.readBuffer(length);
const hex = buffer.toString('hex');
@@ -17,21 +19,23 @@ class BufferReader extends SmartBuffer {
}
export class BinaryReader {
readonly stream: Readable;
readonly readableStream: Readable;
constructor(stream: Readable) {
this.stream = stream;
constructor(readableStream: Readable) {
this.readableStream = readableStream;
}
private readExact(length: number, callback: (error: Error | null, data?: Buffer) => void): void {
const chunk: Buffer = this.stream.read(length);
// TODO: debug logging for this during async perf reading testing..
// console.info(`___INFO: ${(this.readableStream as any).readableFlowing}`);
const chunk: Buffer = this.readableStream.read(length);
if (chunk !== null) {
if (chunk.length !== length) {
callback(new Error(`Unexpected chunk length, expected '${length}', received '${chunk.length}'`));
}
callback(null, chunk);
} else {
this.stream.once('readable', () => {
this.readableStream.once('readable', () => {
this.readExact(length, callback);
});
}
@@ -53,6 +57,18 @@ export class BinaryReader {
return this.readBuffer(1).then(buffer => buffer[0]);
}
async readUInt8Enum<T extends string, TEnumValue extends number>(
enumVariable: { [key in T]: TEnumValue },
invalidEnumErrorFormatter: (val: number) => Error
): Promise<TEnumValue> {
const num = await this.readUInt8();
if (isEnum(enumVariable, num)) {
return num;
} else {
throw invalidEnumErrorFormatter(num);
}
}
readUInt16BE(): Promise<number> {
return this.readBuffer(2).then(buffer => buffer.readUInt16BE(0));
}
@@ -69,7 +85,13 @@ export class BinaryReader {
return this.readBuffer(32);
}
sync(length: number): Promise<BufferReader> {
/**
* Read a fixed amount of bytes into a buffer which provides much faster synchronous
* buffer read operations. This should always be used for reading a span of fixed-length
* fields.
* @param length - Byte count to read into a buffer.
*/
readFixed(length: number): Promise<BufferReader> {
return this.readBuffer(length).then(buffer => new BufferReader({ buff: buffer }));
}
}

View File

@@ -45,7 +45,7 @@ export interface BlockHeader {
}
export async function readBlockHeader(stream: BinaryReader): Promise<BlockHeader> {
const cursor = await stream.sync(blockHeaderSize);
const cursor = await stream.readFixed(blockHeaderSize);
const header: BlockHeader = {
version: cursor.readUInt8(),
workScore: {

View File

@@ -1,18 +1,12 @@
import { BinaryReader } from './binaryReader';
import { readBlockHeader, BlockHeader } from './blockHeaderReader';
import { readTransactions, Transaction } from './txReader';
import { StacksMessageTypeID } from './stacks-p2p';
export interface Block {
header: BlockHeader;
transactions: Transaction[];
}
export interface StacksMessageBlocks {
messageTypeId: StacksMessageTypeID.Blocks;
blocks: Block[];
}
export async function readBlocks(stream: BinaryReader): Promise<Block[]> {
const blockCount = await stream.readUInt32BE();
const blocks = new Array<Block>(blockCount);

17
src/errors.ts Normal file
View File

@@ -0,0 +1,17 @@
export class StacksMessageParsingError extends Error {
constructor(message: string) {
super(message);
this.message = message;
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
export class NotImplementedError extends Error {
constructor(message: string) {
super(message);
this.message = message;
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}

65
src/helpers.ts Normal file
View File

@@ -0,0 +1,65 @@
function createEnumChecker<T extends string, TEnumValue extends number>(
enumVariable: { [key in T]: TEnumValue }
): (value: number) => value is TEnumValue {
// Create a set of valid enum number values.
const enumValues = Object.values<number>(enumVariable).filter(v => typeof v === 'number');
const enumValueSet = new Set<number>(enumValues);
return (value: number): value is TEnumValue => enumValueSet.has(value);
}
const enumCheckFunctions = new Map<object, (value: number) => boolean>();
/**
* Type guard to check if a given value is a valid enum value.
* @param enumVariable - Literal `enum` type.
* @param value - A value to check against the enum's values.
* @example
* ```ts
* enum Color {
* Purple = 3,
* Orange = 5
* }
* const val: number = 3;
* if (isEnum(Color, val)) {
* // `val` is known as enum type `Color`, e.g.:
* const colorVal: Color = val;
* }
* ```
*/
export function isEnum<T extends string, TEnumValue extends number>(
enumVariable: { [key in T]: TEnumValue },
value: number
): value is TEnumValue {
const checker = enumCheckFunctions.get(enumVariable);
if (checker !== undefined) {
return checker(value);
}
const newChecker = createEnumChecker(enumVariable);
enumCheckFunctions.set(enumVariable, newChecker);
return isEnum(enumVariable, value);
}
const enumMaps = new Map<object, Map<unknown, unknown>>();
export function getEnumDescription<T extends string, TEnumValue extends number>(
enumVariable: { [key in T]: TEnumValue },
value: number
): string {
const enumMap = enumMaps.get(enumVariable);
if (enumMap !== undefined) {
const enumKey = enumMap.get(value);
if (enumKey !== undefined) {
return `${value} '${enumKey}'`;
} else {
return `${value}`;
}
}
// Create a map of `[enumValue: number]: enumNameString`
const enumValues = Object.entries(enumVariable)
.filter(([, v]) => typeof v === 'number')
.map<[number, string]>(([k, v]) => [v as number, k]);
const newEnumMap = new Map(enumValues);
enumMaps.set(enumVariable, newEnumMap);
return getEnumDescription(enumVariable, value);
}

View File

@@ -11,19 +11,30 @@ async function readSocket(socket: net.Socket): Promise<void> {
}
}
const server = net.createServer(c => {
// 'connection' listener.
const server = net.createServer(clientSocket => {
console.log('client connected');
readSocket(c);
c.on('end', () => {
readSocket(clientSocket).catch(error => {
console.error(`error reading messages from socket: ${error}`);
console.error(error);
clientSocket.destroy();
server.close();
});
clientSocket.on('end', () => {
console.log('client disconnected');
});
// c.write('hello\r\n');
// c.pipe(c);
});
server.on('error', err => {
console.error('socket server error:');
console.error(err);
throw err;
});
server.listen(3700, () => {
console.log('server bound');
const addr = server.address();
if (addr === null) {
throw new Error('server missing address');
}
const addrStr = typeof addr === 'string' ? addr : `${addr.address}:${addr.port}`;
console.log(`server listening at ${addrStr}`);
});

View File

@@ -1,5 +1,8 @@
import { BinaryReader } from './binaryReader';
import { readBlocks, Block, StacksMessageBlocks } from './blockReader';
import { readBlocks, Block } from './blockReader';
import { Transaction, readTransaction } from './txReader';
import { StacksMessageParsingError, NotImplementedError } from './errors';
import { getEnumDescription } from './helpers';
export enum StacksMessageTypeID {
Handshake = 0,
@@ -20,11 +23,31 @@ export enum StacksMessageTypeID {
Reserved = 255,
}
type StacksMessage = StacksMessageBlocks;
export interface StacksMessageBlocks {
messageTypeId: StacksMessageTypeID.Blocks;
blocks: Block[];
}
export function isStacksMessageBlocks(msg: StacksMessage): msg is StacksMessageBlocks {
return msg.messageTypeId === StacksMessageTypeID.Blocks;
}
export interface StacksMessageTransaction {
messageTypeId: StacksMessageTypeID.Transaction;
transaction: Transaction;
}
export function isStacksMessageTransaction(msg: StacksMessage): msg is StacksMessageTransaction {
return msg.messageTypeId === StacksMessageTypeID.Transaction;
}
type StacksMessage = StacksMessageBlocks | StacksMessageTransaction;
export async function* readMessages(stream: BinaryReader): AsyncGenerator<StacksMessage> {
while (!stream.stream.destroyed) {
const messageTypeId: StacksMessageTypeID = await stream.readUInt8();
while (!stream.readableStream.destroyed) {
const messageTypeId = await stream.readUInt8Enum(StacksMessageTypeID, n => {
throw new StacksMessageParsingError(`unexpected Stacks message type ID ${n}`);
});
if (messageTypeId === StacksMessageTypeID.Blocks) {
const blocks = await readBlocks(stream);
const msg: StacksMessageBlocks = {
@@ -32,8 +55,15 @@ export async function* readMessages(stream: BinaryReader): AsyncGenerator<Stacks
blocks: blocks,
};
yield msg;
} else if (messageTypeId === StacksMessageTypeID.Transaction) {
const tx = await readTransaction(stream);
const msg: StacksMessageTransaction = {
messageTypeId: StacksMessageTypeID.Transaction,
transaction: tx,
};
yield msg;
} else {
throw new Error(`Not implemented - StacksMessageID ${messageTypeId}`);
throw new NotImplementedError(`stacks message type: ${getEnumDescription(StacksMessageTypeID, messageTypeId)}`);
}
}
}

View File

@@ -1,23 +1,25 @@
import { Readable } from 'stream';
import { BinaryReader } from './binaryReader';
import { getEnumDescription } from './helpers';
import { StacksMessageParsingError, NotImplementedError } from './errors';
const enum SingleSigHashMode {
enum SigHashMode {
/** SingleSigHashMode */
P2PKH = 0x00,
/** SingleSigHashMode */
P2WPKH = 0x02,
}
const enum MultiSigHashMode {
/** MultiSigHashMode */
P2SH = 0x01,
/** MultiSigHashMode */
P2WSH = 0x03,
}
const enum TransactionPublicKeyEncoding {
enum TransactionPublicKeyEncoding {
Compressed = 0x00,
Uncompressed = 0x01,
}
interface TransactionSpendingConditionSingleSig {
hashMode: SingleSigHashMode; // u8
hashMode: SigHashMode.P2PKH | SigHashMode.P2WPKH; // u8
signer: Buffer; // 20 bytes, HASH160
nonce: bigint; // u64
feeRate: bigint; // u64
@@ -25,7 +27,7 @@ interface TransactionSpendingConditionSingleSig {
signature: Buffer; // 65 bytes
}
const enum TransactionAuthFieldID {
enum TransactionAuthFieldTypeID {
PublicKeyCompressed = 0x00,
PublicKeyUncompressed = 0x01,
SignatureCompressed = 0x02,
@@ -33,26 +35,26 @@ const enum TransactionAuthFieldID {
}
interface TransactionAuthFieldPublicKey {
typeId: TransactionAuthFieldID.PublicKeyCompressed | TransactionAuthFieldID.PublicKeyUncompressed; // u8
typeId: TransactionAuthFieldTypeID.PublicKeyCompressed | TransactionAuthFieldTypeID.PublicKeyUncompressed; // u8
publicKey: Buffer; // 33 bytes
}
interface TransactionAuthFieldSignature {
typeId: TransactionAuthFieldID.SignatureCompressed | TransactionAuthFieldID.SignatureUncompressed; // u8
typeId: TransactionAuthFieldTypeID.SignatureCompressed | TransactionAuthFieldTypeID.SignatureUncompressed; // u8
signature: Buffer; // 65 bytes
}
type TransactionAuthField = TransactionAuthFieldPublicKey | TransactionAuthFieldSignature;
interface TransactionSpendingConditionMultiSig {
hashMode: MultiSigHashMode; // u8
hashMode: SigHashMode.P2SH | SigHashMode.P2WSH; // u8
signer: Buffer; // 20 bytes, HASH160
nonce: bigint; // u64
feeRate: bigint; // u64
authFields: TransactionAuthField[];
}
const enum TransactionAuthType {
enum TransactionAuthTypeID {
Standard = 0x04,
Sponsored = 0x05,
}
@@ -60,49 +62,49 @@ const enum TransactionAuthType {
type TransactionSpendingCondition = TransactionSpendingConditionSingleSig | TransactionSpendingConditionMultiSig;
interface TransactionAuthStandard {
typeId: TransactionAuthType.Standard; // u8
typeId: TransactionAuthTypeID.Standard; // u8
originCondition: TransactionSpendingCondition;
}
interface TransactionAuthSponsored {
typeId: TransactionAuthType.Sponsored; // u8
typeId: TransactionAuthTypeID.Sponsored; // u8
originCondition: TransactionSpendingCondition;
sponsorCondition: TransactionSpendingCondition;
}
const enum TransactionPostConditionMode {
enum TransactionPostConditionMode {
Allow = 0x01,
Deny = 0x02,
}
const enum TransactionVersion {
enum TransactionVersion {
Mainnet = 0x00,
Testnet = 0x80,
}
const enum AssetInfoID {
enum AssetInfoTypeID {
STX = 0,
FungibleAsset = 1,
NonfungibleAsset = 2,
}
const enum PostConditionPrincipalID {
enum PostConditionPrincipalTypeID {
Origin = 0x01,
Standard = 0x02,
Contract = 0x03,
}
interface PostConditionPrincipalOrigin {
typeId: PostConditionPrincipalID.Origin; // u8
typeId: PostConditionPrincipalTypeID.Origin; // u8
}
interface PostConditionPrincipalStandard {
typeId: PostConditionPrincipalID.Standard; // u8
typeId: PostConditionPrincipalTypeID.Standard; // u8
address: StacksAddress;
}
interface PostConditionPrincipalContract {
typeId: PostConditionPrincipalID.Contract; // u8
typeId: PostConditionPrincipalTypeID.Contract; // u8
address: StacksAddress;
contractName: string;
}
@@ -112,7 +114,7 @@ type PostConditionPrincipal =
| PostConditionPrincipalStandard
| PostConditionPrincipalContract;
const enum FungibleConditionCode {
enum FungibleConditionCode {
SentEq = 0x01,
SentGt = 0x02,
SentGe = 0x03,
@@ -120,7 +122,7 @@ const enum FungibleConditionCode {
SentLe = 0x05,
}
const enum NonfungibleConditionCode {
enum NonfungibleConditionCode {
Sent = 0x10,
NotSent = 0x11,
}
@@ -137,14 +139,14 @@ interface AssetInfo {
}
interface TransactionPostConditionStx {
assetInfoId: AssetInfoID.STX; // u8
assetInfoId: AssetInfoTypeID.STX; // u8
principal: PostConditionPrincipal;
conditionCode: FungibleConditionCode; // u8
amount: bigint; // u64
}
interface TransactionPostConditionFungible {
assetInfoId: AssetInfoID.FungibleAsset; // u8
assetInfoId: AssetInfoTypeID.FungibleAsset; // u8
principal: PostConditionPrincipal;
asset: AssetInfo;
conditionCode: FungibleConditionCode; // u8
@@ -153,18 +155,23 @@ interface TransactionPostConditionFungible {
// TODO: incomplete
interface TransactionPostConditionNonfungible {
assetInfoId: AssetInfoID.NonfungibleAsset; // u8
assetInfoId: AssetInfoTypeID.NonfungibleAsset; // u8
asset: AssetInfo;
assetValue: any; // TODO: Value (Clarity value)
assetValue: ClarityValue;
conditionCode: NonfungibleConditionCode; // u8
}
// TODO: placeholder, needs clarity-js / stacks-transactions-js
interface ClarityValue {
value: Buffer; // wrong
}
type TransactionPostCondition =
| TransactionPostConditionStx
| TransactionPostConditionFungible
| TransactionPostConditionNonfungible;
const enum TransactionPayloadID {
enum TransactionPayloadTypeID {
TokenTransfer = 0,
SmartContract = 1,
ContractCall = 2,
@@ -173,30 +180,30 @@ const enum TransactionPayloadID {
}
interface TransactionPayloadTokenTransfer {
typeId: TransactionPayloadID.TokenTransfer;
typeId: TransactionPayloadTypeID.TokenTransfer;
address: StacksAddress;
amount: bigint; // u64
memo: Buffer; // 34 bytes
}
interface TransactionPayloadCoinbase {
typeId: TransactionPayloadID.Coinbase;
typeId: TransactionPayloadTypeID.Coinbase;
payload: Buffer; // 32 bytes
}
// TODO: incomplete
interface TransactionPayloadContractCall {
typeId: TransactionPayloadID.ContractCall;
typeId: TransactionPayloadTypeID.ContractCall;
}
// TODO: incomplete
interface TransactionPayloadSmartContract {
typeId: TransactionPayloadID.SmartContract;
typeId: TransactionPayloadTypeID.SmartContract;
}
// TODO: incomplete
interface TransactionPayloadPoisonMicroblock {
typeId: TransactionPayloadID.PoisonMicroblock;
typeId: TransactionPayloadTypeID.PoisonMicroblock;
}
type TransactionPayload =
@@ -216,76 +223,82 @@ export interface Transaction {
payload: TransactionPayload;
}
export async function readTransaction(stream: BinaryReader): Promise<Transaction> {
const version = await stream.readUInt8Enum(TransactionVersion, n => {
throw new StacksMessageParsingError(`unexpected transactions version: ${n}`);
});
const chainId = await stream.readUInt32BE();
const authType = await stream.readUInt8Enum(TransactionAuthTypeID, n => {
throw new StacksMessageParsingError(`unexpected transaction auth type: ${n}`);
});
let auth: TransactionAuthStandard | TransactionAuthSponsored;
if (authType === TransactionAuthTypeID.Standard) {
const originCondition = await readTransactionSpendingCondition(stream);
const txAuth: TransactionAuthStandard = {
typeId: authType,
originCondition: originCondition,
};
auth = txAuth;
} else if (authType === TransactionAuthTypeID.Sponsored) {
const originCondition = await readTransactionSpendingCondition(stream);
const sponsorCondition = await readTransactionSpendingCondition(stream);
const txAuth: TransactionAuthSponsored = {
typeId: authType,
originCondition: originCondition,
sponsorCondition: sponsorCondition,
};
auth = txAuth;
} else {
throw new NotImplementedError(`tx auth type: ${getEnumDescription(TransactionAuthTypeID, authType)}`);
}
const anchorMode = await stream.readUInt8Enum(TransactionPostConditionMode, n => {
throw new StacksMessageParsingError(`unexpected tx post condition anchor mode: ${n}`);
});
const postConditionMode = await stream.readUInt8Enum(TransactionPostConditionMode, n => {
throw new StacksMessageParsingError(`unexpected tx post condition mode: ${n}`);
});
const postConditions = await readTransactionPostConditions(stream);
const txPayload = await readTransactionPayload(stream);
const tx: Transaction = {
version: version,
chainId: chainId,
auth: auth,
anchorMode: anchorMode,
postConditionMode: postConditionMode,
postConditions: postConditions,
payload: txPayload,
};
return tx;
}
export async function readTransactions(stream: BinaryReader): Promise<Transaction[]> {
const txCount = await stream.readUInt32BE();
const txs = new Array<Transaction>(txCount);
for (let i = 0; i < txCount; i++) {
const version = await stream.readUInt8();
const chainId = await stream.readUInt32BE();
const authType: TransactionAuthType = await stream.readUInt8();
let auth: TransactionAuthStandard | TransactionAuthSponsored;
if (authType === TransactionAuthType.Standard) {
const originCondition = await readTransactionSpendingCondition(stream);
const txAuth: TransactionAuthStandard = {
typeId: authType,
originCondition: originCondition,
};
auth = txAuth;
} else if (authType === TransactionAuthType.Sponsored) {
const originCondition = await readTransactionSpendingCondition(stream);
const sponsorCondition = await readTransactionSpendingCondition(stream);
const txAuth: TransactionAuthSponsored = {
typeId: authType,
originCondition: originCondition,
sponsorCondition: sponsorCondition,
};
auth = txAuth;
} else {
throw new Error(`Unexpected tx auth type: ${authType}`);
}
const anchorMode: TransactionPostConditionMode = await stream.readUInt8();
if (anchorMode !== TransactionPostConditionMode.Allow && anchorMode !== TransactionPostConditionMode.Deny) {
throw new Error(`Unexpected tx post condition anchor mode: ${anchorMode}`);
}
const postConditionMode: TransactionPostConditionMode = await stream.readUInt8();
if (
postConditionMode !== TransactionPostConditionMode.Allow &&
postConditionMode !== TransactionPostConditionMode.Deny
) {
throw new Error(`Unexpected tx post condition mode: ${postConditionMode}`);
}
const postConditions = await readTransactionPostConditions(stream);
const txPayload = await readTransactionPayload(stream);
const tx: Transaction = {
version: version,
chainId: chainId,
auth: auth,
anchorMode: anchorMode,
postConditionMode: postConditionMode,
postConditions: postConditions,
payload: txPayload,
};
const tx = await readTransaction(stream);
txs[i] = tx;
}
return txs;
}
async function readTransactionPayload(stream: BinaryReader): Promise<TransactionPayload> {
const txPayloadType: TransactionPayloadID = await stream.readUInt8();
if (txPayloadType === TransactionPayloadID.Coinbase) {
const txPayloadType = await stream.readUInt8Enum(TransactionPayloadTypeID, n => {
throw new StacksMessageParsingError(`unexpected tx payload type: ${n}`);
});
if (txPayloadType === TransactionPayloadTypeID.Coinbase) {
const payload: TransactionPayloadCoinbase = {
typeId: txPayloadType,
payload: await stream.readBuffer(32),
};
return payload;
} else if (txPayloadType === TransactionPayloadID.TokenTransfer) {
const cursor = await stream.sync(63);
} else if (txPayloadType === TransactionPayloadTypeID.TokenTransfer) {
const cursor = await stream.readFixed(63);
const payload: TransactionPayloadTokenTransfer = {
typeId: txPayloadType,
address: {
@@ -296,14 +309,8 @@ async function readTransactionPayload(stream: BinaryReader): Promise<Transaction
memo: cursor.readBuffer(34),
};
return payload;
} else if (txPayloadType === TransactionPayloadID.PoisonMicroblock) {
throw new Error('not yet implemented');
} else if (txPayloadType === TransactionPayloadID.SmartContract) {
throw new Error('not yet implemented');
} else if (txPayloadType === TransactionPayloadID.ContractCall) {
throw new Error('not yet implemented');
} else {
throw new Error(`Unexpected tx payload type: ${txPayloadType}`);
throw new NotImplementedError(`tx payload type: ${getEnumDescription(TransactionPayloadTypeID, txPayloadType)}`);
}
}
@@ -311,10 +318,12 @@ async function readTransactionPostConditions(stream: BinaryReader): Promise<Tran
const conditionCount = await stream.readUInt32BE();
const conditions = new Array<TransactionPostCondition>(conditionCount);
for (let i = 0; i < conditionCount; i++) {
const typeId: AssetInfoID = await stream.readUInt8();
if (typeId === AssetInfoID.STX) {
const typeId = await stream.readUInt8Enum(AssetInfoTypeID, n => {
throw new StacksMessageParsingError(`unexpected tx asset info type: ${n}`);
});
if (typeId === AssetInfoTypeID.STX) {
const principal = await readTransactionPostConditionPrincipal(stream);
const cursor = await stream.sync(9);
const cursor = await stream.readFixed(9);
const conditionCode: FungibleConditionCode = cursor.readUInt8();
const condition: TransactionPostConditionStx = {
assetInfoId: typeId,
@@ -323,26 +332,24 @@ async function readTransactionPostConditions(stream: BinaryReader): Promise<Tran
amount: cursor.readBigInt64BE(),
};
conditions[i] = condition;
} else if (typeId === AssetInfoID.FungibleAsset) {
throw new Error('not yet implemented');
} else if (typeId === AssetInfoID.NonfungibleAsset) {
throw new Error('not yet implemented');
} else {
throw new Error(`Unexpected tx type ID: ${typeId}`);
throw new NotImplementedError(`tx asset info type ${getEnumDescription(AssetInfoTypeID, typeId)}`);
}
}
return conditions;
}
async function readTransactionPostConditionPrincipal(stream: BinaryReader): Promise<PostConditionPrincipal> {
const typeId: PostConditionPrincipalID = await stream.readUInt8();
if (typeId === PostConditionPrincipalID.Origin) {
const typeId = await stream.readUInt8Enum(PostConditionPrincipalTypeID, n => {
throw new StacksMessageParsingError(`unexpected tx post condition principal type: ${n}`);
});
if (typeId === PostConditionPrincipalTypeID.Origin) {
const principal: PostConditionPrincipalOrigin = {
typeId: typeId,
};
return principal;
} else if (typeId === PostConditionPrincipalID.Standard) {
const cursor = await stream.sync(21);
} else if (typeId === PostConditionPrincipalTypeID.Standard) {
const cursor = await stream.readFixed(21);
const principal: PostConditionPrincipalStandard = {
typeId: typeId,
address: {
@@ -351,8 +358,8 @@ async function readTransactionPostConditionPrincipal(stream: BinaryReader): Prom
},
};
return principal;
} else if (typeId === PostConditionPrincipalID.Contract) {
const cursor = await stream.sync(22);
} else if (typeId === PostConditionPrincipalTypeID.Contract) {
const cursor = await stream.readFixed(22);
const address: StacksAddress = {
version: cursor.readUInt8(),
bytes: cursor.readBuffer(20),
@@ -366,14 +373,18 @@ async function readTransactionPostConditionPrincipal(stream: BinaryReader): Prom
};
return principal;
} else {
throw new Error(`Unexpected tx post condition principal type ID: ${typeId}`);
throw new NotImplementedError(
`tx post condition principal type: ${getEnumDescription(PostConditionPrincipalTypeID, typeId)}`
);
}
}
async function readTransactionSpendingCondition(stream: BinaryReader): Promise<TransactionSpendingCondition> {
const conditionType = await stream.readUInt8();
if (conditionType === SingleSigHashMode.P2PKH || conditionType === SingleSigHashMode.P2WPKH) {
const cursor = await stream.sync(102);
const conditionType = await stream.readUInt8Enum(SigHashMode, n => {
throw new StacksMessageParsingError(`unexpected tx spend condition hash mode: ${n}`);
});
if (conditionType === SigHashMode.P2PKH || conditionType === SigHashMode.P2WPKH) {
const cursor = await stream.readFixed(102);
const condition: TransactionSpendingConditionSingleSig = {
hashMode: conditionType,
signer: cursor.readBuffer(20),
@@ -383,8 +394,8 @@ async function readTransactionSpendingCondition(stream: BinaryReader): Promise<T
signature: cursor.readBuffer(65),
};
return condition;
} else if (conditionType === MultiSigHashMode.P2SH || conditionType === MultiSigHashMode.P2WSH) {
const cursor = await stream.sync(40);
} else if (conditionType === SigHashMode.P2SH || conditionType === SigHashMode.P2WSH) {
const cursor = await stream.readFixed(40);
const condition: TransactionSpendingConditionMultiSig = {
hashMode: conditionType,
signer: cursor.readBuffer(20),
@@ -393,10 +404,12 @@ async function readTransactionSpendingCondition(stream: BinaryReader): Promise<T
authFields: new Array<TransactionAuthField>(cursor.readUInt32BE()),
};
for (let i = 0; i < condition.authFields.length; i++) {
const authType: TransactionAuthFieldID = await stream.readUInt8();
const authType = await stream.readUInt8Enum(TransactionAuthFieldTypeID, n => {
throw new StacksMessageParsingError(`unexpected tx auth field type: ${n}`);
});
if (
authType === TransactionAuthFieldID.PublicKeyCompressed ||
authType === TransactionAuthFieldID.PublicKeyUncompressed
authType === TransactionAuthFieldTypeID.PublicKeyCompressed ||
authType === TransactionAuthFieldTypeID.PublicKeyUncompressed
) {
const authFieldPubkey: TransactionAuthFieldPublicKey = {
typeId: authType,
@@ -404,8 +417,8 @@ async function readTransactionSpendingCondition(stream: BinaryReader): Promise<T
};
condition.authFields[i] = authFieldPubkey;
} else if (
authType === TransactionAuthFieldID.SignatureCompressed ||
authType === TransactionAuthFieldID.SignatureUncompressed
authType === TransactionAuthFieldTypeID.SignatureCompressed ||
authType === TransactionAuthFieldTypeID.SignatureUncompressed
) {
const authFieldSig: TransactionAuthFieldSignature = {
typeId: authType,
@@ -413,11 +426,13 @@ async function readTransactionSpendingCondition(stream: BinaryReader): Promise<T
};
condition.authFields[i] = authFieldSig;
} else {
throw new Error(`Unexpected tx auth field ID type: ${authType}`);
throw new NotImplementedError(
`tx auth field type: ${getEnumDescription(TransactionAuthFieldTypeID, authType)}`
);
}
}
return condition;
} else {
throw new Error(`Unexpected tx spend condition hash mode: ${conditionType}`);
throw new NotImplementedError(`tx spend condition hash mode: ${getEnumDescription(SigHashMode, conditionType)}`);
}
}

View File

@@ -2,13 +2,14 @@
"compilerOptions": {
"target": "es2019",
"module": "commonjs",
"esModuleInterop": false,
"moduleResolution": "node",
"declaration": true,
"strict": true,
"outDir": "lib",
"sourceMap": true,
"sourceRoot": "."
"sourceRoot": ".",
"esModuleInterop": true,
"allowSyntheticDefaultImports": false,
},
"include": [
"src/**/*"