kokio-sdk 1.1.0 → 2.0.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 (106) hide show
  1. package/README.md +68 -19
  2. package/dist/esm/abis/BeaconProxy.js +30 -46
  3. package/dist/esm/abis/DeviceWallet.js +546 -535
  4. package/dist/esm/abis/DeviceWalletFactory.js +537 -544
  5. package/dist/esm/abis/ESIMWallet.js +440 -299
  6. package/dist/esm/abis/ESIMWalletFactory.js +372 -289
  7. package/dist/esm/abis/LazyWalletRegistry.js +941 -421
  8. package/dist/esm/abis/P256Verifier.js +29 -29
  9. package/dist/esm/abis/ProtocolAdmin.js +1228 -0
  10. package/dist/esm/abis/Registry.js +1207 -466
  11. package/dist/esm/abis/RegistryHelper.js +414 -20
  12. package/dist/esm/abis/index.js +2 -1
  13. package/dist/esm/admin/config-admin.js +7 -0
  14. package/dist/esm/admin/interface/deviceWalletClass.js +27 -6
  15. package/dist/esm/admin/interface/deviceWalletFactoryClass.js +41 -19
  16. package/dist/esm/admin/interface/eSIMWalletClass.js +10 -1
  17. package/dist/esm/admin/interface/eSIMWalletFactoryClass.js +24 -2
  18. package/dist/esm/admin/interface/lazyWalletRegistryClass.js +83 -4
  19. package/dist/esm/admin/interface/protocolAdminClass.js +195 -0
  20. package/dist/esm/admin/interface/registryClass.js +93 -2
  21. package/dist/esm/config.js +42 -7
  22. package/dist/esm/interface/deviceWalletClass.js +45 -5
  23. package/dist/esm/interface/deviceWalletFactoryClass.js +20 -1
  24. package/dist/esm/interface/eSIMWalletClass.js +12 -3
  25. package/dist/esm/interface/registryClass.js +47 -0
  26. package/dist/esm/interface/smartAccountClass.js +2 -4
  27. package/dist/esm/logic/account-kit/createSmartAccount.js +156 -102
  28. package/dist/esm/logic/admin/deviceWallet.eoa.js +23 -12
  29. package/dist/esm/logic/admin/deviceWalletFactory.eoa.js +63 -42
  30. package/dist/esm/logic/admin/eSIMWallet.eoa.js +3 -5
  31. package/dist/esm/logic/admin/eSIMWalletFactory.eoa.js +76 -8
  32. package/dist/esm/logic/admin/lazyWalletRegistry.eoa.js +330 -19
  33. package/dist/esm/logic/admin/protocolAdmin.eoa.js +524 -0
  34. package/dist/esm/logic/admin/reads/deviceWallet.reads.js +82 -7
  35. package/dist/esm/logic/admin/reads/deviceWalletFactory.reads.js +82 -23
  36. package/dist/esm/logic/admin/reads/eSIMWallet.reads.js +50 -6
  37. package/dist/esm/logic/admin/reads/eSIMWalletFactory.reads.js +63 -5
  38. package/dist/esm/logic/admin/reads/lazyWalletRegistry.reads.js +171 -10
  39. package/dist/esm/logic/admin/reads/protocolAdmin.reads.js +154 -0
  40. package/dist/esm/logic/admin/reads/registry.reads.js +289 -9
  41. package/dist/esm/logic/admin/registry.eoa.js +274 -4
  42. package/dist/esm/logic/constants.js +53 -26
  43. package/dist/esm/logic/deviceWallet.js +224 -31
  44. package/dist/esm/logic/deviceWalletFactory.js +89 -0
  45. package/dist/esm/logic/eSIMWallet.js +113 -43
  46. package/dist/esm/logic/eSIMWalletFactory.js +8 -8
  47. package/dist/esm/logic/errors.js +64 -0
  48. package/dist/esm/logic/registry.js +236 -0
  49. package/dist/esm/logic/utils.js +2 -1
  50. package/dist/types/abis/BeaconProxy.d.ts +21 -33
  51. package/dist/types/abis/DeviceWallet.d.ts +372 -365
  52. package/dist/types/abis/DeviceWalletFactory.d.ts +438 -448
  53. package/dist/types/abis/ESIMWallet.d.ts +352 -245
  54. package/dist/types/abis/ESIMWalletFactory.d.ts +278 -216
  55. package/dist/types/abis/LazyWalletRegistry.d.ts +710 -317
  56. package/dist/types/abis/P256Verifier.d.ts +16 -16
  57. package/dist/types/abis/ProtocolAdmin.d.ts +947 -0
  58. package/dist/types/abis/Registry.d.ts +960 -393
  59. package/dist/types/abis/RegistryHelper.d.ts +317 -16
  60. package/dist/types/abis/index.d.ts +2 -1
  61. package/dist/types/admin/config-admin.d.ts +3 -0
  62. package/dist/types/admin/interface/deviceWalletClass.d.ts +10 -3
  63. package/dist/types/admin/interface/deviceWalletFactoryClass.d.ts +14 -7
  64. package/dist/types/admin/interface/eSIMWalletClass.d.ts +3 -0
  65. package/dist/types/admin/interface/eSIMWalletFactoryClass.d.ts +8 -1
  66. package/dist/types/admin/interface/lazyWalletRegistryClass.d.ts +38 -2
  67. package/dist/types/admin/interface/protocolAdminClass.d.ts +95 -0
  68. package/dist/types/admin/interface/registryClass.d.ts +30 -0
  69. package/dist/types/config.d.ts +26 -5
  70. package/dist/types/interface/P256VerifierClass.d.ts +3 -4
  71. package/dist/types/interface/deviceWalletClass.d.ts +22 -2569
  72. package/dist/types/interface/deviceWalletFactoryClass.d.ts +10 -2564
  73. package/dist/types/interface/eSIMWalletClass.d.ts +11 -8
  74. package/dist/types/interface/eSIMWalletFactoryClass.d.ts +5 -2565
  75. package/dist/types/interface/registryClass.d.ts +19 -0
  76. package/dist/types/interface/smartAccountClass.d.ts +6 -2568
  77. package/dist/types/logic/P256Verifier.d.ts +2 -2
  78. package/dist/types/logic/account-kit/createSmartAccount.d.ts +43 -19
  79. package/dist/types/logic/admin/deviceWallet.eoa.d.ts +15 -9
  80. package/dist/types/logic/admin/deviceWalletFactory.eoa.d.ts +34 -23
  81. package/dist/types/logic/admin/eSIMWallet.eoa.d.ts +0 -5
  82. package/dist/types/logic/admin/eSIMWalletFactory.eoa.d.ts +27 -9
  83. package/dist/types/logic/admin/lazyWalletRegistry.eoa.d.ts +102 -10
  84. package/dist/types/logic/admin/protocolAdmin.eoa.d.ts +163 -0
  85. package/dist/types/logic/admin/reads/deviceWallet.reads.d.ts +28 -8
  86. package/dist/types/logic/admin/reads/deviceWalletFactory.reads.d.ts +45 -14
  87. package/dist/types/logic/admin/reads/eSIMWallet.reads.d.ts +25 -6
  88. package/dist/types/logic/admin/reads/eSIMWalletFactory.reads.d.ts +21 -6
  89. package/dist/types/logic/admin/reads/lazyWalletRegistry.reads.d.ts +44 -11
  90. package/dist/types/logic/admin/reads/protocolAdmin.reads.d.ts +53 -0
  91. package/dist/types/logic/admin/reads/registry.reads.d.ts +105 -9
  92. package/dist/types/logic/admin/registry.eoa.d.ts +102 -5
  93. package/dist/types/logic/constants.d.ts +2 -0
  94. package/dist/types/logic/deviceWallet.d.ts +78 -6
  95. package/dist/types/logic/deviceWalletFactory.d.ts +32 -3
  96. package/dist/types/logic/eSIMWallet.d.ts +48 -7
  97. package/dist/types/logic/eSIMWalletFactory.d.ts +3 -3
  98. package/dist/types/logic/errors.d.ts +45 -0
  99. package/dist/types/logic/registry.d.ts +78 -0
  100. package/dist/types/types-export.d.ts +1 -1
  101. package/dist/types/types.d.ts +111 -1
  102. package/package.json +11 -13
  103. package/dist/esm/interface/lazyWalletRegistryClass.js +0 -10
  104. package/dist/esm/logic/lazyWalletRegistry.js +0 -19
  105. package/dist/types/interface/lazyWalletRegistryClass.d.ts +0 -6
  106. package/dist/types/logic/lazyWalletRegistry.d.ts +0 -2
