Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
d85bb88
Add local bundler and mock paymaster for tests, drop unused asn1 deps
ManulParihar Sep 18, 2026
3579f1e
Export the lazy deployment error classes from both entry points
ManulParihar Sep 18, 2026
1725c85
Add consumer fork and live test tiers
ManulParihar Sep 18, 2026
0a69274
Let getSmartWalletClient take a bundler URL
ManulParihar Sep 18, 2026
10142de
Test device wallet deploy and registration through the public SDK on …
ManulParihar Sep 18, 2026
5f76921
Add deployAndBindESIMWallet and eSIM wallet address prediction
ManulParihar Sep 18, 2026
982442a
Add buyDataBundleWithTransfer
ManulParihar Sep 18, 2026
a4823b7
Throw ContractRevertError when a user operation reverts
ManulParihar Sep 18, 2026
1734ff7
Test the eSIM wallet and purchase flow through the public SDK on a fork
ManulParihar Sep 18, 2026
299f94c
Decode ProtocolAdmin and PaymentAdapter errors
ManulParihar Sep 18, 2026
d5a40d6
Check an unaccepted asset is refused with a decoded error
ManulParihar Sep 18, 2026
b6306f5
Pass the client's account to writeContract so local keys sign
ManulParihar Sep 18, 2026
6cbbd6a
Test admin writes with a local private key on a fork
ManulParihar Sep 18, 2026
a25dd1d
Ask each client for its chain id once and run independent reads together
ManulParihar Sep 18, 2026
721ba1f
Note that registry.bindESIMWallet binds the registry side only
ManulParihar Sep 18, 2026
afb4c59
Send the gas policy as sponsorshipPolicyId, and none when unset
ManulParihar Sep 18, 2026
157bef8
Let getSmartWallet work with a wallet client that has no account
ManulParihar Sep 18, 2026
a9b08f0
Test high-s passkey signatures and message signing through the public…
ManulParihar Sep 18, 2026
c09c3ee
Pin forks, pick free ports, align the fork clock, and fail when anvil…
ManulParihar Sep 18, 2026
b2168f3
Cover the untested passkey branches, ProtocolAdmin reads and batch op…
ManulParihar Sep 18, 2026
546187a
Describe what writeContractOrThrow and toContractRevertError actually…
ManulParihar Sep 18, 2026
6c2f5ff
Remove the unused DER parser, the last user of the asn1 packages
ManulParihar Sep 18, 2026
ccc6a35
Correct the ProtocolAdmin address and who pays an operation's ETH
ManulParihar Sep 18, 2026
03582ca
Document write results, revert errors and the batched helpers
ManulParihar Sep 18, 2026
2b1c55b
Add the live Base Sepolia journey with Pimlico
ManulParihar Sep 18, 2026
aeac09c
Name the token the live purchase needs instead of pointing at a faucet
ManulParihar Sep 18, 2026
f917cd7
Pay with the USDCt test token in the live purchase test
ManulParihar Sep 18, 2026
f227611
Merge remote-tracking branch 'origin/main' into update/tests-and-fixes
ManulParihar Sep 18, 2026
d35d67e
Hide the mock paymaster's error printouts in test output
ManulParihar Sep 18, 2026
50744ca
Replace @simplewebauthn/server with @scure/base for base64url
ManulParihar Sep 18, 2026
508b7ed
Set package version to 3.1.0
ManulParihar Sep 18, 2026
257a754
Wait for confirmations and count nonces locally in the live tests
ManulParihar Sep 18, 2026
8d7f926
Let live user operations settle before reading their results
ManulParihar Sep 18, 2026
f9e7716
Fail any unit test whose write does not pass the client's account object
ManulParihar Sep 19, 2026
f278297
Remove unused helpers and tests the public SDK journeys now cover
ManulParihar Sep 19, 2026
771b1df
Buy with the USDCt test token on the fork too
ManulParihar Sep 19, 2026
386054f
Make react-native-passkey an optional peer dependency
ManulParihar Sep 19, 2026
ad1664e
Document the batched helpers, bundler URL, revert errors and consumer…
ManulParihar Sep 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,19 @@ surface, with a code example and return type, see the

## Installation

In the mobile app:

```sh
npm install kokio-sdk react-native-passkey
```

On a backend that only uses `kokio-sdk/admin`:

```sh
npm install kokio-sdk
```

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.
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.

## Mobile client (Expo / React Native)

Expand All @@ -39,18 +45,16 @@ is ever held in the app.

You will need:

- a viem `WalletClient` connected to the target chain, carrying an `account`
and an explicit RPC URL (see the note under the example),
- 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.
- the passkey `credentialId` and `rpId` registered for the device,
- a Pimlico API key and a gas policy id (used by the bundler and paymaster).
- a Pimlico API key, and optionally a Pimlico sponsorship policy id (`sp_...`). Pass `""` to be sponsored without a policy.

