NEM(新经币)公链对接

2018-09-06 15:04:10

首先祝Chrome十周年生快,升级后的69耳目一新,继续加油。

前言

通常,当某个交易所要上新币的时候,都会提前下发通知。如果仔细点就会发现,如果是ERC20 Token的话,交易所基本是“秒上”,而对于其他非以太坊公链的Token,交易所一般会提前半月至一月发上币通知。原因很简单,对于ERC20 Token,由于都是运行在以太坊公链上,所以有着共通的运行机制,对于不同的Token,通过调用geth节点的API只需传入不同的合约地址,就能执行不同Token的转账、查余额等等一系列合约方法。也就是说代码可以完全复用,交易所上币只是添加了一个新币的合约地址,就能和其他ERC20 Token共用一套代码。

而对于非以太坊公链的Token,它们运行在其他完全不同的公链上。由于公链之间的开发语言、账户设计、Token设计、Token转账流程、共识机制等等都存在着天差地别,所以对这些公链上的Token进行操作,流程是不同于ERC20 Token的,也就是说代码不能与以太坊的复用。需要重新写一套适配该公链的程序,来完成对该公链上Token的发行、转账、查询等一些列操作。这就是为什么交易所在上非ERC20 Token时耗时较长的原因。

本文记录对于NEM(新经币)的对接过程。

新经币介绍

维基百科介绍:

新经币(New Economy Movement,缩写 NEM),是一种点对点虚拟货币。2015年初发布,其源代码由Java编写并100%属于原创。[1]NEM 广泛发布于人群中[2],其块链采用了全新发明的基于重要性证明POI的同步解决方案。NEM特征也包括:完整的点对点安全系统加密信息系统和基于Eigentrust++算法的声望系统。[3]

新经币NEM使用Java开发,且使用独创的POI(重要性证明机制),并且融合多重签名技术。单从这三点来看,NEM在技术和创新上,在各公链中属于上等马。

NEM新经币的原生Token为XEM。市值在60~100亿之间浮动,排名在10~20名之间。

新经币允许用户在其链上发型资产“mosaic”(翻译过来就是马赛克🌚),对标以太坊上发行的ERC20 Token。区别在于,发行的mosaic并不是以合约形式进行的,所以功能非常简单,不能像以太坊智能合约一样实现多样化的功能。它只具备Token的基础属性,即名称、发行量、精度。以及一些附加属性如发行量是否可修改、是否可转账、描述等。

mosaic代币在链上的唯一标识为mosaic id,对标以太坊合约的合约地址。mosaic id由两部分组成:namespace + mosaic name。所以在创建一个新的mosaic前,需要先创建一个namespace,然后在该namespace下创建mosaic name。以pundix这个Token来说,它在NEM链上发行的mosaic的id为 pundix:npxs , 其中 pundix是它的namespace,npxs是其mosaic name。创建一个namespace需要花费100XEM,且全网唯一,不可重复。创建完namespace后,就可以在该namespace下创建mosaic,创建一个mosaic花费10XEM。所以在NEM链上发行一个mosaic代币共需要110XEM+交易手续费(约0.3XEM)。且namespace不得与已存在的重复,该namespace下创建的mosaic的名称也不得出现重复。

NEM的账户地址分为测试网可主网。测试网地址以T开头,主网地址以N开头。(创建的时候可以挑NB开头的地址👍🏿)

开发文档

官网: https://nem.io
GitHub: https://github.com/NemProject
文档: http://docs.nem.io/en
NIS节点API文档: https://nemproject.github.io/

NEM官方提供的API SDK比较全面,相当良心:

NEM节点部署

如果想在本地启动节点并加入到NEM网络当中,过程非常简单。

http://bob.nem.ninja/下载nis最新包之后,解压。nis目录下的config.properties是一些节点信息配置。可以根据需要修改。然后直接运行nix.runNis.sh即可启动节点。

 ✘ ludis@Mac  ~/Downloads/package  ./nix.runNis.sh
