let axios = require('axios')
let Web3 = require('web3')
let EthSignUtil = require('eth-sig-util')
let ethUtil = require('ethereumjs-util')
let BigNumber = require('bignumber.js')
let Buffer = require('safe-buffer').Buffer
let sendTx = require('./sendTx')
let accountTool = require('./tools/account')
let fs = require('fs')
let appRootPath = require('app-root-path').resolve('');
const DbotJson = require('./contracts/dbot/dbot.json')
const TransferChannelJson = require('./contracts/channel/transferChannel.json')
const MockBlockNumber = 1
const transferChannelAddress = "0x0000000000000000000000000000000000000012";
const defaultDeposit = 10e18
/**
* @class
* @classdesc Atn class contains all the method
*/
class Atn {
/**
* @construct Atn
*
* @param {string} private_key - 账户私钥
* @param {string} rpc_provider - 'https://rpc-test.atnio.net' - 连接节点默认为https://rpc-test.atnio.net
* @param {string} env = 'prod' - 环境变量默认 prod
*/
constructor(private_key, {rpc_provider = 'https://rpc-test.atnio.net', env = 'prod'} = {}) {
this.web3 = new Web3(rpc_provider)
this.account = this.web3.eth.accounts.privateKeyToAccount(private_key)
this.channelContract = new this.web3.eth.Contract(TransferChannelJson.abi, transferChannelAddress)
/**
* @constant {string}
* @default {string} prod
*/
this.PRODUCT_ENV = 'prod'
if (env && typeof env === 'string' && env.toLowerCase() !== this.PRODUCT_ENV) {
this.hyperProtocolType = 'http'
} else {
this.hyperProtocolType = 'https'
}
}
/**
* @method
* @desc 创建账户和DBotServer之间的调用通道
*
* @param {string} receiverAddress - AI Market上DBot地址
* @param {string} deposit - 创建调用通道所需的总次数(1e18的倍数)
* @returns {Promise<PromiEvent<TransactionReceipt>>} - json example :
```json
{
"blockHash": "0x60a779ef28803a01bf825fb0d378e9e40edd9c33ef5d8379274d0fdfe4b1b451",
"blockNumber": 400442,
"contractAddress": null,
"cumulativeGasUsed": 66603,
"from": "0xa474a12b81b443854414c3c3928b0ec44adfde06",
"gasUsed": 66603,
"logs": [
{
"address": "0x0000000000000000000000000000000000000012",
"topics": [
"0xa55ac5ebdb9bee5da90c5d4a6f104e5e2c116f97967ae2eb73f5fdfbdbb75bcb",
"0x000000000000000000000000a474a12b81b443854414c3c3928b0ec44adfde06",
"0x0000000000000000000000000aa602cb3a0919a95cb066b9d3ed99d391c395d5"
],
"data": "0x00000000000000000000000000000000000000000000000029a2241af62c0000",
"blockNumber": 400442,
"transactionHash": "0x54168dfed7d0d1b3e5bac787b76ea490fd0c1b5e80674acdea4e241869462220",
"transactionIndex": 0,
"blockHash": "0x60a779ef28803a01bf825fb0d378e9e40edd9c33ef5d8379274d0fdfe4b1b451",
"logIndex": 0,
"removed": false,
"id": "log_a4ba0d17"
}
],
"logsBloom": "0x00000000000000000000020000000000000000000000000000000000000010000000000000000000000000000000000000020000000000000000000000000400000000088000000000000000000000000000000000100000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000400",
"status": true,
"to": "0x0000000000000000000000000000000000000012",
"transactionHash": "0x54168dfed7d0d1b3e5bac787b76ea490fd0c1b5e80674acdea4e241869462220",
"transactionIndex": 0
}
```
*/
async createChannel(receiverAddress, deposit) {
// Check if a channel is exist
let info = await this.getChannelInfo(receiverAddress)
if (info['deposit'] != 0 || info['blockNumber'] != 0) {
throw new Error('Channel has exist.')
}
return sendTx(this.web3, this.account, this.channelContract, 'createChannel(address)', [receiverAddress], deposit)
}
/**
* @method
* @desc 获取账户和DBotServer之间通道信息
*
* @param {string} receiverAddress - DBot地址
* @returns {Promise<{key: any, deposit: *, blockNumber: *}>}
*/
async getChannelInfo(receiverAddress) {
let key = await this.channelContract.methods.getKey(this.account.address, receiverAddress, MockBlockNumber).call()
let channel = await this.channelContract.methods.channels(key).call()
let info = {'key': key, 'deposit': channel[0], 'blockNumber': channel[1]}
return info
}
/**
* @method
* @descri 查询DBotServer 上存在的AI调用通道信息
*
* @param {string} receiverAddress - DBot地址
* @returns {Promise<*>} - json example :
```json
{
"status": 1,
"data": {
"receiver": "0xE4640e4005903e147EbB54dd9DDf17e85Ce53303",
"sender": "0xA474A12B81B443854414C3C3928B0eC44AdFdE06",
"open_block_number": "353401",
"deposit": "15000000000000000000",
"balance": "12000000000000000000",
"state": "OPEN",
"settle_block_number": "-1",
"confirmed": true,
"unconfirmed_topups": {},
"domain": "https://dbot02.atnio.net"
}
}
```
*/
async getChannelDetail(receiverAddress) {
let dbotAddress = receiverAddress
let domain = await this.getDBotDomain(dbotAddress)
const channelInfoURL = `${this.handlerDBotDomain(domain, this.hyperProtocolType)}/api/v1/dbots/${dbotAddress}/channels/${this.account.address}`
console.log('-----------getChannelDetail-----------', channelInfoURL)
try {
let res = await axios.get(channelInfoURL)
if (res.data.length > 0) {
// channel contract ensure only one channel is allowed.
let channelDetail = res.data[0]
channelDetail.domain = domain
return channelDetail
} else {
return undefined
}
} catch (e) {
console.error(e.name, e.message)
throw new Error('Get channel detail failed.')
}
}
/**
* @method
* @descr 关闭AI调用通道
*
* @param {string} receiverAddress - DBot地址
* @param {string | BigNumber} balance - 关闭调用通道所需的Balance
* @returns {Promise<*>} - json
*/
async closeChannel(receiverAddress, balance) {
let dbotContract
try {
dbotContract = new this.web3.eth.Contract(DbotJson.abi, receiverAddress)
} catch (e) {
return new Error(e.toString())
}
const dbot = {}
const from = this.account.address
dbot.domain = Web3.utils.hexToString(await dbotContract.methods.domain().call({from}))
const channelDetail = await this.getChannelInfo(receiverAddress)
const blockNumber = channelDetail.blockNumber
const targetUrl = this.handlerDBotDomain(dbot.domain, this.hyperProtocolType)
const URL = `${targetUrl}/api/v1/dbots/${receiverAddress}/channels/${from}/${blockNumber}`
let closeSignChannelInfo
try {
closeSignChannelInfo = await axios.delete(URL, {params: {balance: balance}})
} catch (e) {
console.error(e.name, e.message)
return new Error(e.toString())
}
const closeSignature = closeSignChannelInfo.data.close_signature
let balanceSignature = this.signBalanceProof(receiverAddress, balance)
return sendTx(this.web3, this.account, this.channelContract, 'cooperativeClose(address,uint32,uint256,bytes,bytes)',
[receiverAddress, MockBlockNumber, balance, balanceSignature, closeSignature])
}
/**
* @method
* @desc 增加通道调用次数
*
* @param {string} receiverAddress - DBot地址
* @param {string | BigNumber} value - 增加Channel账户余额
* @returns {Promise<*>} - json example :
```json
{
"blockHash": "0xbda32b2468966071cd59e7595b280b3c4eaf3e35125ed9a9ea41b793863a1dd9",
"blockNumber": 559929,
"contractAddress": null,
"cumulativeGasUsed": 32501,
"from": "0xa474a12b81b443854414c3c3928b0ec44adfde06",
"gasUsed": 32501,
"logs": [
{
"address": "0x0000000000000000000000000000000000000012",
"topics": [
"0x19034e235e9fae58965e705631a9e662529152bc990b7db2aca8aeb6389f006f",
"0x000000000000000000000000a474a12b81b443854414c3c3928b0ec44adfde06",
"0x000000000000000000000000e4640e4005903e147ebb54dd9ddf17e85ce53303",
"0x000000000000000000000000000000000000000000000000000000000006be88"
],
"data": "0x00000000000000000000000000000000000000000000000029a2241af62c0000",
"blockNumber": 559929,
"transactionHash": "0x1eff8db75a957a8ce80ac6f33cea0342365ef74d275c55eecde349bb47b837f1",
"transactionIndex": 0,
"blockHash": "0xbda32b2468966071cd59e7595b280b3c4eaf3e35125ed9a9ea41b793863a1dd9",
"logIndex": 0,
"removed": false,
"id": "log_0782a4ac"
}
],
"logsBloom": "0x00000000000000000000020000000000000000000000000000000000000010000000000000000000000100000000000000000000000000000000000000000400000000008000000000000000000000000000000000001000200000000201000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000100000000000000008000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000",
"status": true,
"to": "0x0000000000000000000000000000000000000012",
"transactionHash": "0x1eff8db75a957a8ce80ac6f33cea0342365ef74d275c55eecde349bb47b837f1",
"transactionIndex": 0
}
```
*/
async topUpChannel(receiverAddress, value) {
return sendTx(this.web3, this.account, this.channelContract, 'topUp(address,uint32)', [receiverAddress, MockBlockNumber], value)
}
/**
* @method
* @desc 设置默认账户
*
* @param {string} private_key - 账户私钥
* @returns {Promise<void>}
*/
async setDefaultAccount(private_key) {
let account = await this.web3.eth.accounts.privateKeyToAccount(privateKey)
this.web3.eth.defaultAccount = account.address
}
/**
* @method
* @descri 根据私钥后去创建account信息,如果不传私钥则自动创建
*
* @param {string} [private_key] - 账户私钥
* @returns {Promise<*>} - example json :
```json
{
"address": "0xd73f8f3882CF279FC556eDBd96acFa9143868745",
"privateKey": "01adc971225be058c7031b536375b79115ed58993c86a4ec4288f36fc9eb51b7"
}
```
*/
async initAccount(private_key) {
let account
if (private_key === undefined && private_key == null) {
account = await this.createAccount()
console.log('-----------account 1----------', account.address)
} else {
account = await this.web3.eth.accounts.privateKeyToAccount(private_key)
console.log('-----------account 2----------', account.address)
}
return account
}
/**
* @method
* @desc 生成私钥文件
*
* @param private_key - 账户私钥
* @param dirNameFile - 所在项目根目录下的文件名 eg. '/mypath/key.json'
* @returns {Promise<void>}
*/
async generateKeyFile(private_key, dirNameFile) {
let account
if (private_key === undefined && private_key == null) {
account = await this.createAccount()
console.log('-----------account 1----------', account.address)
} else {
account = await this.web3.eth.accounts.privateKeyToAccount(private_key)
console.log('-----------account 2----------', account.address)
}
let data = JSON.stringify(account)
let outputFileName = appRootPath.concat(dirNameFile)
console.log('------------outputFileName', outputFileName)
console.log('------------generateKeyFile', data)
fs.writeFile(outputFileName, JSON.stringify(data, null, 3), function (err) {
if (err) {
console.log(err);
} else {
console.log("JSON saved to " + outputFileName);
}
});
}
/**
* @method
* @desc 初始化调用通道
*
* @param {string} dirNameFile - 私钥所在项目根路径名称(默认为JSON文件)
* @param {string} dbotAddress - DBot地址
* @param {string} private_key - 账户私钥
* @param {string} defaultTopup - 设置默认增加账户值,不传:10e18
* @returns {Promise<*>}
*/
async initConfig(dirNameFile, dbotAddress, private_key, defaultTopup) {
if (defaultTopup === undefined) {
defaultTopup = defaultDeposit
} else {
let bn = new BigNumber(1e18, 10)
defaultTopup = bn.multipliedBy(defaultTopup).toString()
}
//1. 将私钥转换为账户
let account
if (private_key === undefined && private_key == null) {
account = await this.createAccount()
console.log('-----------account 1----------', account.address)
} else {
account = await this.web3.eth.accounts.privateKeyToAccount(private_key)
console.log('-----------account 2----------', account.address)
}
let data = {
key: account.privateKey
}
let outputFileName = appRootPath.concat(dirNameFile)
console.log('------------outputFileName', outputFileName)
console.log('------------generateKeyFile', data)
fs.writeFile(outputFileName, JSON.stringify(data, null, 3), function (err) {
if (err) {
console.log(err);
} else {
console.log("JSON saved to " + outputFileName);
}
});
//获取当前账户余额
let balanace = await this.web3.eth.getBalance(account.address)
const balanceBN = Web3.utils.toBN(balanace)
const defaultDepositBN = Web3.utils.toBN(defaultTopup)
// lt <
if (balanace === 0 || balanceBN.lt(defaultDepositBN)) {
return {
status: 0,
account: account,
data: this.web3.utils.fromWei(balanace, 'ether'),
msg: "You need get atn, url: https://faucet-test.atnio.net"
}
}
// if channel exits , just topup the channel
let channelDetail = await this.getChannelDetail(dbotAddress)
if (channelDetail) {
const topupResult = await this.topUpChannel(dbotAddress, defaultDepositBN.toString())
return {
status: 1,
account: account,
data: topupResult,
msg: "topupChannel success"
};
}
// 创建通道
let CResult
try {
CResult = await this.createChannel(dbotAddress, balanceBN)
} catch (e) {
return {
status: 0,
msg: "create channel fail"
}
}
return {
status: 1,
account: account,
data: CResult,
msg: "create channel success"
};
}
/**
* @method
* @desc 签名Hash算法
*
* @param {string} hash - 需要签名的hash
* @returns {String}
*/
signMessage(hash) {
let hashNoHex = Buffer.from(hash.slice(2), 'hex')
let privateKey = Buffer.from(this.account.privateKey.slice(2), 'hex')
const rsv = ethUtil.ecsign(hashNoHex, privateKey)
var rsvArray = new Array()
rsvArray.push(ethUtil.toBuffer(rsv.r))
rsvArray.push(ethUtil.toBuffer(rsv.s))
rsvArray.push(ethUtil.toBuffer(rsv.v))
// if (this._chainId > 0) {
// rsv.v += this._chainId * 2 + 8;
// }
const signMsg = ethUtil.bufferToHex(Buffer.concat(rsvArray))
return signMsg
}
/**
* @func
* @desc 获取Balance的签名
*
* @param {string} receiverAddress - DBot地址
* @param {string} balance - 需要签名的Balance
* @returns {String}
*/
signBalanceProof(receiverAddress, balance) {
const data = this.getBalanceProofData(receiverAddress, balance)
const hash = EthSignUtil.typedSignatureHash(data)
return this.signMessage(hash)
}
/**
* @method
* @desc 获取DBot名称
*
* @param {string} dbotAddress - DBot地址
* @returns {Promise<string>} - 返回DBot名称
*/
async getDBotName(dbotAddress) {
let dbotContract = new this.web3.eth.Contract(DbotJson.abi, dbotAddress)
return Web3.utils.hexToString(await dbotContract.methods.name().call())
}
/**
* @method
* @desc 获取DBot域名
*
* @param {string} dbotAddress - DBot地址
* @returns {Promise<string>}
*/
async getDBotDomain(dbotAddress) {
let dbotContract = new this.web3.eth.Contract(DbotJson.abi, dbotAddress)
return Web3.utils.hexToString(await dbotContract.methods.domain().call())
}
/**
* @method
* @desc 获取DBot下的API的价格
*
* @param {string} dbotAddress - DBot地址
* @param {string} uri - DBot下的指定API相对路径
* @param {string} method - DBot下的API请求方法类型
* @returns {Promise<void>}
*/
async getPrice(dbotAddress, uri, method) {
let dbotContract = new this.web3.eth.Contract(DbotJson.abi, dbotAddress)
let key = await dbotContract.methods.getKey(Web3.utils.stringToHex(method), Web3.utils.stringToHex(uri)).call()
let endpoint = await dbotContract.methods.keyToEndPoints(key).call()
return endpoint.price
}
/**
* @method
* @descri 调用DBotServer AI服务
*
* @param {string} dbotAddress - DBot地址
* @param {string} uri - DBot下的指定API相对路径
* @param {string} method - DBot下的API请求方法类型
* @param {object} option - option
* @returns {Promise<*|void>}
*/
async callDBotAI(dbotAddress, uri, method, option) {
let channelDetail = await this.getChannelDetail(dbotAddress)
if (channelDetail == undefined) {
throw new Error('Channel is not synced by dbot server.')
}
let price = await this.getPrice(dbotAddress, uri, method)
let domain = channelDetail['domain']
let remainBalance = new BigNumber(channelDetail['balance'])
let balance = remainBalance.plus(new BigNumber(price)).toString()
let blockNumber = channelDetail['open_block_number']
return this.callAPI(dbotAddress, domain, uri, method, option, balance, blockNumber)
}
/**
* @func
* @desc callAPI
*
* @param {string} dbotAddress - DBot地址
* @param {string} domain - DBot域名
* @param {string} uri - EndPoint相对路径
* @param {string} method - EndPoint请求类型
* @param {object} option - 调用指定DBot需要的请求配置
* @param {string | number} balance - 要签名的Balance
* @param {string} blockNumber - 创建channel所在的块号
* @returns {Promise<void>}
*/
async callAPI(dbotAddress, domain, uri, method, option, balance, blockNumber) {
option.url = `${this.handlerDBotDomain(domain, this.hyperProtocolType)}/call/${dbotAddress}${uri}`
option.method = method
if (option.headers == undefined || option.headers == null) {
option.headers = {}
}
let balanceSignature = this.signBalanceProof(dbotAddress, balance)
option.headers.RDN_balance = balance
option.headers.RDN_balance_signature = balanceSignature
option.headers.RDN_sender_address = this.account.address
option.headers.RDN_receiver_address = dbotAddress
option.headers.RDN_open_block = blockNumber
try {
return await axios(option)
} catch (e) {
console.error(e.response.data)
throw e
}
}
/**
* @func
* @desc 轮询查询交易信息,设置超时时间和确认块数
*
* @param {string} txHash - 交易hash值
* @param {number} timeout - 每个块交易确认时间
* @param {number} confirmations - 要确认的交易的块数
*
* @returns {Promise<{status: string, msg: string}> | Promise<TransactionReceipt>} - json example :
```json
{
status:0,
msg:"timeout"
}
```
```json
{
"blockHash": "0x60a779ef28803a01bf825fb0d378e9e40edd9c33ef5d8379274d0fdfe4b1b451",
"blockNumber": 400442,
"contractAddress": null,
"cumulativeGasUsed": 66603,
"from": "0xa474a12b81b443854414c3c3928b0ec44adfde06",
"gasUsed": 66603,
"logs": [
{
"address": "0x0000000000000000000000000000000000000012",
"topics": [
"0xa55ac5ebdb9bee5da90c5d4a6f104e5e2c116f97967ae2eb73f5fdfbdbb75bcb",
"0x000000000000000000000000a474a12b81b443854414c3c3928b0ec44adfde06",
"0x0000000000000000000000000aa602cb3a0919a95cb066b9d3ed99d391c395d5"
],
"data": "0x00000000000000000000000000000000000000000000000029a2241af62c0000",
"blockNumber": 400442,
"transactionHash": "0x54168dfed7d0d1b3e5bac787b76ea490fd0c1b5e80674acdea4e241869462220",
"transactionIndex": 0,
"blockHash": "0x60a779ef28803a01bf825fb0d378e9e40edd9c33ef5d8379274d0fdfe4b1b451",
"logIndex": 0,
"removed": false,
"id": "log_a4ba0d17"
}
],
"logsBloom": "0x00000000000000000000020000000000000000000000000000000000000010000000000000000000000000000000000000020000000000000000000000000400000000088000000000000000000000000000000000100000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000400",
"status": true,
"to": "0x0000000000000000000000000000000000000012",
"transactionHash": "0x54168dfed7d0d1b3e5bac787b76ea490fd0c1b5e80674acdea4e241869462220",
"transactionIndex": 0
}
```
*/
async waitTx(txHash, timeout, confirmations) {
console.log('-----------------hash------------', txHash)
const blockStart = await this.web3.eth.getBlockNumber()
const startTime = new Date().getTime()
const endTime = startTime + timeout * (confirmations + 1)
obj.startTime = startTime
do {
await this.asyncSleep(1e3)
if (typeof obj === 'object') {
const flag = obj.flag
if (!flag) break
}
const [receipt, block] = await Promise.call([
await this.web3.eth.getTransactionReceipt(txHash),
await this.web3.eth.getBlockNumber()
])
if (!receipt || !receipt.blockNumber) {
console.log('Waiting tx..', block - blockStart)
} else if (block - receipt.blockNumber < confirmations) {
console.log('Waiting confirmations...', block - receipt.blockNumber)
} else {
return receipt
}
if (endTime < new Date().getTime()) {
break
}
} while (true)
return {status: "0", msg: "timeout"}
}
/**
* @method
* @desc 通过私钥获取账户信息
*
* @param {string} private_key - 账户私钥
* @returns {Promise<Account>}
*/
async privateKeyToAccount(private_key) {
return this.web3.eth.accounts.privateKeyToAccount(private_key);
}
/**
* @method unlockAccountsIfNeeded
* @descri 解锁单个账户
*
* @param account
* @param password
* @param unlock_duration_sec
* @returns {Promise<void>}
*/
async unlockAccountsIfNeeded(account, password, unlock_duration_sec) {
accountTool.unlockSingleAccountIfNeeded(account, password, unlock_duration_sec)
}
/**
* @method
* @desc 创建账户
*
* @returns {Promise<Account>}
*/
async createAccount() {
return this.web3.eth.accounts.create();
}
/**
* @method
* @descri 从DBotServer服务获取Balance签名
*
* @param {string} receiverAddress - DBot地址
* @param {string | BigNumber} balance - 需要签名的Balance
* @returns {Promise<*>}
*/
async requestCloseSignature(receiverAddress, balance) {
let detail = await this.getChannelDetail(receiverAddress)
const blockNumber = detail.open_block_number
const from = this.account.address
const URL = `${this.handlerDBotDomain(detail.domain, this.hyperProtocolType)}/api/v1/dbots/${receiverAddress}/channels/${from}/${blockNumber}`.toString()
console.log('-----------getChannelDetail-----------', URL)
let resp
try {
resp = await axios.delete(URL, {params: {balance: balance}})
return resp.data.close_signature;
} catch (e) {
throw new Error('Get close signature error')
}
}
/**
* @method
* @descri DBotServer的domain适配
*
* @param {string} domain - DBot域名
* @param hyperProtocolType - 默认协议类型http或https
* @returns {string} - 返回适配之后的URL前缀
*/
handlerDBotDomain(domain, hyperProtocolType) {
const result = domain.toLowerCase().startsWith('http') ? domain : hyperProtocolType.concat('://').concat(domain)
return result
}
/**
* @func
* @desc 设置当前现成休眠时间
*
* @param {number} timeout - 设置等待时间
* @returns {Promise<*>}
*/
async asyncSleep(timeout) {
return new Promise(resolve => setTimeout(resolve, timeout))
}
/**
* @func
* @desc 获取Balance签名Scheme
*
* @param {string} receiverAddress - DBot地址
* @param {string | BigNumber } balance - 要签名的Balance
* @returns {object[]} - 返回Scheme数组
*/
getBalanceProofData(receiverAddress, balance) {
return [
{
type: 'string',
name: 'message_id',
value: 'Sender balance proof signature'
},
{
type: 'address',
name: 'receiver',
value: receiverAddress
},
{
type: 'uint256',
name: 'balance',
value: balance.toString()
},
{
type: 'address',
name: 'contract',
value: transferChannelAddress
}
]
}
}
module.exports = Atn