-
Notifications
You must be signed in to change notification settings - Fork 148
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(contract): smart contract supported #287
- Loading branch information
eagleHovering
committed
Oct 17, 2018
1 parent
18719ec
commit ab27463
Showing
5 changed files
with
397 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) | ||
}, | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | ||
] | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | ||
] | ||
} | ||
|
Oops, something went wrong.