UNPKG

@akashicpay/sdk

Version:

SDK to interact with the Akashic ecosystem

507 lines 25.8 kB
"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