2018-09-06 06:26:38.598 信息 NEM logging has been bootstrapped! (org.nem.deploy.g a)
2018-09-06 06:26:38.617 信息 Acquiring exclusive lock to lock file: /Users/ludis/nem/nis.lock (org.nem.deploy.CommonStarter tryAcquireLock)
2018-09-06 06:26:38.623 警告 no certificate found for (file:/Users/ludis/Downloads/package/nis/nem-deploy-0.6.95-BETA.jar <no signer certificates>) (org.nem.core.metadata.CodeSourceFacade <init>)
2018-09-06 06:26:38.626 信息 Analyzing meta data in <nem-deploy-0.6.95-BETA.jar> (org.nem.core.metadata.JarFacade <init>)
2018-09-06 06:26:38.636 信息 Meta data title <NEM Deploy>, version <0.6.95-BETA> (org.nem.core.metadata.JarFacade <init>)
2018-09-06 06:26:38.639 信息 Starting embedded Jetty Server. (org.nem.deploy.CommonStarter main)
2018-09-06 06:26:39.148 信息 Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@56ef9176: startup date [Thu Sep 06 14:26:39 CST 2018]; root of context hierarchy (org.springframework.context.annotation.AnnotationConfigApplicationContext prepareRefresh)
2018-09-06 06:26:40.751 信息 Loaded JDBC driver: org.h2.Driver (org.springframework.jdbc.datasource.DriverManagerDataSource setDriverClassName)
2018-09-06 06:26:41.394 信息 Database: jdbc:h2:/Users/ludis/nem/nis/data/nis5_mainnet (H2 1.3) (org.flywaydb.core.internal.dbsupport.DbSupportFactory info)
2018-09-06 06:26:41.553 信息 Current version of schema "PUBLIC": 1.0.7 (org.flywaydb.core.internal.command.DbMigrate info)
2018-09-06 06:26:41.555 信息 Schema "PUBLIC" is up to date. No migration necessary. (org.flywaydb.core.internal.command.DbMigrate info)
2018-09-06 06:26:41.834 INFO HCANN000001: Hibernate Commons Annotations {4.0.4.Final} (org.hibernate.annotations.common.reflection.java.JavaReflectionManager <clinit>)
2018-09-06 06:26:41.848 INFO HHH000412: Hibernate Core {4.3.0.Final} (org.hibernate.Version logVersion)
2018-09-06 06:26:41.853 INFO HHH000206: hibernate.properties not found (org.hibernate.cfg.Environment <clinit>)
2018-09-06 06:26:41.860 INFO HHH000021: Bytecode provider name : javassist (org.hibernate.cfg.Environment buildBytecodeProvider)
2018-09-06 06:26:42.398 INFO HHH000400: Using dialect: org.hibernate.dialect.H2Dialect (org.hibernate.dialect.Dialect <init>)
2018-09-06 06:26:42.782 INFO HHH000399: Using default transaction strategy (direct JDBC transactions) (org.hibernate.engine.transaction.internal.TransactionFactoryInitiator initiateService)
2018-09-06 06:26:42.791 INFO HHH000397: Using ASTQueryTranslatorFactory (org.hibernate.hql.internal.ast.ASTQueryTranslatorFactory <init>)
2018-09-06 06:26:42.898 INFO HV000001: Hibernate Validator 5.0.2.Final (org.hibernate.validator.internal.util.Version <clinit>)
2018-09-06 06:26:45.175 信息 Using DataSource [org.springframework.jdbc.datasource.DriverManagerDataSource@5792c08c] of Hibernate SessionFactory for HibernateTransactionManager (org.springframework.orm.hibernate4.HibernateTransactionManager afterPropertiesSet)
2018-09-06 06:26:45.239 警告 context ================== current: 108627620 (org.nem.nis.NisMain init)

......

......

2018-09-06 14:26:51.640:INFO:oejsh.ContextHandler:main: Started o.e.j.s.ServletContextHandler@38c460e8{/,null,AVAILABLE}
2018-09-06 14:26:51.640:INFO:oejs.ServerConnector:main: Started ServerConnector@7a814310{HTTP/1.1}{0.0.0.0:7890}
2018-09-06 14:26:51.644:INFO:oejs.Server:main: Started @14689ms
2018-09-06 06:26:51.644 信息 NEM Deploy is ready to serve. URL is "http://192.168.128.10:7890/". (org.nem.deploy.CommonStarter a)
2018-09-06 06:26:51.651 信息 loadBlocks (from height 2802 to height 2901) needed 40ms (org.nem.nis.dao.BlockDaoImpl d)
2018-09-06 06:26:51.717 信息 loadBlocks (from height 2902 to height 3001) needed 28ms (org.nem.nis.dao.BlockDaoImpl d)
2018-09-06 06:26:51.762 信息 loadBlocks (from height 3002 to height 3101) needed 27ms (org.nem.nis.dao.BlockDaoImpl d)
2018-09-06 06:26:51.811 信息 loadBlocks (from height 3102 to height 3201) needed 25ms (org.nem.nis.dao.BlockDaoImpl d)
2018-09-06 06:26:51.855 信息 loadBlocks (from height 3202 to height 3301) needed 25ms (org.nem.nis.dao.BlockDaoImpl d)
2018-09-06 06:26:51.911 信息 loadBlocks (from height 3302 to height 3401) needed 28ms (org.nem.nis.dao.BlockDaoImpl d)
2018-09-06 06:26:51.958 信息 loadBlocks (from height 3402 to height 3501) needed 30ms (org.nem.nis.dao.BlockDaoImpl d)
2018-09-06 06:26:52.004 信息 loadBlocks (from height 3502 to height 3601) needed 27ms (org.nem.nis.dao.BlockDaoImpl d)
2018-09-06 06:26:52.063 信息 loadBlocks (from height 3602 to height 3701) needed 24ms (org.nem.nis.dao.BlockDaoImpl d)
......

