UNPKG

accounts

Version:

Tempo Accounts SDK

789 lines (697 loc) 27.6 kB
import { Address as core_Address, Hex, Secp256k1, Signature } from 'ox' import { decodeFunctionData } from 'viem' import type { Address } from 'viem/accounts' import { Abis } from 'viem/tempo' import { describe, expect, test } from 'vp/test' import * as Storage from '../Storage.js' import * as Store from '../Store.js' import { privy } from './privy.js' // Deterministic test keys so addresses and signatures are reproducible across // runs. Real signing is required by upcoming signer-recovery validation, and // keeps the mocks honest about what the production adapter sees from Privy. const privateKeyA = Hex.padLeft('0x01', 32) const privateKeyB = Hex.padLeft('0x02', 32) const address = core_Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey: privateKeyA })) const other = core_Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey: privateKeyB })) function signWithKey(privateKey: Hex.Hex, payload: Hex.Hex): Hex.Hex { const signature = Secp256k1.sign({ payload, privateKey }) return Signature.toHex(signature) } function privateKeyForAddress(walletAddress: string): Hex.Hex { if (core_Address.from(walletAddress) === address) return privateKeyA if (core_Address.from(walletAddress) === other) return privateKeyB throw new Error(`No test private key for ${walletAddress}`) } describe('privy', () => { test('default: createAccount delegates registration and signs the requested digest', async () => { const { adapter, client } = setup() const result = await adapter.actions.createAccount( { digest: '0x1234', name: 'Ada' }, { method: 'wallet_connect', params: undefined }, ) expect(client.initCalls).toMatchInlineSnapshot(`1`) expect(client.signPayloads).toMatchInlineSnapshot(` [ "0x1234", ] `) expect(result).toMatchInlineSnapshot(` { "accounts": [ { "address": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf", "label": "Ada", }, ], "signature": "0xced9d002f487622c7e218274065c327bdfe274ea7da91349bb48fe7c4495baeb71cc6b2f9b3d5f34e5b404cec0ed0dcb085f990a7b7a7f4cb81a5e8abb76aa981b", } `) }) test('default: createAccount falls back to loadAccounts when not provided', async () => { const { adapter, client } = setup({ createAccount: false }) const result = await adapter.actions.createAccount( { digest: '0x1234', name: 'Ada' }, { method: 'wallet_connect', params: undefined }, ) expect(client.createCalls).toMatchInlineSnapshot(`0`) expect(client.loadCalls).toMatchInlineSnapshot(`1`) expect(client.signPayloads).toMatchInlineSnapshot(` [ "0x1234", ] `) expect(result.accounts).toMatchInlineSnapshot(` [ { "address": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf", "label": "Ada", }, ] `) }) test('default: createAccount can select the active embedded wallet', async () => { const { adapter, client } = setup({ createAddresses: [other] }) client.addWallet(other) const result = await adapter.actions.createAccount( { digest: '0x1234', name: 'Ada' }, { method: 'wallet_connect', params: undefined }, ) expect(client.signWith).toMatchInlineSnapshot(` [ "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf", ] `) expect(result.accounts).toMatchInlineSnapshot(` [ { "address": "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf", "label": "Ada", }, ] `) }) test('default: loadAccounts delegates login and caches embedded wallets for signing', async () => { const { adapter, client, store } = setup() await connect({ adapter, store }) const result = await adapter.actions.signPersonalMessage( { address, data: '0x68656c6c6f' }, { method: 'personal_sign', params: ['0x68656c6c6f', address] }, ) expect(client.loadCalls).toMatchInlineSnapshot(`1`) expect(client.signWith).toMatchInlineSnapshot(` [ "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf", ] `) expect(result).toMatchInlineSnapshot( `"0xe5ddc160e4c8f92de507c7db9b982d4f9b7197bfa421864aeadc586bc96b09ae0ba0c5b131650ae4994cff1839341d00f3735ef5abc62ac8fe2cf50f65208e2a1b"`, ) }) test('default: loadAccounts can select and order embedded wallets', async () => { const { adapter, client } = setup({ loadAddresses: [other, address] }) client.addWallet(other) const result = await adapter.actions.loadAccounts( { digest: '0x1234' }, { method: 'wallet_connect', params: undefined }, ) expect(client.signWith).toMatchInlineSnapshot(` [ "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf", ] `) expect(result.accounts).toMatchInlineSnapshot(` [ { "address": "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf", }, { "address": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf", }, ] `) }) test('default: loadAccounts can provision an external access key', async () => { const { adapter, client } = setup() const result = await adapter.actions.loadAccounts( { authorizeAccessKey: { address: other, expiry: 123, keyType: 'secp256k1', }, }, { method: 'wallet_connect', params: undefined }, ) expect(client.signPayloads).toMatchInlineSnapshot(` [ "0xe77ac2b1d13a90cbd8c4912ff18d0d044cc89c5c6781941001640b8d251f3783", ] `) expect(result).toMatchInlineSnapshot(` { "accounts": [ { "address": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf", }, ], "keyAuthorization": { "chainId": "0x1", "expiry": "0x7b", "keyId": "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf", "keyType": "secp256k1", "limits": undefined, "signature": { "r": "0xb364cd8e50555239adf9f7d655b018ea386764d44ed9b56e894f4a101f0b1a6b", "s": "0x4910cc8497358eb73a08df09c9cfb2618e3c949b3847ab310ad7ab0d76a9c624", "type": "secp256k1", "yParity": "0x1", }, }, "signature": undefined, } `) }) test('default: signs transactions with a materialized Privy account', async () => { const { adapter, client, store } = setup() await connect({ adapter, store }) const result = await adapter.actions.signTransaction( { chainId: 1, from: address, gas: 21_000n, maxFeePerGas: 1n, maxPriorityFeePerGas: 1n, nonce: 0, to: other, value: 1n, }, { method: 'eth_signTransaction', params: [{ from: address }] }, ) expect(client.signWith).toMatchInlineSnapshot(` [ "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf", ] `) expect(client.signPayloads).toMatchInlineSnapshot(` [ "0x62f087d34b8a023e0461eb1b9a01267ba5b8400c13d2ffdac615ccec872cc288", ] `) expect(result).toMatchInlineSnapshot( `"0x76f86a010101825208d8d7942b5ad5c4795c026514f8317c7a215e218dccd6cf0180c0808080808080c0b8418b0c18077cb78666296a4c0e8149935124f70d7820ec4e4ae81de428659d6c305c098dea43ccb536e813bb1c136eab5e96611de69489be6291169b2bb2318cde1b"`, ) }) test('default: authorizeAccessKey signs with the connected Privy account', async () => { const { adapter, client, store } = setup() store.setState({ accounts: [{ address }], activeAccount: 0 }) const result = await adapter.actions.authorizeAccessKey!( { address: other, expiry: 123, keyType: 'secp256k1', }, { method: 'wallet_authorizeAccessKey', params: [{ expiry: 123 }] }, ) expect(client.loadCalls).toMatchInlineSnapshot(`0`) expect(client.restoreCalls).toMatchInlineSnapshot(`1`) expect(result).toMatchInlineSnapshot(` { "keyAuthorization": { "chainId": "0x1", "expiry": "0x7b", "keyId": "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf", "keyType": "secp256k1", "limits": undefined, "signature": { "r": "0xb364cd8e50555239adf9f7d655b018ea386764d44ed9b56e894f4a101f0b1a6b", "s": "0x4910cc8497358eb73a08df09c9cfb2618e3c949b3847ab310ad7ab0d76a9c624", "type": "secp256k1", "yParity": "0x1", }, }, "rootAddress": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf", } `) }) test('error: secp256k1 access key requires external key material', async () => { const { adapter, store } = setup() store.setState({ accounts: [{ address }], activeAccount: 0 }) await expect( adapter.actions.authorizeAccessKey!( { expiry: 123, keyType: 'secp256k1' }, { method: 'wallet_authorizeAccessKey', params: [{ expiry: 123, keyType: 'secp256k1' }] }, ), ).rejects.toThrowErrorMatchingInlineSnapshot( `[RpcResponse.InvalidParamsError: \`keyType: "secp256k1"\` requires externally generated key material; provide \`publicKey\` or \`address\`.]`, ) }) test('default: revokeAccessKey revokes with the connected Privy account', async () => { const { adapter, client, store } = setup() store.setState({ accounts: [{ address }], activeAccount: 0, accessKeys: [ { access: address, address: other, chainId: 1, keyType: 'secp256k1', } as never, ], }) await adapter.actions.revokeAccessKey!( { accessKeyAddress: other, address }, { method: 'wallet_revokeAccessKey', params: [{ accessKeyAddress: other, address }] }, ) const transaction = client.transactions[0] as | { account: { address: Address }; data: Hex.Hex; to: Address } | undefined const decoded = transaction ? decodeFunctionData({ abi: Abis.accountKeychain, data: transaction.data }) : undefined expect( transaction && decoded && { account: transaction.account.address, args: decoded.args, functionName: decoded.functionName, to: transaction.to, }, ).toMatchInlineSnapshot(` { "account": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf", "args": [ "0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF", ], "functionName": "revokeKey", "to": "0xaAAAaaAA00000000000000000000000000000000", } `) expect(store.getState().accessKeys).toMatchInlineSnapshot(`[]`) }) test('behavior: signing silently restores wallet accounts via the Privy SDK', async () => { const { adapter, client, store } = setup() store.setState({ accounts: [{ address }], activeAccount: 0 }) await adapter.actions.signPersonalMessage( { address, data: '0x68656c6c6f' }, { method: 'personal_sign', params: ['0x68656c6c6f', address] }, ) expect(client.restoreCalls).toMatchInlineSnapshot(`1`) expect(client.loadCalls).toMatchInlineSnapshot(`0`) }) test('behavior: silent restore does not connect accounts when the provider store is empty', async () => { const { adapter, client } = setup() await expect( adapter.actions.signPersonalMessage( { address, data: '0x68656c6c6f' }, { method: 'personal_sign', params: ['0x68656c6c6f', address] }, ), ).rejects.toMatchInlineSnapshot('[Provider.DisconnectedError: No Privy account connected.]') expect(client.loadCalls).toMatchInlineSnapshot(`0`) expect(client.restoreCalls).toMatchInlineSnapshot(`0`) expect(client.signPayloads).toMatchInlineSnapshot(`[]`) }) test('behavior: silent restore only reconnects persisted provider accounts', async () => { const { adapter, client, store } = setup() client.addWallet(other) store.setState({ accounts: [{ address: other }], activeAccount: 0 }) await adapter.actions.signPersonalMessage( { address: other, data: '0x68656c6c6f' }, { method: 'personal_sign', params: ['0x68656c6c6f', other] }, ) expect(client.signWith).toMatchInlineSnapshot(` [ "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf", ] `) expect(store.getState().accounts).toMatchInlineSnapshot(` [ { "address": "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf", }, ] `) }) test('behavior: expired sessions clear provider accounts', async () => { const { adapter, client, store } = setup({ token: null }) store.setState({ accounts: [{ address }], activeAccount: 0 }) await expect( adapter.actions.signPersonalMessage( { address, data: '0x68656c6c6f' }, { method: 'personal_sign', params: ['0x68656c6c6f', address] }, ), ).rejects.toMatchInlineSnapshot('[Provider.DisconnectedError: Privy session expired.]') expect(client.signPayloads).toMatchInlineSnapshot(`[]`) expect(store.getState().accounts).toMatchInlineSnapshot(`[]`) }) test('behavior: server session errors clear provider accounts', async () => { const { adapter, store } = setup({ signError: { code: 'embedded_wallet_request_error' } }) store.setState({ accounts: [{ address }], activeAccount: 0 }) await expect( adapter.actions.signPersonalMessage( { address, data: '0x68656c6c6f' }, { method: 'personal_sign', params: ['0x68656c6c6f', address] }, ), ).rejects.toMatchInlineSnapshot('[Provider.DisconnectedError: Privy session expired.]') expect(store.getState().accounts).toMatchInlineSnapshot(`[]`) }) test('behavior: session errors are recognized via fuzzy code match', async () => { const { adapter, store } = setup({ signError: { code: 'session_invalid_token' } }) store.setState({ accounts: [{ address }], activeAccount: 0 }) await expect( adapter.actions.signPersonalMessage( { address, data: '0x68656c6c6f' }, { method: 'personal_sign', params: ['0x68656c6c6f', address] }, ), ).rejects.toMatchInlineSnapshot('[Provider.DisconnectedError: Privy session expired.]') expect(store.getState().accounts).toMatchInlineSnapshot(`[]`) }) test('behavior: session errors are recognized via nested cause messages', async () => { const inner = new Error('User must be logged in to sign.') const outer = new Error('Wallet operation failed', { cause: inner }) const { adapter, store } = setup({ signError: outer }) store.setState({ accounts: [{ address }], activeAccount: 0 }) await expect( adapter.actions.signPersonalMessage( { address, data: '0x68656c6c6f' }, { method: 'personal_sign', params: ['0x68656c6c6f', address] }, ), ).rejects.toMatchInlineSnapshot('[Provider.DisconnectedError: Privy session expired.]') expect(store.getState().accounts).toMatchInlineSnapshot(`[]`) }) test('behavior: session errors are recognized via message fallback', async () => { const { adapter, store } = setup({ signError: Object.assign(new Error('User must be logged in to sign.'), {}), }) store.setState({ accounts: [{ address }], activeAccount: 0 }) await expect( adapter.actions.signPersonalMessage( { address, data: '0x68656c6c6f' }, { method: 'personal_sign', params: ['0x68656c6c6f', address] }, ), ).rejects.toMatchInlineSnapshot('[Provider.DisconnectedError: Privy session expired.]') expect(store.getState().accounts).toMatchInlineSnapshot(`[]`) }) test('behavior: silent restore clears stale persisted accounts when Privy no longer has them', async () => { const { adapter, client, store } = setup() // Persisted address that is NOT linked on the Privy user. store.setState({ accounts: [{ address: other }], activeAccount: 0 }) await expect( adapter.actions.signPersonalMessage( { address: other, data: '0x68656c6c6f' }, { method: 'personal_sign', params: ['0x68656c6c6f', other] }, ), ).rejects.toMatchInlineSnapshot( '[Provider.DisconnectedError: Privy session no longer matches persisted accounts.]', ) expect(client.signPayloads).toMatchInlineSnapshot(`[]`) // Stale persisted accounts are wiped so the adapter and store agree. expect(store.getState().accounts).toMatchInlineSnapshot(`[]`) }) test('behavior: failed account selection does not poison silent restore cache', async () => { const { adapter, store } = setup({ loadAddresses: [other] }) store.setState({ accounts: [{ address: other }], activeAccount: 0 }) await expect( adapter.actions.loadAccounts(undefined, { method: 'wallet_connect', params: undefined }), ).rejects.toThrowErrorMatchingInlineSnapshot( `[Provider.UnauthorizedError: Privy callback returned address "${other}" that was not found in the user's embedded wallets.]`, ) await expect( adapter.actions.signPersonalMessage( { address: other, data: '0x68656c6c6f' }, { method: 'personal_sign', params: ['0x68656c6c6f', other] }, ), ).rejects.toMatchInlineSnapshot( '[Provider.DisconnectedError: Privy session no longer matches persisted accounts.]', ) expect(store.getState().accounts).toMatchInlineSnapshot(`[]`) }) test('behavior: failed empty createAccount selection does not poison silent restore cache', async () => { const { adapter, store } = setup({ createAddresses: [] }) store.setState({ accounts: [{ address: other }], activeAccount: 0 }) await expect( adapter.actions.createAccount( { digest: '0x1234', name: 'Ada' }, { method: 'wallet_connect', params: undefined }, ), ).rejects.toThrowErrorMatchingInlineSnapshot( `[Provider.DisconnectedError: Privy returned no wallet.]`, ) await expect( adapter.actions.signPersonalMessage( { address: other, data: '0x68656c6c6f' }, { method: 'personal_sign', params: ['0x68656c6c6f', other] }, ), ).rejects.toMatchInlineSnapshot( '[Provider.DisconnectedError: Privy session no longer matches persisted accounts.]', ) expect(store.getState().accounts).toMatchInlineSnapshot(`[]`) }) test('error: silent restore rejects non-hex secp256k1_sign results', async () => { const { adapter, store } = setup({ signResult: 'not-hex' }) store.setState({ accounts: [{ address }], activeAccount: 0 }) await expect( adapter.actions.signPersonalMessage( { address, data: '0x68656c6c6f' }, { method: 'personal_sign', params: ['0x68656c6c6f', address] }, ), ).rejects.toMatchInlineSnapshot( '[ProviderRpcError: Privy provider returned a non-hex secp256k1_sign result.]', ) }) test('error: app-returned wallet with malformed address is rejected at connect', async () => { const { adapter, client } = setup() client.wallets = [client.makeWallet('0xnot-an-address')] await expect( adapter.actions.loadAccounts(undefined, { method: 'wallet_connect', params: undefined }), ).rejects.toThrowError(/Address.*invalid/i) }) test('error: malformed secp256k1_sign result is rejected by signer recovery', async () => { const { adapter, store } = setup({ signResult: '0x1234' }) await connect({ adapter, store }) await expect( adapter.actions.signPersonalMessage( { address, data: '0x68656c6c6f' }, { method: 'personal_sign', params: ['0x68656c6c6f', address] }, ), ).rejects.toMatchInlineSnapshot( `[Provider.UnauthorizedError: Privy provider returned a signature for "unknown" that does not match the requested wallet "${address}".]`, ) }) test('error: signing for an unconnected address while others are connected throws Unauthorized', async () => { const { adapter, store } = setup() await connect({ adapter, store }) await expect( adapter.actions.signPersonalMessage( { address: other, data: '0x68656c6c6f' }, { method: 'personal_sign', params: ['0x68656c6c6f', other] }, ), ).rejects.toThrowErrorMatchingInlineSnapshot( `[Provider.UnauthorizedError: Account "${other}" not found.]`, ) }) test('error: unsupported secp256k1_sign maps to UnsupportedMethodError', async () => { const { adapter, store } = setup({ signError: { code: 4200, message: 'Method not supported' } }) store.setState({ accounts: [{ address }], activeAccount: 0 }) await expect( adapter.actions.signPersonalMessage( { address, data: '0x68656c6c6f' }, { method: 'personal_sign', params: ['0x68656c6c6f', address] }, ), ).rejects.toMatchInlineSnapshot( '[Provider.UnsupportedMethodError: Privy adapter requires raw secp256k1 hash signing via `secp256k1_sign` for Tempo transactions and access keys.]', ) }) test('disconnect: clears provider accounts and logs the user out of Privy', async () => { const { adapter, client, store } = setup() store.setState({ accounts: [{ address }], activeAccount: 0 }) await adapter.actions.disconnect!() expect(client.logoutCalls).toMatchInlineSnapshot(`1`) expect(store.getState().accounts).toMatchInlineSnapshot(`[]`) }) test('behavior: restore surfaces silent-restore session errors as `Privy session expired.`', async () => { const { adapter, store } = setup({ restoreError: Object.assign(new Error('boom'), { code: 'session_expired' }), }) store.setState({ accounts: [{ address }], activeAccount: 0 }) await expect( adapter.actions.signPersonalMessage( { address, data: '0x68656c6c6f' }, { method: 'personal_sign', params: ['0x68656c6c6f', address] }, ), ).rejects.toMatchInlineSnapshot('[Provider.DisconnectedError: Privy session expired.]') expect(store.getState().accounts).toMatchInlineSnapshot(`[]`) }) test('error: signature recovered from a different key is rejected as Unauthorized', async () => { const { adapter, store } = setup({ signWithPrivateKey: privateKeyB }) await connect({ adapter, store }) await expect( adapter.actions.signPersonalMessage( { address, data: '0x68656c6c6f' }, { method: 'personal_sign', params: ['0x68656c6c6f', address] }, ), ).rejects.toThrowErrorMatchingInlineSnapshot( `[Provider.UnauthorizedError: Privy provider returned a signature for "${other}" that does not match the requested wallet "${address}".]`, ) }) }) function setup(options: setup.Options = {}) { const storage = Storage.memory() const store = Store.create({ chainId: 1, storage }) const client = createClient(options) const adapter = privy({ client, ...(options.createAccount === false ? {} : { createAccount: async () => { client.createCalls++ return options.createAddresses }, }), loadAccounts: async () => { client.loadCalls++ return options.loadAddresses }, })({ getAccount: (() => { throw new Error('not implemented') }) as never, getClient: (() => ({ chain: { id: 1 }, sendTransaction: async (parameters: unknown) => { client.transactions.push(parameters) return Hex.padLeft('0x1', 32) }, })) as never, storage, store, }) return { adapter, client, store } } async function connect(options: Pick<ReturnType<typeof setup>, 'adapter' | 'store'>) { const { adapter, store } = options const loaded = await adapter.actions.loadAccounts(undefined, { method: 'wallet_connect', params: undefined, }) store.setState({ accounts: loaded.accounts, activeAccount: 0 }) return loaded } declare namespace setup { type Options = { /** Pass `false` to omit the adapter's `createAccount` callback (tests fallback to `loadAccounts`). */ createAccount?: false | undefined createAddresses?: readonly Address[] | undefined loadAddresses?: readonly Address[] | undefined /** Make the mock client's `user.get` throw, to test restore-side session errors. */ restoreError?: unknown token?: string | null | undefined signError?: unknown /** Override the value returned by the embedded provider's `secp256k1_sign`. */ signResult?: unknown /** Force the test wallet to sign with this private key (for wrong-signer tests). */ signWithPrivateKey?: Hex.Hex | undefined } } type MockClient = privy.Client & { createCalls: number initCalls: number loadCalls: number logoutCalls: number logoutWith: (string | undefined)[] restoreCalls: number signPayloads: Hex.Hex[] signWith: string[] transactions: unknown[] wallets: privy.EmbeddedWallet[] makeWallet: (address: string) => privy.EmbeddedWallet addWallet: (address: string) => void } function createClient(options: setup.Options = {}) { const client: MockClient = { createCalls: 0, initCalls: 0, loadCalls: 0, logoutCalls: 0, logoutWith: [] as (string | undefined)[], restoreCalls: 0, signPayloads: [] as Hex.Hex[], signWith: [] as string[], transactions: [] as unknown[], wallets: [] as privy.EmbeddedWallet[], makeWallet(address: string): privy.EmbeddedWallet { return { address, provider: { async request(req: { method: string params?: readonly unknown[] | undefined }): Promise<unknown> { if (req.method !== 'secp256k1_sign') throw new Error(`unexpected method: ${req.method}`) if (options.signError) throw options.signError const hash = (req.params as readonly Hex.Hex[])[0] as Hex.Hex client.signPayloads.push(hash) client.signWith.push(address) if (options.signResult !== undefined) return options.signResult const privateKey = options.signWithPrivateKey ?? (() => { try { return privateKeyForAddress(address) } catch { return privateKeyA } })() return signWithKey(privateKey, hash) }, }, } }, /** Adds an embedded wallet so silent restore (`user.get`) returns it. */ addWallet(address: string) { client.wallets.push(client.makeWallet(address)) }, auth: { logout(parameters?: { userId: string } | undefined) { client.logoutCalls++ client.logoutWith.push(parameters?.userId) }, }, embeddedWallet: { async getEthereumProvider({ wallet }) { const existing = client.wallets.find( (w) => core_Address.from(w.address) === core_Address.from(wallet.address as string), ) return (existing ?? client.makeWallet(wallet.address as string)).provider }, }, async getAccessToken() { return options.token === undefined ? 'token' : options.token }, initialize() { client.initCalls++ }, user: { async get() { client.restoreCalls++ if (options.restoreError) throw options.restoreError return { user: { id: 'user_1', linked_accounts: client.wallets.map((wallet, index) => ({ address: wallet.address, chain_type: 'ethereum', connector_type: 'embedded', type: 'wallet', wallet_client_type: 'privy', wallet_index: index, })), }, } }, }, } client.addWallet(address) return client }