kokio-sdk 3.0.2 → 3.2.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 (53) hide show
  1. package/README.md +15 -15
  2. package/dist/esm/admin/config-admin.js +6 -1
  3. package/dist/esm/admin/interface/callsClass.js +30 -0
  4. package/dist/esm/config.js +1 -1
  5. package/dist/esm/interface/deviceWalletClass.js +10 -1
  6. package/dist/esm/interface/eSIMWalletClass.js +17 -1
  7. package/dist/esm/interface/eSIMWalletFactoryClass.js +9 -1
  8. package/dist/esm/interface/registryClass.js +8 -0
  9. package/dist/esm/interface/smartAccountClass.js +9 -2
  10. package/dist/esm/logic/P256Verifier.js +2 -2
  11. package/dist/esm/logic/account-kit/createSmartAccount.js +47 -36
  12. package/dist/esm/logic/admin/deviceWallet.eoa.js +5 -5
  13. package/dist/esm/logic/admin/deviceWalletFactory.eoa.js +13 -13
  14. package/dist/esm/logic/admin/eSIMWallet.eoa.js +3 -3
  15. package/dist/esm/logic/admin/eSIMWalletFactory.eoa.js +9 -9
  16. package/dist/esm/logic/admin/lazyWalletRegistry.eoa.js +13 -13
  17. package/dist/esm/logic/admin/paymentAdapter.eoa.js +11 -11
  18. package/dist/esm/logic/admin/protocolAdmin.eoa.js +14 -14
  19. package/dist/esm/logic/admin/reads/deviceWalletFactory.reads.js +9 -9
  20. package/dist/esm/logic/admin/reads/eSIMWalletFactory.reads.js +7 -7
  21. package/dist/esm/logic/admin/reads/lazyWalletRegistry.reads.js +18 -18
  22. package/dist/esm/logic/admin/reads/paymentAdapter.reads.js +8 -8
  23. package/dist/esm/logic/admin/reads/protocolAdmin.reads.js +2 -2
  24. package/dist/esm/logic/admin/reads/registry.reads.js +31 -31
  25. package/dist/esm/logic/admin/registry.eoa.js +27 -27
  26. package/dist/esm/logic/calls/eSIMWallet.calls.js +72 -0
  27. package/dist/esm/logic/constants.js +14 -1
  28. package/dist/esm/logic/deviceWallet.js +35 -1
  29. package/dist/esm/logic/deviceWalletFactory.js +11 -11
  30. package/dist/esm/logic/eSIMWallet.js +32 -0
  31. package/dist/esm/logic/eSIMWalletFactory.js +15 -3
  32. package/dist/esm/logic/errors.js +15 -9
  33. package/dist/esm/logic/paymentAdapter.js +8 -8
  34. package/dist/esm/logic/registry.js +17 -17
  35. package/dist/esm/logic/utils.js +1 -81
  36. package/dist/types/admin/config-admin.d.ts +4 -1
  37. package/dist/types/admin/interface/callsClass.d.ts +26 -0
  38. package/dist/types/config.d.ts +1 -1
  39. package/dist/types/interface/deviceWalletClass.d.ts +12 -0
  40. package/dist/types/interface/eSIMWalletClass.d.ts +14 -0
  41. package/dist/types/interface/eSIMWalletFactoryClass.d.ts +6 -0
  42. package/dist/types/interface/registryClass.d.ts +8 -0
  43. package/dist/types/interface/smartAccountClass.d.ts +10 -1
  44. package/dist/types/logic/account-kit/createSmartAccount.d.ts +1 -1
  45. package/dist/types/logic/calls/eSIMWallet.calls.d.ts +25 -0
  46. package/dist/types/logic/constants.d.ts +3 -0
  47. package/dist/types/logic/deviceWallet.d.ts +12 -0
  48. package/dist/types/logic/eSIMWallet.d.ts +16 -0
  49. package/dist/types/logic/eSIMWalletFactory.d.ts +2 -0
  50. package/dist/types/logic/errors.d.ts +6 -5
  51. package/dist/types/logic/utils.d.ts +0 -21
  52. package/dist/types/types.d.ts +1 -1
  53. package/package.json +21 -7
@@ -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 { Registry } from "../../abis/index.js";
4
4
  // Admin-EOA logic for `Registry`. Most of this is `onlyOwner`, so the `client`
@@ -11,7 +11,7 @@ import { Registry } from "../../abis/index.js";
11
11
  // deployments whose owner is a plain EOA or multisig.
12
12
  /** Wire (or rewire) the LazyWalletRegistry into the Registry. `onlyOwner`. */