这样本地的NIS节点就启动且连接到NEM网络了。可以对照文档进行各种功能测试。

区块链浏览器

主网
http://explorer.nemchina.com
http://chain.nem.ninja

测试网
http://bob.nem.ninja:8765

nem faucet

测试时一些Token是必须的,但nem水龙头是真的不好找啊,能搜到的一些基本都停用了。。且用且珍惜。

https://xarleecm.com/en/nemfaucet

提示:使用这个水龙头申请Token时,需要先用该页面的插件挖矿几分钟后,才可以申请🤔,有点意思哈。没有免费的午餐么,大夏天的,我这air风扇转的那叫一个欢乐。风扇转完点提交,一般几个小时内就会把Token发到你的账户,可以去浏览器查询。

每小时最多申请100XEM,我申请了10个却意外地得到了500个,果然是个看脸的时代😏。因为创建mosaic需要110+的NEM,所以最好申请多点。

转账

这里使用node sdk,直接将XEM及mosaic转账及余额查询封装起来。需要的可以直接拿去用,转账的具体流程也写的比较清楚,可以参考注释和文档理解。

/**
 * nem及mosaic发送交易封装
 */

const nem = require("nem-sdk").default;
const config = require('./nem_config')
const logger = require('./logger');

/**
 * 创建 endpoint 对象(节点信息)
 * host	string	An NIS uri
 * port	string	An NIS port
 */
const endpoint = nem.model.objects.create("endpoint")(config.endpointHost, config.endpointPort)
logger.info('endpoint', endpoint)

/**
 * 创建 common 对象 (账户信息)
 * password	string	A password
 * privateKey	string	A private key
 */
const common = nem.model.objects.create("common")(config.password, config.privatekey)
// console.log('common', common)

init()

/**
 * nem及mosaic转账入口
 */
const doNemTransaction = (req, callback) => {

    let {
        name,
        address: recipient,
        value: amount
    } = req.body

    let option = {
        name: name,
        recipient: recipient,
        amount: amount
    }
    logger.info('option', option)

    // mosaic Token转账时,transferTransaction amount代表:要执行后续定义的mosaicAttachment的次数!!!
    // 而非代表nem的金额,此处与eth Token转账完全不同。为0时转账会成功,但mosaic不会到账~~
    if (name == 'NEM') {
        amount /= Math.pow(10, 6)
        option.amount = amount
    } else {
        amount = 1
    }

    /**
     * 创建 transferTransaction 对象(交易信息)
     * recipient    string	A recipient address
     * amount	number	An amount
     * message	string	A message to join
     */
    option.transferTransaction = nem.model.objects.create("transferTransaction")(recipient, amount, config.message);
    logger.info('transferTransaction:', option.transferTransaction)

    if (name == 'NEM') transferNem(option, callback)
    else transferMosaic(option, callback)
}

/**
 * 转账nem
 */