@@ -1,15 +1,18 @@
1
1
  import { _getChainSpecificConstants } from "../constants.js";
2
2
  import { MissingEOAWalletError } from "../errors.js";
3
3
  import { DeviceWalletFactory } from "../../abis/index.js";
4
- /**
5
- * Admin-EOA logic for `DeviceWalletFactory`.
6
- *
7
- * Every function here is `onlyAdmin` / `onlyAdminOrRegistry` / `onlyOwner` on
8
- * chain, i.e. the caller must be the `eSIMWalletAdmin` (or `upgradeManager`)
9
- * EOA - never a device-wallet userOp. They therefore live on the EOA surface
10
- * (`KokioAdmin`) and use `writeContract`, mirroring `_createAccountWithEOA`
11
- * (which is reused as-is from `../deviceWalletFactory.js`).
12
- */
4
+ // Admin-EOA logic for `DeviceWalletFactory`.
5
+ //
6
+ // Every function here is `onlyAdmin` / `onlyAdminOrRegistry` / `onlyOwner` on
7
+ // chain, i.e. the caller must be the `eSIMWalletAdmin` (or `upgradeManager`)
8
+ // EOA - never a device-wallet userOp. They therefore live on the EOA surface
9
+ // (`KokioAdmin`) and use `writeContract`, mirroring `_createAccountWithEOA`
10
+ // (which is reused as-is from `../deviceWalletFactory.js`).
11
+ //
12
+ // On the live deployment the owner is the `ProtocolAdmin` timelock, not an EOA,
13
+ // so an `onlyOwner` call sent directly reverts. Route those through
14
+ // `protocolAdmin.proposer.schedule` instead. The direct path stays for
15
+ // deployments whose owner is a plain EOA or multisig.
13
16
  /**
14
17
  * Batch-deploy device wallets for lazy/fiat users. `onlyAdminOrRegistry`,
15
18
  * `payable`: `value` is the total ETH pot from which each `depositAmounts[i]`
@@ -31,8 +34,12 @@ export const _deployDeviceWalletForUsers = async (client, deviceUniqueIdentifier
31
34
  value
32
35
  });
33
36
  };
34
- /** Register a freshly created device wallet with the factory. `onlyAdminOrRegistry`. */
35
- export const _postCreateAccount = async (client, deviceWallet, deviceUniqueIdentifier, deviceWalletOwnerKey) => {
37
+ /**
38
+ * Register a freshly created device wallet with the factory. `onlyAdminOrRegistry`.
39
+ * The salt has to be the one the deploying `createAccount` used, since the
40
+ * factory rederives the counterfactual address from it to check the wallet.
41
+ */
42
+ export const _postCreateAccount = async (client, deviceWallet, deviceUniqueIdentifier, deviceWalletOwnerKey, salt) => {
36
43
  const chainID = await client.getChainId();
37
44
  const rpcURL = client.transport.url;
38
45
  const values = _getChainSpecificConstants(chainID, rpcURL);
@@ -44,7 +51,7 @@ export const _postCreateAccount = async (client, deviceWallet, deviceUniqueIdent
44
51
  account: client.account.address,
45
52
  abi: DeviceWalletFactory,
46
53
  functionName: 'postCreateAccount',
47
- args: [deviceWallet, deviceUniqueIdentifier, deviceWalletOwnerKey]
54
+ args: [deviceWallet, deviceUniqueIdentifier, deviceWalletOwnerKey, salt]
48
55
  });
49
56
  };
