diff --git a/src/helper/error.ts b/src/helper/error.ts new file mode 100644 index 0000000..4d564fa --- /dev/null +++ b/src/helper/error.ts @@ -0,0 +1,6 @@ +export const ERROR = { + ACCOUNT_NOT_SOURCE: + "Transfer contains authorization entry for a different account", + AUTH_SUB_INVOCATIONS: + "Transfer authorizes sub-invocations to another contract", +}; diff --git a/src/helper/horizon-rpc.ts b/src/helper/horizon-rpc.ts index 91b7a4f..d5522db 100644 --- a/src/helper/horizon-rpc.ts +++ b/src/helper/horizon-rpc.ts @@ -248,7 +248,7 @@ export const submitTransaction = async ( error: null, }; } catch (e: any) { - if (e.response.status === 504) { + if (e.response?.status === 504) { // in case of 504, keep retrying this tx until submission succeeds or we get a different error // https://developers.stellar.org/api/errors/http-status-codes/horizon-specific/timeout // https://developers.stellar.org/docs/encyclopedia/error-handling diff --git a/src/route/index.ts b/src/route/index.ts index 410f4f4..19d470c 100644 --- a/src/route/index.ts +++ b/src/route/index.ts @@ -25,8 +25,10 @@ import { Transaction, TransactionBuilder, XdrLargeInt, + xdr, } from "stellar-sdk"; import { buildTransfer, simulateTx } from "../helper/soroban-rpc"; +import { ERROR } from "../helper/error"; const API_VERSION = "v1"; @@ -545,17 +547,41 @@ export async function initApiServer( const simulationResponse = (await server.simulateTransaction( tx )) as SorobanRpc.Api.SimulateTransactionSuccessResponse; + const preparedTransaction = SorobanRpc.assembleTransaction( tx, simulationResponse ); + + const built = preparedTransaction.build(); + switch (built.operations[0].type) { + case "invokeHostFunction": { + const sorobanOp = built + .operations[0] as Operation.InvokeHostFunction; + const auths = sorobanOp.auth || []; + + for (const auth of auths) { + if ( + auth.credentials().switch() !== + xdr.SorobanCredentialsType.sorobanCredentialsSourceAccount() + ) { + throw new Error(ERROR.ACCOUNT_NOT_SOURCE); + } + + if (auth.rootInvocation().subInvocations().length) { + throw new Error(ERROR.AUTH_SUB_INVOCATIONS); + } + } + } + } + const data = { simulationResponse, - preparedTransaction: preparedTransaction.build().toXDR(), + preparedTransaction: built.toXDR(), }; reply.code(200).send(data); } catch (error) { - reply.code(400).send(JSON.stringify(error)); + reply.code(400).send(error); } }, }); diff --git a/src/service/mercury/index.ts b/src/service/mercury/index.ts index 2bc4273..6bfd40b 100644 --- a/src/service/mercury/index.ts +++ b/src/service/mercury/index.ts @@ -34,7 +34,7 @@ enum NETWORK_URLS { } const ERROR_MESSAGES = { - JWT_EXPIRED: "jwt expired", + JWT_EXPIRED: "1_kJdMBB7ytvgRIqF1clh2iz2iI", }; function getGraphQlError(error?: CombinedError) { @@ -179,7 +179,7 @@ export class MercuryClient { } catch (error: unknown) { // renew and retry 0n 401, otherwise throw the error back up to the caller if (error instanceof Error) { - if (error.message === ERROR_MESSAGES.JWT_EXPIRED) { + if (error.message.includes(ERROR_MESSAGES.JWT_EXPIRED)) { await this.renewMercuryToken(); this.logger.info("renewed expired jwt"); return await method(); @@ -518,9 +518,10 @@ export class MercuryClient { ) => { const balances = []; const balanceMap = {} as Record; - try { - const server = await getServer(network, customSorobanRpcUrl); - for (const id of contractIds) { + + const server = await getServer(network, customSorobanRpcUrl); + for (const id of contractIds) { + try { const builder = await getTxBuilder(pubKey, network, server); const params = [new Address(pubKey).toScVal()]; const balance = await getTokenBalance(id, params, server, builder); @@ -530,11 +531,12 @@ export class MercuryClient { balance, ...tokenDetails, }); + } catch (error) { + this.logger.error(error); + continue; } - } catch (error) { - this.logger.error(error); - return balanceMap; } + for (const balance of balances) { balanceMap[`${balance.symbol}:${balance.id}`] = { token: {