const transferNem = (option, callback) => {
    /**
     * 签名/打包交易信息
     * common	object	A common object
     * tx	object	A transferTransaction object
     * network	number	A network id
     */
    const transactionEntity = nem.model.transactions.prepare("transferTransaction")(common, option.transferTransaction, config.networkId)
    logger.info('transactionEntity:', transactionEntity)

    /**
     * 计算nem交易手续费
     * 0.05 XEM per 10,000 XEM transferred, capped at 1.25 XEM
     * Example: 0.20 XEM fee for a 45,000 XEM transfer, 1.25 XEM fee for a 500,000 XEM transfer.
     */
    if (option.amount > 500000) return callback(new Error('转账nem不得超过500000'), null)
    let nemFee = Math.floor(option.amount / 10000) * 0.05
    nemFee = nemFee < 0.05 ? 0.05 : nemFee
    nemFee = nemFee > 1.25 ? 1.25 : nemFee

    /**
     * 计算message fee
     * message fee. 0.05 XEM per commenced 32 bytes
     * If the message is empty, the fee will be 0
     * @param {object} message - An message object
     * @param {boolean} isHW - True if hardware wallet, false otherwise
     * @return {number} - The message fee
     */
    let messageFee = nem.model.fees.calculateMessage(transactionEntity.message, false)

    let totalFee = nemFee + messageFee
    totalFee = totalFee * Math.pow(10, 6)
    logger.info('nemFee:', nemFee, 'messageFee', messageFee, 'totalFee', totalFee)
    transactionEntity.fee = totalFee

    /**
     * 发送交易(广播交易)
     * common	object	A common object
     * entity	object	A prepared transaction object
     * endpoint	object	An endpoint object
     */

    // Serialize transfer transaction and announce
    nem.model.transactions.send(common, transactionEntity, endpoint)
        .then(function (res) {
            logger.info("交易详情:", res)
            //callback(null, res)
            if (res && res.message == 'SUCCESS') callback(null, res)
            else callback(new Error(res.message), null)
        })
        .catch(error => {
            logger.error('交易失败:', error)
            callback(error, null)
        });
}

/**
 * 转账mosaic Token
 */