50
57
  /** One-time wiring of the registry into the factory. `onlyAdmin`. */
@@ -63,8 +70,8 @@ export const _addRegistryAddress = async (client, registryContractAddress) => {
63
70
  args: [registryContractAddress]
64
71
  });
65
72
  };
66
- /** Update the vault that receives eSIM payments. `onlyAdmin`. */
67
- export const _updateVaultAddress = async (client, newVaultAddress) => {
73
+ /** Point the device-wallet beacon at a new implementation. `onlyOwner` (upgradeManager). */
74
+ export const _updateDeviceWalletImplementation = async (client, newDeviceImpl) => {
68
75
  const chainID = await client.getChainId();
69
76
  const rpcURL = client.transport.url;
70
77
  const values = _getChainSpecificConstants(chainID, rpcURL);
@@ -75,48 +82,62 @@ export const _updateVaultAddress = async (client, newVaultAddress) => {
75
82
  chain: values.chain,
76
83
  account: client.account.address,
77
84
  abi: DeviceWalletFactory,
78
- functionName: 'updateVaultAddress',
79
- args: [newVaultAddress]
85
+ functionName: 'updateDeviceWalletImplementation',
86
+ args: [newDeviceImpl]
80
87
  });
81
88
  };
82
- /** Step 1 of the 2-step admin handover: propose a new admin. `onlyAdmin`. */
83
- export const _requestAdminUpdate = async (client, newAdmin) => {
89
+ // ---------------------------------------------------------------------------
90
+ // Owner payloads - only reachable through schedule
91
+ // ---------------------------------------------------------------------------
92
+ // Both are `onlyOwner`, and on the live deployment the owner is the timelock, so
93
+ // they exist as something to schedule rather than to send. Each returns the
94
+ // `OwnerCall` to hand to `protocolAdmin.schedule`.
95
+ /**
96
+ * Offer ownership to a new address. Pass the result to `schedule`.
97
+ *
98
+ * Ownable2Step, so the offer changes nothing until the named address calls
99
+ * `acceptOwnership`. Note this hands over the beacon too, since the factory owns
100
+ * it, so the new owner can move every deployed device wallet at once.
101
+ */
102
+ export const _transferOwnershipCall = async (client, newOwner) => {
84
103
  const chainID = await client.getChainId();
85
104
  const rpcURL = client.transport.url;
86
105
  const values = _getChainSpecificConstants(chainID, rpcURL);
87
- if (!client.account)
88
- throw new MissingEOAWalletError();
89
- return client.writeContract({
106
+ return {
90
107
  address: values.factoryAddresses.DEVICE_WALLET_FACTORY,
91
- chain: values.chain,
92
- account: client.account.address,
93
108
  abi: DeviceWalletFactory,
94
- functionName: 'requestAdminUpdate',
95
- args: [newAdmin]
96
- });
109
+ functionName: 'transferOwnership',
110
+ args: [newOwner],
111
+ };
97
112
  };
98
113
  /**
99
- * Step 2 of the 2-step admin handover: the proposed admin accepts. The chain
100
- * requires `msg.sender` to equal the pending admin, so the `client` here must
101
- * be the newly proposed admin EOA.
114
+ * Point the factory's own proxy at a new implementation. Builds
115
+ * `upgradeToAndCall`. Pass the result to `schedule`.
116
+ *
117
+ * This does not touch deployed device wallets. They read their implementation
118
+ * from the beacon, which moves through `updateDeviceWalletImplementation`
119
+ * instead. Changing what the factory deploys next is a beacon update, not this.
102
120
  */
103
- export const _acceptAdminUpdate = async (client) => {
121
+ export const _upgradeCall = async (client, newImplementation, data = '0x') => {
104
122
  const chainID = await client.getChainId();
105
123
  const rpcURL = client.transport.url;
106
124
  const values = _getChainSpecificConstants(chainID, rpcURL);
107
- if (!client.account)
108
- throw new MissingEOAWalletError();
109
- return client.writeContract({
125
+ return {
110
126
  address: values.factoryAddresses.DEVICE_WALLET_FACTORY,
111
- chain: values.chain,
112
- account: client.account.address,
113
127
  abi: DeviceWalletFactory,
114
- functionName: 'acceptAdminUpdate',
115
- args: []
116
- });
128
+ functionName: 'upgradeToAndCall',
129
+ args: [newImplementation, data],
130
+ };
117
131
  };
118
- /** Point the device-wallet beacon at a new implementation. `onlyOwner` (upgradeManager). */
119
- export const _updateDeviceWalletImplementation = async (client, newDeviceImpl) => {
132
+ /**
133
+ * Take ownership after a `transferOwnership` named this client. `msg.sender`
134
+ * must equal `pendingOwner`, so the `client` is the incoming owner.
135
+ *
136
+ * Where the incoming owner is the timelock, use
137
+ * `protocolAdmin.acceptOwnershipBatch` instead, which accepts for every contract
138
+ * at once.
139
+ */
140
+ export const _acceptOwnership = async (client) => {
120
141
  const chainID = await client.getChainId();
121
142
  const rpcURL = client.transport.url;
122
143
  const values = _getChainSpecificConstants(chainID, rpcURL);
@@ -127,7 +148,7 @@ export const _updateDeviceWalletImplementation = async (client, newDeviceImpl) =
127
148
  chain: values.chain,
128
149
  account: client.account.address,
129
150
  abi: DeviceWalletFactory,
130
- functionName: 'updateDeviceWalletImplementation',
131
- args: [newDeviceImpl]
151
+ functionName: 'acceptOwnership',
152
+ args: []
132
153
  });
133
154
  };
@@ -1,11 +1,9 @@
1
1
  import { _getChainSpecificConstants } from "../constants.js";
2
2
  import { MissingEOAWalletError } from "../errors.js";
3
3
  import { ESIMWallet } from "../../abis/index.js";
4
- /**
5
- * Admin-EOA logic targeting a specific `ESIMWallet` instance (address passed in).
6
- * `buyDataBundle` is `onlyDeviceWalletOrESIMWalletAdmin`, so the admin EOA can
7
- * call it directly.
8
- */
4
+ // Admin-EOA logic targeting a specific `ESIMWallet` instance (address passed in).
5
+ // `buyDataBundle` is `onlyDeviceWalletOrESIMWalletAdmin`, so the admin EOA can
6
+ // call it directly.
9
7
  /**
10
8
  * Buy a data bundle for an eSIM wallet. `onlyDeviceWalletOrESIMWalletAdmin`,
11
9
  * `payable`. `value` is optional: the contract pulls any shortfall from the
@@ -1,14 +1,17 @@
1
1
  import { _getChainSpecificConstants } from "../constants.js";
2
2
  import { MissingEOAWalletError } from "../errors.js";
3
3
  import { ESIMWalletFactory } from "../../abis/index.js";
4
- /**
5
- * Admin-EOA logic for `ESIMWalletFactory`. Both functions are owner-gated
6
- * (`addRegistryAddress` requires `msg.sender == owner()`, `updateESIMWalletImplementation`
7
- * is `onlyOwner`), so the `client` must carry the `upgradeManager` EOA.
8
- *
9
- * Note: `ESIMWalletFactory.deployESIMWallet` is intentionally NOT exposed - it is
10
- * `onlyRegistryOrDeviceWalletFactoryOrDeviceWallet`, so a bare EOA always reverts.
11
- */
4
+ // Admin-EOA logic for `ESIMWalletFactory`. Both functions are owner-gated
5
+ // (`addRegistryAddress` requires `msg.sender == owner()`, `updateESIMWalletImplementation`
6
+ // is `onlyOwner`), so the `client` must carry the `upgradeManager` EOA.
7
+ //
8
+ // On the live deployment the owner is the `ProtocolAdmin` timelock, not an EOA,
9
+ // so either call sent directly reverts. Route them through
10
+ // `protocolAdmin.proposer.schedule` instead. The direct path stays for
11
+ // deployments whose owner is a plain EOA or multisig.
12
+ //
13
+ // Note: `ESIMWalletFactory.deployESIMWallet` is intentionally NOT exposed - it is
14
+ // `onlyRegistryOrDeviceWalletFactoryOrDeviceWallet`, so a bare EOA always reverts.
12
15
  /** One-time wiring of the registry into the eSIM factory. Owner only. */
13
16
  export const _addRegistryAddress = async (client, registryContractAddress) => {
14
17
  const chainID = await client.getChainId();
@@ -41,3 +44,68 @@ export const _updateESIMWalletImplementation = async (client, eSIMWalletImpl) =>
41
44
  args: [eSIMWalletImpl]
42
45
  });
