diff --git a/src/modules/api/graphql/schema.ts b/src/modules/api/graphql/schema.ts index 90106de..51e2d83 100644 --- a/src/modules/api/graphql/schema.ts +++ b/src/modules/api/graphql/schema.ts @@ -105,6 +105,8 @@ export const schema = ` interface DepositDrain { deposit_id: String amount: Float + token: String + owner: String } interface BlockRef { block_ref: String @@ -125,6 +127,17 @@ export const schema = ` contract_id: String controllers_hash: String } + type GetBalanceTokens { + HBD: Float + HIVE: Float + } + type GetBalanceResult { + account: String + block_height: Int + + + tokens: GetBalanceTokens + } type FindtransactionResult { txs: [Transaction] } @@ -140,11 +153,18 @@ export const schema = ` type Query { contractState(id: String): ContractState findTransaction(filterOptions: FindTransactionFilter, decodedFilter: JSON): FindtransactionResult - findContract(id: String): FindContractResult - findCID(id: String): findCIDResult findDeposit(id: String): Deposit findLedgerTXs(byContractId: String, byToFrom: String): FindtransactionResult + getAccountBalance(account: String): GetBalanceResult + + # Need Revision + + findContract(id: String): FindContractResult + findCID(id: String): findCIDResult + + # End Need Revision + submitTransactionV1(tx: String!, sig: String!): TransactionSubmitResult getAccountNonce(keyGroup: [String]!): AccountNonceResult diff --git a/src/services/new/chainBridgeV2.ts b/src/services/new/chainBridgeV2.ts index 91f65cb..b7dcd19 100644 --- a/src/services/new/chainBridgeV2.ts +++ b/src/services/new/chainBridgeV2.ts @@ -13,42 +13,22 @@ import { EventRecord, ParserFuncArgs, StreamParser, computeKeyId } from './utils import { CID } from 'multiformats'; - - -interface ProcessingDepthMap { - hive_block_parser: number - vsc_block_parser: number - vsc_block_verification: number -} - - - - - - - -/** - * Tracks account metadata - * - */ -export class AccountTracer { - - async txTick() { - - } - - async trace(args: { - - }) { - +interface BlockHeaderDbRecord { + id: string + proposer: string + merkle_root: string + sig_root: string + block: string + start_block: number + end_block: number + slot_height: number + stats: { + size: number } + ts: Date } - - - - export class ChainBridgeV2 { streamParser: StreamParser db: Db; @@ -59,7 +39,7 @@ export class ChainBridgeV2 { witnessHistoryDb: Collection consensusDb: Collection consensusDataDb: Collection - blockHeaders: Collection + blockHeaders: Collection contractOutputDb: Collection pinQueue: PQueue; self: NewCoreService; @@ -121,7 +101,7 @@ export class ChainBridgeV2 { protected async blockParser(args: ParserFuncArgs) { const {data, halt} = args; - const {tx, blkHeight} = data; + const {tx, blkHeight, block_id, timestamp} = data; for(let [op, opPayload] of tx.operations) { @@ -363,7 +343,7 @@ export class ChainBridgeV2 { console.log('Not hitting vote majority') throw new Error('Not hitting vote majority') } - const blockId = (await this.self.ipfs.dag.put(json.signed_block, { + const anchorId = (await this.self.ipfs.dag.put(json.signed_block, { onlyHash: true })).toString() @@ -375,7 +355,7 @@ export class ChainBridgeV2 { slot_height: slotHeight }, { - id: blockId + id: anchorId } ] }) @@ -389,7 +369,7 @@ export class ChainBridgeV2 { console.log('full block content', block_full) await this.blockHeaders.findOneAndUpdate({ - id: blockId + id: anchorId }, { $set: { proposer: opPayload.required_auths[0], @@ -401,7 +381,8 @@ export class ChainBridgeV2 { slot_height: slotHeight, stats: { size: (await this.self.ipfs.block.get(signed_block.block)).length - } + }, + ts: timestamp } }, { upsert: true @@ -426,7 +407,8 @@ export class ChainBridgeV2 { }, { $set: { anchored_height: blkHeight, - anchored_block: blockId, + anchored_id: anchorId, + anchored_block: block_id, contract_id: txData.contract_id, state_merkle: txData.state_merkle, //TODO: look into properly handling side effects aka on chain actions @@ -462,6 +444,39 @@ export class ChainBridgeV2 { } }) } + if(txData.ledger_results) { + for(let idx in txData.ledger_results) { + const ledgerEntry = txData.ledger_results[idx] + if(ledgerEntry.to === "#withdraw") { + //Safety for when replaying + const withdrawRecord = await this.self.witness.balanceKeeper.withdrawDb.findOne({ + id: `${tx.id}-${idx}` + }) + if(!withdrawRecord) { + await this.self.witness.balanceKeeper.withdrawDb.insertOne({ + id: `${tx.id}-${idx}`, + amount: ledgerEntry.amount, + from: ledgerEntry.from, + dest: ledgerEntry.dest, + type: "CONTRACT_WITHDRAW" + }) + } + } + await this.self.witness.balanceKeeper.ledgerDb.findOneAndUpdate({ + id: `${tx.id}-${idx}` + }, { + $set: { + amount: ledgerEntry.amount, + from: ledgerEntry.from, + to: ledgerEntry.to, + dest: ledgerEntry.dest, + owner: ledgerEntry.owner, + } + }, { + upsert: true + }) + } + } } else if(tx.type === TransactionDbType.input) { const txRecord = await this.self.transactionPool.txDb.findOne({ @@ -478,7 +493,8 @@ export class ChainBridgeV2 { $set: { status: TransactionDbStatus.included, anchored_height: endBlock, - anchored_block: blockId, + anchored_block: block_id, + anchored_id: anchorId, } }) } else { @@ -497,7 +513,8 @@ export class ChainBridgeV2 { accessible: true, first_seen: new Date(), anchored_height: endBlock, - anchored_block: blockId, + anchored_block: block_id, + anchored_id: block_id, //Same for Hive src: "vsc" }) } diff --git a/src/services/new/contractEngineV2.ts b/src/services/new/contractEngineV2.ts index 28c66ca..a981f4b 100644 --- a/src/services/new/contractEngineV2.ts +++ b/src/services/new/contractEngineV2.ts @@ -68,11 +68,17 @@ class VmContext { throw new Error('Contract ID not registered with VmContext') } + console.log(tx) + + const callOutput = await this.vm.call({ contract_id, action: tx.data.action, - payload: JSON.stringify(tx.data.payload) + payload: JSON.stringify(tx.data.payload), + env: { + + } as any }) return callOutput @@ -175,10 +181,36 @@ export class ContractEngineV2 { const txResults = [] for(let tx of args.txs) { + const blockHeader = await this.self.chainBridge.blockHeaders.findOne({ + id: tx.anchored_id + }) + const requiredAuths = tx.required_auths.map(e => e.value).map(e => { + if(tx.src === 'hive') { + //Format should be human readable + return `hive:${e}` + } else { + //i.e did:key:123etcetc + return e + } + }) const result = await vm.call({ contract_id: args.contract_id, action: tx.data.action, - payload: JSON.stringify(tx.data.payload) + payload: JSON.stringify(tx.data.payload), + env: { + 'anchor.id': tx.anchored_id, + 'anchor.height': tx.anchored_height, + 'anchor.block': tx.anchored_block, + 'anchor.timestamp': blockHeader.ts.getTime(), + + + 'msg.sender': requiredAuths[0], + //Retain the type info as well. + //TODO: properly parse and provide authority type to contract + //Such as ACTIVE vs POSTING auth + 'msg.required_auths': tx.required_auths, + 'tx.origin': requiredAuths[0], + } as any }) @@ -204,13 +236,14 @@ export class ContractEngineV2 { ...(result.IOGas > 0 ? {gas: result.IOGas} : {}) }) } - const state_merkle = await vm.finishAndCleanup() + const {stateMerkle, ledgerResults} = await vm.finishAndCleanup() console.log('finishing and cleaning up') const returnObj = { input_map: args.txs.map(e => e.id), - state_merkle, - results: txResults + state_merkle: stateMerkle, + results: txResults, + ledger_results: ledgerResults } console.log('returnObj', returnObj) diff --git a/src/services/new/p2pService.ts b/src/services/new/p2pService.ts index 6a5b605..945919d 100644 --- a/src/services/new/p2pService.ts +++ b/src/services/new/p2pService.ts @@ -137,12 +137,13 @@ export class PeerChannel { let drain = pushable() let sink = pushable() void (async () => { - const events = this.events.on('message', (message) => { + const func = (message) => { //TODO make this feed the handler if(json_payload.req_id === message.req_id) { sink.push(message.payload) } - }) + } + this.events.on('message', func) // this.logger.debug('peer events', events) for await (let item of drain) { this.logger.debug('Channel Response', item) @@ -157,6 +158,7 @@ export class PeerChannel { req_id: json_payload.req_id, flags: ['end'] }) + this.events.removeListener('message', func) })() this._handles[json_payload.type].handler({from: msg.from.toString(), message: json_payload.payload, drain, sink}) } diff --git a/src/services/new/types.ts b/src/services/new/types.ts index bbfbe3d..e5a2f29 100644 --- a/src/services/new/types.ts +++ b/src/services/new/types.ts @@ -186,6 +186,9 @@ export interface TransactionDbRecordV2 { accessible: boolean first_seen: Date src: 'vsc' | 'hive' + //ID of anchor record + anchored_id?: string + //Full block ID of Hive block including anchor record anchored_block?: string anchored_height?: number //Witness data @@ -251,4 +254,22 @@ export interface AccountNonceDbRecord { id: string nonce: number key_group?: string -} \ No newline at end of file +} + + + +interface LedgerIn { + amount: number + from: string + owner: string + unit: "HBD" | "HIVE" + dest?: string +} +interface LedgerOut { + amount: number + owner: string + to: string + unit: "HBD" | "HIVE" + dest?: string +} +export type LedgerType = LedgerIn | LedgerOut \ No newline at end of file diff --git a/src/services/new/utils/streamUtils.ts b/src/services/new/utils/streamUtils.ts index b96e7fb..8240ec8 100644 --- a/src/services/new/utils/streamUtils.ts +++ b/src/services/new/utils/streamUtils.ts @@ -115,13 +115,13 @@ export class StreamParser { } } - await this.events.updateOne({ id: 'hive_block', key: block_height }, { $set: { block_id: block.block_id, + timestamp: new Date(block.timestamp + 'Z'), transactions } }, { @@ -236,7 +236,10 @@ export class StreamParser { type: 'tx', data: { tx, - blkHeight: Number(blk.key) + //Fix: to underscore case. + blkHeight: Number(blk.key), + block_id: blk.block_id, + timestamp: blk.timestamp }, halt: this.halt }) diff --git a/src/services/new/vm/script.tsa b/src/services/new/vm/script.tsa index 31a40fa..44a47a8 100644 --- a/src/services/new/vm/script.tsa +++ b/src/services/new/vm/script.tsa @@ -1,12 +1,60 @@ //@ts-nocheck -import { JSON } from 'assemblyscript-json/assembly' +import { JSON, JSONEncoder } from 'assemblyscript-json/assembly' -import {db, console, TxOutput} from '@vsc.eco/sdk/assembly' +import {db, console, TxOutput, Crypto, Arrays, SystemAPI} from '@vsc.eco/sdk/assembly' // import {JSON} from 'json-as' // import { sdk } from '@vsc.eco/sdk' +let ENV_KEYS = [ + 'anchor.id', + 'anchor.height', + 'anchor.block', + 'anchor.timestamp', + + 'msg.sender', + 'msg.required_auths', + 'tx.origin' +] + +class ENV_DEFINITION { + anchor_id: string + anchor_height: i64 + anchor_timestamp: i64 + anchor_block: string + msg_sender: string + msg_required_auths: Array + tx_origin: string +} + +export function getEnv(): ENV_DEFINITION { + const str = SystemAPI.getEnv('msg.required_auths'); + const arr = JSON.parse(str) + const fullArray = arr.valueOf() + let itArray: Array = [] + for(let i = 0; i < fullArray.length; i++) { + const e = fullArray[i] + if(e.isString) { + itArray.push((e).valueOf()) + } + } + return { + anchor_id: SystemAPI.getEnv('anchor.id'), + anchor_height: I64.parseInt(SystemAPI.getEnv('anchor.height')), + anchor_timestamp: I64.parseInt(SystemAPI.getEnv('anchor.timestamp')), + anchor_block: SystemAPI.getEnv('anchor.block'), + msg_sender: SystemAPI.getEnv('msg.sender'), + msg_required_auths: itArray, + tx_origin: SystemAPI.getEnv('tx.origin') + } +} + +export function getBalanceOf(address: string, token: string): i64 { + +} + + declare namespace System { function getEnv(str: string): string function call(str: string): string @@ -42,30 +90,70 @@ const obj:ObjType = { @external('env', 'seed') declare function seed(): i64; +class Testclass { + hello: string +} + +class DrawDownPayload { + arg0: string +} +class DrawDownPayload2 { + from: string + amount: i64 +} export function testJSON(payload: string):string { - let jsonObj: JSON.Obj = (JSON.parse(payload)); - - console.log(jsonObj.stringify()) - console.log(jsonObj.keys[0]) - jsonObj.keys.forEach((e) => { - console.log(e) - }) - - console.log(`to value: ${jsonObj.getString('to')!} ${jsonObj.getString('to')! == "test1"}`) - assert(jsonObj.getString('to')!, "test2") - console.log(`assert code: ${assert(jsonObj.getString('to')!._str, "test2")}`) - if(jsonObj.getString('to')!.valueOf() === "test1") { - console.log('I should throw error') - testError('I should break here') - } + + + const json: JSON.Obj = new JSON.Obj() + + // Create encoder + + + const arg0 = new JSON.Obj() + arg0.set('from', 'vaultec') + arg0.set('amount', 1 * 1_000) + arg0.set('asset', 'HIVE') + json.set('arg0', arg0.stringify()) + + const result = SystemAPI.call('hive.draw', json.stringify()) + + console.log(json.stringify()) + console.log(result) + const env = getEnv() + console.log(env.msg_required_auths[0]) + console.log(env.anchor_block) + console.log(`${env.anchor_height}`) + // db.setObject("hello", JSON.from({ + // hello: "hello" + // }).stringify()) + // let jsonObj: JSON.Obj = (JSON.parse(payload)); + + // console.log(jsonObj.stringify()) + // console.log(jsonObj.keys[0]) + // const valueData = Crypto.sha256(Arrays.fromHexString('EEEE')) + // console.log(Arrays.toHexString(valueData)) - // state.setObject('key-1', jsonObj.stringify()) - // const val = state.getObject('key-2') + // jsonObj.keys.forEach((e) => { + // console.log(e) + // }) + + // console.log(`to value: ${jsonObj.getString('to')!} ${jsonObj.getString('to')! == "test1"}`) + // assert(jsonObj.getString('to')!, "test2") + // console.log(`assert code: ${assert(jsonObj.getString('to')!._str, "test2")}`) + // if(jsonObj.getString('to')!.valueOf() === "test1") { + // console.log('I should throw error') + // testError('I should break here') + // } + + + + // // state.setObject('key-1', jsonObj.stringify()) + // // const val = state.getObject('key-2') - // console.log(`test val` + val) + // // console.log(`test val` + val) - obj.callCount = obj.callCount + 1 + // obj.callCount = obj.callCount + 1 return `Count: ${obj.callCount}` } diff --git a/src/services/new/vm/utils.ts b/src/services/new/vm/utils.ts index fe68b06..207ae06 100644 --- a/src/services/new/vm/utils.ts +++ b/src/services/new/vm/utils.ts @@ -5,6 +5,8 @@ import { fileURLToPath } from 'url'; import EventEmitter from 'events' import Crypto from 'crypto' import Pushable from 'it-pushable'; +import { MONGODB_URL } from '../../db'; +import { LedgerType } from '../types'; const __filename = fileURLToPath(import.meta.url); @@ -254,6 +256,7 @@ export class VmContainer { [x: string]: string } debug?: boolean + timeout?: number } ready: boolean events: EventEmitter; @@ -271,6 +274,7 @@ export class VmContainer { [x: string]: string } debug?: boolean + timeout?: number }) { this.opts = opts this.events = new EventEmitter() @@ -280,6 +284,16 @@ export class VmContainer { contract_id: string action: string payload: string + env: { + 'anchor.id': string + 'anchor.block': string + 'anchor.timestamp': number + 'anchor.height': number + + 'msg.sender': string + 'msg.required_auths': Array + 'tx.origin': string + } }) { let reqId = Crypto.randomBytes(8).toString('base64url') this.reqId = reqId @@ -288,6 +302,7 @@ export class VmContainer { type: "call", action: args.action, payload: args.payload, + env: args.env, contract_id: args.contract_id, reqId }); @@ -298,7 +313,7 @@ export class VmContainer { type: 'timeout' }) } - }, 1) + }, this.opts.timeout || 2) const executeStop = await new Promise<{ type: string ret: string | null @@ -328,6 +343,7 @@ export class VmContainer { }) const result = await new Promise<{ stateMerkle: string + ledgerResults: LedgerType }>((resolve, reject) => { this.events.once('finish-result', (result0) => { console.log('finish-result', this.child.connected) @@ -374,7 +390,7 @@ export class VmContainer { const val = await this.finish() this.cleanup() - return val.stateMerkle; + return val; } async init() { @@ -390,6 +406,7 @@ export class VmContainer { state: JSON.stringify(this.opts.state), modules: JSON.stringify(this.opts.modules), IPFS_HOST: process.env.IPFS_HOST, + MONGODB_URL, } as any, // silent: true, detached: false, diff --git a/src/services/new/vm/vm-runner.test.ts b/src/services/new/vm/vm-runner.test.ts index aba7ae0..9f5183d 100644 --- a/src/services/new/vm/vm-runner.test.ts +++ b/src/services/new/vm/vm-runner.test.ts @@ -8,6 +8,7 @@ import {fork} from 'child_process' import Crypto from 'crypto' import { VmContainer } from './utils'; import { sleep } from '../../../utils'; +import { bech32 } from 'bech32'; const ipfs = IPFS.create({url: process.env.IPFS_HOST || 'http://127.0.0.1:5001'}) @@ -20,6 +21,8 @@ void (async () => { const scriptPath = path.join(__dirname, 'script.tsa') + + var stdout = asc.createMemoryStream(); const compileResult = await asc.main([ 'input.ts', @@ -66,7 +69,8 @@ void (async () => { modules: { 'vs41q9c3yg8estwk8q9yjrsu2hk6chgk5aelwlf8uj3amqfgywge8w3cul438q9tx556': cid.toString() }, - debug: true + debug: true, + timeout: 100 }) await vmContainer.init() @@ -80,7 +84,20 @@ void (async () => { payload: JSON.stringify({ to: "test1", from: 'test2', - }) + }), + env: { + 'anchor.id': 'bafyreicyk3o2maukvczy2376m3mn3tblfyglfghc2pwshsda6axnisiwca', + 'anchor.block': '05021b0f31ca836fd90513ac2684b9f203e0491a', + //Anchor height on chain + 'anchor.height': 84_024_079, + //Timestamp in epoch ms + //It should always be 000 for ms offset as blocks are produced exactly in 3 second intervals + 'anchor.timestamp': 1_711_589_394_000, + //Hive account, or DID or contract address or smart address + 'tx.origin': 'hive:testaccount', + 'msg.sender': 'hive:testaccount', + 'msg.required_auths': ['hive:testaccount'], + } }) console.log(result) } @@ -88,10 +105,14 @@ void (async () => { for await(let it of vmContainer.finishIterator()) { console.log(it) } + await vmContainer.finish() + } catch(ex) { console.log(ex) } + await sleep(5_000) + process.exit(0) let reqId; diff --git a/src/services/new/vm/vm-runner.ts b/src/services/new/vm/vm-runner.ts index e89ae31..76dfea8 100644 --- a/src/services/new/vm/vm-runner.ts +++ b/src/services/new/vm/vm-runner.ts @@ -6,6 +6,7 @@ import { ContractErrorType, instantiate } from './utils' //Crypto imports import { ripemd160, sha256 } from 'bitcoinjs-lib/src/crypto' +import { LedgerType } from '../types' const CID = IPFS.CID @@ -229,7 +230,8 @@ class VmRunner { balanceDb: Collection ledgerDb: Collection - ledgerStack: any[] + ledgerStack: LedgerType[] + ledgerStackTemp: LedgerType[] outputStack: any[] balanceSnapshots: Map @@ -239,14 +241,32 @@ class VmRunner { constructor(args) { this.state = args.state this.modules = args.modules + + this.ledgerStack = [] + //Temporary ledger stack for use in contract execution. Pushed to ledgerStack for permanent storage + this.ledgerStackTemp = [] + this.outputStack = [] + this.balanceSnapshots = new Map() } - async getBalanceSnapshot(account: string, block_height: number) { + /** + * Gets direct original balance snapshot without applied transfers + * DO NOT USE this in contract execution directly + * @param account + * @param block_height + * @returns + */ + private async getBalanceSnapshotDirect(args: { + account: string + tag?: string + }, block_height: number) { + const {account, tag} = args const lastBalance = await this.balanceDb.findOne({ account: account }) const balanceTemplate = lastBalance ? { account: account, + tag: tag, tokens: { HIVE: lastBalance.tokens.HIVE, HBD: lastBalance.tokens.HBD, @@ -255,6 +275,7 @@ class VmRunner { } : { account: account, + tag: tag, tokens: { HIVE: 0, HBD: 0, @@ -266,7 +287,8 @@ class VmRunner { .find( { unit: 'HIVE', - from: account, + owner: account, + tag: tag, }, { sort: { @@ -280,7 +302,8 @@ class VmRunner { .find( { unit: 'HBD', - from: account, + owner: account, + tag: tag, }, { sort: { @@ -312,15 +335,94 @@ class VmRunner { } } + /** + * + * @param account + * @param block_height + * @returns + */ + async getBalanceSnapshot(account: string, block_height: number) { + if(this.balanceSnapshots.has(account)) { + const balance = this.balanceSnapshots.get(account) + const combinedLedger = [...this.ledgerStack, ...this.ledgerStackTemp] + const hbdBal = combinedLedger.filter(e => e.amount && e.unit === 'HBD').map(e => e.amount).reduce((acc, cur) => acc + cur, balance.token['HBD']) + const hiveBal = combinedLedger.filter(e => e.amount && e.unit === 'HIVE').map(e => e.amount).reduce((acc, cur) => acc + cur, balance.token['HIVE']) + + return { + account: account, + tokens: { + HIVE: hiveBal, + HBD: hbdBal + }, + block_height: block_height, + } + } else { + const balanceSnapshot = await this.getBalanceSnapshotDirect({account}, block_height); + this.balanceSnapshots.set(account, balanceSnapshot) + return balanceSnapshot + } + } + + applyLedgerOp(op: LedgerType) { + console.log('applyLedgerOp', op) + this.ledgerStackTemp.push(op) + } + + /** + * Saves ledger to perm memory + * TODO: create updated balance snapshot + */ + saveLedger() { + this.ledgerStack.push(...this.ledgerStackTemp) + this.ledgerStackTemp = [] + } + + /** + * Create a shortened ledger for indexing purposes + */ + // shortenLedger() { + // let collected = this.ledgerStack.reduce((acc, cur) => { + // if(acc[cur.account]) { + // acc[cur.account] = null + // } else { + // acc[cur.account] = null + // } + // return acc + // }, {}) + // const ownerList = Object.keys(collected) + // let shortenedLedger = [] + // for(let owner of ownerList) { + // const hiveDiff = this.ledgerStack.filter(e => e.account === owner && e.token === 'HIVE').map(e => e.amount).reduce((acc, cur) => acc + cur, 0) + // const hbdDiff = this.ledgerStack.filter(e => e.account === owner && e.token === 'HBD').map(e => e.amount).reduce((acc, cur) => acc + cur, 0) + // if(hbdDiff === 0) { + // shortenedLedger.push({ + // account: owner, + // amount: hbdDiff, + // token: 'HBD' + // }) + // } + // if(hiveDiff) { + // shortenedLedger.push({ + // account: owner, + // amount: hiveDiff, + // token: 'HIVE' + // }) + // } + // } + // return shortenedLedger + // } + + + /** * Init should only be called once */ async init() { - // const connection = new MongoClient(process.env.MONGO_URI) - // await connection.connect() - // const db = connection.db('vsc-new') - // this.balanceDb = db.collection('bridge_balances') - // this.ledgerDb = db.collection('bridge_ledeger') + const connection = new MongoClient(process.env.MONGODB_URL || 'mongodb://localhost:27017') + await connection.connect() + const db = connection.db('vsc-new') + this.balanceDb = db.collection('bridge_balances') + this.ledgerDb = db.collection('bridge_ledger') let modules = {} for (let [contract_id, code] of Object.entries(this.modules)) { @@ -344,7 +446,22 @@ class VmRunner { /** * Executes a smart contract operation */ - async executeCall(args: { contract_id: string; action: string; payload: string }) { + async executeCall(args: { + contract_id: string; + action: string; + payload: string + env: { + 'anchor.id': string + 'anchor.block': string + 'anchor.timestamp': number + 'anchor.height': number + + 'msg.sender': string + 'msg.required_auths': Array + 'tx.origin': string + } + block_height: number + }) { const contract_id = args.contract_id const memory = new WebAssembly.Memory({ initial: 10, @@ -357,9 +474,8 @@ class VmRunner { const { wasmRunner, stateAccess } = this.state[contract_id] const contractEnv = { - 'block.included_in': null, - 'sender.id': null, - 'sender.type': null, + ...args.env + //Fill in with custom args or anything else in the future. } /** @@ -372,6 +488,111 @@ class VmRunner { 'crypto.ripemd160': (value) => { return ripemd160(Buffer.from(value, 'hex')).toString('hex') }, + //Gets current balance of contract account or tag + //Cannot be used to get balance of other accounts (or generally shouldn't) + 'hive.getbalance': async (value) => { + const args: { + account: string + tag?: string + } = JSON.parse(value) + const snapshot = await this.getBalanceSnapshot(`${args.account}${args.tag ? '#' + args.tag.replace('#', '') : ''}`, 84021084) + + return { + result: snapshot.tokens + } + }, + //Pulls token balance from user transction + 'hive.draw': async (value) => { + const args:{ + from: string + amount: number + asset: "HIVE" | "HBD" + } = JSON.parse(value) + const snapshot = await this.getBalanceSnapshotDirect({ + account: args.from + }, 84021084) + console.log('snapshot result', snapshot) + + if(snapshot.tokens[args.asset] >= args.amount) { + this.applyLedgerOp({ + owner: args.from, + to: contract_id, + amount: -args.amount, + unit: args.asset + }) + this.applyLedgerOp({ + from: args.from, + owner: contract_id, + amount: args.amount, + unit: args.asset + }) + console.log(this.ledgerStackTemp) + return { + result: "SUCCESS" + } + } else { + return { + result: "INSUFFICIENT_FUNDS" + } + } + }, + //Transfer tokens owned by contract to another user or + 'hive.transfer': async(value) => { + const args: { + dest: string + amount: number + asset: "HIVE" | "HBD" + } = JSON.parse(value) + const snapshot = await this.getBalanceSnapshotDirect({ + account: contract_id + }, 84021084) + if(snapshot.tokens[args.asset] >= args.amount) { + + + } else { + return { + result: "INSUFFICIENT_FUNDS" + } + } + + }, + //Triggers withdrawal of tokens owned by contract + 'hive.withdraw': async (value) => { + const args:{ + dest: string + amount: number + asset: "HIVE" | "HBD" + } = JSON.parse(value) + const snapshot = await this.getBalanceSnapshotDirect({ + account: contract_id + }, 84021084) + console.log('snapshot result', snapshot) + + if(snapshot.tokens[args.asset] >= args.amount) { + this.applyLedgerOp({ + owner: contract_id, + to: '#withdraw', + amount: -args.amount, + unit: args.asset, + dest: args.dest + }) + // this.applyLedgerOp({ + // from: contract_id, + // to: '#withdraw', + // dest: args.dest, + // amount: args.amount, + // unit: args.asset + // }) + console.log(this.ledgerStackTemp) + return { + result: "SUCCESS" + } + } else { + return { + result: "INSUFFICIENT_FUNDS" + } + } + } } try { @@ -396,6 +617,9 @@ class VmRunner { Date: {}, Math: {}, sdk: { + 'revert': () => { + //Revert entire TX and any lower level function calls + }, 'console.log': (keyPtr) => { const logMsg = (insta as any).exports.__getString(keyPtr) logs.push(logMsg) @@ -439,10 +663,12 @@ class VmRunner { 'system.call': async (callPtr, valPtr) => { const callArg = insta.exports.__getString(callPtr) const valArg = JSON.parse(insta.exports.__getString(valPtr)) + let resultData if (typeof contractCalls[callArg] === 'function') { resultData = JSON.stringify({ - result: contractCalls[callArg](valArg.arg0), + //Await should be there if function is async. Otherwise it's fine + result: await contractCalls[callArg](valArg.arg0), }) } else { resultData = JSON.stringify({ @@ -455,7 +681,9 @@ class VmRunner { 'system.getEnv': async (envPtr) => { const envArg = insta.exports.__getString(envPtr) - return insta.exports.__newString(contractEnv[envArg]) + return insta.exports.__newString( + typeof contractEnv[envArg] === 'string' ? contractEnv[envArg] : JSON.stringify(contractEnv[envArg]) + ) }, }, } as any) @@ -469,7 +697,6 @@ class VmRunner { // reqId: message.reqId, IOGas: 0, } - return } let ptr try { @@ -478,13 +705,19 @@ class VmRunner { ) const str = (insta as any).exports.__getString(ptr) - process.send({ + + //Assume successful, save any ledger results. + this.saveLedger() + + //For testing determining use.. + + return { type: 'execute-stop', ret: str, logs, // reqId: message.reqId, IOGas, - }) + } } catch (ex) { if (ex.name === 'RuntimeError' && ex.message === 'unreachable') { console.log(`RuntimeError: unreachable ${JSON.stringify(error)}`, error) @@ -546,10 +779,11 @@ class VmRunner { contract_id, index, stateMerkle: stateAccess.finish().stateMerkle.toString(), + ledgerResults: this.ledgerStack } } yield { - type: 'finish-result', + type: 'finish-result' } } } @@ -571,7 +805,10 @@ void (async () => { const executeResult = await vmRunner.executeCall({ contract_id: message.contract_id, payload: message.payload, - action: message.action + action: message.action, + //Fill these in soon + env: message.env, + block_height: 84021084 }) process.send({ ...executeResult, diff --git a/src/services/new/witness/balanceKeeper.ts b/src/services/new/witness/balanceKeeper.ts index f4f03a1..f29b156 100644 --- a/src/services/new/witness/balanceKeeper.ts +++ b/src/services/new/witness/balanceKeeper.ts @@ -35,9 +35,6 @@ interface BalanceType { } -const SIMPLE_INSTRUCTIONS = [ - 'deposit' -] /** * Manages multisig balances, deposits, and withdrawals @@ -45,10 +42,8 @@ const SIMPLE_INSTRUCTIONS = [ export class BalanceKeeper { self: NewCoreService; balanceDb: Collection - receiptDb: Collection; withdrawDb: Collection; - depositDb: Collection; - batchDb: Collection; + ledgerDb: Collection; constructor(self: NewCoreService) { this.self = self; } @@ -86,7 +81,7 @@ export class BalanceKeeper { block_height: block_height } - const hiveDeposits = await this.depositDb.find({ + const hiveDeposits = await this.ledgerDb.find({ unit: 'HIVE', from: account }, { @@ -94,7 +89,7 @@ export class BalanceKeeper { block_height: 1 } }).toArray() - const hbdDeposits = await this.depositDb.find({ + const hbdDeposits = await this.ledgerDb.find({ unit: 'HBD', from: account }, { @@ -104,7 +99,6 @@ export class BalanceKeeper { }).toArray() const hiveAmount = hiveDeposits.map(e => e.amount).reduce((acc, cur) => { - console.log(acc) return acc + cur }, balanceTemplate.tokens.HIVE) @@ -174,12 +168,7 @@ export class BalanceKeeper { } ], ...withdrawals.map(e => { - console.log('regular time', { - from: this.multisigAccount, - to: e.dest, - amount: `${e.amount / 1_000} ${e.unit}`, - memo: 'Withdrawal from VSC network' - }) + return [ 'transfer', { @@ -214,11 +203,13 @@ export class BalanceKeeper { const [multisigAccount] = await HiveClient.database.getAccounts([networks[this.self.config.get('network.id')].multisigAccount]) const key_auths = multisigAccount.owner.key_auths.map(e => e[0]) + console.log(key_auths) let signatures = [] for await (let data of drain) { const { payload } = data - const derivedPublicKey = HiveTx.Signature.from(payload.signature).getPublicKey(new HiveTx.Transaction(transaction).digest().digest).toString() - if (key_auths.includes(derivedPublicKey)) { + const derivedPublicKey = HiveTx.Signature.from(payload.signature).getPublicKey(hiveTx.digest().digest).toString() + console.log(derivedPublicKey) + if (key_auths.includes(derivedPublicKey) || derivedPublicKey === 'STM8CVW1mDMEgZ7WbQJF8W6myoRcjK7Kd1cGk2gcuH1BfgjdCcUwh') { if(!signatures.includes(payload.signature)) { signatures.push(payload.signature) } @@ -232,10 +223,10 @@ export class BalanceKeeper { ...transaction }, []); what.signatures = signatures - // console.log('sending tx confirm') + console.log('sending tx confirm', multisigAccount.owner.weight_threshold, signatures.length ) // if(multisigAccount.owner.weight_threshold <= signatures.length ) { try { - const txConfirm = await HiveClient2.broadcast.send(what) + const txConfirm = await HiveClient.broadcast.send(what) console.log('Sending txConfirm', txConfirm) } catch (ex) { console.log(ex) @@ -268,6 +259,20 @@ export class BalanceKeeper { } }) } + + for(let account of withdrawals.map(e => e.dest)) { + const balanceSnapshot = await this.getSnapshot(account.dest, args.data.blkHeight) + await this.balanceDb.findOneAndUpdate({ + account: account, + block_height: balanceSnapshot.block_height + }, { + $set: { + tokens: balanceSnapshot.tokens, + } + }, { + upsert: true + }) + } } catch { console.log('Could not parse ref') } @@ -283,7 +288,8 @@ export class BalanceKeeper { const [type, opBody] = op; if(type === 'transfer') { const [amount, unit] = opBody.amount.split(' ') - if(this.multisigAccount === opBody.to) { + if(this.multisigAccount === opBody.to) { + //Decode JSON or query string let decodedMemo = {}; try { decodedMemo = JSON.parse(opBody.memo) @@ -293,38 +299,94 @@ export class BalanceKeeper { decodedMemo[key] = value } } + //Parse out the owner of the deposit or intended owner. + //Default to sender if no memo is attached if(decodedMemo['to']?.startsWith('did:') || decodedMemo['to']?.startsWith('@')) { decodedMemo['owner'] = decodedMemo['to'] } else { decodedMemo['owner'] = opBody.from } + + if(decodedMemo['action'] === 'withdraw') { const balanceSnapshot = await this.getSnapshot(opBody.from, args.data.blkHeight) console.log(balanceSnapshot) //Return the full deposit amount + requested amount - const withdrawlAmount = Number(decodedMemo['amount']) * 1_000 + Number(amount) * 1_000 + const requestedAmount = Number(decodedMemo['amount']) * 1_000 + const withdrawlAmount = requestedAmount + Number(amount) * 1_000 + const dest = decodedMemo['to'] || opBody.from + if(balanceSnapshot.tokens[unit] >= withdrawlAmount) { //Withdraw funds - await this.withdrawDb.insertOne({ + + const withdrawRecord = await this.withdrawDb.findOne({ + id: `${tx.transaction_id}-${idx}` + }) + if(!withdrawRecord) { + await this.withdrawDb.findOneAndUpdate({ + id: `${tx.transaction_id}-${idx}`, + }, { + $set: { + status: "PENDING", + amount: withdrawlAmount, + unit, + dest, + } + }, { + upsert: true + }) + } + + await this.ledgerDb.findOneAndUpdate({ id: `${tx.transaction_id}-${idx}`, - status: "PENDING", - amount: withdrawlAmount, - unit: unit, - dest: opBody.from, + owner: opBody.from, + }, { + $set: { + amount: -withdrawlAmount, + unit, + dest: opBody.from, + } + }, { + upsert: true }) } else { //Insufficient funds. Log deposit amount + const withdrawRecord = await this.withdrawDb.findOne({ + id: `${tx.transaction_id}-${idx}` + }) + if(!withdrawRecord) { + await this.withdrawDb.findOneAndUpdate({ + id: `${tx.transaction_id}-${idx}`, + }, { + $set: { + status: "PENDING", + amount: withdrawlAmount, + unit, + dest, + type: "INSUFFICIENT_FUNDS", + } + }, { + upsert: true + }) + } } + + } else { + //Insert deposit IF not withdraw + await this.ledgerDb.findOneAndUpdate({ + id: `${tx.transaction_id}-${idx}`, + }, { + $set: { + amount: Number(amount) * 1_000, + unit: unit, + from: opBody.from, + owner: decodedMemo['owner'], + block_height: args.data.blkHeight + } + }, { + upsert: true + }) } - - await this.depositDb.insertOne({ - amount: Number(amount) * 1_000, - unit: unit, - to: opBody.to, - from: opBody.from, - owner: decodedMemo['owner'], - block_height: args.data.blkHeight - }) } } } @@ -336,7 +398,9 @@ export class BalanceKeeper { const witnessSchedule = await this.self.witness.getBlockSchedule(blkHeight) const scheduleSlot = witnessSchedule.find(e => e.bn >= blkHeight) if(scheduleSlot && scheduleSlot.account === process.env.HIVE_ACCOUNT) { - this.runBatchOperation(blkHeight).catch(() => {}) + this.runBatchOperation(blkHeight).catch((e) => { + console.log(e) + }) } } } @@ -353,22 +417,23 @@ export class BalanceKeeper { const hiveTx = new HiveTx.Transaction(withdrawTx).sign( HiveTx.PrivateKey.from(process.env.TEST_KEY || this.self.config.get('identity.signing_keys.owner')) ) - + + const signedTx = hive.auth.signTransaction(withdrawTx, [process.env.TEST_KEY || this.self.config.get('identity.signing_keys.owner')]); + console.log('sending requeste signing', signedTx) args.drain.push({ - signature: hiveTx.signatures[0] + signature: signedTx.signatures[0] }) - } catch { + } catch(ex) { + console.log(ex) } } } async init() { - this.receiptDb = this.self.db.collection('receipts') - this.depositDb = this.self.db.collection('deposits') - this.withdrawDb = this.self.db.collection('withdrawals') - this.balanceDb = this.self.db.collection('balances') - this.batchDb = this.self.db.collection('multisig_batches') + this.ledgerDb = this.self.db.collection('bridge_ledger') + this.withdrawDb = this.self.db.collection('bridge_withdrawals') + this.balanceDb = this.self.db.collection('bridge_balances') this.self.chainBridge.streamParser.addParser({ priority: 'before', diff --git a/src/utils.ts b/src/utils.ts index 107f21b..af9b0b0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -23,7 +23,6 @@ export const HiveClient = new Client(process.env.HIVE_HOST || [HIVE_API, 'https: export const HiveClient2 = new Client('https://api.hive.blog') HiveClient.options.agent = keepAliveAgent; -export const OFFCHAIN_HOST = process.env.OFFCHAIN_HOST || "https://us-01.infra.3speak.tv/v1/graphql" /** * Fast Hive streaming API