const transferMosaic = async (option, callback) => {

    const mosaicData = config.mosaicDefinitions[option.name]
    if (!mosaicData) return callback(new Error('不支持该币种'), null)
    const {
        namespaceId,
        name: mosaicName
    } = mosaicData.mosaic.id
    // mosaic需转换单位
    let divisibility
    mosaicData.mosaic.properties.forEach(item => {
        if (item.name == 'divisibility') divisibility = item.value
    })
    if (!divisibility) return callback(new Error('divisibility not found'), null)
    //blockchain.server 发送转账请求时已经将进制转换,不需要进行二次转换
    //option.amount *= Math.pow(10, divisibility)

    /**
     * 创建 mosaicDefinitionMetaDataPair 对象
     * Create variable to store our mosaic definitions, needed to calculate fees properly (already contains xem definition)
     * doc: https://nemproject.github.io/#mosaicDefinitionMetaDataPair
     */
    const mosaicDefinitionMetaDataPair = nem.model.objects.get("mosaicDefinitionMetaDataPair");
    logger.info('mosaicDefinitionMetaDataPair', mosaicDefinitionMetaDataPair)

    /**
     * 创建 mosaic 对象(mosaic是nem上的Token,类比Erc20 Token)
     * namespaceId	string	A namespace name
     * mosaicName	string	A mosaic name
     * quantity	long number	A quantity in micro-units(根据divisibility转换为micro-units,npxsxem: 1000000 = 1 )
     * doc: https://nemproject.github.io/#retrieving-mosaic-definitions
     */
    var mosaicAttachment = nem.model.objects.create("mosaicAttachment")(namespaceId, mosaicName, option.amount);
    logger.info('mosaicAttachment', mosaicAttachment)

    // Push attachment into transaction mosaics
    option.transferTransaction.mosaics.push(mosaicAttachment);
    logger.info('transferTransaction', option.transferTransaction)



    // 可通过接口实时获取mosaic属性 nem.com.requests.namespace.mosaicDefinitions(endpoint, mosaicAttachment.mosaicId.namespaceId)
    // 当前mosaic较少,事先通过接口获取后写在配置文件
    mosaicDefinitionMetaDataPair[mosaicData.fullMosaicName] = {};
    mosaicDefinitionMetaDataPair[mosaicData.fullMosaicName].mosaicDefinition = mosaicData.mosaic;

    // nem mosaic 转账bug: supply为空,导致calculateMosaics返回NaN
    // https://github.com/QuantumMechanics/NEM-sdk/issues/36
    // https://qiita.com/xiaca/items/9fa40061cd4977b13147
    let res = await nem.com.requests.mosaic.supply(endpoint, mosaicData.fullMosaicName)
    mosaicDefinitionMetaDataPair[mosaicData.fullMosaicName].supply = res.supply;

    /**
     * 签名/打包交易信息
     * common	object	A common object
     * tx	object	A transferTransaction object
     * mosaicDefinitionMetaDataPair	object	A mosaicDefinitionMetaDataPair object
     * network	number	A network id
     */
    let transactionEntity = nem.model.transactions.prepare("mosaicTransferTransaction")(common, option.transferTransaction, mosaicDefinitionMetaDataPair, config.networkId);
    logger.info('transactionEntity', transactionEntity)

    /**
     * 计算mosaic交易手续费
     * https://nemproject.github.io/#transaction-fees
     * @param {number} multiplier - A quantity multiplier
     * @param {object} mosaics - A mosaicDefinitionMetaDataPair object
     * @param {array} attachedMosaics - An array of mosaics to send
     * @return {number} - The fee amount for the mosaics in the transaction
     */
    let mosaicsFee = nem.model.fees.calculateMosaics(1000000, mosaicDefinitionMetaDataPair, option.transferTransaction.mosaics)

    /**
     * 计算message fee
     * message fee. 0.05 XEM per commenced 32 bytes
     * If the message is empty, the fee will be 0
     * @param {object} message - An message object
     * @param {boolean} isHW - True if hardware wallet, false otherwise
     * @return {number} - The message fee
     */
    let messageFee = nem.model.fees.calculateMessage(transactionEntity.message, false)

    let totalFee = mosaicsFee + messageFee
    totalFee = totalFee * Math.pow(10, 6)
    logger.info('mosaicsFee:', mosaicsFee, 'messageFee', messageFee, 'totalFee', totalFee)
    transactionEntity.fee = totalFee
    // transactionEntity.fee = 1000000
    logger.info('transactionEntity', transactionEntity)

    // Serialize transfer transaction and announce
    nem.model.transactions.send(common, transactionEntity, endpoint)
        .then(function (res) {
            logger.info("交易详情:", res)
            //callback(null, res.transactionHash.data)
            if (res && res.message == 'SUCCESS') callback(null, res.transactionHash.data)
            else callback(new Error(res.message), null)
        })
        .catch(error => {
            logger.error('交易失败:', error)
            callback(error, null)
        });

    /*
    // 通过接口实时获取mosaic信息,有levy属性的需要根据levy增加nem amount
    nem.com.requests.namespace.mosaicDefinitions(endpoint, mosaicAttachment.mosaicId.namespaceId).then(function(res) {

        // Look for the mosaic definition(s) we want in the request response
        var neededDefinition = nem.utils.helpers.searchMosaicDefinitionArray(res.data, ["nem"]);
        console.log('neededDefinition', neededDefinition)
        // Get full name of mosaic to use as object key
        var fullMosaicName  = nem.utils.format.mosaicIdToName(mosaicAttachment.mosaicId);

        // Check if the mosaic was found
        if(undefined === neededDefinition[fullMosaicName]) return console.error("Mosaic not found !");

        // Set eur mosaic definition into mosaicDefinitionMetaDataPair
        mosaicDefinitionMetaDataPair[fullMosaicName] = {};
        mosaicDefinitionMetaDataPair[fullMosaicName].mosaicDefinition = neededDefinition[fullMosaicName];

        // Prepare the transfer transaction object
        var transactionEntity = nem.model.transactions.prepare("mosaicTransferTransaction")(common, option.transferTransaction, mosaicDefinitionMetaDataPair, config.networkId);
        transactionEntity.fee = 1000000
        // Serialize transfer transaction and announce
        nem.model.transactions.send(common, transactionEntity, endpoint)
        .then(function (res) {
            console.log("交易详情:", res)
            callback(null, res)
        })
        .catch(error => {
            console.log('交易失败:', error)
            callback(error, null)
        });
    },
    function(err) {
        console.error(err);
    });
     */

}

/**
 * 获取账户余额
 */
const getNemBalance = (req, callback) => {
    let {
        coin: name,
        address
    } = req.query
    address = address ? address : config.adminAddress
    let namespaceId, mosaicName
    let balance = 0
    if (name == 'NEM') {
        namespaceId = 'nem'
        mosaicName = 'xem'
    } else {
        if (!config.mosaicDefinitions[name]) return callback(new Error('不支持该mosaic'), null)
        let mosaicData = config.mosaicDefinitions[name].mosaic.id
        namespaceId = mosaicData.namespaceId
        mosaicName = mosaicData.name
    }
    // http://192.3.61.243:7890/account/get?address=TCKUVV6PETWYVUBGTWRCLUROJOJVPY4JZXNRSDKT
    // http://192.3.61.243:7890/account/mosaic/owned?address=TCKUVV6PETWYVUBGTWRCLUROJOJVPY4JZXNRSDKT
    nem.com.requests.account.mosaics.owned(endpoint, address).then(result => {
        result.data.forEach(mosaic => {
            if (mosaic.mosaicId.namespaceId == namespaceId && mosaic.mosaicId.name == mosaicName) balance = mosaic.quantity
        })
        logger.info('get mosaics owned', JSON.stringify(result))
        callback(null, String(balance))
    }).catch(error => {
        logger.error('get balance error', error)
        callback(error, null)
    })
}

