UNPKG

@custonomy/community-sdk

Version:

Custonomy Lib for Community

485 lines (441 loc) 17.8 kB
import { ACTION } from "./consts"; import { fetchURL } from "./common"; import { EventEmitter } from "events"; import { generate } from 'hmac-auth-express'; import {v4 as uuid} from 'uuid'; import { CHAIN, CommunityUser, Network, Option, RequestResponse, SignData, Transaction } from "./types"; import { isNativeEventSourceAvailable } from "./utils"; // const {} = require('event-source-polyfill'); const EventSourcePolyfill = require('event-source-polyfill').EventSourcePolyfill; // // global.EventSource = ESPolyfill.NativeEventSource || ESPolyfill.EventSourcePolyfill; export default class Community extends EventEmitter { private _endPoint: string; private _apiKey: string; private _apiSecret?: string; private MAX_COUNT: number = 60; private _eventDrivenMode: boolean; private _es: EventSource | typeof EventSourcePolyfill | null = null; private _esRequests: string[] = []; private _esRequestShowHex: Map<string, boolean> = new Map(); constructor(option: Option) { super(); this._endPoint = option.endPoint; this._apiKey = option.apiKey; this._apiSecret = option.apiSecret; this._eventDrivenMode = option.eventDrivenMode ?? false; } private _callApi(path: string, params: any, action: string): Promise<any> { let option = { headers: { Accept: "application/json", "Content-Type": "application/json", }, }; Object.assign(option.headers, { "x-api-key": `${this._apiKey}` }); if (params) { Object.assign(option, { body: JSON.stringify(params) }); } Object.assign(option, { method: action }); return fetchURL(path, option); } private _callHMACApi(path: string, request_body: any, action: string): Promise<any> { let option = {}; if (this._apiSecret == null) throw new Error ('apiSecret is required'); let _path = path.startsWith('/') ? path : '/' + path; _path = _path.indexOf('?') < 0 ? `${_path}?requestId=${uuid()}` : (_path.endsWith('&') ? `${_path}requestId=${uuid()}` : `${path}&requestId=${uuid()}`); const time = Date.now().toString(); const digest = generate(this._apiSecret, 'sha512', time, action, _path, request_body ?? {}).digest('hex'); const hmac = `HMAC ${time}:${digest}`; // the request header should be like this Object.assign(option, { headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'api-key': this._apiKey, 'Authorization': hmac, }, 'method': action }) if (request_body && Object.keys(request_body).length > 0) Object.assign(option, { body: JSON.stringify(request_body) }); return fetchURL(this._endPoint + _path, option); } public signalPull(requestId: string): void { this.emit(`ready-to-pull-${requestId}`); } public setEventDrivenMode(mode: boolean): void { this._eventDrivenMode = mode; } public async registerEventListener (projectId: string, session: string): Promise<void> { if(this._eventDrivenMode) { var _es: any; if(this._es == null) { if(isNativeEventSourceAvailable()) { _es = new EventSource(this._endPoint + `/v0/community2/events?session=${session}&projectId=${projectId}`); } else { _es = new EventSourcePolyfill(this._endPoint + `/v0/community2/events?session=${session}&projectId=${projectId}`); } if (_es) { _es.onmessage = (e: any) => { try { console.log("EventSource message:", e.data); let requestDone = true; let data = JSON.parse(e.data); let showHex = this._esRequestShowHex.get(data.id); if (["completed"].includes(data.txnStatus)) { this.emit("completed", data.id, showHex ? data.hex : data.txnId); } else if (["sent"].includes(data.txnStatus)) { this.emit("sent", data.id, showHex ? data.hex : data.txnId); } else if (["error", "sent_error"].includes(data.txnStatus)) { this.emit("error", data.id, data.errorReason ?? data.txnErrorReason); } else if (data.status === "cancelled") { this.emit("cancelled", data.id); } else if (["warning", "error"].includes(data.status)) { this.emit("error", data.id, data.errorReason ?? data.txnErrorReason); } else if (data.address) { this.emit("address_generated", data.id, data.address); } else if (data.newAddress) { this.emit("new_address_generated", data.id, data.newAddress); } else { requestDone = false; } //remove request id from _esRequests and if(requestDone) { this._esRequests = this._esRequests.filter((id) => id !== data.id); this._esRequestShowHex.delete(data.id); } // determine if all the requests are responded if (this._esRequests.length === 0) { // console.log("Closing EventSource") _es.close(); this._es = null; } } catch (ex) { console.error(ex); } }; this._es = _es; } } } } private _pull(requestId: string, session: string, showHex: boolean = false): void { if(this._eventDrivenMode) { // this.once(`ready-to-pull-${requestId}`, () => { // this._pullRequest(requestId, session, showHex); // }); this._esRequests.push(requestId); this._esRequestShowHex.set(requestId, showHex); var _es: any; if(this._es == null ) { if(isNativeEventSourceAvailable()) { _es = new EventSource(this._endPoint + `/v0/community2/events?session=${session}&requestId=${requestId}`); } else { _es = new EventSourcePolyfill(this._endPoint + `/v0/community2/events?session=${session}&requestId=${requestId}`); } if (_es) { _es.onmessage = (e: any) => { try { console.log("EventSource message:", e.data); let requestDone = true; let data = JSON.parse(e.data); if (["completed"].includes(data.txnStatus)) { this.emit("completed", data.id, showHex ? data.hex : data.txnId); } else if (["sent"].includes(data.txnStatus)) { this.emit("sent", data.id, showHex ? data.hex : data.txnId); } else if (["error", "sent_error"].includes(data.txnStatus)) { this.emit("error", data.id, data.errorReason ?? data.txnErrorReason); } else if (data.status === "cancelled") { this.emit("cancelled", data.id); } else if (["warning", "error"].includes(data.status)) { this.emit("error", data.id, data.errorReason ?? data.txnErrorReason); } else if (data.address) { this.emit("address_generated", data.id, data.address); } else if (data.newAddress) { this.emit("new_address_generated", data.id, data.newAddress); } else { requestDone = false; } //remove request id from _esRequests and if(requestDone) { this._esRequests = this._esRequests.filter((id) => id !== data.id); this._esRequestShowHex.delete(data.id); } // determine if all the requests are responded if (this._esRequests.length === 0) { // console.log("Closing EventSource") _es.close(); this._es = null; } } catch (ex) { console.error(ex); } }; this._es = _es; } } } else { this._pullRequest(requestId, session, showHex); } } private _pullRequest(requestId: string, session: string, showHex: boolean): void { let count = 0; const interval = setInterval(() => { if (count >= this.MAX_COUNT) { clearInterval(interval); } count++; this._callApi(this._endPoint + `/v0/community2/status_pull/requests/${requestId}?session=${session}`, null, ACTION.GET) .then((res2) => { if (["completed"].includes(res2.body.txnStatus)) { this.emit("completed", res2.body.id, showHex ? res2.body.hex : res2.body.txnId); clearInterval(interval); } else if (["error", "sent_error"].includes(res2.body.txnStatus)) { this.emit("error", res2.body.id, res2.body.errorReason ?? res2.body.txnErrorReason); clearInterval(interval); } else if (res2.body.status === "cancelled") { this.emit("cancelled", res2.body.id); clearInterval(interval); } else if (["warning", "error"].includes(res2.body.status)) { this.emit("error", res2.body.id, res2.body.errorReason ?? res2.body.txnErrorReason); clearInterval(interval); } else if (res2.body.address) { this.emit("address_generated", res2.body.id, res2.body.address); clearInterval(interval); } else if (res2.body.newAddress) { this.emit("new_address_generated", res2.body.id, res2.body.newAddress); clearInterval(interval); } }) .catch((ex) => { console.error(ex); }); }, 3000); } /** * Create Community User in Custonomy Community * @param projectId - The UUID of the project * @param session - The external user id of the project site * @param chain - either ETH or SOL * @returns The status and callback url for user pin input */ createUser(projectId: string, session: string, chain: CHAIN = CHAIN.ETH): Promise<RequestResponse> { return this._callApi( this._endPoint + "/v0/community2/requests", { projectId: projectId, session: session, type: "createaccount", params: {chain: chain} }, ACTION.POST ).then((response: any) => { let count = 0; const { body, status } = response; if (status != 200) { throw new Error(JSON.stringify({ ...body, status })); } else { //TODO: temparay fake the procdess this._pull(body.id, session); return { id: body.id, status: body.status, callbackURL: body.callbackURL, } as RequestResponse; } }); } /** * Get User Address * @param projectId - The UUID of the project * @param session - The external user id * @param chain - either ETH or SOL * @returns */ getUserInfo(projectId: string, session: string, chain?: CHAIN): Promise<CommunityUser | null> { return this._callApi(this._endPoint + `/v0/community2/users?projectId=${projectId}&session=${session}${chain ? `&chain=${chain}` : ''}`, null, ACTION.GET).then((response: any) => { if (response?.body?.error != null) throw new Error(JSON.stringify(response.body)); if (response == null) return null; let user = response.body as CommunityUser; return user; }); } /** * Get User Address * @param projectId - The UUID of the project * @param session - The external user id * @returns */ getAddress(projectId: string, session: string, chain?: CHAIN): Promise<string | null> { return this._callApi(this._endPoint + `/v0/community2/users?projectId=${projectId}&session=${session}${chain ? `&chain=${chain}` : ''}`, null, ACTION.GET).then((response: any) => { if (response?.body?.error != null) throw new Error(JSON.stringify(response.body)); if (response == null) return null; let user = response.body as CommunityUser; return user.addresses?.length > 0 ? user.addresses[0]?.address : null; }); } /** * Get User Address * @param projectId - The UUID of the project * @param session - The external user id * @returns */ getAddressWithPubkey(projectId: string, session: string, chain?: CHAIN): Promise<{address: string, pubkey: string} | null> { return this._callApi(this._endPoint + `/v0/community2/users?projectId=${projectId}&session=${session}${chain ? `&chain=${chain}` : ''}`, null, ACTION.GET).then((response: any) => { if (response?.body?.error != null) throw new Error(JSON.stringify(response.body)); if (response == null) return null; let user = response.body as CommunityUser; return user.addresses?.length > 0 ? {address: user.addresses[0]?.address, pubkey: user.addresses[0]?.pubkey} : null; }); } /** * Submit transaction to Custonomy Community * @param projectId - The UUID of the project * @param externalUserId - The external user id of the project site for signing * @param transaction - The transaction detail that rqeuired to be signed by Custonomy Community * @returns The status and callback url for user pin input */ submitTransaction(projectId: string, session: string, transaction: Transaction, showHex: boolean = false): Promise<RequestResponse> { return this._callApi( this._endPoint + "/v0/community2/requests", { projectId: projectId, session: session, type: "createtxn", params: transaction, }, ACTION.POST ) .then((response: any) => { const { body, status } = response; let count = 0; if (status != 200) { throw new Error(JSON.stringify({ ...body, status })); } else { //TODO: temparay fake the procdess this._pull(body.id, session, showHex); return { id: body.id, status: body.status, callbackURL: body.callbackURL, } as RequestResponse; } }) } public async signTransaction(projectId: string, session: string, data: Transaction | SignData) { let txn: Transaction = data as Transaction; if (txn.gas) txn.gasLimit = txn.gas; let obj: Transaction | SignData = txn; return this._callApi( this._endPoint + "/v0/community2/requests", { projectId: projectId, session: session, type: "createtxn", params: { ...obj, method: obj.method ?? "eth_signTransaction" }, }, ACTION.POST ).then((response: any) => { // console.log({response}) const { body, status } = response; let count = 0; if (status != 200) { throw new Error(JSON.stringify({ ...body, status })); } else { //TODO: temparay fake the procdess this._pull(body.id, session, true); return { id: body.id, status: body.status, callbackURL: body.callbackURL, } as RequestResponse; } }); } public async signData(projectId: string, session: string, data: SignData) { return this.signTransaction(projectId, session, { type: 9999, chainId: data.chainId, from: data.from, method: data.method, data: data.data, sourceImage: data.sourceImage, sourceName: data.sourceName, sourceUrl: data.sourceUrl, } as SignData); } public async listNetworks(projectId: string, session: string) { return this._callApi(this._endPoint + `/v0/community2/networks?projectId=${projectId}&session=${session}`, null, ACTION.GET).then((response: any) => { if (response?.body?.error != null) throw new Error(JSON.stringify(response.body)); if (response == null) return null; let networks = response.body as Network[]; return networks?.length > 0 ? networks : []; }); } public async submitGenPasskeyOptRequest(projectId: string, session: string) { return this._callApi( this._endPoint + "/v0/community2/requests", { projectId: projectId, session: session, type: "genpasskeyopt", }, ACTION.POST ) .then((response: any) => { const { body, status } = response; if (status != 200) { throw new Error(JSON.stringify({ ...body, status })); } else { return { id: body.id, status: body.status, callbackURL: body.callbackURL, } as RequestResponse; } }) .catch((ex) => { console.error(ex); throw ex; }); } /** * Submit transaction to Custonomy Community using HMAC * @param projectId - The UUID of the project * @param externalUserId - The external user id of the project site for signing * @param transaction - The transaction detail that rqeuired to be signed by Custonomy Community * @returns The status and callback url for user pin input */ submitAPITransaction(projectId: string, session: string, data: Transaction | SignData) { return this._callHMACApi( "/v0/community2/apirequests", { projectId: projectId, session: session, type: "createtxn", params: {type: 9999, ...data }, }, ACTION.POST ).then((response: any) => { // console.log({response}) const { body, status } = response; let count = 0; if (status != 200) { throw new Error(JSON.stringify({ ...body, status })); } else { //TODO: temparay fake the procdess this._pull(body.id, session, data.type == 9999 || data.method == 'eth_signTransaction' ); return { id: body.id, status: body.status, } as RequestResponse; } }); } }