kokio-sdk 3.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +15 -15
  2. package/dist/esm/admin/config-admin.js +1 -1
  3. package/dist/esm/config.js +1 -1
  4. package/dist/esm/interface/deviceWalletClass.js +10 -1
  5. package/dist/esm/interface/eSIMWalletClass.js +9 -1
  6. package/dist/esm/interface/eSIMWalletFactoryClass.js +9 -1
  7. package/dist/esm/interface/registryClass.js +8 -0
  8. package/dist/esm/interface/smartAccountClass.js +9 -2
  9. package/dist/esm/logic/P256Verifier.js +2 -2
  10. package/dist/esm/logic/account-kit/createSmartAccount.js +47 -36
  11. package/dist/esm/logic/admin/deviceWallet.eoa.js +5 -5
  12. package/dist/esm/logic/admin/deviceWalletFactory.eoa.js +13 -13
  13. package/dist/esm/logic/admin/eSIMWallet.eoa.js +3 -3
  14. package/dist/esm/logic/admin/eSIMWalletFactory.eoa.js +9 -9
  15. package/dist/esm/logic/admin/lazyWalletRegistry.eoa.js +13 -13
  16. package/dist/esm/logic/admin/paymentAdapter.eoa.js +11 -11
  17. package/dist/esm/logic/admin/protocolAdmin.eoa.js +14 -14
  18. package/dist/esm/logic/admin/reads/deviceWalletFactory.reads.js +9 -9
  19. package/dist/esm/logic/admin/reads/eSIMWalletFactory.reads.js +7 -7
  20. package/dist/esm/logic/admin/reads/lazyWalletRegistry.reads.js +18 -18
  21. package/dist/esm/logic/admin/reads/paymentAdapter.reads.js +8 -8
  22. package/dist/esm/logic/admin/reads/protocolAdmin.reads.js +2 -2
  23. package/dist/esm/logic/admin/reads/registry.reads.js +31 -31
  24. package/dist/esm/logic/admin/registry.eoa.js +27 -27
  25. package/dist/esm/logic/constants.js +14 -1
  26. package/dist/esm/logic/deviceWallet.js +35 -1
  27. package/dist/esm/logic/deviceWalletFactory.js +11 -11
  28. package/dist/esm/logic/eSIMWallet.js +50 -2
  29. package/dist/esm/logic/eSIMWalletFactory.js +15 -3
  30. package/dist/esm/logic/errors.js +15 -9
  31. package/dist/esm/logic/paymentAdapter.js +8 -8
  32. package/dist/esm/logic/registry.js +17 -17
  33. package/dist/esm/logic/utils.js +1 -81
  34. package/dist/esm/types-export.js +1 -1
  35. package/dist/types/admin/config-admin.d.ts +1 -1
  36. package/dist/types/config.d.ts +1 -1
  37. package/dist/types/interface/deviceWalletClass.d.ts +12 -0
  38. package/dist/types/interface/eSIMWalletClass.d.ts +7 -1
  39. package/dist/types/interface/eSIMWalletFactoryClass.d.ts +6 -0
  40. package/dist/types/interface/registryClass.d.ts +8 -0
  41. package/dist/types/interface/smartAccountClass.d.ts +10 -1
  42. package/dist/types/logic/account-kit/createSmartAccount.d.ts +1 -1
  43. package/dist/types/logic/constants.d.ts +3 -0
  44. package/dist/types/logic/deviceWallet.d.ts +12 -0
  45. package/dist/types/logic/eSIMWallet.d.ts +8 -0
  46. package/dist/types/logic/eSIMWalletFactory.d.ts +2 -0
  47. package/dist/types/logic/errors.d.ts +6 -5
  48. package/dist/types/logic/utils.d.ts +0 -21
  49. package/dist/types/types-export.d.ts +3 -2
  50. package/dist/types/types.d.ts +1 -1
  51. package/package.json +21 -7
package/README.md CHANGED
@@ -22,13 +22,19 @@ surface, with a code example and return type, see the
22
22
 
23
23
  ## Installation
24
24
 
25
+ In the mobile app:
26
+
27
+ ```sh
28
+ npm install kokio-sdk react-native-passkey
29
+ ```
30
+
31
+ On a backend that only uses `kokio-sdk/admin`:
32
+
25
33
  ```sh
26
34
  npm install kokio-sdk
27
35
  ```
28
36
 
29
- The package ships as ES modules and requires Node 18 or newer (or a React Native
30
- runtime). `viem` is bundled as a dependency, so you do not need to install it
31
- separately.
37
+ The package ships as ES modules and requires Node 18 or newer (or a React Native runtime). `viem` is bundled as a dependency, so you do not need to install it separately. `react-native-passkey` is a native module, so the app installs it itself: Expo only links native modules the app lists directly, and one copy avoids version clashes.
32
38
 
33
39
  ## Mobile client (Expo / React Native)
34
40
 
@@ -39,10 +45,9 @@ is ever held in the app.
39
45
 
40
46
  You will need:
41
47
 
42
- - a viem `WalletClient` connected to the target chain, carrying an `account`
43
- and an explicit RPC URL (see the note under the example),
48
+ - a viem `WalletClient` connected to the target chain with an explicit RPC URL (see the note under the example). It needs no `account`: the passkey signs every user operation.
44
49
  - the passkey `credentialId` and `rpId` registered for the device,
45
- - a Pimlico API key and a gas policy id (used by the bundler and paymaster).
50
+ - a Pimlico API key, and optionally a Pimlico sponsorship policy id (`sp_...`). Pass `""` to be sponsored without a policy.
46
51
 