module.exports = {
    doNemTransaction: doNemTransaction,
    getNemBalance: getNemBalance
}

进行测试


const nem = require('./nem')

// test nem transfer
const testNemTransfer = () => {
    let req = {}
    req.body = {
        name: 'NEM',
        address: 'TCKUVV6PETWYVUBGTWRCLUROJOJVPY4JZXNRSDKT',
        value: 8729700
    }
    nem.doNemTransaction(req, (error, res) => {
        console.log('doNemTransaction nem', error, res)
    })
}
// test mosaic transfer
const testMosaicTransfer = () => {
    let req = {}
    req.body = {
        name: 'NPXSXEM',
        address: 'TCKUVV6PETWYVUBGTWRCLUROJOJVPY4JZXNRSDKT',
        value: 548250000
    }
    nem.doNemTransaction(req, (error, res) => {
        console.log('doNemTransaction mosaic', error, res)
    })
}
// test get balance
const testGetBalance = () => {
    let req = {}
    req.query = {
        /* coin: 'NEM',
        address: 'TCKUVV6PETWYVUBGTWRCLUROJOJVPY4JZXNRSDKT' */
        coin: 'NPXSXEM',
        address: 'TDV75PQWM2RYWVDM6JAFSPNXR44U63B3I4HQOR6A'
    }
    nem.getNemBalance(req, (error, res) => {
        console.log('getNemBalance', error, res)
    })
}


testNemTransfer()
testMosaicTransfer()
testGetBalance()

其它

账户多签也是NEM的一个重要功能。直白说就是,一个钱包由多个人共同管理,只有超多一半的人签名同意后才可以转账,多签可以将多个账户分散存储,加强资产的安全性。多签的实现和转账流程与上述的略有差别,感兴趣的可以自行研究。

NEM的对开发者来说算是比较友好,完善的文档(细节可以更加完善),多语言的SDK。缺点就是基础设施有点差,比如移动端的钱包,比较鸡肋。而且中文社区较少,中文的开发资料非常少。

https通配符证书配置

通配符证书 即一个证书能给ldsun.com、www.ldsun.com、 btc.www.ldsun.com等所有的*.ldsun.com域名使用的证书,四不四很爽。 免费证书 使用 let's encrypted 免费开源https证书,目前已经支持通配符证书申请。 使用脚本 使用国人开发的脚本acme.sh简化配置流程。 DNS解析商 鉴于国内DNS服务商的尿性,选择digitalocean作为DNS解析商,抛弃dnspod,dnspod在设置证书过程中出现各种问题。 digitalocean需要PayPal支付验证。验证后添加域名,添加解析记录。 配置步骤 1、安装acme脚本 curl https://get.acme.sh | sh source ~/.bashrc 2、申请设置 Digitalocean API export DO_API_KEY="

干锅土豆片+尖椒肉丝

虽然国家一直在推行简化各种手续的办理流程,但是距离像网上购物一样便捷的愿望,真的是还差两个西天取经的路程🙄。不吐槽了,开始主题。忙里偷闲的一天,办完手续中午在家自己整点吃的。冰箱打开只有土豆、洋葱、辣椒、肉丝。那就整两个菜吧,如题。其实应该叫家常土豆片?不过放在干锅里就叫干锅土豆片了...。别问我为什么这么喜欢土豆,因为我种过将近十年土豆😂。非常简单实用的两个小菜。材料土豆洋葱辣椒葱、姜、蒜、干辣椒火锅底料生抽、老抽、盐、胡椒粉、鸡精开搞一、干锅土豆片1、准备食材:食材准备,土豆切片洗净,洋葱切片,辣椒切成丝或者快都行。葱姜蒜切好,少许火锅底料/豆瓣酱。2、炒土豆:开火热锅,锅热后倒少许油。稍许油热后倒入少许火锅底料、干辣椒、葱姜蒜,煸香后倒入土豆翻炒,中途可加入少许食盐、胡椒粉。大约翻炒大约2分钟左右,土豆片已经半熟。倒入洋葱继续翻炒,加入老抽上色。油干的时候边炒边加入少许清水,