/** * @packageDocumentation * @module API-XVM-Operations */ import { Buffer } from "buffer/" import BinTools from "../../utils/bintools" import { XVMConstants } from "./constants" import { NFTTransferOutput, SECPMintOutput, SECPTransferOutput } from "./outputs" import { NBytes } from "../../common/nbytes" import { SigIdx } from "../../common/credentials" import { OutputOwners } from "../../common/output" import { Serializable, Serialization, SerializedEncoding, SerializedType } from "../../utils/serialization" import { InvalidOperationIdError, CodecIdError, ChecksumError, AddressError } from "../../utils/errors" const bintools: BinTools = BinTools.getInstance() const serialization: Serialization = Serialization.getInstance() const cb58: SerializedType = "cb58" const buffer: SerializedType = "Buffer" const hex: SerializedType = "hex" const decimalString: SerializedType = "decimalString" /** * Takes a buffer representing the output and returns the proper [[Operation]] instance. * * @param opid A number representing the operation ID parsed prior to the bytes passed in * * @returns An instance of an [[Operation]]-extended class. */ export const SelectOperationClass = ( opid: number, ...args: any[] ): Operation => { if ( opid === XVMConstants.SECPMINTOPID || opid === XVMConstants.SECPMINTOPID_CODECONE ) { return new SECPMintOperation(...args) } else if ( opid === XVMConstants.NFTMINTOPID || opid === XVMConstants.NFTMINTOPID_CODECONE ) { return new NFTMintOperation(...args) } else if ( opid === XVMConstants.NFTXFEROPID || opid === XVMConstants.NFTXFEROPID_CODECONE ) { return new NFTTransferOperation(...args) } /* istanbul ignore next */ throw new InvalidOperationIdError( `Error - SelectOperationClass: unknown opid ${opid}` ) } /** * A class representing an operation. All operation types must extend on this class. */ export abstract class Operation extends Serializable { protected _typeName = "Operation" protected _typeID = undefined serialize(encoding: SerializedEncoding = "hex"): object { let fields: object = super.serialize(encoding) return { ...fields, sigIdxs: this.sigIdxs.map((s: SigIdx): object => s.serialize(encoding)) } } deserialize(fields: object, encoding: SerializedEncoding = "hex") { super.deserialize(fields, encoding) this.sigIdxs = fields["sigIdxs"].map((s: object): SigIdx => { let sidx: SigIdx = new SigIdx() sidx.deserialize(s, encoding) return sidx }) this.sigCount.writeUInt32BE(this.sigIdxs.length, 0) } protected sigCount: Buffer = Buffer.alloc(4) protected sigIdxs: SigIdx[] = [] // idxs of signers from utxo static comparator = (): ((a: Operation, b: Operation) => 1 | -1 | 0) => (a: Operation, b: Operation): 1 | -1 | 0 => { const aoutid: Buffer = Buffer.alloc(4) aoutid.writeUInt32BE(a.getOperationID(), 0) const abuff: Buffer = a.toBuffer() const boutid: Buffer = Buffer.alloc(4) boutid.writeUInt32BE(b.getOperationID(), 0) const bbuff: Buffer = b.toBuffer() const asort: Buffer = Buffer.concat( [aoutid, abuff], aoutid.length + abuff.length ) const bsort: Buffer = Buffer.concat( [boutid, bbuff], boutid.length + bbuff.length ) return Buffer.compare(asort, bsort) as 1 | -1 | 0 } abstract getOperationID(): number /** * Returns the array of [[SigIdx]] for this [[Operation]] */ getSigIdxs = (): SigIdx[] => this.sigIdxs /** * Returns the credential ID. */ abstract getCredentialID(): number /** * Creates and adds a [[SigIdx]] to the [[Operation]]. * * @param addressIdx The index of the address to reference in the signatures * @param address The address of the source of the signature */ addSignatureIdx = (addressIdx: number, address: Buffer) => { const sigidx: SigIdx = new SigIdx() const b: Buffer = Buffer.alloc(4) b.writeUInt32BE(addressIdx, 0) sigidx.fromBuffer(b) sigidx.setSource(address) this.sigIdxs.push(sigidx) this.sigCount.writeUInt32BE(this.sigIdxs.length, 0) } fromBuffer(bytes: Buffer, offset: number = 0): number { this.sigCount = bintools.copyFrom(bytes, offset, offset + 4) offset += 4 const sigCount: number = this.sigCount.readUInt32BE(0) this.sigIdxs = [] for (let i: number = 0; i < sigCount; i++) { const sigidx: SigIdx = new SigIdx() const sigbuff: Buffer = bintools.copyFrom(bytes, offset, offset + 4) sigidx.fromBuffer(sigbuff) offset += 4 this.sigIdxs.push(sigidx) } return offset } toBuffer(): Buffer { this.sigCount.writeUInt32BE(this.sigIdxs.length, 0) let bsize: number = this.sigCount.length const barr: Buffer[] = [this.sigCount] for (let i: number = 0; i < this.sigIdxs.length; i++) { const b: Buffer = this.sigIdxs[`${i}`].toBuffer() barr.push(b) bsize += b.length } return Buffer.concat(barr, bsize) } /** * Returns a base-58 string representing the [[NFTMintOperation]]. */ toString(): string { return bintools.bufferToB58(this.toBuffer()) } } /** * A class which contains an [[Operation]] for transfers. * */ export class TransferableOperation extends Serializable { protected _typeName = "TransferableOperation" protected _typeID = undefined serialize(encoding: SerializedEncoding = "hex"): object { let fields: object = super.serialize(encoding) return { ...fields, assetID: serialization.encoder(this.assetID, encoding, buffer, cb58, 32), utxoIDs: this.utxoIDs.map((u) => u.serialize(encoding)), operation: this.operation.serialize(encoding) } } deserialize(fields: object, encoding: SerializedEncoding = "hex") { super.deserialize(fields, encoding) this.assetID = serialization.decoder( fields["assetID"], encoding, cb58, buffer, 32 ) this.utxoIDs = fields["utxoIDs"].map((u: object) => { let utxoid: UTXOID = new UTXOID() utxoid.deserialize(u, encoding) return utxoid }) this.operation = SelectOperationClass(fields["operation"]["_typeID"]) this.operation.deserialize(fields["operation"], encoding) } protected assetID: Buffer = Buffer.alloc(32) protected utxoIDs: UTXOID[] = [] protected operation: Operation /** * Returns a function used to sort an array of [[TransferableOperation]]s */ static comparator = (): (( a: TransferableOperation, b: TransferableOperation ) => 1 | -1 | 0) => { return function ( a: TransferableOperation, b: TransferableOperation ): 1 | -1 | 0 { return Buffer.compare(a.toBuffer(), b.toBuffer()) as 1 | -1 | 0 } } /** * Returns the assetID as a {@link https://github.com/feross/buffer|Buffer}. */ getAssetID = (): Buffer => this.assetID /** * Returns an array of UTXOIDs in this operation. */ getUTXOIDs = (): UTXOID[] => this.utxoIDs /** * Returns the operation */ getOperation = (): Operation => this.operation fromBuffer(bytes: Buffer, offset: number = 0): number { this.assetID = bintools.copyFrom(bytes, offset, offset + 32) offset += 32 const numutxoIDs: number = bintools .copyFrom(bytes, offset, offset + 4) .readUInt32BE(0) offset += 4 this.utxoIDs = [] for (let i: number = 0; i < numutxoIDs; i++) { const utxoid: UTXOID = new UTXOID() offset = utxoid.fromBuffer(bytes, offset) this.utxoIDs.push(utxoid) } const opid: number = bintools .copyFrom(bytes, offset, offset + 4) .readUInt32BE(0) offset += 4 this.operation = SelectOperationClass(opid) return this.operation.fromBuffer(bytes, offset) } toBuffer(): Buffer { const numutxoIDs = Buffer.alloc(4) numutxoIDs.writeUInt32BE(this.utxoIDs.length, 0) let bsize: number = this.assetID.length + numutxoIDs.length const barr: Buffer[] = [this.assetID, numutxoIDs] this.utxoIDs = this.utxoIDs.sort(UTXOID.comparator()) for (let i: number = 0; i < this.utxoIDs.length; i++) { const b: Buffer = this.utxoIDs[`${i}`].toBuffer() barr.push(b) bsize += b.length } const opid: Buffer = Buffer.alloc(4) opid.writeUInt32BE(this.operation.getOperationID(), 0) barr.push(opid) bsize += opid.length const b: Buffer = this.operation.toBuffer() bsize += b.length barr.push(b) return Buffer.concat(barr, bsize) } constructor( assetID: Buffer = undefined, utxoids: UTXOID[] | string[] | Buffer[] = undefined, operation: Operation = undefined ) { super() if ( typeof assetID !== "undefined" && assetID.length === XVMConstants.ASSETIDLEN && operation instanceof Operation && typeof utxoids !== "undefined" && Array.isArray(utxoids) ) { this.assetID = assetID this.operation = operation for (let i: number = 0; i < utxoids.length; i++) { const utxoid: UTXOID = new UTXOID() if (typeof utxoids[`${i}`] === "string") { utxoid.fromString(utxoids[`${i}`] as string) } else if (utxoids[`${i}`] instanceof Buffer) { utxoid.fromBuffer(utxoids[`${i}`] as Buffer) } else if (utxoids[`${i}`] instanceof UTXOID) { utxoid.fromString(utxoids[`${i}`].toString()) // clone } this.utxoIDs.push(utxoid) } } } } /** * An [[Operation]] class which specifies a SECP256k1 Mint Op. */ export class SECPMintOperation extends Operation { protected _typeName = "SECPMintOperation" protected _codecID = XVMConstants.LATESTCODEC protected _typeID = this._codecID === 0 ? XVMConstants.SECPMINTOPID : XVMConstants.SECPMINTOPID_CODECONE serialize(encoding: SerializedEncoding = "hex"): object { let fields: object = super.serialize(encoding) return { ...fields, mintOutput: this.mintOutput.serialize(encoding), transferOutputs: this.transferOutput.serialize(encoding) } } deserialize(fields: object, encoding: SerializedEncoding = "hex") { super.deserialize(fields, encoding) this.mintOutput = new SECPMintOutput() this.mintOutput.deserialize(fields["mintOutput"], encoding) this.transferOutput = new SECPTransferOutput() this.transferOutput.deserialize(fields["transferOutputs"], encoding) } protected mintOutput: SECPMintOutput = undefined protected transferOutput: SECPTransferOutput = undefined /** * Set the codecID * * @param codecID The codecID to set */ setCodecID(codecID: number): void { if (codecID !== 0 && codecID !== 1) { /* istanbul ignore next */ throw new CodecIdError( "Error - SECPMintOperation.setCodecID: invalid codecID. Valid codecIDs are 0 and 1." ) } this._codecID = codecID this._typeID = this._codecID === 0 ? XVMConstants.SECPMINTOPID : XVMConstants.SECPMINTOPID_CODECONE } /** * Returns the operation ID. */ getOperationID(): number { return this._typeID } /** * Returns the credential ID. */ getCredentialID(): number { if (this._codecID === 0) { return XVMConstants.SECPCREDENTIAL } else if (this._codecID === 1) { return XVMConstants.SECPCREDENTIAL_CODECONE } } /** * Returns the [[SECPMintOutput]] to be produced by this operation. */ getMintOutput(): SECPMintOutput { return this.mintOutput } /** * Returns [[SECPTransferOutput]] to be produced by this operation. */ getTransferOutput(): SECPTransferOutput { return this.transferOutput } /** * Popuates the instance from a {@link https://github.com/feross/buffer|Buffer} representing the [[SECPMintOperation]] and returns the updated offset. */ fromBuffer(bytes: Buffer, offset: number = 0): number { offset = super.fromBuffer(bytes, offset) this.mintOutput = new SECPMintOutput() offset = this.mintOutput.fromBuffer(bytes, offset) this.transferOutput = new SECPTransferOutput() offset = this.transferOutput.fromBuffer(bytes, offset) return offset } /** * Returns the buffer representing the [[SECPMintOperation]] instance. */ toBuffer(): Buffer { const superbuff: Buffer = super.toBuffer() const mintoutBuff: Buffer = this.mintOutput.toBuffer() const transferOutBuff: Buffer = this.transferOutput.toBuffer() const bsize: number = superbuff.length + mintoutBuff.length + transferOutBuff.length const barr: Buffer[] = [superbuff, mintoutBuff, transferOutBuff] return Buffer.concat(barr, bsize) } /** * An [[Operation]] class which mints new tokens on an assetID. * * @param mintOutput The [[SECPMintOutput]] that will be produced by this transaction. * @param transferOutput A [[SECPTransferOutput]] that will be produced from this minting operation. */ constructor( mintOutput: SECPMintOutput = undefined, transferOutput: SECPTransferOutput = undefined ) { super() if (typeof mintOutput !== "undefined") { this.mintOutput = mintOutput } if (typeof transferOutput !== "undefined") { this.transferOutput = transferOutput } } } /** * An [[Operation]] class which specifies a NFT Mint Op. */ export class NFTMintOperation extends Operation { protected _typeName = "NFTMintOperation" protected _codecID = XVMConstants.LATESTCODEC protected _typeID = this._codecID === 0 ? XVMConstants.NFTMINTOPID : XVMConstants.NFTMINTOPID_CODECONE serialize(encoding: SerializedEncoding = "hex"): object { const fields: object = super.serialize(encoding) return { ...fields, groupID: serialization.encoder( this.groupID, encoding, buffer, decimalString, 4 ), payload: serialization.encoder(this.payload, encoding, buffer, hex), outputOwners: this.outputOwners.map((o) => o.serialize(encoding)) } } deserialize(fields: object, encoding: SerializedEncoding = "hex") { super.deserialize(fields, encoding) this.groupID = serialization.decoder( fields["groupID"], encoding, decimalString, buffer, 4 ) this.payload = serialization.decoder( fields["payload"], encoding, hex, buffer ) // this.outputOwners = fields["outputOwners"].map((o: NFTMintOutput) => { // let oo: NFTMintOutput = new NFTMintOutput() // oo.deserialize(o, encoding) // return oo // }) this.outputOwners = fields["outputOwners"].map( (o: object): OutputOwners => { let oo: OutputOwners = new OutputOwners() oo.deserialize(o, encoding) return oo } ) } protected groupID: Buffer = Buffer.alloc(4) protected payload: Buffer protected outputOwners: OutputOwners[] = [] /** * Set the codecID * * @param codecID The codecID to set */ setCodecID(codecID: number): void { if (codecID !== 0 && codecID !== 1) { /* istanbul ignore next */ throw new CodecIdError( "Error - NFTMintOperation.setCodecID: invalid codecID. Valid codecIDs are 0 and 1." ) } this._codecID = codecID this._typeID = this._codecID === 0 ? XVMConstants.NFTMINTOPID : XVMConstants.NFTMINTOPID_CODECONE } /** * Returns the operation ID. */ getOperationID(): number { return this._typeID } /** * Returns the credential ID. */ getCredentialID = (): number => { if (this._codecID === 0) { return XVMConstants.NFTCREDENTIAL } else if (this._codecID === 1) { return XVMConstants.NFTCREDENTIAL_CODECONE } } /** * Returns the payload. */ getGroupID = (): Buffer => { return bintools.copyFrom(this.groupID, 0) } /** * Returns the payload. */ getPayload = (): Buffer => { return bintools.copyFrom(this.payload, 0) } /** * Returns the payload's raw {@link https://github.com/feross/buffer|Buffer} with length prepended, for use with [[PayloadBase]]'s fromBuffer */ getPayloadBuffer = (): Buffer => { let payloadlen: Buffer = Buffer.alloc(4) payloadlen.writeUInt32BE(this.payload.length, 0) return Buffer.concat([payloadlen, bintools.copyFrom(this.payload, 0)]) } /** * Returns the outputOwners. */ getOutputOwners = (): OutputOwners[] => { return this.outputOwners } /** * Popuates the instance from a {@link https://github.com/feross/buffer|Buffer} representing the [[NFTMintOperation]] and returns the updated offset. */ fromBuffer(bytes: Buffer, offset: number = 0): number { offset = super.fromBuffer(bytes, offset) this.groupID = bintools.copyFrom(bytes, offset, offset + 4) offset += 4 let payloadLen: number = bintools .copyFrom(bytes, offset, offset + 4) .readUInt32BE(0) offset += 4 this.payload = bintools.copyFrom(bytes, offset, offset + payloadLen) offset += payloadLen let numoutputs: number = bintools .copyFrom(bytes, offset, offset + 4) .readUInt32BE(0) offset += 4 this.outputOwners = [] for (let i: number = 0; i < numoutputs; i++) { let outputOwner: OutputOwners = new OutputOwners() offset = outputOwner.fromBuffer(bytes, offset) this.outputOwners.push(outputOwner) } return offset } /** * Returns the buffer representing the [[NFTMintOperation]] instance. */ toBuffer(): Buffer { const superbuff: Buffer = super.toBuffer() const payloadlen: Buffer = Buffer.alloc(4) payloadlen.writeUInt32BE(this.payload.length, 0) const outputownerslen: Buffer = Buffer.alloc(4) outputownerslen.writeUInt32BE(this.outputOwners.length, 0) let bsize: number = superbuff.length + this.groupID.length + payloadlen.length + this.payload.length + outputownerslen.length const barr: Buffer[] = [ superbuff, this.groupID, payloadlen, this.payload, outputownerslen ] for (let i: number = 0; i < this.outputOwners.length; i++) { let b: Buffer = this.outputOwners[`${i}`].toBuffer() barr.push(b) bsize += b.length } return Buffer.concat(barr, bsize) } /** * Returns a base-58 string representing the [[NFTMintOperation]]. */ toString(): string { return bintools.bufferToB58(this.toBuffer()) } /** * An [[Operation]] class which contains an NFT on an assetID. * * @param groupID The group to which to issue the NFT Output * @param payload A {@link https://github.com/feross/buffer|Buffer} of the NFT payload * @param outputOwners An array of outputOwners */ constructor( groupID: number = undefined, payload: Buffer = undefined, outputOwners: OutputOwners[] = undefined ) { super() if ( typeof groupID !== "undefined" && typeof payload !== "undefined" && outputOwners.length ) { this.groupID.writeUInt32BE(groupID ? groupID : 0, 0) this.payload = payload this.outputOwners = outputOwners } } } /** * A [[Operation]] class which specifies a NFT Transfer Op. */ export class NFTTransferOperation extends Operation { protected _typeName = "NFTTransferOperation" protected _codecID = XVMConstants.LATESTCODEC protected _typeID = this._codecID === 0 ? XVMConstants.NFTXFEROPID : XVMConstants.NFTXFEROPID_CODECONE serialize(encoding: SerializedEncoding = "hex"): object { const fields: object = super.serialize(encoding) return { ...fields, output: this.output.serialize(encoding) } } deserialize(fields: object, encoding: SerializedEncoding = "hex") { super.deserialize(fields, encoding) this.output = new NFTTransferOutput() this.output.deserialize(fields["output"], encoding) } protected output: NFTTransferOutput /** * Set the codecID * * @param codecID The codecID to set */ setCodecID(codecID: number): void { if (codecID !== 0 && codecID !== 1) { /* istanbul ignore next */ throw new CodecIdError( "Error - NFTTransferOperation.setCodecID: invalid codecID. Valid codecIDs are 0 and 1." ) } this._codecID = codecID this._typeID = this._codecID === 0 ? XVMConstants.NFTXFEROPID : XVMConstants.NFTXFEROPID_CODECONE } /** * Returns the operation ID. */ getOperationID(): number { return this._typeID } /** * Returns the credential ID. */ getCredentialID(): number { if (this._codecID === 0) { return XVMConstants.NFTCREDENTIAL } else if (this._codecID === 1) { return XVMConstants.NFTCREDENTIAL_CODECONE } } getOutput = (): NFTTransferOutput => this.output /** * Popuates the instance from a {@link https://github.com/feross/buffer|Buffer} representing the [[NFTTransferOperation]] and returns the updated offset. */ fromBuffer(bytes: Buffer, offset: number = 0): number { offset = super.fromBuffer(bytes, offset) this.output = new NFTTransferOutput() return this.output.fromBuffer(bytes, offset) } /** * Returns the buffer representing the [[NFTTransferOperation]] instance. */ toBuffer(): Buffer { const superbuff: Buffer = super.toBuffer() const outbuff: Buffer = this.output.toBuffer() const bsize: number = superbuff.length + outbuff.length const barr: Buffer[] = [superbuff, outbuff] return Buffer.concat(barr, bsize) } /** * Returns a base-58 string representing the [[NFTTransferOperation]]. */ toString(): string { return bintools.bufferToB58(this.toBuffer()) } /** * An [[Operation]] class which contains an NFT on an assetID. * * @param output An [[NFTTransferOutput]] */ constructor(output: NFTTransferOutput = undefined) { super() if (typeof output !== "undefined") { this.output = output } } } /** * Class for representing a UTXOID used in [[TransferableOp]] types */ export class UTXOID extends NBytes { protected _typeName = "UTXOID" protected _typeID = undefined //serialize and deserialize both are inherited protected bytes = Buffer.alloc(36) protected bsize = 36 /** * Returns a function used to sort an array of [[UTXOID]]s */ static comparator = (): ((a: UTXOID, b: UTXOID) => 1 | -1 | 0) => (a: UTXOID, b: UTXOID): 1 | -1 | 0 => Buffer.compare(a.toBuffer(), b.toBuffer()) as 1 | -1 | 0 /** * Returns a base-58 representation of the [[UTXOID]]. */ toString(): string { return bintools.cb58Encode(this.toBuffer()) } /** * Takes a base-58 string containing an [[UTXOID]], parses it, populates the class, and returns the length of the UTXOID in bytes. * * @param bytes A base-58 string containing a raw [[UTXOID]] * * @returns The length of the raw [[UTXOID]] */ fromString(utxoid: string): number { const utxoidbuff: Buffer = bintools.b58ToBuffer(utxoid) if (utxoidbuff.length === 40 && bintools.validateChecksum(utxoidbuff)) { const newbuff: Buffer = bintools.copyFrom( utxoidbuff, 0, utxoidbuff.length - 4 ) if (newbuff.length === 36) { this.bytes = newbuff } } else if (utxoidbuff.length === 40) { throw new ChecksumError( "Error - UTXOID.fromString: invalid checksum on address" ) } else if (utxoidbuff.length === 36) { this.bytes = utxoidbuff } else { /* istanbul ignore next */ throw new AddressError("Error - UTXOID.fromString: invalid address") } return this.getSize() } clone(): this { const newbase: UTXOID = new UTXOID() newbase.fromBuffer(this.toBuffer()) return newbase as this } create(...args: any[]): this { return new UTXOID() as this } /** * Class for representing a UTXOID used in [[TransferableOp]] types */ constructor() { super() } }