13
13
  export const _addOrUpdateLazyWalletRegistryAddress = async (client, lazyWalletRegistry) => {
14
- const chainID = await client.getChainId();
14
+ const chainID = await _chainId(client);
15
15
  const rpcURL = client.transport.url;
16
16
  const values = _getChainSpecificConstants(chainID, rpcURL);
17
17
  if (!client.account)
@@ -19,7 +19,7 @@ export const _addOrUpdateLazyWalletRegistryAddress = async (client, lazyWalletRe
19
19
  return writeContractOrThrow(client, {
20
20
  address: values.factoryAddresses.REGISTRY,
21
21
  chain: values.chain,
22
- account: client.account.address,
22
+ account: client.account,
23
23
  abi: Registry,
24
24
  functionName: 'addOrUpdateLazyWalletRegistryAddress',
25
25
  args: [lazyWalletRegistry]
@@ -27,7 +27,7 @@ export const _addOrUpdateLazyWalletRegistryAddress = async (client, lazyWalletRe
27
27
  };
28
28
  /** Update the vault that receives eSIM payments. `onlyOwner`. */
29
29
  export const _updateVaultAddress = async (client, newVaultAddress) => {
30
- const chainID = await client.getChainId();
30
+ const chainID = await _chainId(client);
31
31
  const rpcURL = client.transport.url;
32
32
  const values = _getChainSpecificConstants(chainID, rpcURL);
33
33
  if (!client.account)
@@ -35,7 +35,7 @@ export const _updateVaultAddress = async (client, newVaultAddress) => {
35
35
  return writeContractOrThrow(client, {
36
36
  address: values.factoryAddresses.REGISTRY,
37
37
  chain: values.chain,
38
- account: client.account.address,
38
+ account: client.account,
39
39
  abi: Registry,
40
40
  functionName: 'updateVaultAddress',
41
41
  args: [newVaultAddress]
@@ -51,7 +51,7 @@ export const _updateVaultAddress = async (client, newVaultAddress) => {
51
51
  * incumbent instead withdraws a pending nomination and hands the role back.
52
52
  */
53
53
  export const _requestAdminUpdate = async (client, newAdmin) => {
54
- const chainID = await client.getChainId();
54
+ const chainID = await _chainId(client);
55
55
  const rpcURL = client.transport.url;
56
56
  const values = _getChainSpecificConstants(chainID, rpcURL);
57
57
  if (!client.account)
@@ -59,7 +59,7 @@ export const _requestAdminUpdate = async (client, newAdmin) => {
59
59
  return writeContractOrThrow(client, {
60
60
  address: values.factoryAddresses.REGISTRY,
61
61
  chain: values.chain,
62
- account: client.account.address,
62
+ account: client.account,
63
63
  abi: Registry,
64
64
  functionName: 'requestAdminUpdate',
65
65
  args: [newAdmin]
@@ -73,7 +73,7 @@ export const _requestAdminUpdate = async (client, newAdmin) => {
73
73
  * rather than passing quietly and leaving the caller believing it acted.
74
74
  */
75
75
  export const _disableAdmin = async (client) => {
76
- const chainID = await client.getChainId();
76
+ const chainID = await _chainId(client);
77
77
  const rpcURL = client.transport.url;
78
78
  const values = _getChainSpecificConstants(chainID, rpcURL);
79
79
  if (!client.account)
@@ -81,7 +81,7 @@ export const _disableAdmin = async (client) => {
81
81
  return writeContractOrThrow(client, {
82
82
  address: values.factoryAddresses.REGISTRY,
83
83
  chain: values.chain,
84
- account: client.account.address,
84
+ account: client.account,
85
85
  abi: Registry,
86
86
  functionName: 'disableAdmin',
87
87
  args: []
@@ -95,7 +95,7 @@ export const _disableAdmin = async (client) => {
95
95
  * incumbent. Reverts if the admin was never suspended.
96
96
  */
97
97
  export const _enableAdmin = 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 _enableAdmin = async (client) => {
103
103
  return writeContractOrThrow(client, {
104
104
  address: values.factoryAddresses.REGISTRY,
105
105
  chain: values.chain,
106
- account: client.account.address,
106
+ account: client.account,
107
107
  abi: Registry,
108
108
  functionName: 'enableAdmin',
109
109
  args: []
@@ -119,7 +119,7 @@ export const _enableAdmin = async (client) => {
119
119
  * funds through their device wallet's `execute`, which a pause never reaches.
120
120
  */
121
121
  export const _pause = async (client) => {
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
  if (!client.account)
@@ -127,7 +127,7 @@ export const _pause = async (client) => {
127
127
  return writeContractOrThrow(client, {
128
128
  address: values.factoryAddresses.REGISTRY,
129
129
  chain: values.chain,
130
- account: client.account.address,
130
+ account: client.account,
131
131
  abi: Registry,
132
132
  functionName: 'pause',
133
133
  args: []
@@ -141,7 +141,7 @@ export const _pause = async (client) => {
141
141
  * `unpauseInstantly` if the wait is not acceptable.
142
142
  */
143
143
  export const _unpause = async (client) => {
144
- const chainID = await client.getChainId();
144
+ const chainID = await _chainId(client);
145
145
  const rpcURL = client.transport.url;
146
146
  const values = _getChainSpecificConstants(chainID, rpcURL);
147
147
  if (!client.account)
@@ -149,7 +149,7 @@ export const _unpause = async (client) => {
149
149
  return writeContractOrThrow(client, {
150
150
  address: values.factoryAddresses.REGISTRY,
151
151
  chain: values.chain,
152
- account: client.account.address,
152
+ account: client.account,
153
153
  abi: Registry,
154
154
  functionName: 'unpause',
155
155
  args: []
@@ -164,7 +164,7 @@ export const _unpause = async (client) => {
164
164
  * for every wallet without one of its own.
165
165
  */
166
166
  export const _setDefaultPriceCapUSDCents = async (client, cap) => {
167
- const chainID = await client.getChainId();
167
+ const chainID = await _chainId(client);
168
168
  const rpcURL = client.transport.url;
169
169
  const values = _getChainSpecificConstants(chainID, rpcURL);
170
170
  if (!client.account)
@@ -172,7 +172,7 @@ export const _setDefaultPriceCapUSDCents = async (client, cap) => {
172
172
  return writeContractOrThrow(client, {
173
173
  address: values.factoryAddresses.REGISTRY,
174
174
  chain: values.chain,
175
- account: client.account.address,
175
+ account: client.account,
176
176
  abi: Registry,
177
177
  functionName: 'setDefaultPriceCapUSDCents',
178
178
  args: [cap]
@@ -185,7 +185,7 @@ export const _setDefaultPriceCapUSDCents = async (client, cap) => {
185
185
  * wallet reverts rather than moving.
186
186
  */
187
187
  export const _assignESIMIdentifier = async (client, eSIMWalletAddress, eSIMUniqueIdentifier) => {
188
- const chainID = await client.getChainId();
188
+ const chainID = await _chainId(client);
189
189
  const rpcURL = client.transport.url;
190
190
  const values = _getChainSpecificConstants(chainID, rpcURL);
191
191
  if (!client.account)
@@ -193,7 +193,7 @@ export const _assignESIMIdentifier = async (client, eSIMWalletAddress, eSIMUniqu
193
193
  return writeContractOrThrow(client, {
194
194
  address: values.factoryAddresses.REGISTRY,
195
195
  chain: values.chain,
196
- account: client.account.address,
196
+ account: client.account,
197
197
  abi: Registry,
198
198
  functionName: 'assignESIMIdentifier',
199
199
  args: [eSIMWalletAddress, eSIMUniqueIdentifier]
@@ -209,7 +209,7 @@ export const _assignESIMIdentifier = async (client, eSIMWalletAddress, eSIMUniqu
209
209
  * `_paymentReference` is spendable once per eSIM wallet.
210
210
  */
211
211
  export const _recordSettledPurchase = async (client, eSIMWalletAddress, dataBundleDetail, asset, tokenAmount, paymentReference) => {
212
- const chainID = await client.getChainId();
212
+ const chainID = await _chainId(client);
213
213
  const rpcURL = client.transport.url;
214
214
  const values = _getChainSpecificConstants(chainID, rpcURL);
215
215
  if (!client.account)
@@ -217,7 +217,7 @@ export const _recordSettledPurchase = async (client, eSIMWalletAddress, dataBund
217
217
  return writeContractOrThrow(client, {
218
218
  address: values.factoryAddresses.REGISTRY,
219
219
  chain: values.chain,
220
- account: client.account.address,
220
+ account: client.account,
221
221
  abi: Registry,
222
222
  functionName: 'recordSettledPurchase',
223
223
  args: [eSIMWalletAddress, dataBundleDetail, asset, tokenAmount, paymentReference]
@@ -229,7 +229,7 @@ export const _recordSettledPurchase = async (client, eSIMWalletAddress, dataBund
229
229
  * newly nominated admin EOA.
230
230
  */
231
231
  export const _acceptAdminUpdate = async (client) => {
232
- const chainID = await client.getChainId();
232
+ const chainID = await _chainId(client);
233
233
  const rpcURL = client.transport.url;
234
234
  const values = _getChainSpecificConstants(chainID, rpcURL);
235
235
  if (!client.account)
@@ -237,7 +237,7 @@ export const _acceptAdminUpdate = async (client) => {
237
237
  return writeContractOrThrow(client, {
238
238
  address: values.factoryAddresses.REGISTRY,
239
239
  chain: values.chain,
240
- account: client.account.address,
240
+ account: client.account,
241
241
  abi: Registry,
242
242
  functionName: 'acceptAdminUpdate',
243
243
  args: []
@@ -253,7 +253,7 @@ export const _acceptAdminUpdate = async (client) => {
253
253
  * accepts for every contract at once and needs no delay.
254
254
  */
255
255
  export const _acceptOwnership = async (client) => {
256
- const chainID = await client.getChainId();
256
+ const chainID = await _chainId(client);
257
257
  const rpcURL = client.transport.url;
258
258
  const values = _getChainSpecificConstants(chainID, rpcURL);
259
259
  if (!client.account)
@@ -261,7 +261,7 @@ export const _acceptOwnership = async (client) => {
261
261
  return writeContractOrThrow(client, {
262
262
  address: values.factoryAddresses.REGISTRY,
263
263
  chain: values.chain,
264
- account: client.account.address,
264
+ account: client.account,
265
265
  abi: Registry,
266
266
  functionName: 'acceptOwnership',
267
267
  args: []
@@ -282,7 +282,7 @@ export const _acceptOwnership = async (client) => {
282
282
  * ownership where it is rather than stranding it.
283
283
  */
284
284
  export const _transferOwnershipCall = async (client, newOwner) => {
285
- const chainID = await client.getChainId();
285
+ const chainID = await _chainId(client);
286
286
  const rpcURL = client.transport.url;
287
287
  const values = _getChainSpecificConstants(chainID, rpcURL);
288
288
  return {
@@ -305,7 +305,7 @@ export const _transferOwnershipCall = async (client, newOwner) => {
305
305
  * again ends the proxy's life. Diff the storage layout before scheduling.
306
306
  */
307
307
  export const _upgradeCall = async (client, newImplementation, data = '0x') => {
308
- const chainID = await client.getChainId();
308
+ const chainID = await _chainId(client);
309
309
  const rpcURL = client.transport.url;
310
310
  const values = _getChainSpecificConstants(chainID, rpcURL);
311
311
  return {
@@ -0,0 +1,72 @@
1
+ import { encodeFunctionData, erc20Abi } from "viem";
2
+ import { DeviceWallet, ESIMWallet, PaymentAdapter, Registry } from "../../abis/index.js";
3
+ import { _chainId, _getChainSpecificConstants } from "../constants.js";
4
+ /**
5
+ * The calls for buying a data bundle with tokens the device wallet sends over
6
+ * in the same user operation, so the eSIM wallet needs no access to the device
7
+ * wallet's funds.
8
+ *
9
+ * Only the shortfall is sent: the quote for the bundle minus what the eSIM
10
+ * wallet already holds of `asset`, worked out when this runs. If that balance
11
+ * or the quote changes before the operation lands, it reverts, so build the
12
+ * calls again rather than resending old ones.
13
+ */
14
+ export const _buyDataBundleWithTransferCalls = async (client, eSIMWalletAddress, dataBundleDetails, asset, maxAmountIn, paymentReference) => {
15
+ const chainID = await _chainId(client);
16
+ const values = _getChainSpecificConstants(chainID, client.transport.url);
17
+ // The eSIM wallet pays through whichever adapter the registry names, so read it there.
18
+ const adapter = await client.readContract({
19
+ address: values.factoryAddresses.REGISTRY, abi: Registry, functionName: "paymentAdapter"
20
+ });
21
+ const [{ token }, amountIn] = await Promise.all([
22
+ client.readContract({
23
+ address: adapter, abi: PaymentAdapter, functionName: "resolveAsset", args: [asset]
24
+ }),
25
+ client.readContract({
26
+ address: adapter, abi: PaymentAdapter, functionName: "quote", args: [asset, dataBundleDetails.priceUSDCents]
27
+ }),
28
+ ]);
29
+ const held = await client.readContract({
30
+ address: token, abi: erc20Abi, functionName: "balanceOf", args: [eSIMWalletAddress]
31
+ });
32
+ const buy = {
33
+ to: eSIMWalletAddress,
34
+ data: encodeFunctionData({
35
+ abi: ESIMWallet,
36
+ functionName: "buyDataBundleWithToken",
37
+ args: [dataBundleDetails, asset, maxAmountIn, paymentReference]
38
+ })
39
+ };
40
+ if (held >= amountIn)
41
+ return [buy];
42
+ return [
43
+ {
44
+ to: token,
45
+ data: encodeFunctionData({ abi: erc20Abi, functionName: "transfer", args: [eSIMWalletAddress, amountIn - held] })
46
+ },
47
+ buy
48
+ ];
49
+ };
50
+ /**
51
+ * The calls for the new device wallet to take over an eSIM wallet another
52
+ * device wallet asked to hand it: accept the transfer, then bind it, which also
53
+ * tells the registry and clears the standby flag.
54
+ *
55
+ * Accepting first is what lets the bind through: it makes this device wallet
56
+ * the owner and clears the pending transfer. Fund access cannot be granted at
57
+ * bind time, so `grantAccessToFunds` adds a `toggleAccessToFunds` after it.
58
+ */
59
+ export const _acceptAndBindESIMWalletCalls = (eSIMWalletAddress, deviceWalletAddress, grantAccessToFunds) => {
60
+ const self = (functionName, hasAccessToFunds) => ({
61
+ to: deviceWalletAddress,
62
+ data: encodeFunctionData({ abi: DeviceWallet, functionName, args: [eSIMWalletAddress, hasAccessToFunds] })
63
+ });
64
+ return [
65
+ {
66
+ to: eSIMWalletAddress,
67
+ data: encodeFunctionData({ abi: ESIMWallet, functionName: "acceptOwnershipTransfer", args: [] })
68
+ },
69
+ self("addESIMWallet", false),
70
+ ...(grantAccessToFunds ? [self("toggleAccessToFunds", true)] : [])
71
+ ];
72
+ };
@@ -54,11 +54,24 @@ export const customErrors = {
54
54
  MISSING_SMART_WALLET: "Error: Client does not have smart wallet account associated",
55
55
  MISSING_EOA_WALLET: "Error: Client does not have EOA wallet associated"
56
56
  };
57
+ // A client's RPC URL is fixed, so its chain id is too. Asking once per client
58
+ // saves every later SDK call a round trip to the node.
59
+ const chainIds = new WeakMap();
60
+ export const _chainId = (client) => {
61
+ let chainId = chainIds.get(client);
62
+ if (!chainId) {
63
+ chainId = client.getChainId();
64
+ chainIds.set(client, chainId);
65
+ // A failed lookup is not remembered, so the next call asks again.
66
+ chainId.catch(() => chainIds.delete(client));
67
+ }
68
+ return chainId;
69
+ };
57
70
  export const _extractChainID = async (client) => {
58
71
  if (!client) {
59
72
  throw new InvalidClientError();
60
73
  }
61
- return client.getChainId();
74
+ return _chainId(client);
62
75
  };
63
76
  // Maps each supported chain id to its factory-address book + viem chain. Base
64
77
  // Mainnet is listed with '0x' placeholders so the guard below rejects it with a
@@ -1,6 +1,8 @@
1
1
  import { encodeFunctionData, getContract } from "viem";
2
- import { DeviceWallet } from "../abis/index.js";
2
+ import { DeviceWallet, ESIMWalletFactory } from "../abis/index.js";
3
3
  import { MissingSmartWalletError } from "./errors.js";
4
+ import { _chainId, _getChainSpecificConstants } from "./constants.js";
5
+ import { _getESIMWalletCounterFactualAddress } from "./eSIMWalletFactory.js";
4
6
  // A userOp from a device wallet runs through `execute`, so at the target contract
5
7
  // msg.sender is the device-wallet account itself. That constrains which DeviceWallet
6
8
  // functions this surface can expose:
@@ -72,6 +74,38 @@ export const _addESIMWallet = async (client, address, eSIMWalletAddress) => {
72
74
  }]
73
75
  });
74
76
  };
77
+ /**
78
+ * Deploy an eSIM wallet for this device wallet and bind it, in one user operation.
79
+ *
80
+ * Fund access cannot be granted at bind time, so `grantAccessToFunds` adds a
81
+ * `toggleAccessToFunds` after the bind in the same operation. The device wallet
82
+ * must already be registered (`registry.isDeviceWalletValid`), or the factory
83
+ * refuses it.
84
+ */
85
+ export const _deployAndBindESIMWallet = async (client, address, salt, grantAccessToFunds) => {
86
+ const chainID = await _chainId(client);
87
+ const rpcURL = client.transport.url;
88
+ const values = _getChainSpecificConstants(chainID, rpcURL);
89
+ if (!client.account)
90
+ throw new MissingSmartWalletError();
91
+ const eSIMWalletAddress = await _getESIMWalletCounterFactualAddress(client, address, salt);
92
+ const self = (functionName, hasAccessToFunds) => ({
93
+ to: address,
94
+ data: encodeFunctionData({ abi: DeviceWallet, functionName, args: [eSIMWalletAddress, hasAccessToFunds] })
95
+ });
96
+ const userOpHash = await client.sendUserOperation({
97
+ account: client.account,
98
+ calls: [
99
+ {
100
+ to: values.factoryAddresses.ESIM_WALLET_FACTORY,
101
+ data: encodeFunctionData({ abi: ESIMWalletFactory, functionName: "deployESIMWallet", args: [address, salt] })
102
+ },
103
+ self("addESIMWallet", false),
104
+ ...(grantAccessToFunds ? [self("toggleAccessToFunds", true)] : [])
105
+ ]
106
+ });
107
+ return { userOpHash, eSIMWalletAddress };
108
+ };
75
109
  /**
76
110
  * Release an eSIM wallet and put it on standby for a transfer.
77
111
  *
@@ -1,8 +1,8 @@
1
- import { _getChainSpecificConstants } from "./constants.js";
1
+ import { _chainId, _getChainSpecificConstants } from "./constants.js";
2
2
  import { MissingEOAWalletError } from "./errors.js";
3
3
  import { DeviceWalletFactory } from "../abis/index.js";
4
4
  export const _createAccountWithEOA = async (client, deviceUniqueIdentifier, deviceWalletOwnerKey, salt, depositAmount) => {
5
- const chainID = await client.getChainId();
5
+ const chainID = await _chainId(client);
6
6
  const rpcURL = client.transport.url;
7
7
  const values = _getChainSpecificConstants(chainID, rpcURL);
8
8
  if (!client.account)
@@ -12,7 +12,7 @@ export const _createAccountWithEOA = async (client, deviceUniqueIdentifier, devi
12
12
  return client.writeContract({
13
13
  address: values.factoryAddresses.DEVICE_WALLET_FACTORY,
14
14
  chain: values.chain,
15
- account: client.account.address,
15
+ account: client.account,
16
16
  abi: DeviceWalletFactory,
17
17
  functionName: 'createAccount',
18
18
  args: [deviceUniqueIdentifier, deviceWalletOwnerKey, salt],
@@ -23,7 +23,7 @@ export const _createAccountWithEOA = async (client, deviceUniqueIdentifier, devi
23
23
  // On-chain arg order is (bytes32[2] ownerKey, string uid, uint256 salt); note this
24
24
  // differs from `createAccount`.
25
25
  export const _getAddress = async (client, deviceUniqueIdentifier, deviceWalletOwnerKey, salt) => {
26
- const chainID = await client.getChainId();
26
+ const chainID = await _chainId(client);
27
27
  const rpcURL = client.transport.url;
28
28
  const values = _getChainSpecificConstants(chainID, rpcURL);
29
29
  return client.readContract({
@@ -35,7 +35,7 @@ export const _getAddress = async (client, deviceUniqueIdentifier, deviceWalletOw
35
35
  };
36
36
  // `getCurrentDeviceWalletImplementation` is a `view` - read it directly instead of a userOp.
37
37
  export const _getCurrentDeviceWalletImplementation = async (client) => {
38
- const chainID = await client.getChainId();
38
+ const chainID = await _chainId(client);
39
39
  const rpcURL = client.transport.url;
40
40
  const values = _getChainSpecificConstants(chainID, rpcURL);
41
41
  return client.readContract({
@@ -57,7 +57,7 @@ export const _getCurrentDeviceWalletImplementation = async (client) => {
57
57
  * Reverts on an empty identifier or a key that is not a point on the P256 curve.
58
58
  */
59
59
  export const _preCreateAccountValidation = async (client, deviceUniqueIdentifier, deviceWalletOwnerKey) => {
60
- const chainID = await client.getChainId();
60
+ const chainID = await _chainId(client);
61
61
  const rpcURL = client.transport.url;
62
62
  const values = _getChainSpecificConstants(chainID, rpcURL);
63
63
  return client.readContract({
@@ -73,7 +73,7 @@ export const _preCreateAccountValidation = async (client, deviceUniqueIdentifier
73
73
  * registers it.
74
74
  */
75
75
  export const _deviceWalletInfoAdded = async (client, deviceWallet) => {
76
- const chainID = await client.getChainId();
76
+ const chainID = await _chainId(client);
77
77
  const rpcURL = client.transport.url;
78
78
  const values = _getChainSpecificConstants(chainID, rpcURL);
79
79
  return client.readContract({
@@ -88,7 +88,7 @@ export const _deviceWalletInfoAdded = async (client, deviceWallet) => {
88
88
  * all of them and no wallet can decline it.
89
89
  */
90
90
  export const _beacon = async (client) => {
91
- const chainID = await client.getChainId();
91
+ const chainID = await _chainId(client);
92
92
  const rpcURL = client.transport.url;
93
93
  const values = _getChainSpecificConstants(chainID, rpcURL);
94
94
  return client.readContract({
@@ -100,7 +100,7 @@ export const _beacon = async (client) => {
100
100
  };
101
101
  /** The registry the factory writes new wallets into. */
102
102
  export const _registry = async (client) => {
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 client.readContract({
@@ -112,7 +112,7 @@ export const _registry = async (client) => {
112
112
  };
113
113
  /** The EntryPoint baked into every device wallet this factory deploys. */
114
114
  export const _entryPoint = async (client) => {
115
- const chainID = await client.getChainId();
115
+ const chainID = await _chainId(client);
116
116
  const rpcURL = client.transport.url;
117
117
  const values = _getChainSpecificConstants(chainID, rpcURL);
118
118
  return client.readContract({
@@ -124,7 +124,7 @@ export const _entryPoint = async (client) => {
124
124
  };
125
125
  /** The contract new device wallets verify WebAuthn assertions through. */
126
126
  export const _verifier = async (client) => {
127
- const chainID = await client.getChainId();
127
+ const chainID = await _chainId(client);
128
128
  const rpcURL = client.transport.url;
129
129
  const values = _getChainSpecificConstants(chainID, rpcURL);
130
130
  return client.readContract({
@@ -2,6 +2,7 @@ import { encodeFunctionData, maxUint256 } from "viem";
2
2
  import { MissingSmartWalletError } from "./errors.js";
3
3
  import { ESIMWallet } from "../abis/index.js";
4
4
  import { _defaultPriceCapUSDCents } from "./registry.js";
5
+ import { _acceptAndBindESIMWalletCalls, _buyDataBundleWithTransferCalls } from "./calls/eSIMWallet.calls.js";
5
6
  // Not exposed on this surface:
6
7
  // - populateHistory and setESIMUniqueIdentifier are `onlyRegistry` - callable
7
8
  // only by the registry contract. Naming an eSIM goes through
@@ -61,6 +62,22 @@ export const _buyDataBundleWithToken = async (client, address, dataBundleDetails
61
62
  }]
62
63
  });
63
64
  };
65
+ /**
66
+ * Buy a data bundle with tokens the device wallet sends over in the same user
67
+ * operation, so the eSIM wallet needs no access to the device wallet's funds.
68
+ *
69
+ * Only the shortfall is sent: the quote for the bundle minus what this eSIM
70
+ * wallet already holds of `asset`. Arguments are as for `buyDataBundleWithToken`.
71
+ * The backend builds the same calls with `admin.calls.buyDataBundleWithTransfer`.
72
+ */
73
+ export const _buyDataBundleWithTransfer = async (client, address, dataBundleDetails, asset, maxAmountIn, paymentReference) => {
74
+ if (!client.account)
75
+ throw new MissingSmartWalletError();
76
+ return client.sendUserOperation({
77
+ account: client.account,
78
+ calls: await _buyDataBundleWithTransferCalls(client, address, dataBundleDetails, asset, maxAmountIn, paymentReference)
79
+ });
80
+ };
64
81
  /**
65
82
  * Send an ERC-20 held by this eSIM wallet back to its owning device wallet.
66
83
  * `onlyDeviceWallet`.
@@ -180,6 +197,21 @@ export const _acceptOwnershipTransfer = async (client, address) => {
180
197
  }]
181
198
  });
182
199
  };
200
+ /**
201
+ * Accept an eSIM wallet another device wallet asked to hand over, and bind it,
202
+ * in one user operation. `grantAccessToFunds` also lets it pull this device
203
+ * wallet's tokens. The backend builds the same calls with
204
+ * `admin.calls.acceptAndBindESIMWallet`.
205
+ */
206
+ export const _acceptAndBindESIMWallet = async (client, address, grantAccessToFunds) => {
207
+ if (!client.account)
208
+ throw new MissingSmartWalletError();
209
+ // UserOp - the sender is the pending `newRequestedOwner`, and then the owner the bind needs.
210
+ return client.sendUserOperation({
211
+ account: client.account,
212
+ calls: _acceptAndBindESIMWalletCalls(address, client.account.address, grantAccessToFunds)
213
+ });
214
+ };
183
215
  export const _sendETHToDeviceWallet = async (client, address, amount) => {
184
216
  if (!client.account)
185
217
  throw new MissingSmartWalletError();
@@ -1,5 +1,5 @@
1
1
  import { encodeFunctionData } from "viem";
2
- import { _getChainSpecificConstants } from "./constants.js";
2
+ import { _chainId, _getChainSpecificConstants } from "./constants.js";
3
3
  import { MissingSmartWalletError } from "./errors.js";
4
4
  import { ESIMWalletFactory } from "../abis/index.js";
5
5
  // Not exposed on this surface:
@@ -10,7 +10,7 @@ import { ESIMWalletFactory } from "../abis/index.js";
10
10
  // `deployESIMWalletWithUserOp` works because a registered device wallet passes
11
11
  // `registry.isDeviceWalletValid(msg.sender)`, so the userOp sender satisfies the modifier.
12
12
  export const _deployESIMWalletWithUserOp = async (client, deviceWalletAddress, salt) => {
13
- const chainID = await client.getChainId();
13
+ const chainID = await _chainId(client);
14
14
  const rpcURL = client.transport.url;
15
15
  const values = _getChainSpecificConstants(chainID, rpcURL);
16
16
  if (!client.account)
@@ -28,8 +28,20 @@ export const _deployESIMWalletWithUserOp = async (client, deviceWalletAddress, s
28
28
  }]
29
29
  });
30
30
  };
31
+ /** Address `deployESIMWallet` deploys to for this device wallet and salt. */
32
+ export const _getESIMWalletCounterFactualAddress = async (client, deviceWalletAddress, salt) => {
33
+ const chainID = await _chainId(client);
34
+ const rpcURL = client.transport.url;
35
+ const values = _getChainSpecificConstants(chainID, rpcURL);
36
+ return client.readContract({
37
+ address: values.factoryAddresses.ESIM_WALLET_FACTORY,
38
+ abi: ESIMWalletFactory,
39
+ functionName: "getCounterFactualAddress",
40
+ args: [deviceWalletAddress, salt]
41
+ });
42
+ };
31
43
  export const _getCurrentESIMWalletImplementation = async (client) => {
32
- const chainID = await client.getChainId();
44
+ const chainID = await _chainId(client);
33
45
  const rpcURL = client.transport.url;
34
46
  const values = _getChainSpecificConstants(chainID, rpcURL);
35
47
  // `getCurrentESIMWalletImplementation` is a `view` - read it directly instead of a userOp.
@@ -1,5 +1,5 @@
1
1
  import { BaseError, ContractFunctionRevertedError, decodeErrorResult, isHex, } from "viem";
2
- import { DeviceWallet, DeviceWalletFactory, ESIMWallet, ESIMWalletFactory, LazyWalletRegistry, P256Verifier, Registry, RegistryHelper, } from "../abis/index.js";
2
+ import { DeviceWallet, DeviceWalletFactory, ESIMWallet, ESIMWalletFactory, LazyWalletRegistry, P256Verifier, Registry, RegistryHelper, ProtocolAdmin, PaymentAdapter, } from "../abis/index.js";
3
3
  /**
4
4
  * Base class for every error the Kokio SDK throws deliberately. Consumers can
5
5
  * `instanceof KokioError` to distinguish SDK-originated failures from viem /
@@ -144,6 +144,8 @@ const REVERTABLE_ABIS = [
144
144
  P256Verifier,
145
145
  Registry,
146
146
  RegistryHelper,
147
+ ProtocolAdmin,
148
+ PaymentAdapter,
147
149
  ];
148
150
  /**
149
151
  * Best-effort decode of raw revert data (the ABI-encoded `Error(string)`,
@@ -181,21 +183,25 @@ export class ContractRevertError extends KokioError {
181
183
  }
182
184
  }
183
185
  /**
184
- * Pulls a ContractRevertError out of an error thrown by viem, or `null` if it
185
- * isn't a revert viem could decode a selector for.
186
+ * Pulls a ContractRevertError out of an error thrown by viem, or `null` when
187
+ * the error carries no revert data (a network failure, a rejected signature).
186
188
  */
187
189
  export const toContractRevertError = (err) => {
188
190
  if (!(err instanceof BaseError))
189
191
  return null;
190
192
  const revert = err.walk((e) => e instanceof ContractFunctionRevertedError);
191
- if (!(revert instanceof ContractFunctionRevertedError) || !revert.raw)
192
- return null;
193
- return new ContractRevertError(revert.raw);
193
+ if (revert instanceof ContractFunctionRevertedError && revert.raw)
194
+ return new ContractRevertError(revert.raw);
195
+ // A bundler rejects a reverting user operation while estimating its gas, and
196
+ // puts the revert data only in the error message.
197
+ const data = err.details?.match(/reverted during simulation with reason: (0x[0-9a-fA-F]+)/)?.[1];
198
+ return data ? new ContractRevertError(data) : null;
194
199
  };
195
200
  /**
196
- * `client.writeContract`, but a recognised on-chain revert comes back as a
197
- * ContractRevertError instead of viem's raw error chain. Anything else -
198
- * network failures, an unrecognised revert selector - is rethrown as-is.
201
+ * `client.writeContract`, but a revert comes back as a ContractRevertError
202
+ * instead of viem's raw error chain. Its `decoded` is null when the selector
203
+ * belongs to none of the Kokio contracts. Anything that is not a revert, such
204
+ * as a network failure, is rethrown as-is.
199
205
  *
200
206
  * Mirrors `WalletClient["writeContract"]`'s own generics rather than reading
201
207
  * them off `Parameters<...>`, since that would collapse the per-call overload