From 2da641b45f935645928e0f6aa75e87f4cc47ad82 Mon Sep 17 00:00:00 2001 From: samsiegart Date: Mon, 29 Jan 2024 23:09:27 -0800 Subject: [PATCH] feat(web-components): allow custom signer instead of keplr --- packages/web-components/index.js | 4 + .../src/wallet-connection/chainInfo.js | 1 + .../src/wallet-connection/connectKeplr.js | 61 +++++ .../src/wallet-connection/makeAgoricSigner.js | 123 +++++++++ .../makeInteractiveSigner.js | 246 ------------------ .../src/wallet-connection/signerOptions.js | 92 +++++++ .../src/wallet-connection/walletConnection.js | 31 ++- .../test/walletConnection.test.js | 55 +++- 8 files changed, 337 insertions(+), 276 deletions(-) create mode 100644 packages/web-components/src/wallet-connection/connectKeplr.js create mode 100644 packages/web-components/src/wallet-connection/makeAgoricSigner.js delete mode 100644 packages/web-components/src/wallet-connection/makeInteractiveSigner.js create mode 100644 packages/web-components/src/wallet-connection/signerOptions.js diff --git a/packages/web-components/index.js b/packages/web-components/index.js index 6c325ba..4b141bc 100644 --- a/packages/web-components/index.js +++ b/packages/web-components/index.js @@ -2,6 +2,10 @@ export { makeAgoricKeplrConnection } from './src/keplr-connection/KeplrConnection.js'; export { makeAgoricWalletConnection } from './src/wallet-connection/walletConnection.js'; +export { + agoricRegistryTypes, + agoricConverters, +} from './src/wallet-connection/signerOptions.js'; export { suggestChain } from './src/wallet-connection/suggestChain.js'; export { Errors as AgoricKeplrConnectionErrors } from './src/errors.js'; export { diff --git a/packages/web-components/src/wallet-connection/chainInfo.js b/packages/web-components/src/wallet-connection/chainInfo.js index 0b0a6e6..7e768a9 100644 --- a/packages/web-components/src/wallet-connection/chainInfo.js +++ b/packages/web-components/src/wallet-connection/chainInfo.js @@ -1,3 +1,4 @@ +// @ts-check /** @typedef {import('@keplr-wallet/types').Bech32Config} Bech32Config */ /** @typedef {import('@keplr-wallet/types').FeeCurrency} FeeCurrency */ diff --git a/packages/web-components/src/wallet-connection/connectKeplr.js b/packages/web-components/src/wallet-connection/connectKeplr.js new file mode 100644 index 0000000..b9887d0 --- /dev/null +++ b/packages/web-components/src/wallet-connection/connectKeplr.js @@ -0,0 +1,61 @@ +// @ts-check +import { Registry } from '@cosmjs/proto-signing'; +import { + SigningStargateClient, + AminoTypes, + defaultRegistryTypes, + createBankAminoConverters, + createAuthzAminoConverters, +} from '@cosmjs/stargate'; +import { Errors } from '../errors.js'; +import { agoricConverters, agoricRegistryTypes } from './signerOptions.js'; + +/** @typedef {import('@keplr-wallet/types').Keplr} Keplr */ + +/** + * + * @param {string} chainId + * @param {string} rpc + */ +export const connectKeplr = async (chainId, rpc) => { + if (!('keplr' in window)) { + throw Error(Errors.noKeplr); + } + /** @type {import('@keplr-wallet/types').Keplr} */ + // @ts-expect-error cast (checked above) + const keplr = window.keplr; + + await null; + try { + await keplr.enable(chainId); + } catch { + throw Error(Errors.enableKeplr); + } + + // Until we have SIGN_MODE_TEXTUAL, + // Use Amino because Direct results in ugly protobuf in the keplr UI. + const offlineSigner = await keplr.getOfflineSignerOnlyAmino(chainId); + console.debug('InteractiveSigner', { offlineSigner }); + + // Currently, Keplr extension manages only one address/public key pair. + const [account] = await offlineSigner.getAccounts(); + const { address } = account; + + const signingClient = await SigningStargateClient.connectWithSigner( + rpc, + offlineSigner, + { + aminoTypes: new AminoTypes({ + ...agoricConverters, + ...createBankAminoConverters(), + ...createAuthzAminoConverters(), + }), + registry: new Registry([...defaultRegistryTypes, ...agoricRegistryTypes]), + }, + ); + + return { + address, + client: signingClient, + }; +}; diff --git a/packages/web-components/src/wallet-connection/makeAgoricSigner.js b/packages/web-components/src/wallet-connection/makeAgoricSigner.js new file mode 100644 index 0000000..3023b82 --- /dev/null +++ b/packages/web-components/src/wallet-connection/makeAgoricSigner.js @@ -0,0 +1,123 @@ +// @ts-check +import { fromBech32, toBase64 } from '@cosmjs/encoding'; +import { assertIsDeliverTxSuccess } from '@cosmjs/stargate'; +import { stableCurrency } from './chainInfo.js'; +import { AgoricMsgs } from './signerOptions.js'; + +/** @typedef {import("@cosmjs/proto-signing").EncodeObject} EncodeObject */ +/** @typedef {import("@cosmjs/stargate").AminoConverters} AminoConverters */ +/** @typedef {import("@cosmjs/stargate").StdFee} StdFee */ +/** @typedef {import('@keplr-wallet/types').ChainInfo} ChainInfo */ +/** @typedef {import('@keplr-wallet/types').Keplr} Keplr */ +/** @typedef {import('@cosmjs/stargate').SigningStargateClient} SigningStargateClient */ + +/** + * @param {string} address + * @returns {Uint8Array} + */ +const toAccAddress = address => { + return fromBech32(address).data; +}; + +// XXX domain of @agoric/cosmic-proto +/** + * non-exhaustive list of powerFlags + * + * See also MsgProvision in golang/cosmos/proto/agoric/swingset/msgs.proto + */ +const PowerFlags = { + SMART_WALLET: 'SMART_WALLET', +}; + +/** @typedef {{owner: string, spendAction: string}} WalletSpendAction */ + +/** + * @returns {StdFee} + */ +const zeroFee = () => { + const { coinMinimalDenom: denom } = stableCurrency; + const fee = { + amount: [{ amount: '0', denom }], + gas: '300000', // TODO: estimate gas? + }; + return fee; +}; + +/** + * Use a signing client to + * @param {SigningStargateClient} signingClient + * @param {string} address + * Ref: https://docs.keplr.app/api/ + */ +export const makeAgoricSigner = (signingClient, address) => { + const fee = zeroFee(); + + return harden({ + /** + * Sign and broadcast Provision for a new smart wallet + * + * @throws if account does not exist on chain, user cancels, + * RPC connection fails, RPC service fails to broadcast ( + * for example, if signature verification fails) + */ + provisionSmartWallet: async () => { + const { accountNumber, sequence } = await signingClient.getSequence( + address, + ); + console.log({ accountNumber, sequence }); + + const b64address = toBase64(toAccAddress(address)); + + const act1 = { + typeUrl: AgoricMsgs.MsgProvision.typeUrl, + value: { + address: b64address, + nickname: 'my wallet', + powerFlags: [PowerFlags.SMART_WALLET], + submitter: b64address, + }, + }; + + const msgs = [act1]; + console.log('sign provision', { address, msgs, fee }); + + const tx = await signingClient.signAndBroadcast(address, msgs, fee); + console.log('spend action result tx', tx); + assertIsDeliverTxSuccess(tx); + + return tx; + }, + + /** + * Sign and broadcast WalletSpendAction + * + * @param {string} spendAction marshaled offer + * @throws if account does not exist on chain, user cancels, + * RPC connection fails, RPC service fails to broadcast ( + * for example, if signature verification fails) + */ + submitSpendAction: async spendAction => { + const { accountNumber, sequence } = await signingClient.getSequence( + address, + ); + console.debug({ accountNumber, sequence }); + + const act1 = { + typeUrl: AgoricMsgs.MsgWalletSpendAction.typeUrl, + value: { + owner: toBase64(toAccAddress(address)), + spendAction, + }, + }; + + const msgs = [act1]; + console.debug('sign spend action', { address, msgs, fee }); + + const tx = await signingClient.signAndBroadcast(address, msgs, fee); + console.debug('spend action result tx', tx); + assertIsDeliverTxSuccess(tx); + + return tx; + }, + }); +}; diff --git a/packages/web-components/src/wallet-connection/makeInteractiveSigner.js b/packages/web-components/src/wallet-connection/makeInteractiveSigner.js deleted file mode 100644 index d503efc..0000000 --- a/packages/web-components/src/wallet-connection/makeInteractiveSigner.js +++ /dev/null @@ -1,246 +0,0 @@ -import { fromBech32, toBech32, fromBase64, toBase64 } from '@cosmjs/encoding'; -import { Registry } from '@cosmjs/proto-signing'; -import { - AminoTypes, - defaultRegistryTypes, - assertIsDeliverTxSuccess, - createBankAminoConverters, - createAuthzAminoConverters, -} from '@cosmjs/stargate'; -import { - MsgWalletSpendAction, - MsgProvision, -} from '@agoric/cosmic-proto/swingset/msgs.js'; -import { stableCurrency, bech32Config } from './chainInfo.js'; -import { Errors } from '../errors.js'; - -/** @typedef {import("@cosmjs/proto-signing").EncodeObject} EncodeObject */ -/** @typedef {import("@cosmjs/stargate").AminoConverters} AminoConverters */ -/** @typedef {import("@cosmjs/stargate").StdFee} StdFee */ -/** @typedef {import('@keplr-wallet/types').ChainInfo} ChainInfo */ -/** @typedef {import('@keplr-wallet/types').Keplr} Keplr */ - -/** - * @param {string} address - * @returns {Uint8Array} - */ -const toAccAddress = address => { - return fromBech32(address).data; -}; - -// XXX domain of @agoric/cosmic-proto -/** - * non-exhaustive list of powerFlags - * - * See also MsgProvision in golang/cosmos/proto/agoric/swingset/msgs.proto - */ -const PowerFlags = { - SMART_WALLET: 'SMART_WALLET', -}; - -/** - * `/agoric.swingset.XXX` matches package agoric.swingset in swingset/msgs.proto - * aminoType taken from Type() in golang/cosmos/x/swingset/types/msgs.go - */ -const SwingsetMsgs = { - MsgWalletSpendAction: { - typeUrl: '/agoric.swingset.MsgWalletSpendAction', - aminoType: 'swingset/WalletSpendAction', - }, - MsgProvision: { - typeUrl: '/agoric.swingset.MsgProvision', - aminoType: 'swingset/Provision', - }, -}; - -/** @typedef {{owner: string, spendAction: string}} WalletSpendAction */ - -const SwingsetRegistry = new Registry([ - ...defaultRegistryTypes, - // XXX should this list be "upstreamed" to @agoric/cosmic-proto? - [SwingsetMsgs.MsgWalletSpendAction.typeUrl, MsgWalletSpendAction], - [SwingsetMsgs.MsgProvision.typeUrl, MsgProvision], -]); - -/** - * @returns {StdFee} - */ -const zeroFee = () => { - const { coinMinimalDenom: denom } = stableCurrency; - const fee = { - amount: [{ amount: '0', denom }], - gas: '300000', // TODO: estimate gas? - }; - return fee; -}; - -const dbg = label => x => { - console.log(label, x); - return x; -}; - -/** - * @type {AminoConverters} - */ -const SwingsetConverters = { - [SwingsetMsgs.MsgWalletSpendAction.typeUrl]: { - aminoType: SwingsetMsgs.MsgWalletSpendAction.aminoType, - toAmino: ({ spendAction, owner }) => ({ - spend_action: spendAction, - owner: toBech32(bech32Config.bech32PrefixAccAddr, fromBase64(owner)), - }), - fromAmino: ({ spend_action: spendAction, owner }) => ({ - spendAction, - owner: toBase64(toAccAddress(owner)), - }), - }, - [SwingsetMsgs.MsgProvision.typeUrl]: { - aminoType: SwingsetMsgs.MsgProvision.aminoType, - toAmino: protoVal => { - const { nickname, address, powerFlags, submitter } = dbg( - 'provision toAmino protoVal', - )(protoVal); - return { - address: toBech32( - bech32Config.bech32PrefixAccAddr, - fromBase64(address), - ), - nickname, - powerFlags, - submitter: toBech32( - bech32Config.bech32PrefixAccAddr, - fromBase64(submitter), - ), - }; - }, - fromAmino: aminoVal => { - const { nickname, address, powerFlags, submitter } = dbg( - 'provision fromAmino aminoVal', - )(aminoVal); - return { - address: toBase64(toAccAddress(address)), - nickname, - powerFlags, - submitter: toBase64(toAccAddress(submitter)), - }; - }, - }, -}; - -/** - * Use Keplr to sign offers and delegate object messaging to local storage key. - * @param {string} chainId - * @param {string} rpc - * @param {Keplr} keplr - * @param {typeof import('@cosmjs/stargate').SigningStargateClient.connectWithSigner} connectWithSigner - * Ref: https://docs.keplr.app/api/ - */ -export const makeInteractiveSigner = async ( - chainId, - rpc, - keplr, - connectWithSigner, -) => { - await null; - try { - await keplr.enable(chainId); - } catch { - throw Error(Errors.enableKeplr); - } - - const key = await keplr.getKey(chainId); - - // Until we have SIGN_MODE_TEXTUAL, - // Use Amino because Direct results in ugly protobuf in the keplr UI. - const offlineSigner = await keplr.getOfflineSignerOnlyAmino(chainId); - console.debug('InteractiveSigner', { offlineSigner }); - - // Currently, Keplr extension manages only one address/public key pair. - const [account] = await offlineSigner.getAccounts(); - const { address } = account; - - const converters = { - ...SwingsetConverters, - ...createBankAminoConverters(), - ...createAuthzAminoConverters(), - }; - const signingClient = await connectWithSigner(rpc, offlineSigner, { - aminoTypes: new AminoTypes(converters), - registry: SwingsetRegistry, - }); - console.debug('InteractiveSigner', { signingClient }); - - const fee = zeroFee(); - - return harden({ - address, // TODO: address can change - isNanoLedger: key.isNanoLedger, - - /** - * Sign and broadcast Provision for a new smart wallet - * - * @throws if account does not exist on chain, user cancels, - * RPC connection fails, RPC service fails to broadcast ( - * for example, if signature verification fails) - */ - provisionSmartWallet: async () => { - const { accountNumber, sequence } = await signingClient.getSequence( - address, - ); - console.log({ accountNumber, sequence }); - - const b64address = toBase64(toAccAddress(address)); - - const act1 = { - typeUrl: SwingsetMsgs.MsgProvision.typeUrl, - value: { - address: b64address, - nickname: 'my wallet', - powerFlags: [PowerFlags.SMART_WALLET], - submitter: b64address, - }, - }; - - const msgs = [act1]; - console.log('sign provision', { address, msgs, fee }); - - const tx = await signingClient.signAndBroadcast(address, msgs, fee); - console.log('spend action result tx', tx); - assertIsDeliverTxSuccess(tx); - - return tx; - }, - - /** - * Sign and broadcast WalletSpendAction - * - * @param {string} spendAction marshaled offer - * @throws if account does not exist on chain, user cancels, - * RPC connection fails, RPC service fails to broadcast ( - * for example, if signature verification fails) - */ - submitSpendAction: async spendAction => { - const { accountNumber, sequence } = await signingClient.getSequence( - address, - ); - console.debug({ accountNumber, sequence }); - - const act1 = { - typeUrl: SwingsetMsgs.MsgWalletSpendAction.typeUrl, - value: { - owner: toBase64(toAccAddress(address)), - spendAction, - }, - }; - - const msgs = [act1]; - console.debug('sign spend action', { address, msgs, fee }); - - const tx = await signingClient.signAndBroadcast(address, msgs, fee); - console.debug('spend action result tx', tx); - assertIsDeliverTxSuccess(tx); - - return tx; - }, - }); -}; diff --git a/packages/web-components/src/wallet-connection/signerOptions.js b/packages/web-components/src/wallet-connection/signerOptions.js new file mode 100644 index 0000000..6b23ca4 --- /dev/null +++ b/packages/web-components/src/wallet-connection/signerOptions.js @@ -0,0 +1,92 @@ +// @ts-check +import { fromBech32, toBech32, fromBase64, toBase64 } from '@cosmjs/encoding'; +import { + MsgWalletSpendAction, + MsgProvision, +} from '@agoric/cosmic-proto/swingset/msgs.js'; +import { bech32Config } from './chainInfo.js'; + +/** @typedef {import("@cosmjs/stargate").AminoConverters} AminoConverters */ +/** @typedef {import("@cosmjs/proto-signing").GeneratedType} GeneratedType } */ + +const dbg = label => x => { + console.debug(label, x); + return x; +}; + +/** + * @param {string} address + * @returns {Uint8Array} + */ +const toAccAddress = address => { + return fromBech32(address).data; +}; + +/** + * `/agoric.swingset.XXX` matches package agoric.swingset in swingset/msgs.proto + * aminoType taken from Type() in golang/cosmos/x/swingset/types/msgs.go + */ +export const AgoricMsgs = { + MsgWalletSpendAction: { + typeUrl: '/agoric.swingset.MsgWalletSpendAction', + aminoType: 'swingset/WalletSpendAction', + }, + MsgProvision: { + typeUrl: '/agoric.swingset.MsgProvision', + aminoType: 'swingset/Provision', + }, +}; + +/** @type {[string, GeneratedType][]} */ +export const agoricRegistryTypes = [ + [AgoricMsgs.MsgWalletSpendAction.typeUrl, MsgWalletSpendAction], + [AgoricMsgs.MsgProvision.typeUrl, MsgProvision], +]; + +/** + * @type {AminoConverters} + */ +export const agoricConverters = { + [AgoricMsgs.MsgWalletSpendAction.typeUrl]: { + aminoType: AgoricMsgs.MsgWalletSpendAction.aminoType, + toAmino: ({ spendAction, owner }) => ({ + spend_action: spendAction, + owner: toBech32(bech32Config.bech32PrefixAccAddr, fromBase64(owner)), + }), + fromAmino: ({ spend_action: spendAction, owner }) => ({ + spendAction, + owner: toBase64(toAccAddress(owner)), + }), + }, + [AgoricMsgs.MsgProvision.typeUrl]: { + aminoType: AgoricMsgs.MsgProvision.aminoType, + toAmino: protoVal => { + const { nickname, address, powerFlags, submitter } = dbg( + 'provision toAmino protoVal', + )(protoVal); + return { + address: toBech32( + bech32Config.bech32PrefixAccAddr, + fromBase64(address), + ), + nickname, + powerFlags, + submitter: toBech32( + bech32Config.bech32PrefixAccAddr, + fromBase64(submitter), + ), + }; + }, + fromAmino: aminoVal => { + const { nickname, address, powerFlags, submitter } = dbg( + 'provision fromAmino aminoVal', + )(aminoVal); + return { + address: toBase64(toAccAddress(address)), + nickname, + powerFlags, + submitter: toBase64(toAccAddress(submitter)), + }; + }, + }, +}; diff --git a/packages/web-components/src/wallet-connection/walletConnection.js b/packages/web-components/src/wallet-connection/walletConnection.js index 3381ae0..51842f8 100644 --- a/packages/web-components/src/wallet-connection/walletConnection.js +++ b/packages/web-components/src/wallet-connection/walletConnection.js @@ -1,34 +1,32 @@ // @ts-check -import { SigningStargateClient } from '@cosmjs/stargate'; import { subscribeLatest } from '@agoric/notifier'; -import { makeInteractiveSigner } from './makeInteractiveSigner.js'; +import { makeAgoricSigner } from './makeAgoricSigner.js'; import { watchWallet } from './watchWallet.js'; -import { Errors } from '../errors.js'; +import { connectKeplr } from './connectKeplr.js'; + +/** @typedef {import("@cosmjs/stargate").SigningStargateClient} SigningStargateClient */ +/** @typedef {{client: SigningStargateClient, address: string }} ClientConfig */ /** * @param {any} chainStorageWatcher * @param {string} rpc * @param {((error: unknown) => void)} [onError] + * @param {ClientConfig} [clientConfig] */ export const makeAgoricWalletConnection = async ( chainStorageWatcher, rpc, onError = undefined, + clientConfig = undefined, ) => { - if (!('keplr' in window)) { - throw Error(Errors.noKeplr); - } - /** @type {import('@keplr-wallet/types').Keplr} */ - // @ts-expect-error cast (checked above) - const keplr = window.keplr; + await null; + const { client, address } = + clientConfig || (await connectKeplr(chainStorageWatcher.chainId, rpc)); - const { address, submitSpendAction, provisionSmartWallet } = - await makeInteractiveSigner( - chainStorageWatcher.chainId, - rpc, - keplr, - SigningStargateClient.connectWithSigner, - ); + const { submitSpendAction, provisionSmartWallet } = await makeAgoricSigner( + client, + address, + ); const walletNotifiers = watchWallet( chainStorageWatcher, @@ -114,6 +112,7 @@ export const makeAgoricWalletConnection = async ( makeOffer, address, provisionSmartWallet, + signingClient: client, ...walletNotifiers, }; }; diff --git a/packages/web-components/test/walletConnection.test.js b/packages/web-components/test/walletConnection.test.js index 82305ae..32b2774 100644 --- a/packages/web-components/test/walletConnection.test.js +++ b/packages/web-components/test/walletConnection.test.js @@ -1,16 +1,17 @@ +// @ts-check import './installSesLockdown.js'; import { describe, vi, expect, it } from 'vitest'; -import { SigningStargateClient } from '@cosmjs/stargate'; import { makeAgoricWalletConnection } from '../src/wallet-connection/walletConnection.js'; import { - makeInteractiveSigner, + makeAgoricSigner, // @ts-expect-error exported by mock below // eslint-disable-next-line import/named submitSpendAction as mockSubmitSpendAction, // @ts-expect-error exported by mock below // eslint-disable-next-line import/named provisionSmartWallet as mockProvisionSmartWallet, -} from '../src/wallet-connection/makeInteractiveSigner.js'; +} from '../src/wallet-connection/makeAgoricSigner.js'; +import { Errors } from '../src/errors.js'; const testAddress = 'agoric123test'; const rpc = 'https://fake.rpc'; @@ -19,12 +20,12 @@ const rpc = 'https://fake.rpc'; // eslint-disable-next-line no-undef global.window = { keplr: {} }; -vi.mock('../src/wallet-connection/makeInteractiveSigner.js', () => { +vi.mock('../src/wallet-connection/makeAgoricSigner.js', () => { const submitSpendAction = vi.fn(); const provisionSmartWallet = vi.fn(); return { - makeInteractiveSigner: vi.fn(() => ({ + makeAgoricSigner: vi.fn(() => ({ address: testAddress, submitSpendAction, provisionSmartWallet, @@ -41,7 +42,7 @@ vi.mock('../src/queryBankBalances.js', () => { }); describe('makeAgoricWalletConnection', () => { - it('gets the address from keplr', async () => { + it('defaults to keplr connection if no client config', async () => { const watcher = { chainId: 'agoric-foo', watchLatest: (_path, onUpdate) => { @@ -49,15 +50,29 @@ describe('makeAgoricWalletConnection', () => { }, }; - const connection = await makeAgoricWalletConnection(watcher, rpc); + // Don't bother faking keplr, just expect it to try to enable and fail. + await expect(() => + makeAgoricWalletConnection(watcher, rpc), + ).rejects.toThrowError(Errors.enableKeplr); + }); - expect(makeInteractiveSigner).toHaveBeenCalledWith( - watcher.chainId, + it('gets the address from the client config', async () => { + const watcher = { + chainId: 'agoric-foo', + watchLatest: (_path, onUpdate) => { + onUpdate({ offerToPublicSubscriberPaths: 'foo' }); + }, + }; + + const connection = await makeAgoricWalletConnection( + watcher, rpc, - // @ts-expect-error shim keplr - window.keplr, - SigningStargateClient.connectWithSigner, + undefined, + // @ts-expect-error fake SigningStargateClient + { address: testAddress, client: {} }, ); + + expect(makeAgoricSigner).toHaveBeenCalledWith({}, testAddress); expect(connection.address).toEqual(testAddress); }); @@ -72,7 +87,13 @@ describe('makeAgoricWalletConnection', () => { }, }; - const connection = await makeAgoricWalletConnection(watcher, rpc); + const connection = await makeAgoricWalletConnection( + watcher, + rpc, + undefined, + // @ts-expect-error fake SigningStargateClient + { address: testAddress, client: {} }, + ); const onStatusChange = () => { expect(mockSubmitSpendAction).toHaveBeenCalledWith( @@ -101,7 +122,13 @@ it('submits a spend action', async () => { }, }; - const connection = await makeAgoricWalletConnection(watcher, rpc); + const connection = await makeAgoricWalletConnection( + watcher, + rpc, + undefined, + // @ts-expect-error fake SigningStargateClient + { address: testAddress, client: {} }, + ); connection.provisionSmartWallet(); expect(mockProvisionSmartWallet).toHaveBeenCalled();