43
46
  };
47
+ // ---------------------------------------------------------------------------
48
+ // Owner payloads - only reachable through schedule
49
+ // ---------------------------------------------------------------------------
50
+ // Both are `onlyOwner`, and on the live deployment the owner is the timelock, so
51
+ // they exist as something to schedule rather than to send. Each returns the
52
+ // `OwnerCall` to hand to `protocolAdmin.schedule`.
53
+ /**
54
+ * Offer ownership to a new address. Pass the result to `schedule`.
55
+ *
56
+ * Ownable2Step, so the offer changes nothing until the named address calls
57
+ * `acceptOwnership`. Note this hands over the beacon too, since the factory owns
58
+ * it, so the new owner can move every deployed eSIM wallet at once.
59
+ */
60
+ export const _transferOwnershipCall = async (client, newOwner) => {
61
+ const chainID = await client.getChainId();
62
+ const rpcURL = client.transport.url;
63
+ const values = _getChainSpecificConstants(chainID, rpcURL);
64
+ return {
65
+ address: values.factoryAddresses.ESIM_WALLET_FACTORY,
66
+ abi: ESIMWalletFactory,
67
+ functionName: 'transferOwnership',
68
+ args: [newOwner],
69
+ };
70
+ };
71
+ /**
72
+ * Point the factory's own proxy at a new implementation. Builds
73
+ * `upgradeToAndCall`. Pass the result to `schedule`.
74
+ *
75
+ * This does not touch deployed eSIM wallets. They read their implementation from
76
+ * the beacon, which moves through `updateESIMWalletImplementation` instead.
77
+ */
78
+ export const _upgradeCall = async (client, newImplementation, data = '0x') => {
79
+ const chainID = await client.getChainId();
80
+ const rpcURL = client.transport.url;
81
+ const values = _getChainSpecificConstants(chainID, rpcURL);
82
+ return {
83
+ address: values.factoryAddresses.ESIM_WALLET_FACTORY,
84
+ abi: ESIMWalletFactory,
85
+ functionName: 'upgradeToAndCall',
86
+ args: [newImplementation, data],
87
+ };
88
+ };
89
+ /**
90
+ * Take ownership after a `transferOwnership` named this client. `msg.sender`
91
+ * must equal `pendingOwner`, so the `client` is the incoming owner.
92
+ *
93
+ * Where the incoming owner is the timelock, use
94
+ * `protocolAdmin.acceptOwnershipBatch` instead, which accepts for every contract
95
+ * at once.
96
+ */
97
+ export const _acceptOwnership = async (client) => {
98
+ const chainID = await client.getChainId();
99
+ const rpcURL = client.transport.url;
100
+ const values = _getChainSpecificConstants(chainID, rpcURL);
101
+ if (!client.account)
102
+ throw new MissingEOAWalletError();
103
+ return client.writeContract({
104
+ address: values.factoryAddresses.ESIM_WALLET_FACTORY,
105
+ chain: values.chain,
106
+ account: client.account.address,
107
+ abi: ESIMWalletFactory,
108
+ functionName: 'acceptOwnership',
109
+ args: []
110
+ });
111
+ };
@@ -1,17 +1,48 @@
1
+ import { BaseError, ContractFunctionRevertedError, isAddressEqual, parseEventLogs, publicActions, } from "viem";
1
2
  import { _getChainSpecificConstants } from "../constants.js";
