@akashicpay/sdk
Version:
SDK to interact with the Akashic ecosystem
507 lines • 25.8 kB
JavaScript
"use strict";
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var _AkashicPay_otk;
Object.defineProperty(exports, "__esModule", { value: true });
exports.AkashicPay = void 0;
const activecrypto_1 = require("@activeledger/activecrypto");
const crypto_1 = require("crypto");
const winston_1 = require("winston");
const akashicChain_1 = require("./akashicChain");
const APACTypes_1 = require("./APACTypes");
const constants_1 = require("./constants");
const error_1 = require("./error");
const FetchHttpClient_1 = require("./FetchHttpClient");
const l1Network_1 = require("./l1Network");
const reconstruct_1 = require("./otk-reconstruction/reconstruct");
const types_1 = require("./types");
const async_1 = require("./utils/async");
const currency_1 = require("./utils/currency");
const http_1 = require("./utils/http");
const prefix_1 = require("./utils/prefix");
class AkashicPay {
static async build(args) {
const { environment, targetNode, apiSecret } = args, initialisationArgs = __rest(args, ["environment", "targetNode", "apiSecret"]);
const ap = new AkashicPay({
environment,
targetNode,
apiSecret,
});
await ap.init(initialisationArgs);
return ap;
}
async payout(recipientId, to, amount, network, token) {
let toAddress = to;
let initiatedToNonL2 = undefined;
let isL2 = false;
const decimalAmount = (0, currency_1.convertToSmallestUnit)(amount, network, token);
const { l2Address } = await this.lookForL2Address(to, network);
if (RegExp(l1Network_1.NetworkDictionary[network].regex.address).exec(to)) {
if (l2Address) {
toAddress = l2Address;
initiatedToNonL2 = to;
isL2 = true;
}
else {
}
}
else if (RegExp(akashicChain_1.L2Regex).exec(to)) {
if (!l2Address) {
return {
error: error_1.AkashicErrorCode.L2AddressNotFound,
};
}
isL2 = true;
}
else {
if (!l2Address) {
return {
error: error_1.AkashicErrorCode.L2AddressNotFound,
};
}
toAddress = l2Address;
initiatedToNonL2 = to;
isL2 = true;
}
if (isL2) {
const signedL2Tx = await this.akashicChain.l2Transaction({
otk: __classPrivateFieldGet(this, _AkashicPay_otk, "f"),
amount: decimalAmount,
toAddress,
coinSymbol: network,
tokenSymbol: this.mapUSDTToTether(network, token),
initiatedToNonL2,
identifier: recipientId,
}, this.isFxBp);
let l2Tx = signedL2Tx;
if (this.isFxBp) {
const response = await this.prepareL2Transaction({
signedTx: signedL2Tx,
});
l2Tx = response.preparedTxn;
}
const acResponse = await this.post(this.targetNode.node, l2Tx);
const chainError = (0, akashicChain_1.checkForAkashicChainError)(acResponse.data);
if (chainError)
return { error: chainError };
this.logger.info(`Paid out ${amount} ${token} to user ${recipientId} at ${to}`);
return {
l2Hash: (0, prefix_1.prefixWithAS)(acResponse.data.$umid),
};
}
else {
const payload = {
toAddress: to,
coinSymbol: network,
amount,
tokenSymbol: token,
identity: __classPrivateFieldGet(this, _AkashicPay_otk, "f").identity,
identifier: recipientId,
feeDelegationStrategy: APACTypes_1.FeeDelegationStrategy.Delegate,
};
let preparedTxn;
try {
preparedTxn = (await this.post(this.akashicUrl + constants_1.AkashicEndpoints.PrepareTx, payload)).data.preparedTxn;
}
catch (error) {
if (error instanceof Error &&
error.message.includes('exceeds total savings')) {
return {
error: error_1.AkashicErrorCode.SavingsExceeded,
};
}
return {
error: error_1.AkashicErrorCode.UnknownError,
};
}
const signedTxn = await this.akashicChain.signPayoutTransaction(preparedTxn, __classPrivateFieldGet(this, _AkashicPay_otk, "f"));
const acResponse = await this.post(this.targetNode.node, signedTxn);
const chainError = (0, akashicChain_1.checkForAkashicChainError)(acResponse.data);
if (chainError)
return { error: chainError };
this.logger.info(`Paid out ${amount} ${token} to user ${recipientId} at $to`);
return {
l2Hash: (0, prefix_1.prefixWithAS)(acResponse.data.$umid),
};
}
}
async getDepositAddress(network, identifier, referenceId) {
return await this.getDepositAddressFunc(network, identifier, referenceId);
}
async getDepositAddressWithRequestedValue(network, identifier, referenceId, requestedCurrency, requestedAmount, token, markupPercentage) {
return await this.getDepositAddressFunc(network, identifier, referenceId, token, requestedCurrency, requestedAmount, markupPercentage);
}
async getDepositAddressFunc(network, identifier, referenceId, token, requestedCurrency, requestedAmount, markupPercentage) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
this.logger.info('getDepositAddress called', { identifier });
if ((this.env === constants_1.Environment.Development && types_1.MainNets.includes(network)) ||
(this.env === constants_1.Environment.Production && types_1.TestNets.includes(network))) {
throw new error_1.AkashicError(error_1.AkashicErrorCode.NetworkEnvironmentMismatch);
}
const { address, unassignedLedgerId } = await this.getByOwnerAndIdentifier({
identifier,
coinSymbol: network,
});
if (address) {
if (unassignedLedgerId) {
const tx = await this.akashicChain.assign(unassignedLedgerId, __classPrivateFieldGet(this, _AkashicPay_otk, "f"), identifier);
const response = await this.post(this.targetNode.node, tx);
const assignedKey = (_a = response.data.$responses) === null || _a === void 0 ? void 0 : _a[0];
if (!assignedKey) {
this.logger.warn(`Failed to assign key for identifier ${identifier} on ${network}. AC: ${JSON.stringify(response.data)}`);
throw new error_1.AkashicError(error_1.AkashicErrorCode.AssignmentFailed);
}
}
if (referenceId) {
const depositRequest = await this.createDepositPayloadAndOrder(referenceId, identifier, address, network, token, requestedCurrency, requestedAmount, markupPercentage);
return {
address,
identifier,
referenceId,
requestedAmount: (_c = (_b = depositRequest.requestedValue) === null || _b === void 0 ? void 0 : _b.amount) !== null && _c !== void 0 ? _c : requestedAmount,
requestedCurrency: (_e = (_d = depositRequest.requestedValue) === null || _d === void 0 ? void 0 : _d.currency) !== null && _e !== void 0 ? _e : requestedCurrency,
network: (_f = depositRequest.coinSymbol) !== null && _f !== void 0 ? _f : network,
token: (_g = depositRequest.tokenSymbol) !== null && _g !== void 0 ? _g : token,
exchangeRate: depositRequest.exchangeRate,
amount: depositRequest.amount,
expires: depositRequest.expires,
markupPercentage: (_h = depositRequest.markupPercentage) !== null && _h !== void 0 ? _h : markupPercentage,
};
}
return {
address,
identifier,
};
}
const tx = await this.akashicChain.keyCreateTransaction(network, __classPrivateFieldGet(this, _AkashicPay_otk, "f"));
const response = await this.post(this.targetNode.node, tx);
const newKey = (_j = response.data.$responses) === null || _j === void 0 ? void 0 : _j[0];
if (!newKey) {
this.logger.warn(`Failed to create key on ${network} for identifier ${identifier}. AC: ${JSON.stringify(response.data)}`);
throw new error_1.AkashicError(error_1.AkashicErrorCode.KeyCreationFailure);
}
const txBody = await this.akashicChain.differentialConsensusTransaction(__classPrivateFieldGet(this, _AkashicPay_otk, "f"), newKey, identifier);
const diffResponse = (await this.post(this.targetNode.node, txBody)).data;
if (diffResponse.$responses && diffResponse.$responses[0] !== 'confirmed') {
this.logger.warn(`Key creation on ${network} failed at differential consensus for identifier ${identifier}. Unhealthy key: ${JSON.stringify(newKey)}`);
throw new error_1.AkashicError(error_1.AkashicErrorCode.UnHealthyKey);
}
if (referenceId) {
const depositRequest = await this.createDepositPayloadAndOrder(referenceId, identifier, newKey.address, network, token, requestedCurrency, requestedAmount, markupPercentage);
return {
address: newKey.address,
identifier,
referenceId,
requestedAmount: (_l = (_k = depositRequest.requestedValue) === null || _k === void 0 ? void 0 : _k.amount) !== null && _l !== void 0 ? _l : requestedAmount,
requestedCurrency: (_o = (_m = depositRequest.requestedValue) === null || _m === void 0 ? void 0 : _m.currency) !== null && _o !== void 0 ? _o : requestedCurrency,
network: (_p = depositRequest.coinSymbol) !== null && _p !== void 0 ? _p : network,
token: (_q = depositRequest.tokenSymbol) !== null && _q !== void 0 ? _q : token,
exchangeRate: depositRequest.exchangeRate,
amount: depositRequest.amount,
expires: depositRequest.expires,
markupPercentage: (_r = depositRequest.markupPercentage) !== null && _r !== void 0 ? _r : markupPercentage,
};
}
return {
address: newKey.address,
identifier,
};
}
async createDepositPayloadAndOrder(referenceId, identifier, address, network, tokenSymbol, requestedCurrency, requestedAmount, markupPercentage) {
const payloadToSign = Object.assign(Object.assign({ identity: __classPrivateFieldGet(this, _AkashicPay_otk, "f").identity, expires: Date.now() + 60 * 1000, referenceId,
identifier, toAddress: address, coinSymbol: network, tokenSymbol }, (requestedCurrency &&
requestedAmount && {
requestedValue: {
currency: requestedCurrency,
amount: requestedAmount,
},
})), { markupPercentage });
return await this.createDepositOrder(Object.assign(Object.assign({}, payloadToSign), { signature: this.sign(__classPrivateFieldGet(this, _AkashicPay_otk, "f").key.prv.pkcs8pem, payloadToSign) }));
}
async getDepositUrl(identifier, referenceId, receiveCurrencies, networks, redirectUrl) {
return await this.getDepositUrlFunc(identifier, referenceId, receiveCurrencies, networks, redirectUrl);
}
async getDepositUrlWithRequestedValue(identifier, referenceId, requestedCurrency, requestedAmount, receiveCurrencies, networks, markupPercentage, redirectUrl) {
return await this.getDepositUrlFunc(identifier, referenceId, receiveCurrencies, networks, redirectUrl, requestedCurrency, requestedAmount, markupPercentage);
}
async getDepositUrlFunc(identifier, referenceId, receiveCurrencies, networks, redirectUrl, requestedCurrency, requestedAmount, markupPercentage) {
const [keys, supportedCurrencies] = await Promise.all([
this.getKeysByOwnerAndIdentifier({
identifier,
}),
this.getSupportedCurrencies(),
]);
for (const coinSymbol of new Set(Object.values(supportedCurrencies).flat())) {
if (!new Set(keys.map((key) => key.coinSymbol)).has(coinSymbol)) {
await this.getDepositAddress(coinSymbol, identifier);
}
}
if (referenceId) {
const payloadToSign = Object.assign(Object.assign({ identity: __classPrivateFieldGet(this, _AkashicPay_otk, "f").identity, expires: Date.now() + 60 * 1000, referenceId,
identifier }, (requestedCurrency &&
requestedAmount && {
requestedValue: {
currency: requestedCurrency,
amount: requestedAmount,
},
})), { markupPercentage });
await this.createDepositOrder(Object.assign(Object.assign({}, payloadToSign), { signature: this.sign(__classPrivateFieldGet(this, _AkashicPay_otk, "f").key.prv.pkcs8pem, payloadToSign) }));
}
const params = {
identity: __classPrivateFieldGet(this, _AkashicPay_otk, "f").identity,
identifier,
};
if (referenceId)
params.referenceId = referenceId;
if (receiveCurrencies)
params.receiveCurrencies = receiveCurrencies
.map((c) => this.mapMainToTestCurrency(c))
.join(',');
if (networks)
params.networks = networks.join(',');
if (redirectUrl) {
params.redirectUrl = Buffer.from(redirectUrl).toString('base64url');
}
const queryString = (0, http_1.encodeURIObject)(params);
return `${this.akashicPayUrl}/sdk/deposit?${queryString}`;
}
verifyDepositAddress() {
throw new Error('Unimplemented');
}
async getExchangeRates(requestedCurrency) {
let url = `${this.akashicUrl}${constants_1.AkashicEndpoints.ExchangeRates}/${requestedCurrency}`;
return (await this.get(url)).data;
}
async lookForL2Address(aliasOrL1OrL2Address, network) {
let url = `${this.akashicUrl}${constants_1.AkashicEndpoints.L2Lookup}?to=${aliasOrL1OrL2Address}`;
if (network) {
url += `&coinSymbol=${network}`;
}
return (await this.get(url)).data;
}
async getTransfers(getTransactionParams) {
const queryParameters = Object.assign(Object.assign({}, getTransactionParams), { identity: __classPrivateFieldGet(this, _AkashicPay_otk, "f").identity });
const query = (0, http_1.encodeURIObject)(queryParameters);
return (await this.get(`${this.akashicUrl}${constants_1.AkashicEndpoints.OwnerTransaction}?${query}`)).data.transactions;
}
async getBalance() {
const response = (await this.get(`${this.akashicUrl}${constants_1.AkashicEndpoints.OwnerBalance}?address=${__classPrivateFieldGet(this, _AkashicPay_otk, "f").identity}`)).data;
return response.totalBalances.map((bal) => ({
networkSymbol: bal.coinSymbol,
tokenSymbol: bal.tokenSymbol,
balance: bal.balance,
}));
}
async getTransactionDetails(l2TxHash) {
const response = await this.get(`${this.akashicUrl}${constants_1.AkashicEndpoints.TransactionsDetails}?l2Hash=${l2TxHash}`);
const transaction = response.data.transaction;
if (transaction) {
return transaction;
}
return undefined;
}
async getSupportedCurrencies() {
return (await this.get(`${this.akashicUrl}${constants_1.AkashicEndpoints.SupportedCurrencies}`)).data;
}
async getByOwnerAndIdentifier(getByOwnerAndIdentifierParams) {
const queryParameters = Object.assign(Object.assign({}, getByOwnerAndIdentifierParams), { identity: __classPrivateFieldGet(this, _AkashicPay_otk, "f").identity, usePreSeed: true });
const query = (0, http_1.encodeURIObject)(queryParameters);
return (await this.get(`${this.akashicUrl}${constants_1.AkashicEndpoints.IdentifierLookup}?${query}`)).data;
}
async createDepositOrder(createDepositOrderParams) {
return (await this.post(`${this.akashicUrl}${constants_1.AkashicEndpoints.CreateDepositOrder}`, createDepositOrderParams)).data;
}
async getKeysByOwnerAndIdentifier(getKeysByOwnerAndIdentifierParams) {
const queryParameters = Object.assign(Object.assign({}, getKeysByOwnerAndIdentifierParams), { identity: __classPrivateFieldGet(this, _AkashicPay_otk, "f").identity });
const query = (0, http_1.encodeURIObject)(queryParameters);
return (await this.get(`${this.akashicUrl}${constants_1.AkashicEndpoints.AllKeysOfIdentifier}?${query}`)).data;
}
constructor(args) {
var _a;
this.isFxBp = false;
_AkashicPay_otk.set(this, void 0);
this.env = (_a = args.environment) !== null && _a !== void 0 ? _a : constants_1.Environment.Production;
this.akashicChain = new akashicChain_1.AkashicChain(this.env);
const logger = (0, winston_1.createLogger)({
level: 'info',
exitOnError: false,
format: winston_1.format.json(),
transports: [
new winston_1.transports.Console({
format: winston_1.format.simple(),
}),
],
});
this.logger = logger;
this.httpClient = new FetchHttpClient_1.FetchHttpClient(this.logger);
this.akashicUrl = (this.env === constants_1.Environment.Development ? constants_1.AkashicBaseUrlDev : constants_1.AkashicBaseUrl);
this.akashicPayUrl = (this.env === constants_1.Environment.Development
? constants_1.AkashicPayBaseUrlDev
: constants_1.AkashicPayBaseUrl);
if (args.targetNode)
this.targetNode = args.targetNode;
if (args.apiSecret)
this.apiSecret = args.apiSecret;
}
verifySignature(body, signature) {
if (!this.apiSecret) {
throw new Error('API secret not set');
}
try {
const sortedBody = this.sortKeys(body);
const computed = (0, crypto_1.createHmac)('sha256', this.apiSecret)
.update(JSON.stringify(sortedBody))
.digest('hex');
return computed === signature;
}
catch (_a) {
return false;
}
}
async init(args) {
this.logger.info('Initialising AkashicPay instance');
if (!this.targetNode) {
this.targetNode = await this.chooseBestACNode();
}
const { data: { isBp, isFxBp }, } = await this.get(`${this.akashicUrl}${constants_1.AkashicEndpoints.IsBp}?address=${args.l2Address}`);
this.isFxBp = isFxBp;
if (!isBp) {
throw new error_1.AkashicError(error_1.AkashicErrorCode.IsNotBp);
}
if ('privateKey' in args && args.privateKey) {
this.setOtkFromKeyPair(args.privateKey, args.l2Address);
}
else if ('recoveryPhrase' in args && args.recoveryPhrase) {
await this.setOtkFromRecoveryPhrase(args.recoveryPhrase, args.l2Address);
}
else {
throw new error_1.AkashicError(error_1.AkashicErrorCode.IncorrectPrivateKeyFormat);
}
this.logger = this.logger.child({ identity: args.l2Address });
this.logger.info('AkashicPay instance initialised');
}
async chooseBestACNode() {
const nodes = this.env === constants_1.Environment.Production ? constants_1.ACNodes : constants_1.ACDevNodes;
try {
const fastestFullHealthNode = await (0, async_1.promiseAny)(Object.values(nodes).map(async (node) => {
const response = (await this.httpClient.get(`${node.node}a/status`)).data;
if (response.status !== 4)
throw new ACHealthError(node, response.status);
return node;
}));
this.logger.info(`Set healthy target node as ${JSON.stringify(fastestFullHealthNode)} by testing for fastest`);
return fastestFullHealthNode;
}
catch (errors) {
if (!(errors instanceof Array))
throw errors;
const healthErrors = errors
.filter((e) => e instanceof ACHealthError)
.sort((e1, e2) => e2.status - e1.status);
if (!healthErrors.length)
throw errors;
const { node, status } = healthErrors[0];
this.logger.warn(`Set least unhealthy target node ${JSON.stringify(node)} (health: ${status})`);
return node;
}
}
setOtkFromKeyPair(privateKey, l2Address) {
if (!AkashicPay.ACPrivateKeyRegex.test(privateKey))
throw new error_1.AkashicError(error_1.AkashicErrorCode.IncorrectPrivateKeyFormat);
__classPrivateFieldSet(this, _AkashicPay_otk, Object.assign(Object.assign({}, (0, reconstruct_1.restoreOtkFromKeypair)(privateKey)), { identity: l2Address }), "f");
this.logger.debug('otk set from private key');
}
async setOtkFromRecoveryPhrase(recoveryPhrase, l2Address) {
__classPrivateFieldSet(this, _AkashicPay_otk, Object.assign(Object.assign({}, (await (0, reconstruct_1.restoreOtkFromPhrase)(recoveryPhrase))), { identity: l2Address }), "f");
this.logger.debug('otk set from recovery phrase');
}
async post(url, payload) {
this.logger.info(JSON.stringify(payload), `POSTing to url: ${url}`);
return await this.httpClient.post(url, payload);
}
async get(url) {
return await this.httpClient.get(url);
}
async prepareL2Transaction(transactionData) {
const response = await this.post(`${this.akashicUrl}${constants_1.AkashicEndpoints.PrepareL2Txn}`, transactionData);
return response.data;
}
sign(otkPriv, data) {
try {
const pemPrivate = '-----BEGIN EC PRIVATE KEY-----\n' +
`${otkPriv}\n` +
'-----END EC PRIVATE KEY-----';
let kp;
if (otkPriv.startsWith('0x')) {
kp = new activecrypto_1.ActiveCrypto.KeyPair('secp256k1', otkPriv);
}
else {
kp = new activecrypto_1.ActiveCrypto.KeyPair('secp256k1', pemPrivate);
}
return kp.sign(data);
}
catch (_a) {
throw new Error('Invalid private key');
}
}
sortKeys(obj) {
if (Array.isArray(obj)) {
return obj.map((item) => this.sortKeys(item));
}
else if (obj !== null &&
typeof obj === 'object' &&
!(obj instanceof Date)) {
return Object.keys(obj)
.sort((a, b) => a.localeCompare(b))
.reduce((acc, key) => {
acc[key] = this.sortKeys(obj[key]);
return acc;
}, {});
}
return obj;
}
mapUSDTToTether(coinSymbol, tokenSymbol) {
if (!tokenSymbol)
return undefined;
return coinSymbol === types_1.NetworkSymbol.Tron_Shasta &&
tokenSymbol === types_1.TokenSymbol.USDT
? types_1.TokenSymbol.TETHER
: tokenSymbol;
}
mapMainToTestCurrency(currency) {
return this.env === constants_1.Environment.Development && currency === types_1.Currency.ETH
? 'SEP'
: currency;
}
}
exports.AkashicPay = AkashicPay;
_AkashicPay_otk = new WeakMap();
AkashicPay.ACPrivateKeyRegex = /^0x[a-f\d]{64}$/;
class ACHealthError extends Error {
constructor(node, status) {
super(`Node ${node.node} is unhealthy with status ${status}`);
this.node = node;
this.status = status;
}
}
//# sourceMappingURL=main.js.map