@custonomy/community-sdk
Version:
Custonomy Lib for Community
485 lines (441 loc) • 17.8 kB
text/typescript
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;
}
});
}
}