2
- import { MissingEOAWalletError } from "../errors.js";
3
- import { LazyWalletRegistry } from "../../abis/index.js";
3
+ import { BatchSizeOutOfRangeError, DepositOnResumeError, ESIMWalletNotLazyDeployedError, MissingBatchEventError, MissingEOAWalletError, StalledBatchError, } from "../errors.js";
4
+ import { LazyWalletRegistry, Registry } from "../../abis/index.js";
5
+ import { _eSIMWalletsDeployed, _lazyDeployedESIMWallet } from "./reads/lazyWalletRegistry.reads.js";
6
+ // Admin-EOA logic for `LazyWalletRegistry`. Every function here is
7
+ // `onlyESIMWalletAdmin` on chain, so they can only succeed from the admin EOA -
8
+ // a device-wallet userOp (whose sender is the smart account) always reverts.
9
+ // This is why they belong on the EOA surface rather than the mobile userOp one.
10
+ //
11
+ // Deployment and the history copy are both paginated on chain. The thin wrappers
12
+ // send one batch each; the two `AllBatches` functions below run a device or an
13
+ // eSIM to completion and are what the backend should normally call.
14
+ const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
4
15
  /**
5
- * Admin-EOA logic for `LazyWalletRegistry`. All three functions are
6
- * `onlyESIMWalletAdmin` on chain, so they can only succeed from the admin EOA -
7
- * a device-wallet userOp (whose sender is the smart account) always reverts.
8
- * This is why they belong on the EOA surface rather than the mobile userOp one.
16
+ * The contract's own caps, mirrored here because they are `constant` on chain and
17
+ * reading them would cost a round trip on every call. The fork tier checks these
18
+ * against the live deployment, so an upgrade that moved one would fail there
19
+ * rather than turning every call into a reverted transaction.
9
20
  */