```ts
import { Kokio } from "kokio-sdk";
import { createWalletClient, http } from "viem";
import { baseSepolia } from "viem/chains";

const walletClient = createWalletClient({
account: knownAddress,
chain: baseSepolia,
transport: http(rpcUrl),
});
Expand Down Expand Up @@ -90,15 +94,11 @@ const receipt = await smartAccountClient.waitForUserOperationReceipt({ hash });
if (!receipt.success) throw new Error("operation reverted");
```

Two things about the wallet client. It has to carry an `account`:
`getSmartWallet` refuses a client without one, though it never asks it for a
signature, since the passkey signs everything. And give `http()` a real RPC
URL, because the SDK reads `client.transport.url` to build the public client
it uses for contract reads.
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.

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.

Check `receipt.success`. An operation whose calls revert is still mined and
still returns a receipt, so the await resolving is not on its own proof the
write landed. `receipt.receipt.transactionHash` is the onchain transaction.
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.

The contract surfaces (`deviceWallet`, `eSIMWallet`, `deviceWalletFactory`,
`eSIMWalletFactory`, `registry`, `paymentAdapter`, `P256Verifier`) are only
Expand Down
17 changes: 17 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,20 @@ Every method entry follows the same shape:
A write (anything that changes state) either sends a user operation (mobile)
or a transaction (admin) and resolves to a hash. A read is a plain `view` call
and resolves to the value itself.

A write the contract would refuse throws `ContractRevertError` before anything is sent, with the contract's error name in `decoded.errorName`:

```ts
import { ContractRevertError } from "kokio-sdk";

try {
await kokio.eSIMWallet!.buyDataBundleWithToken(dataBundleDetails, asset, maxAmountIn, paymentReference);
} catch (err) {
if (err instanceof ContractRevertError && err.decoded?.errorName === "PaymentReferenceAlreadyUsed") {
// this order was already paid for
}
throw err;
}
```

The backend's wallet client can hold a local private key (`privateKeyToAccount`) or a connected wallet. Admin writes sign with whichever account the client carries.
2 changes: 2 additions & 0 deletions docs/admin/device-wallet-factory.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ wallet is not registered until this runs.
The salt has to match the one the deploy used: the factory recomputes the
wallet's address from it to check the wallet is real.

The same applies when the wallet was deployed by its first sponsored user operation: until this runs, the device wallet cannot deploy eSIM wallets. On a hosted RPC, wait a few confirmations before telling the app to go ahead. A bundler that simulates against a node one block behind will still see the wallet as unregistered.

```ts
const hash = await admin.deviceWalletFactory.postCreateAccount(
deviceWalletAddress, deviceUniqueIdentifier, ownerKey, salt,
Expand Down
19 changes: 17 additions & 2 deletions docs/mobile/device-wallet.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ const receipt = await smartAccountClient.waitForUserOperationReceipt({ hash });
if (!receipt.success) throw new Error("operation reverted");
```

An operation whose calls revert is still mined and still returns a receipt,
so check `success` rather than treating a resolved await as confirmation.
A call that would revert is caught before sending: the write throws `ContractRevertError` with the contract's error name in `decoded.errorName`. If the chain changes between that check and inclusion, the operation can still revert on chain. It is then mined and returns a receipt, so check `success` rather than treating a resolved await as confirmation.

## sendUserOperation

