Skip to content

Commit

Permalink
feat(contract): smart contract supported #287
Browse files Browse the repository at this point in the history
  • Loading branch information
eagleHovering committed Oct 17, 2018
1 parent 18719ec commit ab27463
Show file tree
Hide file tree
Showing 5 changed files with 397 additions and 0 deletions.
229 changes: 229 additions & 0 deletions src/contract/contract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
const CONTRACT_ID_SEQUENCE = 'contract_sequence'
const CONTRACT_TRANSFER_ID_SEQUENCE = 'contract_transfer_sequence'
const GAS_CURRENCY = 'BCH'
const XAS_CURRENCY = 'XAS'
const CONTRACT_MODEL = 'Contract'
const CONTRACT_RESULT_MODEL = 'ContractResult'
const ACCOUNT_MODEL = 'Account'
const CONTRACT_TRANSFER_MODEL = 'ContractTransfer'
const GAS_BUY_BACK_ADDRESS = 'ARepurchaseAddr1234567890123456789'
const PAY_METHOD = 'onPay'
const MAX_GAS_LIMIT = 10000000 // 0.1BCH

function require(condition, error) {
if (!condition) throw Error(error)
}

function makeContractAddress(transId, ownerAddress) {
return app.util.address.generateContractAddress(`${transId}_${ownerAddress}`)
}

function makeContext(senderAddress, transaction, block) {
return { senderAddress, transaction, block }
}

async function ensureBCHEnough(address, amount, gasOnly) {
const bchAvalible = app.balances.get(address, GAS_CURRENCY)
if (!gasOnly) {
require(bchAvalible.gte(amount), `Avalible BCH( ${bchAvalible} ) is less than required( ${amount} ) `)
} else {
require(bchAvalible.gte(amount), `Avalible gas( ${bchAvalible} ) is less than gas limit( ${amount} ) `)
}
}

function ensureContractNameValid(name) {
require(name && name.length >= 3 && name.length <= 32, 'Invalid contract name, length should be between 3 and 32 ')
require(name.match(/^[a-zA-Z]([-_a-zA-Z0-9]{3,32})+$/), 'Invalid contract name, please use letter, number or underscore ')
}

function ensureGasLimitValid(gasLimit) {
require(gasLimit > 0 && gasLimit <= MAX_GAS_LIMIT, `gas limit must greater than 0 and less than ${MAX_GAS_LIMIT}`)
}

function createContractTransfer(senderId, recipientId, currency, amount, trans, height) {
app.sdb.create(CONTRACT_TRANSFER_MODEL, {
id: Number(app.autoID.increment(CONTRACT_TRANSFER_ID_SEQUENCE)),
tid: trans.id,
height,
senderId,
recipientId,
currency,
amount: String(amount),
timestamp: trans.timestamp,
})
}

async function transfer(currency, transferAmount, senderId, recipientId, trans, height) {
const bigAmount = app.util.bignumber(transferAmount)
if (currency !== XAS_CURRENCY) {
const balance = app.balances.get(senderId, currency)
require(balance !== undefined && balance.gte(bigAmount), 'Insuffient balance')

app.balances.transfer(currency, bigAmount, senderId, recipientId)
createContractTransfer(senderId, recipientId, currency, bigAmount.toString(), trans, height)
return
}

const amount = Number.parseInt(bigAmount.toString(), 10)
const senderAccount = await app.sdb.load(ACCOUNT_MODEL, { address: senderId })
require(senderAccount !== undefined, 'Sender account not found')
require(senderAccount.xas >= amount, 'Insuffient balance')

app.sdb.increase(ACCOUNT_MODEL, { xas: -amount }, { address: senderId })
recipientAccount = await app.sdb.load(ACCOUNT_MODEL, { address: recipientId })
if (recipientAccount !== undefined) {
app.sdb.increase(ACCOUNT_MODEL, { xas: amount }, { address: recipientId })
} else {
recipientAccount = app.sdb.create(ACCOUNT_MODEL, {
address: recipientId,
xas: amount,
name: null,
})
}
createContractTransfer(senderId, recipientId, currency, amount, trans, height)
}


async function handleContractResult(senderId, contractId, contractAddr, callResult, trans, height) {
const {
success, error, gas, stateChangesHash,
} = callResult

app.sdb.create(CONTRACT_RESULT_MODEL, {
tid: trans.id,
contractId,
success: success ? 1 : 0,
error,
gas,
stateChangesHash,
})

if (callResult.gas && callResult.gas > 0) {
await transfer(GAS_CURRENCY, callResult.gas, senderId, GAS_BUY_BACK_ADDRESS, trans, height)
}

if (callResult.transfers && callResult.transfers.length > 0) {
for (const t of callResult.transfers) {
const bigAmount = app.util.bignumber(t.amount)
await transfer(t.currency, bigAmount, contractAddr, t.recipientId, trans, height)
}
}
}

