diff --git a/README.md b/README.md index 8373db3..55ca756 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,12 @@ You should login to ainize with AI Network account before deploy the container. ainize.login(); ``` +You can also login using [AIN Wallet](https://chromewebstore.google.com/detail/ain-wallet/hbdheoebpgogdkagfojahleegjfkhkpl) on the web. +```typescript +ainize.loginWithSigner(); +``` +This feature is supported from AIN Wallet version 2.0.5 or later. + ### Deploy You can deploy your AI service to ainize. ```typescript diff --git a/package.json b/package.json index 60f1021..e4064d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ainize-team/ainize-js", - "version": "1.0.6", + "version": "1.1.0", "main": "dist/ainize.js", "types": "dist/ainize.d.ts", "scripts": { @@ -42,7 +42,7 @@ "typescript": "^4.6.3" }, "dependencies": { - "@ainblockchain/ain-js": "^1.3.5", + "@ainblockchain/ain-js": "^1.6.3", "axios": "^0.26.1", "express": "^4.18.2", "fast-json-stable-stringify": "^2.1.0", diff --git a/src/ain.ts b/src/ain.ts index c271369..8b6c3fc 100644 --- a/src/ain.ts +++ b/src/ain.ts @@ -2,6 +2,8 @@ import Ain from "@ainblockchain/ain-js"; import { getBlockChainEndpoint } from "./constants"; import { TransactionBody } from "@ainblockchain/ain-util"; import { txResult } from "./types/type"; +import { Signer } from "@ainblockchain/ain-js/lib/signer/signer"; +import { DefaultSigner } from "@ainblockchain/ain-js/lib/signer/default-signer" // NOTE(yoojin): Plz suggest a good name. export default class AinModule { @@ -20,12 +22,6 @@ export default class AinModule { this.ain = new Ain(blockchainEndpoint, chainId); } - isDefaultAccountExist(): boolean { - if (this.getDefaultAccount()) - return true; - return false; - } - createAccount() { this.checkAinInitiated(); const newAccount = this.ain!.wallet.create(1)[0]; @@ -39,24 +35,57 @@ export default class AinModule { this.ain!.wallet.addAndSetDefaultAccount(privateKey); } + setSigner(signer: Signer) { + this.checkAinInitiated(); + this.ain!.setSigner(signer); + } + getDefaultAccount() { this.checkAinInitiated(); return this.ain!.wallet.defaultAccount; } + getSigner() { + this.checkAinInitiated(); + return this.ain!.signer + } + + async getAddress() { + this.checkAinInitiated(); + try { + return this.getSigner().getAddress(); + } catch (e) { + throw new Error("Need to set up an account or signer first."); + } + } + + isAccountSetUp() { + try { + this.checkAinInitiated(); + this.getSigner().getAddress(); + return true; + } catch (e) { + return false; + } + } + removeDefaultAccount() { this.checkAinInitiated(); this.ain!.wallet.removeDefaultAccount(); } - getAddress() { - this.isDefaultAccountExist(); - return this.ain!.wallet.defaultAccount!.address; + removeSigner() { + this.checkAinInitiated(); + const wallet = this.ain!.wallet; + const provider = this.ain!.provider; + wallet.removeDefaultAccount(); + this.ain!.setSigner(new DefaultSigner(wallet, provider)) } async getBalance() { - this.isDefaultAccountExist(); - return await this.ain!.wallet.getBalance(); + const address = await this.getAddress(); + const balancePath = `/accounts/${address}/balance`; + return await this.ain!.db.ref(balancePath).getValue(); } async getValue(path: string) { @@ -64,14 +93,14 @@ export default class AinModule { return await this.ain!.db.ref(path).getValue(); } - private async _sendTransaction(data: TransactionBody) { + private async _sendTransaction(txBody: TransactionBody) { this.checkAinInitiated(); - return await this.ain!.sendTransaction(data); + return await this.ain!.signer.sendTransaction(txBody); } private checkAinInitiated(): boolean { if (!this.ain) - throw new Error('Set initAin(chainId) First.'); + throw new Error('Set initAin(chainId) first.'); return true; } @@ -92,6 +121,13 @@ export default class AinModule { private handleTxResultWrapper(operation: Function) { return async (args: any) => { const res = await operation(args); + // ainWalletSigner return txHash or undefined. + if (typeof res === 'string') { + return res; + } else if (res === undefined) { + throw new Error(`Failed to build transaction.`); + } + // defaultSigner return a result object of transactions. const { tx_hash, result } = res; if (this.hasFailedOpResultList(result)) { throw new Error( diff --git a/src/ainize.ts b/src/ainize.ts index 4bfc00f..1749b60 100644 --- a/src/ainize.ts +++ b/src/ainize.ts @@ -8,6 +8,7 @@ import { deployConfig } from "./types/type"; import AinModule from "./ain"; import Internal from "./internal"; import { Account } from "@ainblockchain/ain-util"; +import { AinWalletSigner } from "@ainblockchain/ain-js/lib/signer/ain-wallet-signer"; export default class Ainize { private cache: NodeCache; @@ -39,14 +40,23 @@ export default class Ainize { async login(privateKey: string) { this.ain.setDefaultAccount(privateKey); await this.handler.connect(); - console.log('login success! address:', this.ain.getAddress()); + console.log('login success! address:', await this.ain.getAddress()); + } + + /** + * Login to ainize using AIN Wallet Signer. + */ + async loginWithSigner() { + const signer = new AinWalletSigner; + this.ain.setSigner(signer); + console.log('login success! address: ', await this.ain.getAddress()); } /** * Logout from ainize. */ async logout() { - this.ain.removeDefaultAccount(); + this.ain.removeSigner(); await this.handler.disconnect(); console.log('logout success!'); } @@ -56,7 +66,7 @@ export default class Ainize { } async getAinBalance(): Promise { - return await this.ain.getBalance(); + return await this.ain.getBalance() || 0; } // FIXME(yoojin): add config type and change param type. @@ -67,12 +77,9 @@ export default class Ainize { */ // TODO(yoojin, woojae): Deploy container, advanced. async deploy({serviceName, billingConfig, serviceUrl}: deployConfig): Promise { - if(!this.ain.isDefaultAccountExist()) { - throw new Error('you should login first'); - } // TODO(yoojin, woojae): Add container deploy logic. const result = await new Promise(async (resolve, reject) => { - const deployer = this.ain.getAddress(); + const deployer = await this.ain.getAddress(); if (!billingConfig) { billingConfig = { ...DEFAULT_BILLING_CONFIG, @@ -83,6 +90,7 @@ export default class Ainize { if (!serviceUrl) { serviceUrl = `https://${serviceName}.ainetwork.xyz`; } + serviceUrl = serviceUrl.replace(/\/$/, ''); const servicePath = Path.app(serviceName).status(); await this.handler.subscribe(servicePath, resolve); await this.appController.createApp({ appName: serviceName, serviceUrl, billingConfig }); diff --git a/src/controllers/appController.ts b/src/controllers/appController.ts index 21899f6..09b1114 100644 --- a/src/controllers/appController.ts +++ b/src/controllers/appController.ts @@ -124,7 +124,7 @@ export default class AppController { } async checkCostAndBalance(appName: string, value: string) { - const requesterAddress = this.ain.getAddress(); + const requesterAddress = await this.ain.getAddress(); const billingConfig = (await this.getBillingConfig(appName)); const token = value.split(' ').length; let cost = token * billingConfig.costPerToken; diff --git a/src/controllers/serviceController.ts b/src/controllers/serviceController.ts index 7f636b1..c08889d 100644 --- a/src/controllers/serviceController.ts +++ b/src/controllers/serviceController.ts @@ -48,9 +48,9 @@ export default class ServiceController { } async chargeCredit(serviceName: string, amount: number): Promise { - this.checkRunning(serviceName); + await this.checkRunning(serviceName); const transferKey = Date.now(); - const userAddress = this.ain.getAddress(); + const userAddress = await this.ain.getAddress(); const depositAddress = await this.getDepositAddress(serviceName); const op_list: SetOperation[] = [ getTransferOp(userAddress, depositAddress, transferKey.toString(), amount), @@ -66,13 +66,13 @@ export default class ServiceController { } async getCreditBalance(serviceName: string): Promise { - const userAddress = this.ain.getAddress(); + const userAddress = await this.ain.getAddress(); const balancePath = Path.app(serviceName).balanceOfUser(userAddress); return await this.ain.getValue(balancePath) as number | 0; } async getCreditHistory(serviceName: string): Promise { - const userAddress = this.ain.getAddress(); + const userAddress = await this.ain.getAddress(); const creditHistoryPath = Path.app(serviceName).historyOfUser(userAddress); return await this.ain.getValue(creditHistoryPath) as creditHistories; } @@ -82,7 +82,7 @@ export default class ServiceController { const result = await new Promise(async (resolve, reject) => { requestKey = requestKey || Date.now().toString(); try { - const requesterAddress = this.ain.getAddress(); + const requesterAddress = await this.ain.getAddress(); const responsePath = Path.app(serviceName).response(requesterAddress, requestKey.toString()); await this.handler.subscribe(responsePath, resolve); const requestPath = Path.app(serviceName).request(requesterAddress, requestKey); @@ -120,17 +120,18 @@ export default class ServiceController { return (await this.ain.getValue(Path.app(serviceName).billingConfig())).depositAddress; } - isLoggedIn(): void { - if(!this.ain.getDefaultAccount()) - throw new Error('You should login First.'); + checkLoggedIn(): void { + if (!this.ain.isAccountSetUp()) { + throw new Error('You should login first.'); + } } async isAdmin(serviceName: string): Promise { - this.isLoggedIn(); + this.checkLoggedIn(); const adminPath = `/manage_app/${serviceName}/config/admin`; const adminList = await this.ain.getValue(adminPath); - if(!adminList[this.ain.getAddress()]) { - throw new Error('You are not admin'); + if(!adminList[(await this.ain.getAddress())]) { + throw new Error('You are not a service admin.'); } } } \ No newline at end of file diff --git a/src/handlers/handler.ts b/src/handlers/handler.ts index d82c7c5..230bdf5 100644 --- a/src/handlers/handler.ts +++ b/src/handlers/handler.ts @@ -32,7 +32,7 @@ export default class Handler { } private async disconnectedCb() { - if(AinModule.getInstance().isDefaultAccountExist()) { + if(await AinModule.getInstance().getAddress()) { console.log('disconnected. reconnecting...'); await this.connect(); } @@ -49,7 +49,7 @@ export default class Handler { }, (valueChangedEvent: any) => { this.unsubscribe(subscribeId); - resolve(valueChangedEvent.values.after.data); + resolve(valueChangedEvent.values.after); }, (err) => { throw new Error(err.message); diff --git a/src/internal.ts b/src/internal.ts index cd96ea0..57b676e 100644 --- a/src/internal.ts +++ b/src/internal.ts @@ -4,6 +4,7 @@ import { getChangeBalanceOp, getResponseOp, getWriteHistoryOp } from "./utils/op import { HISTORY_TYPE, RESPONSE_STATUS, deposit, request, response } from "./types/type"; import { buildTxBody } from "./utils/builder"; import AinModule from "./ain"; +import { extractDataFromDepositRequest, extractDataFromServiceRequest } from "./utils/extractor"; export default class Internal { private ain = AinModule.getInstance(); @@ -35,28 +36,10 @@ export default class Internal { } getDataFromServiceRequest(req: Request) { - if(!req.body.valuePath[1] || !req.body.valuePath[3] || !req.body.valuePath[4] || !req.body.value) { - throw new Error("Not from service request"); - } - const requestData: request = { - appName: req.body.valuePath[1], - requesterAddress: req.body.auth.addr, - requestKey: req.body.valuePath[4], - requestData: req.body.value, - } - return requestData; + return extractDataFromServiceRequest(req); } private getDataFromDepositRequest(req: Request) { - if(!req.body.valuePath[1] || !req.body.valuePath[4] || !req.body.value) { - throw new Error("Not from deposit request"); - } - const depositData: deposit = { - transferKey: req.body.valuePath[4], - transferValue: req.body.value, - appName: req.body.valuePath[1], - requesterAddress: req.body.auth.addr, - } - return depositData; + return extractDataFromDepositRequest(req); } } \ No newline at end of file diff --git a/src/middlewares/middleware.ts b/src/middlewares/middleware.ts index e619353..1b36249 100644 --- a/src/middlewares/middleware.ts +++ b/src/middlewares/middleware.ts @@ -1,51 +1,43 @@ import { Request, Response, NextFunction } from "express"; import NodeCache = require("node-cache"); +import { extractTriggerDataFromRequest } from "../utils/extractor"; +import AinModule from "../ain"; +import _ from "lodash"; export default class Middleware { cache: NodeCache; - constructor(cache: NodeCache,) { + private ain = AinModule.getInstance(); + constructor(cache: NodeCache) { this.cache = cache; } /** * Middleware for AI Network trigger call. It will filter duplicated request triggered by same transaction. - * It will pass request which is not from AI Network trigger. + * It will reject requests which is not from AI Network trigger. * @param {Request} request - Request data * @param {Res} response - Response data * @param {NextFunction} next - Next function * @returns Null if if request is duplicated. */ - triggerDuplicateFilter = (req: Request, res: Response, next: NextFunction) => { - if (req.body.transaction.hash === undefined){ - next(); - } - const txHash = req.body.transaction.hash; - if (this.cache.get(txHash) && this.cache.get(txHash) !== "error") { - res.send("duplicated"); + blockchainTriggerFilter = async (req: Request, res: Response, next: NextFunction) => { + //check if request is from blockchain trigger + const { triggerPath, triggerValue, txHash } = extractTriggerDataFromRequest(req); + if(!triggerPath || !triggerValue || !txHash) { + res.send("Not from blockchain"); return; } - // if request is first request, set cache - this.cache.set(txHash, "in_progress", 500); - next(); - } - /** - * Middleware for AI Network trigger call. It will filter duplicated request triggered by same transaction. - * It will pass request which is not from AI Network trigger. - * You can set filter inside specific api. - * @param {Request} request - Request data - * @param {Res} response - Response data - * @returns Null if if request is duplicated. - */ - triggerFilter = (req: Request, res: Response) => { - if (req.body.fid || req.body.transaction){ - res.send("not from trigger"); - return; - } - const txHash = req.body.transaction.hash; + const result = await this.ain.getValue(triggerPath); + // if request is first reque st, set cache if (this.cache.get(txHash) && this.cache.get(txHash) !== "error") { - res.send("duplicated"); + res.send("Duplicated"); return; } this.cache.set(txHash, "in_progress", 500); + _.isEqual(result, triggerValue) ? next(): res.send("Not from blockchain"); } + /** + * DEPRECATED + * use blockchainTriggerFilter instead + */ + triggerDuplicateFilter = this.blockchainTriggerFilter; } \ No newline at end of file diff --git a/src/service.ts b/src/service.ts index 5f94786..8e7524c 100644 --- a/src/service.ts +++ b/src/service.ts @@ -42,7 +42,7 @@ export default class Service { * @returns {string} Transaction hash. */ async chargeCredit(amount: number) { - this.isLoggedIn(); + this.checkLoggedIn(); return await this.serviceController.chargeCredit(this.serviceName, amount); } @@ -52,7 +52,7 @@ export default class Service { * @returns {string} Transaction hash. */ async withdrawCredit(amount: number) { - this.isLoggedIn(); + this.checkLoggedIn(); return await this.serviceController.withdrawCredit(this.serviceName, amount); } @@ -61,7 +61,7 @@ export default class Service { * @returns {number} Amount of credit balance. */ async getCreditBalance() { - this.isLoggedIn(); + this.checkLoggedIn(); return await this.serviceController.getCreditBalance(this.serviceName); } @@ -70,7 +70,7 @@ export default class Service { * @returns {creditHistories} Histories of credit deposit and usage. */ async getCreditHistory() { - this.isLoggedIn(); + this.checkLoggedIn(); return await this.serviceController.getCreditHistory(this.serviceName); } @@ -80,7 +80,7 @@ export default class Service { * @returns {string} Response data from service. */ async request(requestData: any, requestKey?: string) { - this.isLoggedIn(); + this.checkLoggedIn(); return await this.serviceController.request(this.serviceName, requestData, requestKey); } @@ -113,7 +113,7 @@ export default class Service { return this.serviceController.isAdmin(this.serviceName); } - private isLoggedIn() { - return this.serviceController.isLoggedIn(); + private checkLoggedIn() { + return this.serviceController.checkLoggedIn(); } } diff --git a/src/utils/extractor.ts b/src/utils/extractor.ts new file mode 100644 index 0000000..87c7686 --- /dev/null +++ b/src/utils/extractor.ts @@ -0,0 +1,45 @@ +import { Request } from "express"; +import { deposit, request } from "../types/type"; + +export const extractDataFromServiceRequest = (req:Request) => { + if(!req.body.valuePath[1] || !req.body.valuePath[3] || !req.body.valuePath[4] || !req.body.value) { + throw new Error("Not from service request"); + } + const requestData: request = { + appName: req.body.valuePath[1], + requesterAddress: req.body.auth.addr, + requestKey: req.body.valuePath[4], + requestData: req.body.value, + } + return requestData; +} + +export const extractTriggerDataFromRequest = (req:Request) => { + if(req?.body?.valuePath === undefined) { + throw new Error("Not from trigger request"); + } + let path = ''; + req.body.valuePath.forEach((value:string) => { + path += value + '/'; + } + ); + const triggerData = { + triggerPath: path, + triggerValue: req.body.value, + txHash: req.body.transaction.hash, + } + return triggerData +} + +export const extractDataFromDepositRequest = (req:Request) => { + if(!req.body.valuePath[1] || !req.body.valuePath[4] || !req.body.value) { + throw new Error("Not from deposit request"); + } + const depositData: deposit = { + transferKey: req.body.valuePath[4], + transferValue: req.body.value, + appName: req.body.valuePath[1], + requesterAddress: req.body.auth.addr, + } + return depositData; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 414e102..f3d2629 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@ainblockchain/ain-js@^1.3.5": - version "1.6.0" - resolved "https://registry.npmjs.org/@ainblockchain/ain-js/-/ain-js-1.6.0.tgz" - integrity sha512-REzTJAf8w2TIsJLH7DhKWJF+kxfgMnCCwzWaeD4rYAv4TeD70PhFmYDrDMuy/qZd5KKMXqMigiU9PLWbiu8a7A== +"@ainblockchain/ain-js@^1.6.3": + version "1.6.3" + resolved "https://registry.yarnpkg.com/@ainblockchain/ain-js/-/ain-js-1.6.3.tgz#56ca744a6bf5e558f2acba75f106e8f88f5426ba" + integrity sha512-rdQfT6jcqcF4VP1twwMQkCijZ6SN1RewTjU1D35rJ7ZnRQjoIxekkodkdcIDVvyUEpR6A6iuT9SSSTz9KUMNbA== dependencies: "@ainblockchain/ain-util" "^1.1.9" "@types/node" "^12.7.3"