10
- /** Record fiat/lazy purchase history for a batch of devices. `onlyESIMWalletAdmin`. */
11
- export const _batchPopulateHistory = async (client, deviceUniqueIdentifiers, eSIMUniqueIdentifiers, dataBundleDetails) => {
21
+ export const MAX_ESIM_WALLETS_PER_CALL = 20n;
22
+ export const MAX_HISTORY_ENTRIES_PER_CALL = 50n;
23
+ /**
24
+ * Default batch sizes, both below the caps above.
25
+ *
26
+ * A continuation call costs about 28,000 gas before it deploys anything, against
27
+ * roughly 556,000 per eSIM wallet, so a smaller batch buys headroom for almost
28
+ * nothing: running a 45 eSIM device at 10 rather than 20 costs about 0.4% more
29
+ * gas in total. That headroom matters because a full batch of 20 measures between
30
+ * 9.3M and 11.8M gas depending on identifier length and storage warmth, which is
31
+ * uncomfortably close to a per-transaction gas ceiling.
32
+ */
33
+ export const DEFAULT_ESIM_WALLETS_PER_CALL = 10n;
34
+ export const DEFAULT_HISTORY_ENTRIES_PER_CALL = 25n;
35
+ const _resolve = async (client) => {
12
36
  const chainID = await client.getChainId();
13
37
  const rpcURL = client.transport.url;
14
- const values = _getChainSpecificConstants(chainID, rpcURL);
38
+ return _getChainSpecificConstants(chainID, rpcURL);
39
+ };
40
+ // ---------------------------------------------------------------------------
41
+ // One transaction each, mirroring the contract
42
+ // ---------------------------------------------------------------------------
43
+ /** Record fiat/lazy purchase history for a batch of devices. `onlyESIMWalletAdmin`. */
44
+ export const _batchPopulateHistory = async (client, deviceUniqueIdentifiers, eSIMUniqueIdentifiers, dataBundleDetails) => {
45
+ const values = await _resolve(client);
15
46
  if (!client.account)
16
47
  throw new MissingEOAWalletError();
17
48
  return client.writeContract({
@@ -24,14 +55,16 @@ export const _batchPopulateHistory = async (client, deviceUniqueIdentifiers, eSI
24
55
  });
25
56
  };
26
57
  /**
27
- * Materialise a lazily-provisioned device wallet and its eSIMs on chain.
28
- * `onlyESIMWalletAdmin`, `payable`: the contract requires `depositAmount == msg.value`,
29
- * so `value` is set to `depositAmount` here.
58
+ * Materialise a lazily-provisioned device wallet and the first batch of its eSIMs
59
+ * on chain. `onlyESIMWalletAdmin`, `payable`: the contract requires
60
+ * `depositAmount == msg.value`, so `value` is set to `depositAmount` here.
61
+ *
62
+ * One transaction. `maxWallets` caps how many eSIM wallets it deploys, and the
63
+ * contract refuses anything above `MAX_ESIM_WALLETS_PER_CALL` rather than
64
+ * clamping it. Prefer `_deployLazyWalletAllBatches`, which finishes the device.
30
65
  */
31
- export const _deployLazyWalletAndSetESIMIdentifier = async (client, deviceOwnerPublicKey, deviceUniqueIdentifier, salt, depositAmount) => {
32
- const chainID = await client.getChainId();
33
- const rpcURL = client.transport.url;
34
- const values = _getChainSpecificConstants(chainID, rpcURL);
66
+ export const _deployLazyWalletAndSetESIMIdentifier = async (client, deviceOwnerPublicKey, deviceUniqueIdentifier, salt, depositAmount, maxWallets) => {
67
+ const values = await _resolve(client);
35
68
  if (!client.account)
36
69
  throw new MissingEOAWalletError();
37
70
  return client.writeContract({
@@ -40,12 +73,290 @@ export const _deployLazyWalletAndSetESIMIdentifier = async (client, deviceOwnerP
40
73
  account: client.account.address,
41
74
  abi: LazyWalletRegistry,
42
75
  functionName: 'deployLazyWalletAndSetESIMIdentifier',
43
- args: [deviceOwnerPublicKey, deviceUniqueIdentifier, salt, depositAmount],
76
+ args: [deviceOwnerPublicKey, deviceUniqueIdentifier, salt, depositAmount, maxWallets],
44
77
  value: depositAmount
45
78
  });
46
79
  };
80
+ /**
81
+ * Deploy the next batch of eSIM wallets for a device the lazy route already
82
+ * started. `onlyESIMWalletAdmin`.
83
+ *
84
+ * One transaction. The contract reads its position from a cursor, so a dropped
85
+ * transaction is retried by repeating the identical call. It reverts
86
+ * `AllESIMWalletsDeployed` once nothing is left, which is the terminal condition
87
+ * rather than a failure.
88
+ */
89
+ export const _deployMoreESIMWalletsForLazyDevice = async (client, deviceUniqueIdentifier, maxWallets) => {
90
+ const values = await _resolve(client);
91
+ if (!client.account)
92
+ throw new MissingEOAWalletError();
93
+ return client.writeContract({
94
+ address: values.factoryAddresses.LAZY_WALLET_REGISTRY,
95
+ chain: values.chain,
96
+ account: client.account.address,
97
+ abi: LazyWalletRegistry,
98
+ functionName: 'deployMoreESIMWalletsForLazyDevice',
99
+ args: [deviceUniqueIdentifier, maxWallets]
100
+ });
101
+ };
102
+ /**
103
+ * Copy the next batch of an eSIM's stored purchase history onto its deployed
104
+ * wallet. `onlyESIMWalletAdmin`.
105
+ *
106
+ * One transaction, with its own cursor per eSIM. Reverts `HistoryAlreadyCopied`
107
+ * once nothing is left. Prefer `_setHistoryForLazyWalletAllBatches`.
108
+ */
109
+ export const _setHistoryForLazyWallet = async (client, eSIMIdentifier, maxEntries) => {
110
+ const values = await _resolve(client);
111
+ if (!client.account)
112
+ throw new MissingEOAWalletError();
113
+ return client.writeContract({
114
+ address: values.factoryAddresses.LAZY_WALLET_REGISTRY,
115
+ chain: values.chain,
116
+ account: client.account.address,
117
+ abi: LazyWalletRegistry,
118
+ functionName: 'setHistoryForLazyWallet',
119
+ args: [eSIMIdentifier, maxEntries]
120
+ });
121
+ };
47
122
  /** Re-point an eSIM identifier from an old device to a new one. `onlyESIMWalletAdmin`. */
48
123
  export const _switchESIMIdentifierToNewDeviceIdentifier = async (client, eSIMIdentifier, oldDeviceIdentifier, newDeviceIdentifier) => {
124
+ const values = await _resolve(client);
125
+ if (!client.account)
126
+ throw new MissingEOAWalletError();
127
+ return client.writeContract({
128
+ address: values.factoryAddresses.LAZY_WALLET_REGISTRY,
129
+ chain: values.chain,
130
+ account: client.account.address,
131
+ abi: LazyWalletRegistry,
132
+ functionName: 'switchESIMIdentifierToNewDeviceIdentifier',
133
+ args: [eSIMIdentifier, oldDeviceIdentifier, newDeviceIdentifier]
134
+ });
135
+ };
136
+ // ---------------------------------------------------------------------------
137
+ // Reading a batch back
138
+ // ---------------------------------------------------------------------------
139
+ // `writeContract` returns a hash, not the function's return values, so how much
140
+ // is left comes from the event each batch emits rather than from the call.
141
+ const _batchEvent = async (client, lazyRegistryAddress, hash, eventName) => {
142
+ const receipt = await client.waitForTransactionReceipt({ hash });
143
+ const logs = parseEventLogs({
144
+ abi: LazyWalletRegistry,
145
+ eventName,
146
+ logs: receipt.logs.filter((log) => isAddressEqual(log.address, lazyRegistryAddress)),
147
+ });
148
+ if (logs.length === 0)
149
+ throw new MissingBatchEventError(eventName, hash);
150
+ return logs[0].args;
151
+ };
152
+ /**
153
+ * Whether a call hit its terminal condition. Both paginated calls revert when
154
+ * there is nothing left rather than returning quietly, so a simulation is the
155
+ * only way to tell "already finished" from "more to do" without paying for a
156
+ * transaction that fails.
157
+ */
158
+ const _isFinished = async (client, request, terminalError) => {
159
+ try {
160
+ await client.simulateContract(request);
161
+ return false;
162
+ }
163
+ catch (err) {
164
+ if (err instanceof BaseError) {
165
+ const revert = err.walk((e) => e instanceof ContractFunctionRevertedError);
166
+ if (revert instanceof ContractFunctionRevertedError && revert.data?.errorName === terminalError) {
167
+ return true;
168
+ }
169
+ }
170
+ throw err;
171
+ }
172
+ };
173
+ // ---------------------------------------------------------------------------
174
+ // Run a device or an eSIM to completion
175
+ // ---------------------------------------------------------------------------
176
+ /**
177
+ * Deploy a lazy device and every one of its eSIM wallets, over as many
178
+ * transactions as that takes. `onlyESIMWalletAdmin`.
179
+ *
180
+ * Resumable. A device that is part-deployed, because a batch was dropped or an
181
+ * earlier call threw, is continued from its cursor instead of being restarted,
182
+ * so retrying is just calling this again with the same arguments. Pass a deposit
183
+ * of 0 on a retry: only the first batch is payable and it already took one.
184
+ *
185
+ * All of a device's purchase history has to be recorded before this runs. The
186
+ * first batch creates the device wallet, and `batchPopulateHistory` refuses any
187
+ * device that has one.
188
+ *
189
+ * @param maxWallets eSIM wallets per transaction, 1 to `MAX_ESIM_WALLETS_PER_CALL`.
190
+ */
191
+ export const _deployLazyWalletAllBatches = async (client, deviceOwnerPublicKey, deviceUniqueIdentifier, salt, depositAmount, maxWallets = DEFAULT_ESIM_WALLETS_PER_CALL) => {
192
+ const values = await _resolve(client);
193
+ if (!client.account)
194
+ throw new MissingEOAWalletError();
195
+ const publicClient = client.extend(publicActions);
196
+ const lazyRegistryAddress = values.factoryAddresses.LAZY_WALLET_REGISTRY;
197
+ if (maxWallets < 1n || maxWallets > MAX_ESIM_WALLETS_PER_CALL) {
198
+ throw new BatchSizeOutOfRangeError("maxWallets", maxWallets, MAX_ESIM_WALLETS_PER_CALL);
199
+ }
200
+ const alreadyDeployed = await _eSIMWalletsDeployed(client, deviceUniqueIdentifier);
201
+ const batches = [];
202
+ let deviceWallet;
203
+ let outstanding;
204
+ if (alreadyDeployed === 0n) {
205
+ const hash = await _deployLazyWalletAndSetESIMIdentifier(client, deviceOwnerPublicKey, deviceUniqueIdentifier, salt, depositAmount, maxWallets);
206
+ const first = await _batchEvent(publicClient, lazyRegistryAddress, hash, "LazyESIMWalletsDeployed");
207
+ deviceWallet = first._deviceWallet;
208
+ outstanding = first._remaining > 0n;
209
+ batches.push({
210
+ hash,
211
+ eSIMWallets: first._eSIMWallets,
212
+ eSIMIdentifiers: first._eSIMUniqueIdentifiers,
213
+ remaining: first._remaining,
214
+ });
215
+ }
216
+ else {
217
+ if (depositAmount !== 0n)
218
+ throw new DepositOnResumeError(deviceUniqueIdentifier, depositAmount);
219
+ deviceWallet = await publicClient.readContract({
220
+ address: values.factoryAddresses.REGISTRY,
221
+ abi: Registry,
222
+ functionName: "uniqueIdentifierToDeviceWallet",
223
+ args: [deviceUniqueIdentifier]
224
+ });
225
+ const finished = await _isFinished(publicClient, {
226
+ address: lazyRegistryAddress,
227
+ abi: LazyWalletRegistry,
228
+ functionName: "deployMoreESIMWalletsForLazyDevice",
229
+ args: [deviceUniqueIdentifier, maxWallets],
230
+ account: client.account.address,
231
+ }, "AllESIMWalletsDeployed");
232
+ if (finished) {
233
+ return { deviceWallet, eSIMWallets: [], eSIMIdentifiers: [], batches: [], alreadyComplete: true };
234
+ }
235
+ outstanding = true;
236
+ }
237
+ while (outstanding) {
238
+ const hash = await _deployMoreESIMWalletsForLazyDevice(client, deviceUniqueIdentifier, maxWallets);
239
+ const next = await _batchEvent(publicClient, lazyRegistryAddress, hash, "LazyESIMWalletsDeployed");
240
+ if (next._eSIMWallets.length === 0)
241
+ throw new StalledBatchError(hash, next._remaining);
242
+ batches.push({
243
+ hash,
244
+ eSIMWallets: next._eSIMWallets,
245
+ eSIMIdentifiers: next._eSIMUniqueIdentifiers,
246
+ remaining: next._remaining,
247
+ });
248
+ outstanding = next._remaining > 0n;
249
+ }
250
+ return {
251
+ deviceWallet,
252
+ eSIMWallets: batches.flatMap((batch) => [...batch.eSIMWallets]),
253
+ eSIMIdentifiers: batches.flatMap((batch) => [...batch.eSIMIdentifiers]),
254
+ batches,
255
+ alreadyComplete: false,
256
+ };
257
+ };
258
+ /**
259
+ * Copy an eSIM's whole stored purchase history onto its wallet, over as many
260
+ * transactions as that takes. `onlyESIMWalletAdmin`.
261
+ *
262
+ * Resumable for the same reason as the deployment: the cursor lives on chain, so
263
+ * a partly copied eSIM is continued rather than restarted.
264
+ *
265
+ * The history cursor is per eSIM and the deploy cursor is per device, so this can
266
+ * run against an eSIM whose wallet has landed while its siblings are still
267
+ * undeployed.
268
+ *
269
+ * @param maxEntries entries per transaction, 1 to `MAX_HISTORY_ENTRIES_PER_CALL`.
270
+ */
271
+ export const _setHistoryForLazyWalletAllBatches = async (client, eSIMIdentifier, maxEntries = DEFAULT_HISTORY_ENTRIES_PER_CALL) => {
272
+ const values = await _resolve(client);
273
+ if (!client.account)
274
+ throw new MissingEOAWalletError();
275
+ const publicClient = client.extend(publicActions);
276
+ const lazyRegistryAddress = values.factoryAddresses.LAZY_WALLET_REGISTRY;
277
+ if (maxEntries < 1n || maxEntries > MAX_HISTORY_ENTRIES_PER_CALL) {
278
+ throw new BatchSizeOutOfRangeError("maxEntries", maxEntries, MAX_HISTORY_ENTRIES_PER_CALL);
279
+ }
280
+ // The same lookup the contract authorises on. Checking it here turns a
281
+ // reverted transaction into a typed error.
282
+ const eSIMWallet = await _lazyDeployedESIMWallet(client, eSIMIdentifier);
283
+ if (eSIMWallet === ZERO_ADDRESS)
284
+ throw new ESIMWalletNotLazyDeployedError(eSIMIdentifier);
285
+ const finished = await _isFinished(publicClient, {
286
+ address: lazyRegistryAddress,
287
+ abi: LazyWalletRegistry,
288
+ functionName: "setHistoryForLazyWallet",
289
+ args: [eSIMIdentifier, maxEntries],
290
+ account: client.account.address,
291
+ }, "HistoryAlreadyCopied");
292
+ if (finished)
293
+ return { eSIMWallet, copied: 0n, batches: [], alreadyComplete: true };
294
+ const batches = [];
295
+ let copied = 0n;
296
+ let outstanding = true;
297
+ while (outstanding) {
298
+ const hash = await _setHistoryForLazyWallet(client, eSIMIdentifier, maxEntries);
299
+ const batch = await _batchEvent(publicClient, lazyRegistryAddress, hash, "LazyHistoryCopied");
300
+ if (batch._copied === 0n)
301
+ throw new StalledBatchError(hash, batch._remaining);
302
+ batches.push({ hash, copied: batch._copied, remaining: batch._remaining });
303
+ copied += batch._copied;
304
+ outstanding = batch._remaining > 0n;
305
+ }
306
+ return { eSIMWallet, copied, batches, alreadyComplete: false };
307
+ };
308
+ // ---------------------------------------------------------------------------
309
+ // Owner payloads - only reachable through schedule
310
+ // ---------------------------------------------------------------------------
311
+ // Both are `onlyOwner`, and on the live deployment the owner is the timelock, so
312
+ // they exist as something to schedule rather than to send. Each returns the
313
+ // `OwnerCall` to hand to `protocolAdmin.schedule`.
314
+ /**
315
+ * Offer ownership to a new address. Pass the result to `schedule`.
316
+ *
317
+ * Ownable2Step, so the offer changes nothing until the named address calls
318
+ * `acceptOwnership`. Until then the current owner keeps every power.
319
+ */
320
+ export const _transferOwnershipCall = async (client, newOwner) => {
321
+ const chainID = await client.getChainId();
322
+ const rpcURL = client.transport.url;
323
+ const values = _getChainSpecificConstants(chainID, rpcURL);
324
+ return {
325
+ address: values.factoryAddresses.LAZY_WALLET_REGISTRY,
326
+ abi: LazyWalletRegistry,
327
+ functionName: 'transferOwnership',
328
+ args: [newOwner],
329
+ };
330
+ };
331
+ /**
332
+ * Point the proxy at a new implementation. Builds `upgradeToAndCall`. Pass the
333
+ * result to `schedule`.
334
+ *
335
+ * This contract holds every fiat user's unclaimed purchase history, so a layout
336
+ * change here strands data that has no other copy. Diff the storage layout
337
+ * before scheduling. `data` runs on the proxy straight after the swap and is
338
+ * where a `reinitializer` goes.
339
+ */
340
+ export const _upgradeCall = async (client, newImplementation, data = '0x') => {
341
+ const chainID = await client.getChainId();
342
+ const rpcURL = client.transport.url;
343
+ const values = _getChainSpecificConstants(chainID, rpcURL);
344
+ return {
345
+ address: values.factoryAddresses.LAZY_WALLET_REGISTRY,
346
+ abi: LazyWalletRegistry,
347
+ functionName: 'upgradeToAndCall',
348
+ args: [newImplementation, data],
349
+ };
350
+ };
351
+ /**
352
+ * Take ownership after a `transferOwnership` named this client. `msg.sender`
353
+ * must equal `pendingOwner`, so the `client` is the incoming owner.
354
+ *
355
+ * Where the incoming owner is the timelock, use
356
+ * `protocolAdmin.acceptOwnershipBatch` instead, which accepts for every contract
357
+ * at once.
358
+ */
359
+ export const _acceptOwnership = async (client) => {
49
360
  const chainID = await client.getChainId();
50
361
  const rpcURL = client.transport.url;
51
362
  const values = _getChainSpecificConstants(chainID, rpcURL);
@@ -56,7 +367,7 @@ export const _switchESIMIdentifierToNewDeviceIdentifier = async (client, eSIMIde
56
367
  chain: values.chain,
57
368
  account: client.account.address,
58
369
  abi: LazyWalletRegistry,
59
- functionName: 'switchESIMIdentifierToNewDeviceIdentifier',
60
- args: [eSIMIdentifier, oldDeviceIdentifier, newDeviceIdentifier]
370
+ functionName: 'acceptOwnership',
371
+ args: []
61
372
  });
62
373
  };