/**
* Asch smart contract service code. All functions return transaction id by asch-core ,
* you can get result by api/v2/contracts/?action=getResult&tid={transactionId}
*/
module.exports = {
/**
* Register contract,
* @param {number} gasLimit max gas avalible, 1000000 >= gasLimit >0
* @param {string} name 32 >= name.length > 3 and name must be letter, number or _
* @param {string} version contract engine version
* @param {string} desc desc.length <= 255
* @param {string} code hex encoded source code
*/
async register(gasLimit, name, version, desc, code) {
ensureGasLimitValid(gasLimit)
ensureContractNameValid(name)
require(!desc || desc.length <= 255, 'Invalid description, can not be longer than 255')
require(!version || version.length <= 32, 'Invalid version, can not be longer than 32 ')

await ensureBCHEnough(this.sender.address, gasLimit, true)
const contract = await app.sdb.load(CONTRACT_MODEL, { name })
require(contract === undefined, `Contract '${name}' exists already`)

const contractId = Number(app.autoID.increment(CONTRACT_ID_SEQUENCE))
const context = makeContext(this.sender.address, this.trs, this.block)
const decodedCode = Buffer.from(code, 'hex').toString('utf8')
const registerResult = await app.contract.registerContract(
gasLimit, context,
contractId, name, decodedCode,
)
const contractAddress = makeContractAddress(this.trs.id, this.sender.address)
handleContractResult(
this.sender.address, contractId, contractAddress, registerResult,
this.trs, this.block.height,
)

if (registerResult.success) {
app.sdb.create(CONTRACT_MODEL, {
id: contractId,
tid: this.trs.id,
name,
owner: this.sender.address,
address: contractAddress,
vmVersion: version,
desc,
code,
metadata: registerResult.metadata,
timestamp: this.trs.timestamp,
})
}
},

/**
* Call method of a registered contract
* @param {number} gasLimit max gas avalible, 1000000 >= gasLimit >0
* @param {string} name contract name
* @param {string} method method name of contract
* @param {Array} args method arguments
*/
async call(gasLimit, name, method, args) {
ensureGasLimitValid(gasLimit)
ensureContractNameValid(name)
require(method !== undefined && method !== null, 'method name can not be null or undefined')
require(Array.isArray(args), 'Invalid contract args, should be array')

const contractInfo = await app.sdb.get(CONTRACT_MODEL, { name })
require(contractInfo !== undefined, `Contract '${name}' not found`)
await ensureBCHEnough(this.sender.address, gasLimit, true)

const context = makeContext(this.sender.address, this.trs, this.block)
const callResult = await app.contract.callContract(gasLimit, context, name, method, ...args)

handleContractResult(
this.sender.address, contractInfo.id, contractInfo.address, callResult,
this.trs, this.block.height,
)
},

/**
* Pay money to contract, behavior dependents on contract code.
* @param {number} gasLimit max gas avalible, 1000000 >= gasLimit >0
* @param {string} nameOrAddress contract name or address
* @param {string|number} amount pay amout
* @param {string} currency currency
*/
async pay(gasLimit, nameOrAddress, amount, currency) {
ensureGasLimitValid(gasLimit)
const bigAmount = app.util.bignumber(amount)
require(bigAmount.gt(0), 'Invalid amount, should be greater than 0 ')

const condition = app.util.address.isContractAddress(nameOrAddress) ?
{ address: nameOrAddress } : { name: nameOrAddress }

const contractInfo = await app.sdb.load(CONTRACT_MODEL, condition)
require(contractInfo !== undefined, `Contract name/address '${nameOrAddress}' not found`)

const isBCH = (currency === GAS_CURRENCY)
const miniAmount = app.util.bignumber(gasLimit).plus(isBCH ? bigAmount : 0)
await ensureBCHEnough(this.sender.address, miniAmount, isBCH)

await transfer(
currency, bigAmount, this.sender.address, contractInfo.address,
this.trs, this.block.height,
)

const context = makeContext(this.sender.address, this.trs, this.block)
const payResult = await app.contract.callContract(
gasLimit, context, contractInfo.name,
PAY_METHOD, bigAmount.toString(), currency,
)
handleContractResult(
this.sender.address, contractInfo.id, contractInfo.address, payResult,
this.trs, this.block.height,
)
},
}

126 changes: 126 additions & 0 deletions src/interface/contracts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
const assert = require('assert')

const CONTRACT_MODEL = 'Contract'
const CONTRACT_BASIC_FIELDS = ['id', 'name', 'tid', 'address', 'owner', 'vmVersion', 'desc', 'timestamp']
const CONTRACT_RESULT_MODEL = 'ContractResult'

