Skip to content

Commit

Permalink
prepares tx in token simulation flow (#29)
Browse files Browse the repository at this point in the history
* prepares tx in token simulation flow

* removes unused import

* removes unused import

* parse amount as string for soroban token transfer

* fixes query param validator to accept multiple contract IDs, tweaks history transformer to skip over host fn invocations that are not invokeHostFn

* updates test for fixed contract ids query param pattern, tweaks transformer to allow operation type to match post filter
  • Loading branch information
aristidesstaffieri authored Jan 31, 2024
1 parent 9547e76 commit b45f7c4
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 54 deletions.
2 changes: 1 addition & 1 deletion src/helper/soroban-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ const getOpArgs = (fnName: string, args: xdr.ScVal[]) => {
to = StrKey.encodeEd25519PublicKey(
args[1].address().accountId().ed25519()
);
amount = scValToNative(args[2]);
amount = scValToNative(args[2]).toString();
break;
case SorobanTokenInterface.mint:
to = StrKey.encodeEd25519PublicKey(
Expand Down
22 changes: 11 additions & 11 deletions src/route/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,21 @@ describe("API routes", () => {
});

it("can fetch account balances for a pub key & multiple contract IDs", async () => {
const params = {
contract_ids: [
"CCWAMYJME4H5CKG7OLXGC2T4M6FL52XCZ3OQOAV6LL3GLA4RO4WH3ASP",
"CBGTG7XFRY3L6OKAUTR6KGDKUXUQBX3YDJ3QFDYTGVMOM7VV4O7NCODG",
],
network: "TESTNET",
};
const contractIds = [
"CCWAMYJME4H5CKG7OLXGC2T4M6FL52XCZ3OQOAV6LL3GLA4RO4WH3ASP",
"CBGTG7XFRY3L6OKAUTR6KGDKUXUQBX3YDJ3QFDYTGVMOM7VV4O7NCODG",
];
const server = await getDevServer();
const response = await fetch(
const url = new URL(
`http://localhost:${
(server?.server?.address() as any).port
}/api/v1/account-balances/${pubKey}?${new URLSearchParams(
params as any
)}`
}/api/v1/account-balances/${pubKey}`
);
url.searchParams.append("network", "TESTNET");
for (const id of contractIds) {
url.searchParams.append("contract_ids", id);
}
const response = await fetch(url.href);
expect(response.status).toEqual(200);
register.clear();
await server.close();
Expand Down
34 changes: 21 additions & 13 deletions src/route/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ import {
} from "../helper/validate";
import { submitTransaction } from "../helper/horizon-rpc";
import {
Address,
BASE_FEE,
Memo,
MemoType,
Operation,
SorobanRpc,
Transaction,
TransactionBuilder,
xdr,
XdrLargeInt,
} from "stellar-sdk";
import { buildTransfer, simulateTx } from "../helper/soroban-rpc";

Expand Down Expand Up @@ -161,8 +162,9 @@ export async function initApiServer(
},
querystring: {
["contract_ids"]: {
type: "string",
validator: (qStr: string) => qStr.split(",").every(isContractId),
type: "array",
validator: (qStr: Array<unknown>) =>
qStr.map((q) => String(q)).every(isContractId),
},
["network"]: {
type: "string",
Expand All @@ -180,7 +182,7 @@ export async function initApiServer(
request: FastifyRequest<{
Params: { ["pubKey"]: string };
Querystring: {
["contract_ids"]: string;
["contract_ids"]: string[];
["network"]: NetworkNames;
["horizon_url"]?: string;
["soroban_url"]?: string;
Expand All @@ -190,9 +192,7 @@ export async function initApiServer(
) => {
const pubKey = request.params["pubKey"];
const { network, horizon_url, soroban_url } = request.query;
const contractIds = request.query["contract_ids"]
? request.query["contract_ids"].split(",")
: [];
const contractIds = request.query["contract_ids"] || ([] as string[]);
const { data, error } = await mercuryClient.getAccountBalances(
pubKey,
contractIds,
Expand Down Expand Up @@ -513,7 +513,7 @@ export async function initApiServer(
pub_key: string;
memo: string;
fee?: string;
params: xdr.ScVal[];
params: Record<string, string>;
network_url: string;
network_passphrase: string;
};
Expand All @@ -540,14 +540,22 @@ export async function initApiServer(
fee: _fee,
networkPassphrase: network_passphrase,
});
const tx = buildTransfer(address, params, memo, builder);
const simulationResponse = await simulateTx<unknown>(
tx as Transaction<Memo<MemoType>, Operation[]>,
server
const _params = [
new Address(params.publicKey).toScVal(), // from
new Address(params.destination).toScVal(), // to
new XdrLargeInt("i128", params.amount).toI128(), // amount
];
const tx = buildTransfer(address, _params, memo, builder);
const simulationResponse = (await server.simulateTransaction(
tx
)) as SorobanRpc.Api.SimulateTransactionSuccessResponse;
const preparedTransaction = SorobanRpc.assembleTransaction(
tx,
simulationResponse
);
const data = {
simulationResponse,
raw: tx.toXDR(),
preparedTransaction: preparedTransaction.build().toXDR(),
};
reply.code(200).send(data);
} catch (error) {
Expand Down
2 changes: 1 addition & 1 deletion src/route/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Ajv, {
const ajv = new Ajv({
removeAdditional: true,
useDefaults: true,
coerceTypes: true,
coerceTypes: "array",
});

ajv.addKeyword("validator", {
Expand Down
71 changes: 43 additions & 28 deletions src/service/mercury/helpers/transformers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -866,34 +866,49 @@ const transformAccountHistory = async (
): Promise<Partial<Horizon.ServerApi.OperationRecord>[]> => {
const invokeHostFnEdges =
rawResponse.data?.invokeHostFnByPublicKey.edges || [];
const invokeHostFn = invokeHostFnEdges.map((edge) => {
const hostFn = xdr.HostFunction.fromXDR(
Buffer.from(edge.node.hostFunction, "base64")
);
const invocation = hostFn.invokeContract();
const fnName = invocation.functionName().toString();
return {
auth: edge.node.auth,
created_at: new Date(
edge.node.txInfoByTx.ledgerByLedger.closeTime * 1000
).toISOString(),
sorobanMeta: edge.node.sorobanMeta,
source_account: edge.node.accountBySource.publickey,
tx: edge.node.tx,
type: "invoke_host_function",
type_i: 24,
id: edge.node.opId,
transaction_attr: {
contractId: StrKey.encodeContract(
invocation.contractAddress().contractId()
),
fnName,
args: getOpArgs(fnName, invocation.args()),
operation_count: edge.node.txInfoByTx.opCount,
fee_charged: edge.node.txInfoByTx.fee,
},
} as Partial<Horizon.ServerApi.InvokeHostFunctionOperationRecord>;
});
const invokeHostFn = invokeHostFnEdges
.filter((edge) => {
// we only want to keep these history entries if the Host Fn is
// for invoking a contract, we dont show contract create or wasm upload in wallet history right now.
try {
const hostFn = xdr.HostFunction.fromXDR(
Buffer.from(edge.node.hostFunction, "base64")
);
hostFn.invokeContract();
return true;
} catch (error) {
return false;
}
})
.map((edge) => {
const hostFn = xdr.HostFunction.fromXDR(
Buffer.from(edge.node.hostFunction, "base64")
);

const invocation = hostFn.invokeContract();
const fnName = invocation.functionName().toString();
return {
auth: edge.node.auth,
created_at: new Date(
edge.node.txInfoByTx.ledgerByLedger.closeTime * 1000
).toISOString(),
sorobanMeta: edge.node.sorobanMeta,
source_account: edge.node.accountBySource.publickey,
tx: edge.node.tx,
type: "invoke_host_function",
type_i: 24,
id: edge.node.opId,
transaction_attr: {
contractId: StrKey.encodeContract(
invocation.contractAddress().contractId()
),
fnName,
args: getOpArgs(fnName, invocation.args()),
operation_count: edge.node.txInfoByTx.opCount,
fee_charged: edge.node.txInfoByTx.fee,
},
} as Partial<Horizon.ServerApi.InvokeHostFunctionOperationRecord>;
});

const createAccountEdges =
rawResponse.data?.createAccountByPublicKey.edges || [];
Expand Down

0 comments on commit b45f7c4

Please sign in to comment.