47
52
  ```ts
48
53
  import { Kokio } from "kokio-sdk";
@@ -50,7 +55,6 @@ import { createWalletClient, http } from "viem";
50
55
  import { baseSepolia } from "viem/chains";
51
56
 
52
57
  const walletClient = createWalletClient({
53
- account: knownAddress,
54
58
  chain: baseSepolia,
55
59
  transport: http(rpcUrl),
56
60
  });
@@ -90,15 +94,11 @@ const receipt = await smartAccountClient.waitForUserOperationReceipt({ hash });
90
94
  if (!receipt.success) throw new Error("operation reverted");
91
95
  ```
92
96
 
93
- Two things about the wallet client. It has to carry an `account`:
94
- `getSmartWallet` refuses a client without one, though it never asks it for a
95
- signature, since the passkey signs everything. And give `http()` a real RPC
96
- URL, because the SDK reads `client.transport.url` to build the public client
97
- it uses for contract reads.
97
+ The wallet client needs no `account`, since the passkey signs everything. Give `http()` a real RPC URL, because the SDK reads `client.transport.url` to build the public client it uses for contract reads.
98
+
99
+ Every write resolves with the user operation hash once the bundler accepts it, not once it is mined, so wait for the receipt as above. An operation the bundler can see will revert is refused before it is sent, and the write rejects with a `ContractRevertError` whose `decoded.errorName` names the contract error (for example `PaymentReferenceAlreadyUsed`). One that only reverts once mined still returns a receipt, so check `receipt.success` too. `receipt.receipt.transactionHash` is the onchain transaction.
98
100
 
99
- Check `receipt.success`. An operation whose calls revert is still mined and
100
- still returns a receipt, so the await resolving is not on its own proof the
101
- write landed. `receipt.receipt.transactionHash` is the onchain transaction.
101
+ Calls that belong together have helpers that send them as one user operation, so the user sees one passkey prompt: `deviceWallet.deployAndBindESIMWallet(salt, { grantAccessToFunds: true })` deploys an eSIM wallet, binds it and grants it access to the device wallet's tokens, and `eSIMWallet.buyDataBundleWithTransfer(...)` sends the tokens and buys in one go, without pull access.
102
102
 
103
103
  The contract surfaces (`deviceWallet`, `eSIMWallet`, `deviceWalletFactory`,
104
104
  `eSIMWalletFactory`, `registry`, `paymentAdapter`, `P256Verifier`) are only
@@ -10,7 +10,7 @@ import { AdminPaymentAdapterSubPackage } from "./interface/paymentAdapterClass.j
10
10
  // Re-export the typed error surface so backend consumers can `instanceof
11
11
  // KokioError` (or a subclass) and decode reverts without reaching into internal
12
12
  // module paths - mirroring `config.ts`.
13
- export { KokioError, NullOrUndefinedValueError, MissingSmartWalletError, MissingEOAWalletError, InvalidClientError, UnsupportedChainError, UnconfiguredChainError, CounterfactualMismatchError, ContractRevertError, decodeContractRevert, } from "../logic/errors.js";
13
+ export { KokioError, NullOrUndefinedValueError, MissingSmartWalletError, MissingEOAWalletError, InvalidClientError, UnsupportedChainError, UnconfiguredChainError, CounterfactualMismatchError, BatchSizeOutOfRangeError, DepositOnResumeError, ESIMWalletNotLazyDeployedError, MissingBatchEventError, StalledBatchError, ContractRevertError, decodeContractRevert, } from "../logic/errors.js";
14
14
  // `getOperationState` answers with this, and comparing against it needs the enum
15
15
  // at runtime rather than only in the types.
16
16
  export { OperationState } from "../logic/admin/reads/protocolAdmin.reads.js";
@@ -10,7 +10,7 @@ import { PaymentAdapterSubPackage } from "./interface/paymentAdapterClass.js";
10
10
  // Re-export the typed error surface so consumers can `instanceof KokioError`
11
11
  // (or a specific subclass) and decode on-chain reverts without reaching into
12
12
  // internal module paths.
