import { BehaviorSubject , Observable , Subscription } from 'rxjs' ; import Decimal from 'decimal.js' ; import BN from 'bn.js' ; import { RadixAccount , RadixAtomNodeStatus , RadixAtomNodeStatusUpdate , RadixECIES , RadixNodeConnection , RadixParticleGroup , radixUniverse , } from '../..' ; import { RadixAtom , RadixConsumable , RadixFixedSupplyTokenDefinitionParticle , RadixMessageParticle , RadixMutableSupplyTokenDefinitionParticle , RadixRRIParticle , RadixSpunParticle , RadixTokenPermissions , RadixTokenPermissionsValues , RadixTransferrableTokensParticle , RadixUnallocatedTokensParticle , RadixUniqueParticle , RRI , } from '../atommodel' ; import { logger } from '../common/RadixLogger' ; import { RadixTokenDefinition , RadixTokenSupplyType } from '../token/RadixTokenDefinition' ; import { calculateFeeForAtom } from '../fees/RadixTokenFeeCalculator' ; import RadixIdentity from '../identity/RadixIdentity' ; import { first } from 'rxjs-compat/operator/first' ; import { share } from 'rxjs/operators' ; export default class RadixTransactionBuilder { private BNZERO : BN = new BN ( 0 ) ; private DCZERO : Decimal = new Decimal ( 0 ) ; private particleGroups : RadixParticleGroup [ ] = [ ] ; private subs = new Subscription ( ) ; private spentConsumables : number [ ] = [ ] ; private unspentConsumables : RadixConsumable [ ] = [ ] ; private static getSubUnitsQuantity ( decimalQuantity ) { if ( typeof decimalQuantity !== 'number' && typeof decimalQuantity !== 'string' && ! Decimal . isDecimal ( decimalQuantity ) ) { throw new Error ( 'quantity is not a valid number' ) ; } const unitsQuantity = new Decimal ( decimalQuantity ) ; return RadixTokenDefinition . fromDecimalToSubunits ( unitsQuantity ) ; } param from param to param tokenReference param decimalQuantity param message public static createTransferAtom ( from , to , tokenReference , decimalQuantity , message ? ) { return new RadixTransactionBuilder ( ) . addTransfer ( from , to , tokenReference , decimalQuantity , message ) ; } param from param to param tokenReferenceURI param decimalQuantity param message public addTransfer ( from , to , tokenReference , decimalQuantity , message ? ) { if ( from . address . equals ( to . address ) ) { throw new Error ( `Cannot send money to the same account` ) ; } tokenReference = ( tokenReference instanceof RRI ) ? tokenReference : RRI . fromString ( tokenReference ) ; const subunitsQuantity = RadixTransactionBuilder . getSubUnitsQuantity ( decimalQuantity ) ; if ( subunitsQuantity . lt ( this . BNZERO ) ) { throw new Error ( 'Negative quantity is not allowed' ) ; } else if ( subunitsQuantity . eq ( this . BNZERO ) ) { throw new Error ( `Quantity 0 is not valid` ) ; } const transferSystem = from . transferSystem ; const balanceOfToken = transferSystem . balance [ tokenReference . toString ( ) ] ; if ( balanceOfToken === undefined ) { const errorMsg = `Balance undefined, token ${ tokenReference . toString ( ) }, balances:\n${ JSON . stringify ( transferSystem . balance , null , 4 ) }\n\n` ; throw new Error ( errorMsg ) ; } if ( subunitsQuantity . gt ( balanceOfToken ) ) { throw new Error ( 'Insufficient funds' ) ; } const unspentConsumables = transferSystem . getUnspentConsumables ( ) ; for ( let consumable of this . unspentConsumables ) { unspentConsumables . push ( consumable ) ; } const createTransferAtomParticleGroup = new RadixParticleGroup ( ) ; const consumerQuantity = new BN ( 0 ) ; let granularity = unspentConsumables [ 0 ] . getGranularity ( ) ; let tokenPermissions = unspentConsumables [ 0 ] . getTokenPermissions ( ) ; createTransferAtomParticleGroup . particles . push ( RadixSpunParticle . up ( new RadixTransferrableTokensParticle ( subunitsQuantity , granularity , to . address , tokenReference , tokenPermissions ) ) ) ; for ( const consumable of unspentConsumables . sort ( ( a , b ) => { return a . getAmount ( ) . sub ( b . getAmount ( ) ) . gt ( new BN ( 0 ) ) ? 1 : - 1 ; } ) ) { if ( ! consumable . getTokenDefinitionReference ( ) . equals ( tokenReference ) ) { continue ; } if ( this . spentConsumables . includes ( consumable . nonce ) ) { continue ; } tokenPermissions = consumable . getTokenPermissions ( ) ; createTransferAtomParticleGroup . particles . push ( RadixSpunParticle . down ( consumable ) ) ; consumerQuantity . iadd ( consumable . getAmount ( ) ) ; this . spentConsumables . push ( consumable . nonce ) ; if ( consumerQuantity . gte ( subunitsQuantity ) ) { break ; } } if ( consumerQuantity . sub ( subunitsQuantity ) . gtn ( 0 ) ) { const TTP = new RadixTransferrableTokensParticle ( consumerQuantity . sub ( subunitsQuantity ) , granularity , from . address , tokenReference , tokenPermissions ) ; const remainderParticle = RadixSpunParticle . up ( TTP ) ; createTransferAtomParticleGroup . particles . push ( remainderParticle ) ; this . unspentConsumables . push ( TTP ) ; } if ( ! subunitsQuantity . mod ( granularity ) . eq ( this . BNZERO ) ) { throw new Error ( `This token requires that any tranferred amount is a multiple of it's granularity = 
                ${ RadixTokenDefinition . fromSubunitsToDecimal ( granularity ) }` ) ; } if ( message ) { this . addEncryptedMessage ( from , 'transfer' , message , [ to , from ] ) ; } this . particleGroups . push ( createTransferAtomParticleGroup ) ; return this ; } param { RadixAccount } ownerAccount param { string | RRI } tokenReference param { string | number | Decimal } decimalQuantity public static createBurnAtom ( ownerAccount , tokenReference , decimalQuantity ) { return new this ( ) . burnTokens ( ownerAccount , tokenReference , decimalQuantity ) ; } param { RadixAccount } ownerAccount param { string | RRI } tokenReference param { string | number | Decimal } decimalQuantity public burnTokens ( ownerAccount , tokenReference , decimalQuantity ) { tokenReference = ( tokenReference instanceof RRI ) ? tokenReference : RRI . fromString ( tokenReference ) ; const tokenClass = ownerAccount . tokenDefinitionSystem . getTokenDefinition ( tokenReference . getName ( ) ) ; const subunitsQuantity = RadixTransactionBuilder . getSubUnitsQuantity ( decimalQuantity ) ; if ( subunitsQuantity . lt ( this . BNZERO ) ) { throw new Error ( 'Negative quantity is not allowed' ) ; } else if ( subunitsQuantity . eq ( this . BNZERO ) ) { throw new Error ( `Quantity 0 is not valid` ) ; } const transferSytem = ownerAccount . transferSystem ; if ( ! tokenReference . equals ( radixUniverse . nativeToken ) && tokenClass . tokenSupplyType !== RadixTokenSupplyType . MUTABLE ) { throw new Error ( 'This token is fixed supply' ) ; } if ( subunitsQuantity . gt ( transferSytem . balance [ tokenReference . toString ( ) ] ) ) { throw new Error ( 'Insufficient funds' ) ; } if ( ! subunitsQuantity . mod ( tokenClass . getGranularity ( ) ) . eq ( this . BNZERO ) ) { throw new Error ( `This token requires that any tranferred amount is a multiple of it's granularity = 
                ${ RadixTokenDefinition . fromSubunitsToDecimal ( tokenClass . getGranularity ( ) ) }` ) ; } const unspentConsumables = transferSytem . getUnspentConsumables ( ) ; for ( let consumable of this . unspentConsumables ) { unspentConsumables . push ( consumable ) ; } const burnParticleGroup = new RadixParticleGroup ( ) ; const consumerQuantity = new BN ( 0 ) ; let tokenPermissions ; for ( const consumable of unspentConsumables ) { if ( ! consumable . getTokenDefinitionReference ( ) . equals ( tokenReference ) ) { continue ; } if ( this . spentConsumables . includes ( consumable . nonce ) ) { continue ; } burnParticleGroup . particles . push ( RadixSpunParticle . down ( consumable ) ) ; tokenPermissions = consumable . getTokenPermissions ( ) ; consumerQuantity . iadd ( consumable . getAmount ( ) ) ; this . spentConsumables . push ( consumable . nonce ) ; if ( consumerQuantity . gte ( subunitsQuantity ) ) { break ; } } burnParticleGroup . particles . push ( RadixSpunParticle . up ( new RadixUnallocatedTokensParticle ( subunitsQuantity , tokenClass . getGranularity ( ) , tokenReference , tokenPermissions ) ) ) ; if ( consumerQuantity . sub ( subunitsQuantity ) . gtn ( 0 ) ) { const TTP = new RadixTransferrableTokensParticle ( consumerQuantity . sub ( subunitsQuantity ) , tokenClass . getGranularity ( ) , ownerAccount . address , tokenReference , tokenPermissions ) ; const remainderParticle = RadixSpunParticle . up ( TTP ) ; burnParticleGroup . particles . push ( remainderParticle ) ; this . unspentConsumables . push ( TTP ) ; } this . particleGroups . push ( burnParticleGroup ) ; return this ; } param { RadixAccount } ownerAccount param { string | RRI } tokenReference param { string | number | Decimal } decimalQuantity public static createMintAtom ( ownerAccount , tokenReference , decimalQuantity ) { return new this ( ) . mintTokens ( ownerAccount , tokenReference , decimalQuantity ) ; } param { RadixAccount } ownerAccount param { string | RRI } tokenReference param { string | number | Decimal } decimalQuantity param { RadixAccount } to param { string } message public mintTokens ( ownerAccount , tokenReference , decimalQuantity , to ? , message ? ) { tokenReference = ( tokenReference instanceof RRI ) ? tokenReference : RRI . fromString ( tokenReference ) ; const tokenClass = ownerAccount . tokenDefinitionSystem . getTokenDefinition ( tokenReference . getName ( ) ) ; if ( ! tokenClass ) { throw new Error ( `ERROR: Token definition ${ tokenReference . getName ( ) } not found in owner account.` ) ; } const subunitsQuantity = RadixTransactionBuilder . getSubUnitsQuantity ( decimalQuantity ) ; if ( subunitsQuantity . lt ( this . BNZERO ) ) { throw new Error ( 'Negative quantity is not allowed' ) ; } else if ( subunitsQuantity . eq ( this . BNZERO ) ) { throw new Error ( `Quantity 0 is not valid` ) ; } if ( tokenClass . tokenSupplyType !== RadixTokenSupplyType . MUTABLE ) { throw new Error ( 'This token is fixed supply' ) ; } if ( subunitsQuantity . gte ( tokenClass . getUnallocatedSupply ( ) ) ) { throw new Error ( 'Total supply would exceed 2^256' ) ; } if ( ! subunitsQuantity . mod ( tokenClass . getGranularity ( ) ) . eq ( this . BNZERO ) ) { throw new Error ( `This token requires that any tranferred amount is a multiple of it's granularity = 
                ${ RadixTokenDefinition . fromSubunitsToDecimal ( tokenClass . getGranularity ( ) ) }` ) ; } const unallocatedTokens = tokenClass . getUnallocatedTokens ( ) ; const tokenPermissions = unallocatedTokens [ 0 ] . getTokenPermissions ( ) ; const particleGroup = new RadixParticleGroup ( ) ; const consumerQuantity = new BN ( 0 ) ; for ( const consumable of unallocatedTokens ) { particleGroup . particles . push ( RadixSpunParticle . down ( consumable ) ) ; consumerQuantity . iadd ( consumable . getAmount ( ) ) ; if ( consumerQuantity . gte ( subunitsQuantity ) ) { break ; } } if ( consumerQuantity . sub ( subunitsQuantity ) . gtn ( 0 ) ) { particleGroup . particles . push ( RadixSpunParticle . up ( new RadixUnallocatedTokensParticle ( consumerQuantity . sub ( subunitsQuantity ) , tokenClass . getGranularity ( ) , tokenReference , tokenPermissions ) ) ) ; } const receiverAccount = to || ownerAccount ; const particle = new RadixTransferrableTokensParticle ( subunitsQuantity , tokenClass . getGranularity ( ) , receiverAccount . address , tokenReference , tokenPermissions ) ; if ( to && message ) { this . addEncryptedMessage ( ownerAccount , 'transfer' , message , [ to , ownerAccount ] ) ; } particleGroup . particles . push ( RadixSpunParticle . up ( particle ) ) ; this . particleGroups . push ( particleGroup ) ; return this ; } param owner param name param symbol param description param granularity param decimalQuantity param iconUrl public createTokenSingleIssuance ( owner , name , symbol , description , granularity = new Decimal ( '1e-18' ) , decimalQuantity , url , iconUrl ) { const subunitsQuantity = RadixTransactionBuilder . getSubUnitsQuantity ( decimalQuantity ) ; const subunitsGranularity = RadixTransactionBuilder . getSubUnitsQuantity ( granularity ) ; if ( subunitsQuantity . lte ( this . BNZERO ) ) { throw new Error ( 'Single-issuance tokens must be created with some supply' ) ; } if ( subunitsGranularity . ltn ( 1 ) ) { throw new Error ( 'Granuarity has to be larger than or equal to 1e-18' ) ; } if ( ! subunitsQuantity . mod ( subunitsGranularity ) . eq ( this . BNZERO ) ) { throw new Error ( `The supply should be a multiple of the token granularity = 
                ${ RadixTokenDefinition . fromSubunitsToDecimal ( subunitsGranularity ) }` ) ; } const tokenClassParticle = new RadixFixedSupplyTokenDefinitionParticle ( owner . address , name , symbol , description , subunitsQuantity , subunitsGranularity , url , iconUrl ) ; const rriParticle = new RadixRRIParticle ( tokenClassParticle . getRRI ( ) ) ; const initialSupplyParticle = new RadixTransferrableTokensParticle ( subunitsQuantity , subunitsGranularity , tokenClassParticle . getAddress ( ) , tokenClassParticle . getRRI ( ) , { } ) ; const createTokenParticleGroup = new RadixParticleGroup ( [ RadixSpunParticle . down ( rriParticle ) , RadixSpunParticle . up ( tokenClassParticle ) , RadixSpunParticle . up ( initialSupplyParticle ) , ] ) ; this . particleGroups . push ( createTokenParticleGroup ) ; return this ; } param owner param name param symbol param description param granularity param decimalQuantity param iconUrl param permissions public createTokenMultiIssuance ( owner , name , symbol , description , granularity = new Decimal ( '1e-18' ) , decimalQuantity , iconUrl , url , permissions ? ) { const subunitsQuantity = RadixTransactionBuilder . getSubUnitsQuantity ( decimalQuantity ) ; const subunitsGranularity = RadixTransactionBuilder . getSubUnitsQuantity ( granularity ) ; if ( subunitsQuantity . lt ( this . BNZERO ) ) { throw new Error ( 'Negative quantity is not allowed' ) ; } if ( subunitsGranularity . ltn ( 1 ) ) { throw new Error ( 'Granuarity has to be larger than or equal to 1e-18' ) ; } if ( ! subunitsQuantity . mod ( subunitsGranularity ) . eq ( this . BNZERO ) ) { throw new Error ( `The initual supply should be a multiple of the token granularity = 
                ${ RadixTokenDefinition . fromSubunitsToDecimal ( subunitsGranularity ) }` ) ; } if ( ! permissions ) { permissions = { mint : RadixTokenPermissionsValues . TOKEN_OWNER_ONLY , burn : RadixTokenPermissionsValues . TOKEN_OWNER_ONLY , } ; } if ( permissions && permissions . mint === RadixTokenPermissionsValues . NONE ) { throw new Error ( 'mint permissions cannot be NONE' ) ; } const tokenClassParticle = new RadixMutableSupplyTokenDefinitionParticle ( owner . address , name , symbol , description , subunitsGranularity , iconUrl , url , permissions ) ; const rriParticle = new RadixRRIParticle ( tokenClassParticle . getRRI ( ) ) ; const initialSupplyParticle = new RadixUnallocatedTokensParticle ( new BN ( 2 ) . pow ( new BN ( 256 ) ) . subn ( 1 ) , subunitsGranularity , tokenClassParticle . getRRI ( ) , permissions ) ; const createTokenParticleGroup = new RadixParticleGroup ( [ RadixSpunParticle . down ( rriParticle ) , RadixSpunParticle . up ( tokenClassParticle ) , RadixSpunParticle . up ( initialSupplyParticle ) , ] ) ; this . particleGroups . push ( createTokenParticleGroup ) ; if ( subunitsQuantity . gtn ( 0 ) ) { const mintParticle = new RadixTransferrableTokensParticle ( subunitsQuantity , subunitsGranularity , owner . address , tokenClassParticle . getRRI ( ) , permissions ) ; const mintParticleGroup = new RadixParticleGroup ( [ RadixSpunParticle . down ( initialSupplyParticle ) , RadixSpunParticle . up ( mintParticle ) , ] ) ; const remainder = initialSupplyParticle . getAmount ( ) . sub ( mintParticle . getAmount ( ) ) ; if ( remainder . gten ( 0 ) ) { const remainingSupplyParticle = new RadixUnallocatedTokensParticle ( remainder , subunitsGranularity , tokenClassParticle . getRRI ( ) , permissions ) ; mintParticleGroup . particles . push ( RadixSpunParticle . up ( remainingSupplyParticle ) ) ; } this . particleGroups . push ( mintParticleGroup ) ; } return this ; } param from param recipients param applicationId param payload param encrypted public static createPayloadAtom ( from , recipients , applicationId , payload , encrypted = true ) { if ( encrypted ) { return new RadixTransactionBuilder ( ) . addEncryptedMessage ( from , applicationId , payload , recipients ) ; } else { return new RadixTransactionBuilder ( ) . addUnencryptedMessage ( from , applicationId , payload , recipients ) ; } } param from param to param message public static createRadixMessageAtom ( from , to , message ) { return new RadixTransactionBuilder ( ) . addEncryptedMessage ( from , 'message' , message , [ from , to ] ) ; } param from param applicationId param message param recipients public addEncryptedMessage ( from , applicationId , message , recipients ) { const recipientPubKeys = recipients . map ( r => r . address . getPublic ( ) ) ; const { protectors , ciphertext } = RadixECIES . encryptForMultiple ( recipientPubKeys , Buffer . from ( message ) ) ; this . addMessageParticle ( from , JSON . stringify ( protectors . map ( p => p . toString ( 'base64' ) ) ) , { application : 'encryptor' , contentType : 'application/json' , } , recipients ) ; if ( this . particleGroups . length < 1 ) { throw new Error ( `Expected at least one ParticleGroup` ) ; } const lastParticleGroup = this . particleGroups [ this . particleGroups . length - 1 ] ; this . addMessageParticle ( from , ciphertext , { application : applicationId , } , recipients , lastParticleGroup ) ; return this ; } param from param applicationId param message param recipients public addUnencryptedMessage ( from , applicationId , message , recipients ) { this . addMessageParticle ( from , message , { application : applicationId , } , recipients ) ; return this ; } param from param data param metadata param recipients param toParticleGroup public addMessageParticle ( from , data , metadata , recipients , toParticleGroup ? ) { if ( recipients . length === 0 ) { throw new Error ( `A message must have recipients` ) ; } const particle = new RadixMessageParticle ( from . address , ( recipients . length === 1 ) ? recipients [ 0 ] . address : recipients [ 1 ] . address , data , metadata ) ; const spunParticle = RadixSpunParticle . up ( particle ) ; if ( toParticleGroup ) { toParticleGroup . particles . push ( spunParticle ) ; } else { const particleGroup = new RadixParticleGroup ( [ spunParticle ] ) ; this . particleGroups . push ( particleGroup ) ; } return this ; } param { RadixAccount } account param { string } unique public addUniqueParticle ( account , unique ) { const uniqueParticle = new RadixUniqueParticle ( account . address , unique ) ; const rriParticle = new RadixRRIParticle ( uniqueParticle . getRRI ( ) ) ; const uniqueParticleGroup = new RadixParticleGroup ( [ RadixSpunParticle . down ( rriParticle ) , RadixSpunParticle . up ( uniqueParticle ) , ] ) ; this . particleGroups . push ( uniqueParticleGroup ) ; return this ; } param identity returns public signAndSubmit ( identity ) { const atom = this . buildAtom ( ) ; const stateSubject = new BehaviorSubject < RadixAtomNodeStatusUpdate > ( { status : RadixAtomNodeStatus . PENDING , } ) ; radixUniverse . getNodeConnection ( ) . then ( connection => { this . subs . add ( this . signAndSubmitAtom ( atom , connection , identity ) . subscribe ( stateSubject ) ) ; } ) . catch ( e => { logger . error ( e ) ; stateSubject . error ( { status : RadixAtomNodeStatus . SUBMISSION_ERROR , data : e , } ) ; } ) ; return stateSubject . pipe ( share ( ) ) ; } /**
     * Build an atom from the added particle groups
     */ public buildAtom ( ) { if ( this . particleGroups . length === 0 ) { throw new Error ( 'No particle groups specified' ) ; } const atom = new RadixAtom ( ) ; atom . particleGroups = this . particleGroups ; return atom ; } public addFeeToAtom ( atom , account ) { const xrdsToBurnBN = calculateFeeForAtom ( atom ) ; assertMinNativeTokenBalance ( account , xrdsToBurnBN ) ; const quantity = RadixTokenDefinition . fromSubunitsToDecimal ( xrdsToBurnBN ) ; this . burnTokens ( account , radixUniverse . nativeToken , quantity ) ; } param atom param connection param identity public signAndSubmitAtom ( atom , connection , identity ) { this . addFeeToAtom ( atom , identity . account ) ; const statusSubject = new BehaviorSubject < RadixAtomNodeStatusUpdate > ( { status : RadixAtomNodeStatus . SUBMITTING , } ) ; identity . signAtom ( atom ) . then ( signedAtom => { const submissionSubject = radixUniverse . ledger . submitAtom ( signedAtom , connection ) ; this . subs . add ( submissionSubject . subscribe ( statusSubject ) ) ; } ) . catch ( error => { statusSubject . error ( error ) ; } ) ; return statusSubject ; } } const assertMinNativeTokenBalance = ( account , requiredBalance ) => { const nativeTokenBalance = account . transferSystem . snapshotOfNativeTokenBalance ( ) ; if ( nativeTokenBalance . lt ( requiredBalance ) ) { throw new Error ( `${ account . address . toString ( ) } owns
             only ${ RadixTokenDefinition . fromSubunitsToDecimal ( nativeTokenBalance ) } 
             XRDS but expected at least 
             ${ RadixTokenDefinition . fromSubunitsToDecimal ( requiredBalance ) }
             ` ) ; } } ;