Expand All @@ -33,6 +32,22 @@ const hash = await kokio.deviceWallet!.sendUserOperation([

Returns: `Promise<Hash>`, the user operation hash.

## deployAndBindESIMWallet

Deploys a new eSIM wallet for this device wallet and adds it to the device wallet's list, in one user operation and one passkey prompt. Use it when a user adds a new eSIM. Pass `grantAccessToFunds: true` to also let the new eSIM wallet pull tokens from this device wallet, which `buyDataBundleWithToken` needs when the eSIM wallet holds less than the price.

The device wallet must already be registered by the backend (`admin.deviceWalletFactory.postCreateAccount`), or the deploy reverts. Each salt gives one address, so use a new salt for each eSIM wallet on the same device.

```ts
const { userOpHash, eSIMWalletAddress } = await kokio.deviceWallet!.deployAndBindESIMWallet(
1n, // salt
{ grantAccessToFunds: true },
);
kokio.setESIMWalletAddress(eSIMWalletAddress);
```

Returns: `Promise<{ userOpHash: Hash; eSIMWalletAddress: Address }>`. The address is known before the operation lands, so wait for the receipt before reading from it.

## addESIMWallet

Adds an eSIM wallet this device already owns onto the device wallet's list.
Expand Down
17 changes: 13 additions & 4 deletions docs/mobile/esim-wallet-factory.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,26 @@

`kokio.eSIMWalletFactory`

Deploys a new eSIM wallet for a device wallet. Present as soon as `Kokio` has
Predicts and deploys eSIM wallets for a device wallet. Present as soon as `Kokio` has
a `smartAccountClient`, chain-wide like the device wallet factory.

To deploy a new eSIM wallet, use [`deviceWallet.deployAndBindESIMWallet`](device-wallet.md#deployandbindesimwallet). It deploys and binds in one user operation and returns the new address.

## getCounterFactualAddress

Works out the address an eSIM wallet will have for a device wallet and salt, before it is deployed. A plain read, no user operation.

```ts
const hash = await kokio.eSIMWalletFactory!.deployESIMWalletWithUserOp(deviceWalletAddress, salt);
const eSIMWalletAddress = await kokio.eSIMWalletFactory!.getCounterFactualAddress(deviceWalletAddress, 1n);
```

Returns: `Promise<Address>`.

## deployESIMWalletWithUserOp

Deploys a new eSIM wallet, owned by the given device wallet. Use it when a
user is adding a new eSIM to a device wallet they already have. The device
**Deprecated.** Use `deviceWallet.deployAndBindESIMWallet`. This deploys the eSIM wallet without adding it to the device wallet's list, so the device wallet does not treat it as its own until `addESIMWallet` runs.

Deploys a new eSIM wallet, owned by the given device wallet. The device
wallet sending the user operation has to be a wallet the registry recognizes.

```ts
Expand Down
18 changes: 17 additions & 1 deletion docs/mobile/esim-wallet.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ const hash = await kokio.eSIMWallet!.buyDataBundleWithToken(dataBundleDetails, a
## buyDataBundleWithToken

Buys a data bundle for this eSIM, paid for in an ERC-20 the payment adapter
accepts (USDC on Base Sepolia today). Use this for the everyday purchase
accepts (`USDC` and `USDCt` on Base Sepolia today). Use this for the everyday purchase
flow.

If this eSIM wallet holds less than the price, the contract pulls the rest from the device wallet. That needs `deviceWallet.toggleAccessToFunds(eSIMWalletAddress, true)` first, otherwise the purchase reverts with `FundsAccessRevoked`. Without that access, use `buyDataBundleWithTransfer`.

Check `priceCapUSDCents()` first: a price above the cap reverts. Read
`kokio.paymentAdapter!.quote(asset, priceUSDCents)` to size `maxAmountIn` -
the most of `asset` this purchase may spend, in its smallest unit. Nothing
Expand All @@ -49,6 +51,20 @@ const hash = await kokio.eSIMWallet!.buyDataBundleWithToken(

Returns: `Promise<Hash>`, a user operation hash.

## buyDataBundleWithTransfer

The same purchase as `buyDataBundleWithToken`, for when the eSIM wallet has no access to the device wallet's funds. In one user operation the device wallet sends the eSIM wallet whatever it is short of the quote, then the purchase runs. Arguments are the same.

The device wallet must hold enough of the asset's token. It sends only the shortfall, so tokens already on the eSIM wallet are used first.

```ts
const hash = await kokio.eSIMWallet!.buyDataBundleWithTransfer(
dataBundleDetails, asset, maxAmountIn, paymentReference,
);
```

Returns: `Promise<Hash>`, a user operation hash.

## sendTokenToDeviceWallet

Sends an ERC-20 held by this eSIM wallet back to its owning device wallet.
Expand Down
14 changes: 14 additions & 0 deletions docs/mobile/smart-account.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ const account = await kokio.smartAccount.getSmartWallet(
Returns: `KokioSmartAccount`, a viem smart account object. Pass it to
`getSmartWalletClient`.

The wallet client given to `Kokio` does not need an `account`. The passkey signs, so a client with just a chain and transport is enough.

## getSmartWalletClient

Builds the client that signs with the passkey and sends user operations
Expand All @@ -47,3 +49,15 @@ Returns: `KokioSmartAccountClient`, a bundler client that can also read
contracts directly (it carries viem's public actions too). Pass it as
`smartAccountClient` to a new `Kokio(...)` call so the contract surfaces
(`deviceWallet`, `eSIMWallet`, and the rest) become available.

Gas is sponsored by the paymaster at the same endpoint. The gas policy id given to `Kokio` is optional. Pass `""` to send no policy, or a Pimlico sponsorship policy id to have that policy's rules applied.

To send user operations somewhere other than Pimlico, such as a local bundler in tests, pass `bundlerUrl`. That endpoint must also answer the ERC-7677 paymaster methods.

```ts
const smartAccountClient = await kokio.smartAccount.getSmartWalletClient(account, {
bundlerUrl: "http://127.0.0.1:4337",
});
```

If a user operation would revert, sending it throws `ContractRevertError` with the contract's error name in `decoded.errorName`, and nothing is sent.
Loading
Loading