13
- export { KokioError, NullOrUndefinedValueError, MissingSmartWalletError, MissingEOAWalletError, InvalidClientError, UnsupportedChainError, UnconfiguredChainError, CounterfactualMismatchError, ContractRevertError, decodeContractRevert, } from "./logic/errors.js";
13
+ export { KokioError, NullOrUndefinedValueError, MissingSmartWalletError, MissingEOAWalletError, InvalidClientError, UnsupportedChainError, UnconfiguredChainError, CounterfactualMismatchError, BatchSizeOutOfRangeError, DepositOnResumeError, ESIMWalletNotLazyDeployedError, MissingBatchEventError, StalledBatchError, ContractRevertError, decodeContractRevert, } from "./logic/errors.js";
14
14
  export class Kokio {
15
15
  viemWalletClient;
16
16
  credentialId;
@@ -1,4 +1,4 @@
1
- import { _addDeposit, _addESIMWallet, _canPullFunds, _deviceUniqueIdentifier, _entryPoint, _eSIMWalletFactory, _getDeposit, _getOwner, _getVaultAddress, _isValidESIMWallet, _isValidSignature, _registry, _removeESIMWallet, _sendUserOperation, _toggleAccessToFunds, _transferOwnership, _verifier, _withdrawDepositTo } from "../logic/deviceWallet.js";
1
+ import { _addDeposit, _addESIMWallet, _canPullFunds, _deployAndBindESIMWallet, _deviceUniqueIdentifier, _entryPoint, _eSIMWalletFactory, _getDeposit, _getOwner, _getVaultAddress, _isValidESIMWallet, _isValidSignature, _registry, _removeESIMWallet, _sendUserOperation, _toggleAccessToFunds, _transferOwnership, _verifier, _withdrawDepositTo } from "../logic/deviceWallet.js";
2
2
  export class DeviceWalletSubPackage {
3
3
  smartAccountClient;
4
4
  walletClient;
@@ -17,6 +17,15 @@ export class DeviceWalletSubPackage {
17
17
  addESIMWallet(eSIMWalletAddress) {
18
18
  return _addESIMWallet(this.smartAccountClient, this.address, eSIMWalletAddress);
19
19
  }
20
+ /**
21
+ * Deploy an eSIM wallet and bind it to this device wallet in one user
22
+ * operation, optionally granting it access to this wallet's tokens too.
23
+ * Resolves once the operation is sent, with its hash and the eSIM wallet's
24
+ * address.
25
+ */
26
+ deployAndBindESIMWallet(salt, options = {}) {
27
+ return _deployAndBindESIMWallet(this.smartAccountClient, this.address, salt, options.grantAccessToFunds ?? false);
28
+ }
20
29
  getVaultAddress() {
21
30
  return _getVaultAddress(this.smartAccountClient, this.address);
22
31
  }
@@ -1,4 +1,4 @@
1
- import { _acceptOwnershipTransfer, _buyDataBundleWithToken, _priceCapUSDCents, _deviceWallet, _owner, _requestTransferOwnership, _sendETHToDeviceWallet, _sendTokenToDeviceWallet, _setPriceCapUSDCents, _transactionHistory } from "../logic/eSIMWallet.js";
1
+ import { _acceptOwnershipTransfer, _buyDataBundleWithToken, _buyDataBundleWithTransfer, _priceCapUSDCents, _deviceWallet, _owner, _requestTransferOwnership, _sendETHToDeviceWallet, _sendTokenToDeviceWallet, _setPriceCapUSDCents, _transactionHistory } from "../logic/eSIMWallet.js";
2
2
  export class ESIMWalletSubPackage {
3
3
  client;
4
4
  address;
@@ -12,6 +12,14 @@ export class ESIMWalletSubPackage {
12
12
  buyDataBundleWithToken(dataBundleDetails, asset, maxAmountIn, paymentReference) {
13
13
  return _buyDataBundleWithToken(this.client, this.address, dataBundleDetails, asset, maxAmountIn, paymentReference);
14
14
  }
15
+ /**
16
+ * Same purchase as `buyDataBundleWithToken`, with the device wallet sending
17
+ * the tokens it needs in the same user operation. Works without having granted
18
+ * this eSIM wallet access to the device wallet's funds.
19
+ */
20
+ buyDataBundleWithTransfer(dataBundleDetails, asset, maxAmountIn, paymentReference) {
21
+ return _buyDataBundleWithTransfer(this.client, this.address, dataBundleDetails, asset, maxAmountIn, paymentReference);
22
+ }
15
23
  priceCapUSDCents() {
16
24
  return _priceCapUSDCents(this.client, this.address);
17
25
  }
@@ -1,4 +1,4 @@
1
- import { _deployESIMWalletWithUserOp, _getCurrentESIMWalletImplementation } from "../logic/eSIMWalletFactory.js";
1
+ import { _deployESIMWalletWithUserOp, _getCurrentESIMWalletImplementation, _getESIMWalletCounterFactualAddress } from "../logic/eSIMWalletFactory.js";
2
2
  export class ESIMWalletFactorySubPackage {
3
3
  smartAccountClient;
4
4
  walletClient;
@@ -6,9 +6,17 @@ export class ESIMWalletFactorySubPackage {
6
6
  this.smartAccountClient = smartAccountClient;
7
7
  this.walletClient = walletClient;
8
8
  }
9
+ /**
10
+ * @deprecated Deploys without binding, so the device wallet does not treat the
11
+ * new eSIM wallet as its own. Use `deviceWallet.deployAndBindESIMWallet`.
12
+ */
9
13
  deployESIMWalletWithUserOp(deviceWalletAddress, salt) {
10
14
  return _deployESIMWalletWithUserOp(this.smartAccountClient, deviceWalletAddress, salt);
11
15
  }
16
+ /** Address the eSIM wallet for this device wallet and salt deploys to. */
17
+ getCounterFactualAddress(deviceWalletAddress, salt) {
18
+ return _getESIMWalletCounterFactualAddress(this.smartAccountClient, deviceWalletAddress, salt);
19
+ }
12
20
  getCurrentESIMWalletImplementation() {
13
21
  return _getCurrentESIMWalletImplementation(this.smartAccountClient);
14
22
  }
@@ -4,6 +4,14 @@ export class RegistrySubPackage {
4
4
  constructor(client) {
5
5
  this.client = client;
6
6
  }
7
+ /**
8
+ * Record in the registry that this device wallet holds the eSIM wallet.
9
+ *
10
+ * Registry side only: the device wallet itself still does not treat the eSIM
11
+ * wallet as its own (`isValidESIMWallet` stays false, so it cannot manage it or
12
+ * grant it fund access). To take on an eSIM wallet use `deviceWallet.addESIMWallet`,
13
+ * which writes both sides.
14
+ */
7
15
  bindESIMWallet(eSIMWalletAddress) {
8
16
  return _bindESIMWallet(this.client, eSIMWalletAddress);
9
17
  }
@@ -15,7 +15,14 @@ export class SmartAccountSubPackage {
15
15
  getSmartWallet(deviceUniqueIdentifier, deviceWalletOwnerKey, salt) {
16
16
  return _getSmartWallet(this.client, this.credentialId, this.rpId, deviceUniqueIdentifier, deviceWalletOwnerKey, salt);
17
17
  }
18
- getSmartWalletClient(account) {
19
- return _getSmartWalletClient(this.client, this.pimlicoAPIKey, this.gasPolicyId, account);
18
+ /**
19
+ * Client that sends the account's user operations through a bundler, with
20
+ * gas sponsored by the paymaster at the same endpoint.
21
+ *
22
+ * @param options.bundlerUrl Bundler and ERC-7677 paymaster endpoint. Defaults to
23
+ * Pimlico for the wallet client's chain, keyed by the API key given to `Kokio`.
24
+ */
25
+ getSmartWalletClient(account, options = {}) {
26
+ return _getSmartWalletClient(this.client, this.pimlicoAPIKey, this.gasPolicyId, account, options.bundlerUrl);
20
27
  }
21
28
  }
@@ -1,9 +1,9 @@
1
- import { _getChainSpecificConstants } from "./constants.js";
1
+ import { _chainId, _getChainSpecificConstants } from "./constants.js";
2
2
  import { P256Verifier } from "../abis/index.js";
3
3
  // `verifySignature` is a `view` - read it directly instead of spending a userOp
4
4
  // (the previous userOp form returned a userOp hash, never the boolean result).
5
5
  export const _verifySignature = async (client, message, requireMessageVerification, webAuthnSignature, x, y) => {
6
- const chainID = await client.getChainId();
6
+ const chainID = await _chainId(client);
7
7
  const rpcURL = client.transport.url;
8
8
  const values = _getChainSpecificConstants(chainID, rpcURL);
9
9
  return client.readContract({
@@ -1,11 +1,13 @@
1
1
  import { http, createPublicClient, createTransport, publicActions, encodeFunctionData, encodePacked, encodeAbiParameters, parseAbiParameters, getContract, concat, keccak256, getContractAddress, getAddress, hashMessage, toHex, hashTypedData, hexToBytes, bytesToHex } from "viem";
2
2
  import { createBundlerClient, createPaymasterClient, entryPoint08Abi, getUserOperationHash, toSmartAccount } from "viem/account-abstraction";
3
- import { _getChainSpecificConstants, ZERO, SIGNATURE_VALIDITY_SECONDS, STUB_VERIFICATION_GAS_PAD, STUB_PRE_VERIFICATION_GAS_PAD } from "../constants.js";
4
- import { CounterfactualMismatchError } from "../errors.js";
3
+ import { _chainId, _getChainSpecificConstants, ZERO, SIGNATURE_VALIDITY_SECONDS, STUB_VERIFICATION_GAS_PAD, STUB_PRE_VERIFICATION_GAS_PAD } from "../constants.js";
4
+ import { CounterfactualMismatchError, toContractRevertError } from "../errors.js";
5
5
  import { DeviceWallet, DeviceWalletFactory } from "../../abis/index.js";
6
- import { isoBase64URL } from "@simplewebauthn/server/helpers";
6
+ import { base64urlnopad } from "@scure/base";
7
7
  import { Passkey } from "react-native-passkey";
8
8
  import { p256 } from "@noble/curves/nist.js";
9
+ // Authenticators differ on padding and alphabet, so accept either form.
10
+ const fromBase64URL = (value) => base64urlnopad.decode(value.replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_"));
9
11
  var AuthenticatorTransport;
10
12
  (function (AuthenticatorTransport) {
11
13
  AuthenticatorTransport["usb"] = "usb";
@@ -39,10 +41,7 @@ export const BEACON_PROXY_CREATION_CODE = "0x60a0806040526104e480380380916100178
39
41
  */
40
42
  export const _stamp = async (credentialId, rpId, payload) => {
41
43
  const signingOptions = {
42
- // `Uint8Array.from` gives a fresh ArrayBuffer-backed view, matching the
43
- // `Uint8Array<ArrayBuffer>` that `fromBuffer` expects (viem's `hexToBytes`
44
- // is typed over the wider `ArrayBufferLike`).
45
- challenge: isoBase64URL.fromBuffer(Uint8Array.from(hexToBytes(payload))),
44
+ challenge: base64urlnopad.encode(hexToBytes(payload)),
46
45
  allowCredentials: [{
47
46
  id: credentialId,
48
47
  type: "public-key",
@@ -69,7 +68,7 @@ export const _stamp = async (credentialId, rpId, payload) => {
69
68
  }
70
69
  const { clientDataJSON, authenticatorData, signature } = authenticationResult.response;
71
70
  // 1. Decode clientDataJSON
72
- const clientDataJSONBuffer = isoBase64URL.toBuffer(clientDataJSON);
71
+ const clientDataJSONBuffer = fromBase64URL(clientDataJSON);
73
72
  const clientDataJSONString = new TextDecoder().decode(clientDataJSONBuffer);
74
73
  // 2. Calculate indices for the contract (byte offsets)
75
74
  const typeSearchString = '"type":"webauthn.get"';
@@ -88,14 +87,10 @@ export const _stamp = async (credentialId, rpId, payload) => {
88
87
  throw new Error(`Could not find challenge substring '${challengeSearchString}' in clientDataJSON for index calculation.`);
89
88
  }
90
89
  // 3. Decode authenticatorData
91
- const authenticatorDataBytes = isoBase64URL.toBuffer(authenticatorData);
90
+ const authenticatorDataBytes = fromBase64URL(authenticatorData);
92
91
  const authenticatorDataHex = bytesToHex(authenticatorDataBytes);
93
92
  // 4. Decode signature (ASN.1 DER encoded)
94
- const signatureBytes = isoBase64URL.toBuffer(signature);
95
- // let parsedSignature = p256.Signature.fromDER(signatureBytes);
96
- let parsedSignature = p256.Signature.fromBytes(signatureBytes instanceof Uint8Array
97
- ? signatureBytes
98
- : new Uint8Array(signatureBytes), "der");
93
+ let parsedSignature = p256.Signature.fromBytes(fromBase64URL(signature), "der");
99
94
  // Normalize s
100
95
  const n = p256.Point.CURVE().n;
101
96
  const halfN = n >> 1n;
@@ -134,7 +129,7 @@ export const _encodeCalls = async (calls) => {
134
129
  });
135
130
  };
136
131
  export const _getFactoryArgs = async (client, deviceUniqueIdentifier, deviceWalletOwnerKey, salt) => {
137
- const chainID = await client.getChainId();
132
+ const chainID = await _chainId(client);
138
133
  const rpcURL = client.transport.url;
139
134
  const values = _getChainSpecificConstants(chainID, rpcURL);
140
135
  const factoryData = encodeFunctionData({
@@ -145,7 +140,7 @@ export const _getFactoryArgs = async (client, deviceUniqueIdentifier, deviceWall
145
140
  return { factory: values.factoryAddresses.DEVICE_WALLET_FACTORY, factoryData };
146
141
  };
147
142
  export const getInitCodeHash = async (client, deviceUniqueIdentifier, deviceWalletOwnerKey) => {
148
- const chainID = await client.getChainId();
143
+ const chainID = await _chainId(client);
149
144
  const rpcURL = client.transport.url;
150
145
  const values = _getChainSpecificConstants(chainID, rpcURL);
151
146
  // off-chain computation of the DeviceWallet address
@@ -176,7 +171,7 @@ export const getInitCodeHash = async (client, deviceUniqueIdentifier, deviceWall
176
171
  return keccak256(initCode);
177
172
  };
178
173
  export const getCounterFactualAddress = async (client, deviceUniqueIdentifier, deviceWalletOwnerKey, salt) => {
179
- const chainID = await client.getChainId();
174
+ const chainID = await _chainId(client);
180
175
  const rpcURL = client.transport.url;
181
176
  const values = _getChainSpecificConstants(chainID, rpcURL);
182
177
  const deviceWalletFactoryAddress = values.factoryAddresses.DEVICE_WALLET_FACTORY;
@@ -204,20 +199,23 @@ export const getCounterFactualAddress = async (client, deviceUniqueIdentifier, d
204
199
  * an unconditional check, e.g. right after a contract redeploy.
205
200
  */
206
201
  export const _assertCounterfactualMatchesOnChain = async (client, deviceUniqueIdentifier, deviceWalletOwnerKey, salt) => {
207
- const chainID = await client.getChainId();
202
+ const chainID = await _chainId(client);
208
203
  const rpcURL = client.transport.url;
209
204
  const values = _getChainSpecificConstants(chainID, rpcURL);
210
- const offChain = await getCounterFactualAddress(client, deviceUniqueIdentifier, deviceWalletOwnerKey, salt);
211
205
  const deviceWalletFactory = getContract({
212
206
  abi: DeviceWalletFactory,
213
207
  address: values.factoryAddresses.DEVICE_WALLET_FACTORY,
214
208
  client,
215
209
  });
216
- // on-chain view arg order is (ownerKey, uid, salt) - differs from createAccount
217
- const onChain = await deviceWalletFactory.read.getCounterFactualAddress([
218
- deviceWalletOwnerKey,
219
- deviceUniqueIdentifier,
220
- salt,
210
+ // Neither side needs the other, so both are asked for at once.
211
+ const [offChain, onChain] = await Promise.all([
212
+ getCounterFactualAddress(client, deviceUniqueIdentifier, deviceWalletOwnerKey, salt),
213
+ // on-chain view arg order is (ownerKey, uid, salt) - differs from createAccount
214
+ deviceWalletFactory.read.getCounterFactualAddress([
215
+ deviceWalletOwnerKey,
216
+ deviceUniqueIdentifier,
217
+ salt,
218
+ ]),
221
219
  ]);
222
220
  if (getAddress(offChain) !== getAddress(onChain)) {
223
221
  throw new CounterfactualMismatchError(getAddress(offChain), getAddress(onChain));
@@ -293,11 +291,9 @@ export const _signUserOperationHash = async (credentialId, rpId, userOpHash) =>
293
291
  // same chain skip the extra RPC the drift guard costs.
294
292
  const _counterfactualVerifiedChains = new Set();
295
293
  export const _getSmartWallet = async (client, credentialId, rpId, deviceUniqueIdentifier, deviceWalletOwnerKey, salt) => {
296
- const chainID = await client.getChainId();
294
+ const chainID = await _chainId(client);
297
295
  const rpcURL = client.transport.url;
298
296
  const values = _getChainSpecificConstants(chainID, rpcURL);
299
- if (!client.account)
300
- throw new Error('Error: No signer account found with WalletClient');
301
297
  const accountAddress = _counterfactualVerifiedChains.has(chainID)
302
298
  ? await getCounterFactualAddress(client, deviceUniqueIdentifier, deviceWalletOwnerKey, salt)
303
299
  : await _assertCounterfactualMatchesOnChain(client, deviceUniqueIdentifier, deviceWalletOwnerKey, salt);
@@ -385,20 +381,35 @@ const _splitTransport = (pimlicoRpcURL, rpcURL) => {
385
381
  // RPC has to stay readable here.
386
382
  { url: rpcURL });
387
383
  };
388
- export const _getSmartWalletClient = async (client, pimlicoAPIKey, gasPolicyId, account) => {
389
- const chainID = await client.getChainId();
384
+ export const _getSmartWalletClient = async (client, pimlicoAPIKey, gasPolicyId, account, bundlerUrl) => {
385
+ const chainID = await _chainId(client);
390
386
  const rpcURL = client.transport.url;
391
387
  const values = _getChainSpecificConstants(chainID, rpcURL, pimlicoAPIKey);
392
- // Pimlico sponsors via ERC-7677, keyed by the gas policy.
393
- const paymaster = createPaymasterClient({ transport: http(values.pimlicoRpcURL) });
394
- return createBundlerClient({
388
+ const bundlerURL = bundlerUrl ?? values.pimlicoRpcURL;
389
+ // Pimlico sponsors via ERC-7677 and reads the policy from `sponsorshipPolicyId`.
390
+ // A policy is optional there, so an empty id sends no context at all.
391
+ const paymaster = createPaymasterClient({ transport: http(bundlerURL) });
392
+ const bundlerClient = createBundlerClient({
395
393
  account,
396
394
  chain: values.chain,
397
395
  client: createPublicClient({ chain: values.chain, transport: http(values.rpcURL) }),
398
- transport: _splitTransport(values.pimlicoRpcURL, values.rpcURL),
396
+ transport: _splitTransport(bundlerURL, values.rpcURL),
399
397
  paymaster,
400
- paymasterContext: { policyId: gasPolicyId },
401
- // extend() keeps the bundler fields at runtime but drops them from the
402
- // inferred type, so the result is re-asserted rather than narrowed.
398
+ paymasterContext: gasPolicyId ? { sponsorshipPolicyId: gasPolicyId } : undefined,
403
399
  }).extend(publicActions);
400
+ // Every user operation the SDK sends goes through here, so a revert is
401
+ // decoded once for all of them, the same way admin writes are.
402
+ const sendUserOperation = bundlerClient.sendUserOperation;
403
+ // extend() keeps the bundler fields at runtime but drops them from the
404
+ // inferred type, so the result is re-asserted rather than narrowed.
405
+ return bundlerClient.extend(() => ({
406
+ sendUserOperation: (async (args) => {
407
+ try {
408
+ return await sendUserOperation(args);
409
+ }
410
+ catch (err) {
411
+ throw toContractRevertError(err) ?? err;
412
+ }
413
+ }),
414
+ }));
404
415
  };
@@ -1,4 +1,4 @@
1
- import { _getChainSpecificConstants } from "../constants.js";
1
+ import { _chainId, _getChainSpecificConstants } from "../constants.js";
2
2
  import { MissingEOAWalletError, writeContractOrThrow } from "../errors.js";
3
3
  import { DeviceWallet } from "../../abis/index.js";
4
4
  // Admin-EOA logic targeting a specific `DeviceWallet` instance (its address is
@@ -14,7 +14,7 @@ import { DeviceWallet } from "../../abis/index.js";
14
14
  * `toggleAccessToETH`, which the admin EOA cannot reach.
15
15
  */
16
16
  export const _deployESIMWallet = async (client, deviceWalletAddress, salt) => {
17
- const chainID = await client.getChainId();
17
+ const chainID = await _chainId(client);
18
18
  const rpcURL = client.transport.url;
19
19
  const values = _getChainSpecificConstants(chainID, rpcURL);
20
20
  if (!client.account)
@@ -22,7 +22,7 @@ export const _deployESIMWallet = async (client, deviceWalletAddress, salt) => {
22
22
  return writeContractOrThrow(client, {
23
23
  address: deviceWalletAddress,
24
24
  chain: values.chain,
25
- account: client.account.address,
25
+ account: client.account,
26
26
  abi: DeviceWallet,
27
27
  functionName: 'deployESIMWallet',
28
28
  args: [false, salt]
@@ -36,7 +36,7 @@ export const _deployESIMWallet = async (client, deviceWalletAddress, salt) => {
36
36
  * out, and topping one up is not a way to reach its funds.
37
37
  */
38
38
  export const _addDeposit = async (client, deviceWalletAddress, amount) => {
39
- const chainID = await client.getChainId();
39
+ const chainID = await _chainId(client);
40
40
  const rpcURL = client.transport.url;
41
41
  const values = _getChainSpecificConstants(chainID, rpcURL);
42
42
  if (!client.account)
@@ -44,7 +44,7 @@ export const _addDeposit = async (client, deviceWalletAddress, amount) => {
44
44
  return writeContractOrThrow(client, {
45
45
  address: deviceWalletAddress,
46
46
  chain: values.chain,
47
- account: client.account.address,
47
+ account: client.account,
48
48
  abi: DeviceWallet,
49
49
  functionName: 'addDeposit',
50
50
  args: [],
@@ -1,4 +1,4 @@
1
- import { _getChainSpecificConstants } from "../constants.js";
1
+ import { _chainId, _getChainSpecificConstants } from "../constants.js";
2
2
  import { MissingEOAWalletError, writeContractOrThrow } from "../errors.js";
3
3
  import { DeviceWalletFactory } from "../../abis/index.js";
4
4
  // Admin-EOA logic for `DeviceWalletFactory`.
@@ -19,7 +19,7 @@ import { DeviceWalletFactory } from "../../abis/index.js";
19
19
  * is drawn; any surplus is refunded to the caller on chain.
20
20
  */
21
21
  export const _deployDeviceWalletForUsers = async (client, deviceUniqueIdentifiers, deviceWalletOwnersKey, salts, depositAmounts, value) => {
22
- const chainID = await client.getChainId();
22
+ const chainID = await _chainId(client);
23
23
  const rpcURL = client.transport.url;
24
24
  const values = _getChainSpecificConstants(chainID, rpcURL);
25
25
  if (!client.account)
@@ -27,7 +27,7 @@ export const _deployDeviceWalletForUsers = async (client, deviceUniqueIdentifier
27
27
  return writeContractOrThrow(client, {
28
28
  address: values.factoryAddresses.DEVICE_WALLET_FACTORY,
29
29
  chain: values.chain,
30
- account: client.account.address,
30
+ account: client.account,
31
31
  abi: DeviceWalletFactory,
32
32
  functionName: 'deployDeviceWalletForUsers',
33
33
  args: [deviceUniqueIdentifiers, deviceWalletOwnersKey, salts, depositAmounts],
@@ -40,7 +40,7 @@ export const _deployDeviceWalletForUsers = async (client, deviceUniqueIdentifier
40
40
  * factory rederives the counterfactual address from it to check the wallet.
41
41
  */
42
42
  export const _postCreateAccount = async (client, deviceWallet, deviceUniqueIdentifier, deviceWalletOwnerKey, salt) => {
43
- const chainID = await client.getChainId();
43
+ const chainID = await _chainId(client);
44
44
  const rpcURL = client.transport.url;
45
45
  const values = _getChainSpecificConstants(chainID, rpcURL);
46
46
  if (!client.account)
@@ -48,7 +48,7 @@ export const _postCreateAccount = async (client, deviceWallet, deviceUniqueIdent
48
48
  return writeContractOrThrow(client, {
49
49
  address: values.factoryAddresses.DEVICE_WALLET_FACTORY,
50
50
  chain: values.chain,
51
- account: client.account.address,
51
+ account: client.account,
52
52
  abi: DeviceWalletFactory,
53
53
  functionName: 'postCreateAccount',
54
54
  args: [deviceWallet, deviceUniqueIdentifier, deviceWalletOwnerKey, salt]
@@ -56,7 +56,7 @@ export const _postCreateAccount = async (client, deviceWallet, deviceUniqueIdent
56
56
  };
57
57
  /** One-time wiring of the registry into the factory. `onlyAdmin`. */
58
58
  export const _addRegistryAddress = async (client, registryContractAddress) => {
59
- const chainID = await client.getChainId();
59
+ const chainID = await _chainId(client);
60
60
  const rpcURL = client.transport.url;
61
61
  const values = _getChainSpecificConstants(chainID, rpcURL);
62
62
  if (!client.account)
@@ -64,7 +64,7 @@ export const _addRegistryAddress = async (client, registryContractAddress) => {
64
64
  return writeContractOrThrow(client, {
65
65
  address: values.factoryAddresses.DEVICE_WALLET_FACTORY,
66
66
  chain: values.chain,
67
- account: client.account.address,
67
+ account: client.account,
68
68
  abi: DeviceWalletFactory,
69
69
  functionName: 'addRegistryAddress',
70
70
  args: [registryContractAddress]
@@ -72,7 +72,7 @@ export const _addRegistryAddress = async (client, registryContractAddress) => {
72
72
  };
73
73
  /** Point the device-wallet beacon at a new implementation. `onlyOwner` (upgradeManager). */
74
74
  export const _updateDeviceWalletImplementation = async (client, newDeviceImpl) => {
75
- const chainID = await client.getChainId();
75
+ const chainID = await _chainId(client);
76
76
  const rpcURL = client.transport.url;
77
77
  const values = _getChainSpecificConstants(chainID, rpcURL);
78
78
  if (!client.account)
@@ -80,7 +80,7 @@ export const _updateDeviceWalletImplementation = async (client, newDeviceImpl) =
80
80
  return writeContractOrThrow(client, {
81
81
  address: values.factoryAddresses.DEVICE_WALLET_FACTORY,
82
82
  chain: values.chain,
83
- account: client.account.address,
83
+ account: client.account,
84
84
  abi: DeviceWalletFactory,
85
85
  functionName: 'updateDeviceWalletImplementation',
86
86
  args: [newDeviceImpl]
@@ -100,7 +100,7 @@ export const _updateDeviceWalletImplementation = async (client, newDeviceImpl) =
100
100
  * it, so the new owner can move every deployed device wallet at once.
101
101
  */
102
102
  export const _transferOwnershipCall = async (client, newOwner) => {
103
- const chainID = await client.getChainId();
103
+ const chainID = await _chainId(client);
104
104
  const rpcURL = client.transport.url;
105
105
  const values = _getChainSpecificConstants(chainID, rpcURL);
106
106
  return {
@@ -119,7 +119,7 @@ export const _transferOwnershipCall = async (client, newOwner) => {
119
119
  * instead. Changing what the factory deploys next is a beacon update, not this.
120
120
  */
121
121
  export const _upgradeCall = async (client, newImplementation, data = '0x') => {
122
- const chainID = await client.getChainId();
122
+ const chainID = await _chainId(client);
123
123
  const rpcURL = client.transport.url;
124
124
  const values = _getChainSpecificConstants(chainID, rpcURL);
125
125
  return {
@@ -138,7 +138,7 @@ export const _upgradeCall = async (client, newImplementation, data = '0x') => {
138
138
  * at once.
139
139
  */
140
140
  export const _acceptOwnership = async (client) => {
141
- const chainID = await client.getChainId();
141
+ const chainID = await _chainId(client);
142
142
  const rpcURL = client.transport.url;
143
143
  const values = _getChainSpecificConstants(chainID, rpcURL);
144
144
  if (!client.account)
@@ -146,7 +146,7 @@ export const _acceptOwnership = async (client) => {
146
146
  return writeContractOrThrow(client, {
147
147
  address: values.factoryAddresses.DEVICE_WALLET_FACTORY,
148
148
  chain: values.chain,
149
- account: client.account.address,
149
+ account: client.account,
150
150
  abi: DeviceWalletFactory,
151
151
  functionName: 'acceptOwnership',
152
152
  args: []
@@ -1,4 +1,4 @@
1
- import { _getChainSpecificConstants } from "../constants.js";
1
+ import { _chainId, _getChainSpecificConstants } from "../constants.js";
2
2
  import { MissingEOAWalletError, writeContractOrThrow } from "../errors.js";
3
3
  import { ESIMWallet } from "../../abis/index.js";
4
4
  // Admin-EOA logic targeting a specific `ESIMWallet` instance (address passed in).
@@ -14,7 +14,7 @@ import { ESIMWallet } from "../../abis/index.js";
14
14
  * spendable once per eSIM wallet.
15
15
  */
16
16
  export const _buyDataBundleWithToken = async (client, eSIMWalletAddress, dataBundleDetails, asset, maxAmountIn, paymentReference) => {
17
- const chainID = await client.getChainId();
17
+ const chainID = await _chainId(client);
18
18
  const rpcURL = client.transport.url;
19
19
  const values = _getChainSpecificConstants(chainID, rpcURL);
20
20
  if (!client.account)
@@ -22,7 +22,7 @@ export const _buyDataBundleWithToken = async (client, eSIMWalletAddress, dataBun
22
22
  return writeContractOrThrow(client, {
23
23
  address: eSIMWalletAddress,
24
24
  chain: values.chain,
25
- account: client.account.address,
25
+ account: client.account,
26
26
  abi: ESIMWallet,
27
27
  functionName: 'buyDataBundleWithToken',
28
28
  args: [dataBundleDetails, asset, maxAmountIn, paymentReference]
@@ -1,4 +1,4 @@
1
- import { _getChainSpecificConstants } from "../constants.js";
1
+ import { _chainId, _getChainSpecificConstants } from "../constants.js";
2
2
  import { MissingEOAWalletError, writeContractOrThrow } from "../errors.js";
3
3
  import { ESIMWalletFactory } from "../../abis/index.js";
4
4
  // Admin-EOA logic for `ESIMWalletFactory`. Both functions are owner-gated
@@ -14,7 +14,7 @@ import { ESIMWalletFactory } from "../../abis/index.js";
14
14
  // `onlyRegistryOrDeviceWalletFactoryOrDeviceWallet`, so a bare EOA always reverts.
15
15
  /** One-time wiring of the registry into the eSIM factory. Owner only. */
16
16
  export const _addRegistryAddress = async (client, registryContractAddress) => {
17
- const chainID = await client.getChainId();
17
+ const chainID = await _chainId(client);
18
18
  const rpcURL = client.transport.url;
19
19
  const values = _getChainSpecificConstants(chainID, rpcURL);
20
20
  if (!client.account)
@@ -22,7 +22,7 @@ export const _addRegistryAddress = async (client, registryContractAddress) => {
22
22
  return writeContractOrThrow(client, {
23
23
  address: values.factoryAddresses.ESIM_WALLET_FACTORY,
24
24
  chain: values.chain,
25
- account: client.account.address,
25
+ account: client.account,
26
26
  abi: ESIMWalletFactory,
27
27
  functionName: 'addRegistryAddress',
28
28
  args: [registryContractAddress]
@@ -30,7 +30,7 @@ export const _addRegistryAddress = async (client, registryContractAddress) => {
30
30
  };
31
31
  /** Point the eSIM-wallet beacon at a new implementation. `onlyOwner`. */
32
32
  export const _updateESIMWalletImplementation = async (client, eSIMWalletImpl) => {
33
- const chainID = await client.getChainId();
33
+ const chainID = await _chainId(client);
34
34
  const rpcURL = client.transport.url;
35
35
  const values = _getChainSpecificConstants(chainID, rpcURL);
36
36
  if (!client.account)
@@ -38,7 +38,7 @@ export const _updateESIMWalletImplementation = async (client, eSIMWalletImpl) =>
38
38
  return writeContractOrThrow(client, {
39
39
  address: values.factoryAddresses.ESIM_WALLET_FACTORY,
40
40
  chain: values.chain,
41
- account: client.account.address,
41
+ account: client.account,
42
42
  abi: ESIMWalletFactory,
43
43
  functionName: 'updateESIMWalletImplementation',
44
44
  args: [eSIMWalletImpl]
@@ -58,7 +58,7 @@ export const _updateESIMWalletImplementation = async (client, eSIMWalletImpl) =>
58
58
  * it, so the new owner can move every deployed eSIM wallet at once.
59
59
  */
60
60
  export const _transferOwnershipCall = async (client, newOwner) => {
61
- const chainID = await client.getChainId();
61
+ const chainID = await _chainId(client);
62
62
  const rpcURL = client.transport.url;
63
63
  const values = _getChainSpecificConstants(chainID, rpcURL);
64
64
  return {
@@ -76,7 +76,7 @@ export const _transferOwnershipCall = async (client, newOwner) => {
76
76
  * the beacon, which moves through `updateESIMWalletImplementation` instead.
77
77
  */
78
78
  export const _upgradeCall = async (client, newImplementation, data = '0x') => {
79
- const chainID = await client.getChainId();
79
+ const chainID = await _chainId(client);
80
80
  const rpcURL = client.transport.url;
81
81
  const values = _getChainSpecificConstants(chainID, rpcURL);
82
82
  return {
@@ -95,7 +95,7 @@ export const _upgradeCall = async (client, newImplementation, data = '0x') => {
95
95
  * at once.
96
96
  */
97
97
  export const _acceptOwnership = async (client) => {
98
- const chainID = await client.getChainId();
98
+ const chainID = await _chainId(client);
99
99
  const rpcURL = client.transport.url;
100
100
  const values = _getChainSpecificConstants(chainID, rpcURL);
101
101
  if (!client.account)
@@ -103,7 +103,7 @@ export const _acceptOwnership = async (client) => {
103
103
  return writeContractOrThrow(client, {
104
104
  address: values.factoryAddresses.ESIM_WALLET_FACTORY,
105
105
  chain: values.chain,
106
- account: client.account.address,
106
+ account: client.account,
107
107
  abi: ESIMWalletFactory,
108
108
  functionName: 'acceptOwnership',
109
109
  args: []