Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bugfix/tx history restore bump #945

Merged
merged 11 commits into from
Aug 18, 2023
Merged
31 changes: 0 additions & 31 deletions @shared/api/helpers/soroban.ts

This file was deleted.

105 changes: 26 additions & 79 deletions @shared/api/internal.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import StellarSdk from "stellar-sdk";
import * as SorobanClient from "soroban-client";
import { DataProvider } from "@stellar/wallet-sdk";
import {
getBalance,
getDecimals,
getName,
getSymbol,
} from "@shared/helpers/soroban/token";
import {
Account,
AccountBalancesInterface,
Expand All @@ -24,8 +30,6 @@ import { getIconUrlFromIssuer } from "./helpers/getIconUrlFromIssuer";
import { getDomainFromIssuer } from "./helpers/getDomainFromIssuer";
import { stellarSdkServer } from "./helpers/stellarSdkServer";

import { decodei128, decodeU32, decodeStr } from "./helpers/soroban";

const TRANSACTIONS_LIMIT = 100;

export const createAccount = async (
Expand Down Expand Up @@ -859,94 +863,37 @@ export const getBlockedAccounts = async () => {
return resp;
};

type TxToOp = {
[index: string]: {
tx: SorobanClient.Transaction<
SorobanClient.Memo<SorobanClient.MemoType>,
SorobanClient.Operation[]
>;
decoder: (xdr: string) => string | number;
};
};

interface SorobanTokenRecord {
[key: string]: unknown;
balance: number;
name: string;
symbol: string;
decimals: string;
}

export const getSorobanTokenBalance = (
export const getSorobanTokenBalance = async (
server: SorobanClient.Server,
contractId: string,
txBuilders: {
// need a builder per operation until multi-op transactions are released
// need a builder per operation, Soroban currently has single op transactions
balance: SorobanClient.TransactionBuilder;
name: SorobanClient.TransactionBuilder;
decimals: SorobanClient.TransactionBuilder;
symbol: SorobanClient.TransactionBuilder;
},
params: SorobanClient.xdr.ScVal[],
balanceParams: SorobanClient.xdr.ScVal[],
) => {
const contract = new SorobanClient.Contract(contractId);

// Right now we can only have 1 operation per TX in Soroban
// There is ongoing work to lift this restriction
// but for now we need to do 4 txs to show 1 user balance. :(
const balanceTx = txBuilders.balance
.addOperation(contract.call("balance", ...params))
.setTimeout(SorobanClient.TimeoutInfinite)
.build();

const nameTx = txBuilders.name
.addOperation(contract.call("name"))
.setTimeout(SorobanClient.TimeoutInfinite)
.build();

const symbolTx = txBuilders.symbol
.addOperation(contract.call("symbol"))
.setTimeout(SorobanClient.TimeoutInfinite)
.build();

const decimalsTx = txBuilders.decimals
.addOperation(contract.call("decimals"))
.setTimeout(SorobanClient.TimeoutInfinite)
.build();

const txs: TxToOp = {
balance: {
tx: balanceTx,
decoder: decodei128,
},
name: {
tx: nameTx,
decoder: decodeStr,
},
symbol: {
tx: symbolTx,
decoder: decodeStr,
},
decimals: {
tx: decimalsTx,
decoder: decodeU32,
},
};

const tokenBalanceInfo = Object.keys(txs).reduce(async (prev, curr) => {
const _prev = await prev;
const { tx, decoder } = txs[curr];
const { results } = await server.simulateTransaction(tx);
if (!results || results.length !== 1) {
throw new Error("Invalid response from simulateTransaction");
}
const result = results[0];
_prev[curr] = decoder(result.xdr);

return _prev;
}, Promise.resolve({} as SorobanTokenRecord));
// for now we need to do 4 tx simulations to show 1 user balance. :(
// TODO: figure out how to fetch ledger keys to do this more efficiently
const decimals = await getDecimals(contractId, server, txBuilders.decimals);
const name = await getName(contractId, server, txBuilders.name);
const symbol = await getSymbol(contractId, server, txBuilders.symbol);
const balance = await getBalance(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice - I find this pattern a lot more straightforward!

contractId,
balanceParams,
server,
txBuilders.balance,
);

return tokenBalanceInfo;
return {
balance,
decimals,
name,
symbol,
};
};

export const addTokenId = async (
Expand Down
2 changes: 1 addition & 1 deletion @shared/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export interface SorobanBalance {
total: BigNumber;
name: string;
symbol: string;
decimals: string;
decimals: number;
}

export type AssetType =
Expand Down
17 changes: 17 additions & 0 deletions @shared/constants/soroban/token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// https://github.com/stellar/soroban-examples/blob/main/token/src/contract.rs
export enum SorobanTokenInterface {
transfer = "transfer",
mint = "mint",
}

// TODO: can we generate this at build time using the cli TS generator? Also should we?
export interface SorobanToken {
// only currently holds fields we care about
transfer: (from: string, to: string, amount: number) => void;
mint: (to: string, amount: number) => void;
// values below are in storage
name: string;
balance: number;
symbol: string;
decimals: number;
}
32 changes: 32 additions & 0 deletions @shared/helpers/soroban/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {
Transaction,
Memo,
MemoType,
Operation,
Server,
xdr,
scValToNative,
} from "soroban-client";
import { captureException } from "@sentry/browser";

export const simulateTx = async <ArgType>(
tx: Transaction<Memo<MemoType>, Operation[]>,
server: Server,
): Promise<ArgType> => {
const { results } = await server.simulateTransaction(tx);
if (!results || results.length !== 1) {
throw new Error("Invalid response from simulateTransaction");
}
const result = results[0];
const scVal = xdr.ScVal.fromXDR(result.xdr, "base64");
let convertedScVal: any;
try {
// handle a case where scValToNative doesn't properly handle scvString
convertedScVal = scVal.str().toString();
return convertedScVal;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I believe this should be fixed now in the last soroban-client. So I don't think you need to handle this scvString case

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh yeah I did hear that also, I'll test it out to make sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in 1ba0450

} catch (e) {
console.error(e);
captureException(`Failed to convert SCVal to native val, ${e}`);
}
return scValToNative(scVal);
};
93 changes: 93 additions & 0 deletions @shared/helpers/soroban/token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {
Contract,
TransactionBuilder,
Memo,
Server,
TimeoutInfinite,
xdr,
} from "soroban-client";
import { simulateTx } from "./server";

export const transfer = async (
contractId: string,
params: xdr.ScVal[],
memo: string | undefined,
builder: TransactionBuilder,
) => {
const contract = new Contract(contractId);

const tx = builder
.addOperation(contract.call("transfer", ...params))
.setTimeout(TimeoutInfinite);

if (memo) {
tx.addMemo(Memo.text(memo));
}

return tx.build();
};

export const getBalance = async (
contractId: string,
params: xdr.ScVal[],
server: Server,
builder: TransactionBuilder,
) => {
const contract = new Contract(contractId);

const tx = builder
.addOperation(contract.call("balance", ...params))
.setTimeout(TimeoutInfinite)
.build();

const result = await simulateTx<number>(tx, server);
return result;
};

export const getDecimals = async (
contractId: string,
server: Server,
builder: TransactionBuilder,
) => {
const contract = new Contract(contractId);

const tx = builder
.addOperation(contract.call("decimals"))
.setTimeout(TimeoutInfinite)
.build();

const result = await simulateTx<number>(tx, server);
return result;
};

export const getName = async (
contractId: string,
server: Server,
builder: TransactionBuilder,
) => {
const contract = new Contract(contractId);

const tx = builder
.addOperation(contract.call("name"))
.setTimeout(TimeoutInfinite)
.build();

const result = await simulateTx<string>(tx, server);
return result;
};

export const getSymbol = async (
contractId: string,
server: Server,
builder: TransactionBuilder,
) => {
const contract = new Contract(contractId);

const tx = builder
.addOperation(contract.call("symbol"))
.setTimeout(TimeoutInfinite)
.build();

const result = await simulateTx<string>(tx, server);
return result;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to do this now since things could still change - but if this API holds, in the future we could probably refactor these helpers to share some logic as they're all kinda doing the same thing.

Also, @Shaptic - do we consider this a common enough use case (getting balance + decimals + name + symbol) that soroban-client should offer this?

Copy link
Contributor Author

@aristidesstaffieri aristidesstaffieri Aug 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally - there's also a good convo happening about how we should be fetching data for Soroban now that expired state can be involved. It seems like we'll have to at least augment our approach anyway pretty soon. Some good ideas that came up includes -

  1. Caching things like decimals/name/symbol when a user firsts gets a balance of a token.
  2. Fetching the ledger entries directly(this is cheaper than the rpc call).
  3. Getting these details from Horizon when token is a SAC.
  4. Allowing users to restore their data when their token data is expired(implies we start storing expiration time along with other details).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this belongs in soroban-client, per se, but maybe this be handled by using soroban-cli to generate the TypeScript bindings for the SAC? cc @chadoh @willemneal

};
1 change: 1 addition & 0 deletions extension/src/constants/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export enum OPERATION_TYPES {
revokeTrustlineSponsorship = "Revoke Trustline Sponsorship",
setOptions = "Set Options",
setTrustLineFlags = "Set Trustline Flags",
bumpFootprintExpiration = "Bump Footprint Expiration",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing restoreFootprint?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes it is, thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in 05fa8fe

}

export enum TRANSACTION_WARNING {
Expand Down
9 changes: 4 additions & 5 deletions extension/src/popup/SorobanContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import {

import { settingsNetworkDetailsSelector } from "./ducks/settings";

const BASE_FEE = "100";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should use SorobanClient.BASE_FEE, I think

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, I keep meaning to update that. Done in 05fa8fe


export interface SorobanContextInterface {
server: SorobanClient.Server;
newTxBuilder: () => SorobanClient.TransactionBuilder;
newTxBuilder: (fee?: string) => SorobanClient.TransactionBuilder;
}

export const SorobanContext = React.createContext(
Expand All @@ -25,9 +27,6 @@ export const SorobanProvider = ({
children: React.ReactNode;
pubKey: string;
}) => {
// Were only simluating so the fee here should not matter
// AFAIK there is no fee stats for Soroban yet either
const fee = "100";
const networkDetails = useSelector(settingsNetworkDetailsSelector);
const source = new SorobanClient.Account(pubKey, "0");

Expand All @@ -42,7 +41,7 @@ export const SorobanProvider = ({
allowHttp: networkDetails.networkUrl.startsWith("http://"),
});

const newTxBuilder = () =>
const newTxBuilder = (fee = BASE_FEE) =>
new SorobanClient.TransactionBuilder(source, {
fee,
networkPassphrase: networkDetails.networkPassphrase,
Expand Down
Loading
Loading