Skip to content

Commit

Permalink
optional simulate & wallet, editable TransactionBuilder
Browse files Browse the repository at this point in the history
- Can now pass an `account` OR `wallet` when constructing the
  ContractClient, or none! If you pass none, you can still make view
  calls, since they don't need a signer. You will need to pass a
  `wallet` when calling things that need it, like `signAndSend`.

- You can now pass `simulate: false` when first creating your
  transaction to skip simulation. You can then modify the transaction
  using the TransactionBuilder at `tx.raw` before manually calling
  `simulate`. Example:

      const tx = await myContract.myMethod(
        { args: 'for', my: 'method', ... },
        { simulate: false }
      );
      tx.raw.addMemo(Memo.text('Nice memo, friend!'))
      await tx.simulate();

- Error types are now collected under `AssembledTransaction.Errors` and
  `SentTransaction.Errors`.
  • Loading branch information
chadoh committed Feb 7, 2024
1 parent ac5545b commit 5d637af
Show file tree
Hide file tree
Showing 8 changed files with 250 additions and 172 deletions.
2 changes: 1 addition & 1 deletion src/contract_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -719,7 +719,7 @@ export class ContractSpec {
args: Record<string, any>,
options: MethodOptions
) => {
return await AssembledTransaction.fromSimulation({
return await AssembledTransaction.build({
method: name,
args: spec.funcArgsToScVals(name, args),
...options,
Expand Down
350 changes: 193 additions & 157 deletions src/soroban/assembled_transaction.ts

Large diffs are not rendered by default.

60 changes: 51 additions & 9 deletions src/soroban/contract_client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AssembledTransaction } from '.'
import { ContractSpec, xdr } from '..'
import { Account, ContractSpec, SorobanRpc, xdr } from '..'

export type XDR_BASE64 = string;

Expand All @@ -23,12 +23,7 @@ export interface Wallet {
) => Promise<XDR_BASE64>;
}


export type ContractClientOptions = {
contractId: string;
networkPassphrase: string;
rpcUrl: string;
errorTypes?: Record<number, { message: string }>;
export interface AcceptsWalletOrAccount {
/**
* A Wallet interface, such as Freighter, that has the methods `isConnected`, `isAllowed`, `getUserInfo`, and `signTransaction`. If not provided, will attempt to import and use Freighter. Example:
*
Expand All @@ -42,14 +37,61 @@ export type ContractClientOptions = {
* })
* ```
*/
wallet: Wallet;
wallet?: Wallet;

/**
* You can pass in `wallet` OR `account`, but not both. If you only pass
* `wallet`, `account` will be derived from it. If you can bypass this
* behavior by passing in your own account object.
*/
account?: Account | Promise<Account>;
};

async function getPublicKey(wallet?: Wallet): Promise<string | undefined> {
if (!wallet) return undefined;
if (!(await wallet.isConnected()) || !(await wallet.isAllowed())) {
return undefined;
}
return (await wallet.getUserInfo()).publicKey;
};

export const NULL_ACCOUNT =
"GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";

/**
* Get account details from the Soroban network for the publicKey currently
* selected in user's wallet. If user is not connected to their wallet, {@link
* getPublicKey} returns undefined, and this will return {@link NULL_ACCOUNT}.
* This works for simulations, which is all that's needed for most view calls.
* If you want the transaction to be included in the ledger, you will need to
* provide a connected wallet.
*/
export async function getAccount(server: SorobanRpc.Server, wallet?: Wallet): Promise<Account> {
const publicKey = await getPublicKey(wallet);
return publicKey
? await server.getAccount(publicKey)
: new Account(NULL_ACCOUNT, "0");
};

export type ContractClientOptions = AcceptsWalletOrAccount & {
contractId: string;
networkPassphrase: string;
rpcUrl: string;
errorTypes?: Record<number, { message: string }>;
};

export class ContractClient {
private server: SorobanRpc.Server;

constructor(
public readonly spec: ContractSpec,
public readonly options: ContractClientOptions,
) {}
) {
this.server = new SorobanRpc.Server(this.options.rpcUrl, {
allowHttp: this.options.rpcUrl.startsWith("http://"),
});
options.account = options.account ?? getAccount(this.server, options.wallet);
}

txFromJSON = <T>(json: string): AssembledTransaction<T> => {
const { method, ...tx } = JSON.parse(json)
Expand Down
2 changes: 2 additions & 0 deletions src/soroban/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export function hasOwnProperty<X extends {}, Y extends PropertyKey>(
): obj is X & Record<Y, unknown> {
return obj.hasOwnProperty(prop);
}

export type PartiallyRequired<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
3 changes: 0 additions & 3 deletions test/e2e/initialize.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,8 @@ exe() { echo"${@/eval/}" ; "$@" ; }

function fund_all() {
exe eval "$soroban keys generate root"
exe eval "$soroban keys fund root"
exe eval "$soroban keys generate alice"
exe eval "$soroban keys fund alice"
exe eval "$soroban keys generate bob"
exe eval "$soroban keys fund bob"
}
function upload() {
exe eval "($soroban contract $1 --source root --wasm $dirname/$2 --ignore-checks) > $dirname/$3"
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/src/test-hello-world.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const test = require('ava')
const fs = require('node:fs')
const { ContractSpec } = require('../../..')
const { ContractSpec, SorobanRpc } = require('../../..')
const { root, wallet, rpcUrl, networkPassphrase } = require('./util')
const xdr = require('../wasms/specs/test_hello_world.json')

Expand Down
2 changes: 1 addition & 1 deletion test/e2e/src/test-swap.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ test('calling `signAndSend()` too soon throws descriptive error', async t => {
min_b_for_a: amountBToSwap,
})
const error = await t.throwsAsync(tx.signAndSend())
t.true(error instanceof SorobanRpc.AssembledTransaction.NeedsMoreSignaturesError, `error is not of type 'NeedsMoreSignaturesError'; instead it is of type '${error?.constructor.name}'`)
t.true(error instanceof SorobanRpc.AssembledTransaction.Errors.NeedsMoreSignatures, `error is not of type 'NeedsMoreSignaturesError'; instead it is of type '${error?.constructor.name}'`)
if (error) t.regex(error.message, /needsNonInvokerSigningBy/)
})

Expand Down
1 change: 1 addition & 0 deletions test/e2e/src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ module.exports.rpcUrl = rpcUrl
const networkPassphrase = process.env.SOROBAN_NETWORK_PASSPHRASE ?? "Standalone Network ; February 2017";
module.exports.networkPassphrase = networkPassphrase

// TODO: export Wallet class from soroban-sdk
class Wallet {
constructor(publicKey) {
this.publicKey = publicKey
Expand Down

0 comments on commit 5d637af

Please sign in to comment.