function parseSort(orderBy) {
const sort = {}
const [orderField, sortOrder] = orderBy.split(':')
if (orderField !== undefined && sortOrder !== undefined) {
sort[orderField] = sortOrder.toUpperCase()
}
return sort
}

function makeCondition(params) {
const result = {}
Object.keys(params).forEach((k) => {
if (params[k] !== undefined) result[k] = params[k]
})
return result
}

/**
* Query contract call result
* @param tid ?action=getResult&tid='xxxx'
* @returns query result { result : { tid, contractId, success, gas, error, stateChangesHash } }
*/
async function handleGetResult(req) {
const tid = req.query.tid
assert(tid !== undefined && tid !== null, 'Invalid param \'tid\', can not be null or undefined')
const results = await app.sdb.find(CONTRACT_RESULT_MODEL, { tid })
if (results.length === 0) {
throw new Error(`Result not found (tid = '${tid}')`)
}
const ret = results[0]
return {
result: {
success: ret.success > 0,
gas: ret.gas || 0,
error: ret.error || '',
stateChangesHash: ret.stateChangesHash || '',
},
}
}

async function handleActionRequest(req) {
const action = req.query.action
if (action === 'getResult') {
const result = await handleGetResult(req)
return result
}
// other actions ...
throw new Error(`Invalid action, ${action}`)
}


module.exports = (router) => {
/**
* Query contracts
* @param condition owner, address, name, orderBy = id:ASC, limit = 20, offset = 0,
* orderBy = (timestamp | id | owner):(ASC|DESC)
* @returns query result { count,
* contracts : [ { id, name, tid, address, owner, vmVersion, desc, timestamp } ] }
*/
router.get('/', async (req) => {
if (req.query.action) {
const result = await handleActionRequest(req)
return result
}

const offset = req.query.offset ? Math.max(0, Number(req.query.offset)) : 0
const limit = req.query.limit ? Math.min(100, Number(req.query.limit)) : 20
const orderBy = req.query.orderBy ? req.query.orderBy : 'id:ASC'

const sortOrder = parseSort(orderBy)
const { name, owner, address } = req.query
const condition = makeCondition({ name, owner, address })
const fields = CONTRACT_BASIC_FIELDS

const count = await app.sdb.count(CONTRACT_MODEL, condition)
const range = { limit, offset }
const contracts = await app.sdb.find(CONTRACT_MODEL, condition, range, sortOrder, fields)

return { count, contracts }
})


/**
* Get contract details
* @param name name of contract
* @returns contract detail { contract : { id, name, tid, address, owner, vmVersion,
* desc, timestamp, metadata } }
*/
router.get('/:name', async (req) => {
const name = req.params.name
const contracts = await app.sdb.find(CONTRACT_MODEL, { name })
if (!contracts || contracts.length === 0) throw new Error('Not found')
return { contract: contracts[0] }
})

/**
* Get state of contract
* @param name name of contract
* @param stateName name of mapping state
* @param key key of mapping state
* @returns state value
*/
router.get('/:name/states/:stateName/:key', async (req) => {
const { name, stateName, key } = req.params
const state = await app.contract.queryState(name, stateName, key)
return { state }
})

/**
* Get state of contract
* @param name name of contract
* @param stateName state name
* @returns state value
*/
router.get('/:name/states/:stateName', async (req) => {
const { name, stateName } = req.params
const state = await app.contract.queryState(name, stateName)
return { state }
})
}
12 changes: 12 additions & 0 deletions src/model/contract-result.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = {
table: 'contract_results',
tableFields: [
{ name: 'tid', type: 'String', length: 64, primary_key: true },
{ name: 'contractId', type: 'Number', not_null: true, index: true },
{ name: 'success', type: 'Number', not_null: true },
{ name: 'error', type: 'String', length: 128 },
{ name: 'gas', type: 'Number' },
{ name: 'stateChangesHash', type: 'String', length: 64 }
]
}

14 changes: 14 additions & 0 deletions src/model/contract-transfer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = {
table: 'contract_transfers',
tableFields: [
{ name: 'id', type: 'Number', not_null: true, primary_key: true },
{ name: 'tid', type: 'String', length: 64, not_null: true, index: true },
{ name: 'height', type: 'Number', not_null: true, index: true },
{ name: 'senderId', type: 'String', length: 50, not_null: true, index: true },
{ name: 'recipientId', type: 'String', length: 50, not_null: true, index: true },
{ name: 'currency', type: 'String', length: 30, not_null: true, index: true },
{ name: 'amount', type: 'String', length: 50, not_null: true },
{ name: 'timestamp', type: 'Number', index: true }
]
}

Loading

0 comments on commit ab27463

Please sign in to comment.