# Kaufmich KM$ --- ### 2 Transaction types 1. Transfer transaction 2. Mass transfer transaction --- #### Transfer transaction ``` { "amount": 1001, // KM$ 10.01 "attachment": "txId:101", "sender": "3PN2bVFxJjgudPKqEGZ41TVsD5ZJmxqnPSu", // user "assetId": "7uncmN7dZfV3fYVvNdYTngrrbamPYMgwpDnYG1bGy6nA", // KM$ "recipient": "3PFmoN5YLoPNsL4cmNGkRxbUKrUVntwyAhf", // escort ... } ``` --- #### Mass transfer transaction ``` { "totalAmount": 1000, "attachment": "txId:102", "sender": "3PN2bVFxJjgudPKqEGZ41TVsD5ZJmxqnPSu", // user "assetId": "Fx2rhWK36H1nfXsiD4orNpBm2QG1JrMhx3eUcPVcoZm2", // another KM$ "transfers": [ { "recipient": "3PFmoN5YLoPNsL4cmNGkRxbUKrUVntwyAhf", // escort "amount": 970 // KM$ 9.70 }, { "recipient": "3PGNLwUG2GPpw74teTAxXFLxgFt3T2uQJsF", // ideawise "amount": 30 // KM$ 0.30 commission }, ], ... } ``` The implication is that before doing a transaction, we have to know what the commission fee is. We use that info to split the transaction into 2 transactions: for the *escort* and *ideawise*. Then we bundle both transactions with the mass transfer transaction. Then a script in the blockchain checks for the conditions (correct amounts, commission, recipient of beneficiary etc.). Note: it is easy to fetch the commission fee from the blockchain. --- #### Asset script Whenever an asset is created, a "smart contract" or `asset script` is attached to the asset. There is basically a `hook` that calls the script at any transaction. The `script` has two outcomes: 1. **allow** the transaction 2. **deny** the transaction It checks for various conditions on recipient and amount. > Any transaction to `Compay` or `ideawise` does not incur a commission fee, thus a simple `transfer transaction` is allowed. > Any transaction to an "escort" or another "user" needs to be a `mass transfer transaction`, where a smaller part of the total transaction (KM$) is paid as a commission to a beneficiary (Ideawise) ([v2](https://github.com/dexterslabor/kaufmich/tree/v2): and another commission to a "karma" pot). The % fee can be updated at any time by the admin. The trading of an asset can be frozen at any time by the admin. The trading of an asset can be un-frozen at any time by the admin. --- #### Data transaction A `data transaction` updates a key-value store that is associated with an account. We will use the asset issuer's account (Compay). The `asset script` reads that storage. > The size of an account data storage is unlimited. Example data: `(assetId, feePercentage)` `(assetIdFrozen, boolean)` --- ### Reference Example of the custom asset script written for Kaufmich (V2). Note: difference to v1 is a major refactoring but also a new feature. It is possible to send a commission fee to ideawise AND a "karma" pot. ![](https://i.imgur.com/bGCXUns.png) ```typescript # Token owners can transfer asset only if they pay a commission fee to a beneficiary address # The fee is defined by the token issuer in a data transaction # (key, value) = (assetId, feePercentage) {-# STDLIB_VERSION 3 #-} {-# CONTENT_TYPE EXPRESSION #-} {-# SCRIPT_TYPE ASSET #-} ##### Constants ##### let errorAssetWithoutIssuer = "asset does not have issuer" let dataTxKeyBeneficiary = "beneficiary" let dataTxKeyFrozen = "frozen" let dataTxKeyKarma = "karma" ##### Getters ##### func getAssetIssuerAddress(assetId: ByteVector) = { let asset = assetInfo(assetId) match(asset) { case asset: Asset => { let issuerAddress = addressFromPublicKey(asset.issuerPublicKey) issuerAddress } case _ => throw(errorAssetWithoutIssuer) } } func getAddressFromDataTx(issuerAddress: Address, dataTxKey: String) = { let beneficiaryAddressString = getStringValue(issuerAddress, dataTxKey) let beneficiaryAddress = addressFromString(beneficiaryAddressString) beneficiaryAddress } func getBeneficiaryAddress(issuerAddress: Address) = { let beneficiaryAddress = getAddressFromDataTx(issuerAddress, dataTxKeyBeneficiary) beneficiaryAddress } func getKarmaAdddress(issuerAddress: Address) = { let karmaAddress = getAddressFromDataTx(issuerAddress, dataTxKeyKarma) karmaAddress } ##### Permission helpers ###### func checkAssetIsFrozen(assetId: ByteVector) = { let asset = assetInfo(assetId) match(asset) { case asset: Asset => { let issuerAddress = addressFromPublicKey(asset.issuerPublicKey) let isFrozen = getBooleanValue(issuerAddress, dataTxKeyFrozen) isFrozen } case _ => throw(errorAssetWithoutIssuer) } } func checkFeeRequired(assetId: ByteVector, senderAddress: Address) = { let issuerAddress = getAssetIssuerAddress(assetId) let beneficiaryAddress = getBeneficiaryAddress(issuerAddress) # if the sender is the creator OR beneficiary, do not apply commission fee calculation if(senderAddress == issuerAddress || senderAddress == beneficiaryAddress) then { false } else { true } } # Check that commission fee is above threshold # The fee is defined by the token issuer in a data transaction # (key, value) = (assetId, feePercentage) func checkFeeAmountCondition(issuerAddress: Address, assetId: ByteVector, massTransferTransaction: MassTransferTransaction) = { # check commission fee on first transaction in mass transfer let feePercentage = getIntegerValue(issuerAddress, toBase58String(assetId)) let firstAmount = massTransferTransaction.transfers[0].amount let secondAmount = massTransferTransaction.transfers[1].amount let totalFee = firstAmount + secondAmount # no floating point math that's why 100 is on left side and not divisor on right side of equation let totalFeeConditionIsMet = totalFee * 100 >= massTransferTransaction.totalAmount * feePercentage # TODO change key for karma fee # probably string operation is best appraoch # getIntegerValue(issuerAddress, toBase58String(`assetIdK`)) let karmaFeePercentage = getIntegerValue(issuerAddress, toBase58String(assetId)) let karmaFeeConditionIsMet = totalFee * 100 >= secondAmount * karmaFeePercentage let feeAmountCondition = totalFeeConditionIsMet == karmaFeeConditionIsMet feeAmountCondition } # checks that the first transaction in the batch goes to the beneficiary address func checkFeeRecipientCondition(issuerAddress: Address, massTransferTransaction: MassTransferTransaction) = { let beneficiaryAddress = getBeneficiaryAddress(issuerAddress) let firstRecipient = massTransferTransaction.transfers[0].recipient let firstFeeRecipientCondition = beneficiaryAddress == firstRecipient let karmaAddress = getKarmaAdddress(issuerAddress) let secondRecipient = massTransferTransaction.transfers[1].recipient let secondFeeRecipientCondition = karmaAddress == secondRecipient let feeRecipientConditions = karmaAddress == secondRecipient feeRecipientConditions } func checkMassTxForConditions(massTransferTransaction: MassTransferTransaction, assetId: ByteVector) = { let issuerAddress = getAssetIssuerAddress(assetId) let feeRecipientCondition = checkFeeRecipientCondition(issuerAddress, massTransferTransaction) let feeAmountCondition = checkFeeAmountCondition(issuerAddress, assetId, massTransferTransaction) let allConditions = feeRecipientCondition && feeAmountCondition allConditions } ##### MAIN FUNCTION OF ASSET SCRIPT ##### match tx { case transferTransaction: TransferTransaction => { let assetId = extract(transferTransaction.assetId) let senderAddress = transferTransaction.sender let feeRequired = checkFeeRequired(assetId, senderAddress) if(feeRequired) # if fee is required then block transfer transaction then false # if fee is not require then let the transaction go through else true } # a mass transfer is a batch transaction case massTransferTransaction: MassTransferTransaction => { let assetId = extract(massTransferTransaction.assetId) let isFrozen = checkAssetIsFrozen(assetId) if(isFrozen) # block transaction then false else { let senderAddress = massTransferTransaction.sender let feeRequired = checkFeeRequired(assetId, senderAddress) if(feeRequired) then { checkMassTxForConditions(massTransferTransaction, assetId) } else { # if fee is not required then let the transaction pass without checks true } } } # all other transactions are deactivated case _ => false } ```