import * as _ from "lodash" ; import { U64Value } from "codechain-primitives/lib" ; import { blake128 , blake256 , blake256WithKey , encodeSignatureTag , SignatureTag } from "../../utils" ; import { Asset } from "../Asset" ; import { AssetAddress , H160 , H256 , Order , OrderOnTransfer , U64 } from "../classes" ; import { AssetTransaction , Transaction } from "../Transaction" ; import { AssetTransferOutputValue , NetworkId } from "../types" ; import { AssetTransferInput , AssetTransferInputJSON } from "./AssetTransferInput" ; import { AssetTransferOutput , AssetTransferOutputJSON } from "./AssetTransferOutput" ; import { OrderOnTransferJSON } from "./OrderOnTransfer" ; const RLP = require ( "rlp" ) ; export interface AssetTransferTransactionJSON { networkId : string ; burns : AssetTransferInputJSON [ ] ; inputs : AssetTransferInputJSON [ ] ; outputs : AssetTransferOutputJSON [ ] ; orders : OrderOnTransferJSON [ ] ; } export interface TransferAssetActionJSON extends AssetTransferTransactionJSON { metadata : string ; approvals : string [ ] ; expiration : number | null ; } export class TransferAsset extends Transaction implements AssetTransaction { private readonly _transaction : AssetTransferTransaction ; private readonly approvals : string [ ] ; private readonly metadata : string ; private readonly expiration : number | null ; public constructor ( input ) { super ( input . networkId ) ; this . _transaction = new AssetTransferTransaction ( input ) ; this . metadata = typeof input . metadata === "string" ? input . metadata : JSON . stringify ( input . metadata ) ; this . approvals = input . approvals ; this . expiration = input . expiration ; } returns public tracker ( ) { return new H256 ( blake256 ( this . _transaction . rlpBytes ( ) ) ) ; } param approval public addApproval ( approval ) { this . approvals . push ( approval ) ; } param burns returns public addBurns ( burns , ... rest ) { if ( ! Array . isArray ( burns ) ) { burns = [ burns , ... rest ] ; } burns . forEach ( ( burn , index ) => { if ( burn instanceof AssetTransferInput ) { this . _transaction . burns . push ( burn ) ; } else if ( burn instanceof Asset ) { this . _transaction . burns . push ( burn . createTransferInput ( ) ) ; } else { throw Error ( `Expected burn param to be either AssetTransferInput or Asset but found ${ burn } at ${ index }` ) ; } } ) ; return this ; } public burns ( ) { return this . _transaction . burns ; } public burn ( index ) { if ( this . _transaction . burns . length <= index ) { return null ; } return this . _transaction . burns [ index ] ; } param inputs returns public addInputs ( inputs , ... rest ) { if ( ! Array . isArray ( inputs ) ) { inputs = [ inputs , ... rest ] ; } inputs . forEach ( ( input , index ) => { if ( input instanceof AssetTransferInput ) { this . _transaction . inputs . push ( input ) ; } else if ( input instanceof Asset ) { this . _transaction . inputs . push ( input . createTransferInput ( ) ) ; } else { throw Error ( `Expected input param to be either AssetTransferInput or Asset but found ${ input } at ${ index }` ) ; } } ) ; return this ; } public inputs ( ) { return this . _transaction . inputs ; } public input ( index ) { if ( this . _transaction . inputs . length <= index ) { return null ; } return this . _transaction . inputs [ index ] ; } param outputs param output . quantity param output . assetType param output . recipient public addOutputs ( outputs , ... rest ) { if ( ! Array . isArray ( outputs ) ) { outputs = [ outputs , ... rest ] ; } outputs . forEach ( ( output ) => { if ( output instanceof AssetTransferOutput ) { this . _transaction . outputs . push ( output ) ; } else { const { assetType , shardId , quantity , recipient } = output ; this . _transaction . outputs . push ( new AssetTransferOutput ( { recipient : AssetAddress . ensure ( recipient ) , quantity : U64 . ensure ( quantity ) , assetType : H160 . ensure ( assetType ) , shardId } ) ) ; } } ) ; return this ; } public outputs ( ) { return this . _transaction . outputs ; } public output ( index ) { if ( this . _transaction . outputs . length <= index ) { return null ; } return this . _transaction . outputs [ index ] ; } param params . order param params . spentQuantity param params . inputIndices param params . outputIndices public addOrder ( params ) { const { order , spentQuantity , inputFromIndices = [ ] , inputFeeIndices = [ ] , outputFromIndices = [ ] , outputToIndices = [ ] , outputOwnedFeeIndices = [ ] , outputTransferredFeeIndices = [ ] } = params ; if ( inputFromIndices . length === 0 ) { throw Error ( `inputFromIndices should not be empty` ) ; } const inputIndices = inputFromIndices . concat ( inputFeeIndices ) ; const outputIndices = outputFromIndices . concat ( outputToIndices ) . concat ( outputOwnedFeeIndices ) . concat ( outputTransferredFeeIndices ) ; for ( const orderOnTx of this . _transaction . orders ) { const setInputs = new Set ( orderOnTx . inputFromIndices . concat ( orderOnTx . inputFeeIndices ) ) ; const setOutputs = new Set ( orderOnTx . outputFromIndices . concat ( orderOnTx . outputToIndices ) . concat ( orderOnTx . outputOwnedFeeIndices ) . concat ( orderOnTx . outputTransferredFeeIndices ) ) ; const inputIntersection = inputIndices . filter ( x => setInputs . has ( x ) ) ; const outputIntersection = outputIndices . filter ( x => setOutputs . has ( x ) ) ; if ( inputIntersection . length > 0 || outputIntersection . length > 0 ) { throw Error ( `inputIndices and outputIndices should not intersect with other orders: ${ orderOnTx }` ) ; } } this . _transaction . orders . push ( new OrderOnTransfer ( { order , spentQuantity : U64 . ensure ( spentQuantity ) , inputFromIndices , inputFeeIndices , outputFromIndices , outputToIndices , outputOwnedFeeIndices , outputTransferredFeeIndices } ) ) ; return this ; } public orders ( ) { return this . _transaction . orders ; } param index returns public getTransferredAsset ( index ) { if ( index >= this . _transaction . outputs . length ) { throw Error ( "invalid output index" ) ; } const output = this . _transaction . outputs [ index ] ; const { assetType , shardId , lockScriptHash , parameters , quantity } = output ; return new Asset ( { assetType , shardId , lockScriptHash , parameters , quantity , tracker : this . tracker ( ) , transactionOutputIndex : index } ) ; } returns public getTransferredAssets ( ) { return _ . range ( this . _transaction . outputs . length ) . map ( i => this . getTransferredAsset ( i ) ) ; } returns public hashWithoutScript ( params ? ) { const { networkId } = this . _transaction ; const { tag = { input : "all" , output : "all" } as SignatureTag , type = null , index = null } = params || { } ; let burns ; let inputs ; let outputs ; if ( this . _transaction . orders . length > 0 && ( tag . input !== "all" || tag . output !== "all" ) ) { throw Error ( `Partial signing is unavailable with orders` ) ; } if ( tag . input === "all" ) { inputs = this . _transaction . inputs . map ( input => input . withoutScript ( ) ) ; burns = this . _transaction . burns . map ( input => input . withoutScript ( ) ) ; } else if ( tag . input === "single" ) { if ( typeof index !== "number" ) { throw Error ( `Unexpected value of the index param: ${ index }` ) ; } if ( type === "input" ) { inputs = [ this . _transaction . inputs [ index ] . withoutScript ( ) ] ; burns = [ ] ; } else if ( type === "burn" ) { inputs = [ ] ; burns = [ this . _transaction . burns [ index ] . withoutScript ( ) ] ; } else { throw Error ( `Unexpected value of the type param: ${ type }` ) ; } } else { throw Error ( `Unexpected value of the tag input: ${ tag . input }` ) ; } if ( tag . output === "all" ) { outputs = this . _transaction . outputs ; } else if ( Array . isArray ( tag . output ) ) { outputs = Array . from ( new Set ( tag . output ) ) . sort ( ( a , b ) => a - b ) . map ( i => this . _transaction . outputs [ i ] ) ; } else { throw Error ( `Unexpected value of the tag output: ${ tag . output }` ) ; } return new H256 ( blake256WithKey ( new AssetTransferTransaction ( { burns , inputs , outputs , orders : this . _transaction . orders , networkId } ) . rlpBytes ( ) , Buffer . from ( blake128 ( encodeSignatureTag ( tag ) ) , "hex" ) ) ) ; } public type ( ) { return "transferAsset" ; } protected actionToEncodeObject ( ) { const { expiration , metadata , approvals } = this ; const encoded = this . _transaction . toEncodeObject ( ) ; encoded . push ( metadata ) ; encoded . push ( approvals ) ; if ( expiration == null ) { encoded . push ( [ ] ) ; } else { encoded . push ( [ expiration ] ) ; } return encoded ; } protected actionToJSON ( ) { const json = this . _transaction . toJSON ( ) ; return { ... json , metadata : this . metadata , approvals : this . approvals , expiration : this . expiration } ; } } interface AssetTransferTransactionData { burns : AssetTransferInput [ ] ; inputs : AssetTransferInput [ ] ; outputs : AssetTransferOutput [ ] ; orders : OrderOnTransfer [ ] ; networkId : NetworkId ; } /**
 * Spends the existing asset and creates a new asset. Ownership can be transferred during this process.
 *
 * An AssetTransferTransaction consists of:
 *  - A list of AssetTransferInput to burn.
 *  - A list of AssetTransferInput to spend.
 *  - A list of AssetTransferOutput to create.
 *  - A network ID. This must be identical to the network ID of which the
 *  transaction is being sent to.
 *
 * All inputs must be valid for the transaction to be valid. When each asset
 * types' quantity have been summed, the sum of inputs and the sum of outputs
 * must be identical.
 */ class AssetTransferTransaction { public readonly burns : AssetTransferInput [ ] ; public readonly inputs : AssetTransferInput [ ] ; public readonly outputs : AssetTransferOutput [ ] ; public readonly orders : OrderOnTransfer [ ] ; public readonly networkId : NetworkId ; param params . burns param params . inputs param params . outputs param params . networkId constructor ( params ) { const { burns , inputs , outputs , orders , networkId } = params ; this . burns = burns ; this . inputs = inputs ; this . outputs = outputs ; this . orders = orders ; this . networkId = networkId ; } returns public toJSON ( ) { const { networkId , burns , inputs , outputs , orders } = this ; return { networkId , burns : burns . map ( input => input . toJSON ( ) ) , inputs : inputs . map ( input => input . toJSON ( ) ) , outputs : outputs . map ( output => output . toJSON ( ) ) , orders : orders . map ( order => order . toJSON ( ) ) } ; } /**
     * Convert to an object for RLP encoding.
     */ public toEncodeObject ( ) { return [ 0x14 , this . networkId , this . burns . map ( input => input . toEncodeObject ( ) ) , this . inputs . map ( input => input . toEncodeObject ( ) ) , this . outputs . map ( output => output . toEncodeObject ( ) ) , this . orders . map ( order => order . toEncodeObject ( ) ) ] ; } /**
     * Convert to RLP bytes.
     */ public rlpBytes ( ) { return RLP . encode ( this . toEncodeObject ( ) ) ; } }