import { Address , constant , Deploy , Hash256 , MapStorage , crypto , Fixed8 , createEventNotifier , SmartContract , } from '@neo-one/smart-contract' ; const notifyDeposited = createEventNotifier < Address , Address , Fixed8 > ( 'deposited' , 'address' , 'assetID' , 'amount' ) ; const notifyWithdrawn = createEventNotifier < Address , Address , Fixed8 > ( 'withdrawn' , 'address' , 'assetID' , 'amount' ) ; const notifyOfferCreated = createEventNotifier < Address , Address , Fixed8 , Address , Fixed8 , Hash256 > ( 'offerCreated' , 'makerAddress' , 'offerAssetID' , 'offerAmount' , 'wantAssetID' , 'wantAmount' , 'offerHash' ) ; const notifyOfferCancelled = createEventNotifier < Hash256 > ( 'offerCancelled' , 'offerHash' ) ; const notifyBurnt = createEventNotifier < Address , Address , Fixed8 > ( 'burnt' , 'filler' , 'takerFeeAssetID' , 'takerFeeAmount' ) ; const notifyFilled = createEventNotifier < Address , Hash256 , Fixed8 , Address , Fixed8 , Address , Fixed8 , Fixed8 > ( 'offerFilled' , 'filler' , 'offerHash' , 'amountToFill' , 'offerAssetID' , 'offerAmount' , 'wantAssetID' , 'wantAmount' , 'amountToTake' ) ; interface NEP17 { readonly transfer : ( from : Address , to : Address , amount : Fixed8 ) => boolean ; } type Offer = { readonly maker : Address ; readonly offerAssetID : Address ; readonly offerAmount : Fixed8 ; readonly wantAssetID : Address ; readonly wantAmount : Fixed8 ; readonly makerFeeAssetID : Address ; readonly makerFeeAvailableAmount : Fixed8 ; readonly nonce : string ; } ; export class Exchange extends SmartContract { private mutableFeeAddress : Address ; private readonly balances = MapStorage . for < [ Address , Address ] , Fixed8 > ( ) ; private readonly offers = MapStorage . for < Hash256 , Offer > ( ) ; public constructor ( public readonly owner = Deploy . senderAddress ) { super ( ) ; if ( ! Address . isCaller ( owner ) ) { throw new Error ( 'Sender was not the owner.' ) ; } this . mutableFeeAddress = this . address ; } @ constant public balanceOf ( address , assetID ) { const balance = this . balances . get ( [ address , assetID ] ) ; return balance === undefined ? 0 : balance ; } @ constant public getOffer ( offerHash ) { return this . offers . get ( offerHash ) ; } public depositNEP17 ( from , assetID , amount ) { if ( ! Address . isCaller ( from ) ) throw new Error ( 'Caller was not the sender!' ) ; if ( amount < 1 ) throw new Error ( 'Amount must be greater than 0!' ) ; this . transferNEP17 ( from , this . address , assetID , amount ) ; this . balances . set ( [ from , assetID ] , this . balanceOf ( from , assetID ) + amount ) ; notifyDeposited ( from , assetID , amount ) ; } public withdrawNEP17 ( from , assetID , amount ) { if ( amount < 0 ) throw new Error ( `Amount must be greater than 0: ${ amount }` ) ; const balance = this . balanceOf ( from , assetID ) ; if ( balance < amount ) throw new Error ( `Not enough Balance to withdraw ${ amount }!` ) ; if ( ! Address . isCaller ( from ) ) throw new Error ( 'Caller is not authorized to withdraw funds!' ) ; this . transferNEP17 ( this . address , from , assetID , amount ) ; this . balances . set ( [ from , assetID ] , this . balanceOf ( from , assetID ) - amount ) ; notifyWithdrawn ( from , assetID , amount ) ; } public makeOffer ( maker , offerAssetID , offerAmount , wantAssetID , wantAmount , makerFeeAssetID , makerFeeAvailableAmount , nonce ) { if ( ! Address . isCaller ( maker ) ) throw new Error ( 'Caller is not authorized to make an offer on behalf of the maker!' ) ; const offerHash = this . getOfferHash ( maker , offerAssetID , offerAmount , wantAssetID , wantAmount , makerFeeAssetID , makerFeeAvailableAmount , nonce ) ; if ( this . getOffer ( offerHash ) !== undefined ) throw new Error ( 'Offer does not exist!' ) ; if ( offerAmount <= 0 || wantAmount <= 0 ) throw new Error ( 'Invalid amount. All amounts must be > 0!' ) ; if ( offerAssetID === wantAssetID ) throw new Error ( 'Cannot make an offer for the same asset!' ) ; if ( makerFeeAvailableAmount < 0 ) throw new Error ( 'Fee available amount cannot be < 0!' ) ; this . balances . set ( [ maker , offerAssetID ] , this . balanceOf ( maker , offerAssetID ) - offerAmount ) ; if ( makerFeeAssetID !== wantAssetID && makerFeeAvailableAmount > 0 ) { this . balances . set ( [ maker , makerFeeAssetID ] , this . balanceOf ( maker , makerFeeAssetID ) - makerFeeAvailableAmount ) ; } this . offers . set ( offerHash , { maker , offerAssetID , offerAmount , wantAssetID , wantAmount , makerFeeAssetID , makerFeeAvailableAmount , nonce , } ) ; notifyOfferCreated ( maker , offerAssetID , offerAmount , wantAssetID , wantAmount , offerHash ) ; } public fillOffer ( filler , offerHash , amountToTake , takerFeeAssetID , takerFeeAmount , burnTakerFee , makerFeeAmount , burnMakerFee ) { if ( ! Address . isCaller ( filler ) ) throw new Error ( 'Caller is not authorized to fill offer on behalf of the filler!' ) ; if ( takerFeeAmount < 0 ) throw new Error ( 'takerFeeAmount must not be < 0!' ) ; if ( makerFeeAmount < 0 ) throw new Error ( 'makerFeeAmount must not be < 0!' ) ; const offer = this . getOffer ( offerHash ) ; if ( offer === undefined ) throw new Error ( 'Offer does not exist!' ) ; if ( filler === offer . maker ) throw new Error ( 'Filler cannot be the same as the maker!' ) ; if ( amountToTake < 1 ) throw new Error ( 'Filler must take a nonzero amount of the offered asset!' ) ; if ( amountToTake > offer . offerAmount ) throw new Error ( 'Filler requests more than available in offer!' ) ; const amountToFill = ( amountToTake * offer . wantAmount ) / offer . offerAmount ; if ( amountToFill < 1 ) throw new Error ( 'Must fill a nonzero amount of the offered asset!' ) ; const wantAssetBalance = this . balanceOf ( filler , offer . wantAssetID ) ; if ( wantAssetBalance < amountToFill ) throw new Error ( 'Not enough balance to fill offer!' ) ; const deductTakerFeesSeparately = takerFeeAssetID !== offer . offerAssetID ; const feeAssetBalance = this . balanceOf ( filler , takerFeeAssetID ) ; if ( deductTakerFeesSeparately && takerFeeAmount > 0 && feeAssetBalance <= takerFeeAmount ) { throw new Error ( 'Filler does not have enough balance for fee!' ) ; } if ( offer . makerFeeAvailableAmount < makerFeeAmount ) throw new Error ( 'Maker does not have enough balance for fee!' ) ; const deductMakerFeesSeparately = offer . makerFeeAssetID !== offer . wantAssetID ; this . balances . set ( [ filler , offer . wantAssetID ] , this . balanceOf ( filler , offer . wantAssetID ) - amountToFill ) ; const amountForMakerAfterFees = deductMakerFeesSeparately ? amountToFill : amountToFill - makerFeeAmount ; this . balances . set ( [ offer . maker , offer . wantAssetID ] , this . balanceOf ( offer . maker , offer . wantAssetID ) + amountForMakerAfterFees ) ; const amountToTakeAfterFees = deductTakerFeesSeparately ? amountToTake : amountToTake - takerFeeAmount ; this . balances . set ( [ filler , offer . offerAssetID ] , this . balanceOf ( filler , offer . offerAssetID ) + amountToTakeAfterFees ) ; if ( takerFeeAmount > 0 ) { if ( deductTakerFeesSeparately ) { this . balances . set ( [ filler , takerFeeAssetID ] , this . balanceOf ( filler , takerFeeAssetID ) - takerFeeAmount ) ; } if ( burnTakerFee ) { notifyBurnt ( filler , takerFeeAssetID , takerFeeAmount ) ; } else { this . balances . set ( [ this . feeAddress , takerFeeAssetID ] , this . balanceOf ( this . feeAddress , takerFeeAssetID ) + takerFeeAmount ) ; } } let makerFeeAvailableAmount = offer . makerFeeAvailableAmount ; if ( makerFeeAmount > 0 ) { makerFeeAvailableAmount = offer . makerFeeAvailableAmount - makerFeeAmount ; if ( burnMakerFee ) { notifyBurnt ( offer . maker , offer . makerFeeAssetID , makerFeeAmount ) ; } else { this . balances . set ( [ this . feeAddress , offer . makerFeeAssetID ] , this . balanceOf ( this . feeAddress , offer . makerFeeAssetID ) + makerFeeAmount ) ; } } const offerAmount = offer . offerAmount - amountToTake ; const wantAmount = offer . wantAmount - amountToFill ; if ( offerAmount === 0 ) { this . offers . delete ( offerHash ) ; } else { this . offers . set ( offerHash , { ... offer , offerAmount , wantAmount , makerFeeAvailableAmount } ) ; } notifyFilled ( filler , offerHash , amountToFill , offer . offerAssetID , offer . offerAmount , offer . wantAssetID , offer . wantAmount , amountToTake ) ; } public cancelOffer ( maker , offerHash ) { if ( ! Address . isCaller ( maker ) ) throw new Error ( 'Only the offer maker may cancel an offer!' ) ; const offer = this . getOffer ( offerHash ) ; if ( offer === undefined ) throw new Error ( 'Offer does not exist!' ) ; this . balances . set ( [ maker , offer . offerAssetID ] , this . balanceOf ( maker , offer . offerAssetID ) + offer . offerAmount ) ; this . balances . set ( [ maker , offer . makerFeeAssetID ] , this . balanceOf ( maker , offer . makerFeeAssetID ) + offer . makerFeeAvailableAmount ) ; this . offers . delete ( offerHash ) ; notifyOfferCancelled ( offerHash ) ; } public set feeAddress ( address ) { if ( ! Address . isCaller ( this . owner ) ) { throw new Error ( 'Only the contract owner can change the FeeAddress!' ) ; } this . mutableFeeAddress = address ; } @ constant public get feeAddress ( ) : Address { return this . mutableFeeAddress ; } private getOfferHash ( maker , offerAssetID , offerAmount , wantAssetID , wantAmount , makerFeeAssetID , makerFeeAvailableAmount , nonce ) { const offerBuffer = Buffer . concat ( [ maker , offerAssetID , wantAssetID , makerFeeAssetID , Buffer . from ( [ offerAmount , wantAmount , makerFeeAvailableAmount ] . toString ( ) ) , Buffer . from ( nonce ) , ] ) ; return crypto . hash256 ( offerBuffer ) ; } private transferNEP17 ( from , to , assetID , amount ) { const nep17Asset = SmartContract . for < NEP17 > ( assetID ) ; if ( ! nep17Asset . transfer ( from , to , amount ) ) { throw new Error ( 'Failed to transfer NEP-5 tokens!' ) ; } } }